trek 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.slim-lint.yml +0 -3
  3. data/AGENTS.md +9 -2
  4. data/CHANGELOG.md +73 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +23 -3
  7. data/Makefile +11 -9
  8. data/README.md +1 -1
  9. data/app/assets/icons/trek/chevron-down.svg +1 -0
  10. data/app/assets/icons/trek/chevron-up.svg +1 -0
  11. data/app/assets/icons/trek/external.svg +1 -0
  12. data/app/assets/icons/trek/theme-auto.svg +1 -0
  13. data/app/assets/icons/trek/theme-dark.svg +1 -0
  14. data/app/assets/icons/trek/theme-light.svg +1 -0
  15. data/app/assets/stylesheets/trek/base.css +13 -0
  16. data/app/components/trek/button_component/button_component.css +9 -0
  17. data/app/components/trek/button_component.rb +13 -1
  18. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +1 -1
  19. data/app/components/trek/dashboard/tile_component.rb +14 -0
  20. data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
  21. data/app/components/trek/dialog_component/dialog_component.js +62 -2
  22. data/app/components/trek/dropdown_component/dropdown_component.css +69 -0
  23. data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
  24. data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
  25. data/app/components/trek/dropdown_component.rb +34 -0
  26. data/app/components/trek/form/group_component/group_component.css +34 -0
  27. data/app/components/trek/header_component/header_component.css +9 -4
  28. data/app/components/trek/header_component/header_component.html.slim +17 -10
  29. data/app/components/trek/header_component/header_component.js +29 -5
  30. data/app/components/trek/header_component.rb +24 -3
  31. data/app/components/trek/menu_component/menu_component.css +6 -0
  32. data/app/components/trek/menu_component.rb +29 -4
  33. data/app/components/trek/toaster/toast_component.rb +1 -1
  34. data/app/controllers/concerns/trek/authorization.rb +12 -0
  35. data/app/controllers/concerns/trek/filters.rb +18 -1
  36. data/app/helpers/trek/page_path_helper.rb +4 -3
  37. data/app/helpers/trek/theme_helper.rb +16 -1
  38. data/app/models/concerns/trek/phonable.rb +15 -3
  39. data/app/models/concerns/trek/sectionable.rb +5 -0
  40. data/app/models/concerns/trek/users/validations.rb +15 -6
  41. data/app/uploaders/audio_uploader.rb +11 -0
  42. data/app/uploaders/record_image_uploader.rb +3 -0
  43. data/config/locales/trek.en.yml +3 -2
  44. data/config/locales/trek.fr.yml +1 -0
  45. data/docs/.vitepress/config.mjs +20 -1
  46. data/docs/.vitepress/theme/style.css +36 -8
  47. data/docs/changelog.md +9 -0
  48. data/docs/customize.md +18 -2
  49. data/docs/deploy.md +7 -2
  50. data/docs/getting-started.md +1 -1
  51. data/docs/index.md +8 -8
  52. data/docs/public/apple-touch-icon.png +0 -0
  53. data/docs/public/favicon.ico +0 -0
  54. data/docs/public/favicon.png +0 -0
  55. data/docs/public/trek-mark.webp +0 -0
  56. data/docs/reference/components.md +15 -0
  57. data/docs/reference/concerns/phonable.md +27 -1
  58. data/docs/reference/controllers.md +8 -3
  59. data/docs/reference/environment-variables.md +2 -0
  60. data/docs/reference/forms.md +2 -0
  61. data/docs/reference/generators/install.md +4 -1
  62. data/docs/reference/icons.md +1 -1
  63. data/docs/reference/models/tag.md +1 -1
  64. data/docs/reference/models/user.md +6 -4
  65. data/docs/reference/uploaders.md +31 -6
  66. data/docs/what-is-trek.md +1 -1
  67. data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
  68. data/lib/generators/trek/install/appsignal_generator.rb +102 -0
  69. data/lib/generators/trek/install/model_specs_generator.rb +64 -0
  70. data/lib/generators/trek/install/postmark_generator.rb +3 -0
  71. data/lib/generators/trek/install/ruby_generator.rb +1 -1
  72. data/lib/generators/trek/install/scalingo_generator.rb +1 -1
  73. data/lib/generators/trek/install/uploaders_generator.rb +2 -0
  74. data/lib/generators/trek/install_generator.rb +5 -0
  75. data/lib/generators/trek/taxonomies_generator.rb +7 -0
  76. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  77. data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
  78. data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
  79. data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +7 -0
  80. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
  81. data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
  82. data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
  83. data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
  84. data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
  85. data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
  86. data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
  87. data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
  88. data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
  89. data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
  90. data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
  91. data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
  92. data/lib/generators/trek/templates/models/tag.rb +6 -1
  93. data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
  94. data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
  95. data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
  96. data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
  97. data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
  98. data/lib/generators/trek/templates/spec/models/user_spec.rb +109 -0
  99. data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
  100. data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
  101. data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
  102. data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
  103. data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
  104. data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
  105. data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
  106. data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
  107. data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
  108. data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
  109. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +78 -0
  110. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +51 -0
  111. data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
  112. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
  113. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
  114. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
  115. data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
  116. data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
  117. data/lib/tasks/trek/attachments.rake +18 -0
  118. data/lib/trek/cached_attachments.rb +53 -0
  119. data/lib/trek/saved_attachments_reload.rb +44 -0
  120. data/lib/trek/version.rb +1 -1
  121. data/lib/trek.rb +4 -0
  122. data/package.json +1 -1
  123. data/postcss.config.js +36 -0
  124. data/trek.gemspec +6 -0
  125. metadata +89 -3
  126. data/app/assets/icons/trek/theme.svg +0 -11
@@ -0,0 +1,102 @@
1
+ require "rails/generators/actions"
2
+ require "generators/trek/helpers"
3
+
4
+ module Trek
5
+ module Generators
6
+ module Install
7
+ class AppsignalGenerator < Rails::Generators::Base
8
+ include Trek::Generators::Helpers
9
+
10
+ desc "Install AppSignal (error tracking and performance monitoring)."
11
+
12
+ source_root File.expand_path("../templates", __dir__)
13
+
14
+ def add_gem
15
+ return if in_gemfile?("appsignal")
16
+
17
+ gem "appsignal", comment: "Error tracking and performance monitoring [https://appsignal.com]"
18
+ bundle_install
19
+ end
20
+
21
+ # Like Postmark's API token: the secret lives in the Rails credentials, not in
22
+ # config/appsignal.rb (where `appsignal install` would write it in plain text).
23
+ def inject_appsignal_to_credentials
24
+ return if credentials_encrypted_file.read&.include?("appsignal:")
25
+
26
+ inject_into_credentials(
27
+ <<~YAML
28
+ appsignal:
29
+ push_api_key: #{ENV.fetch("APPSIGNAL_PUSH_API_KEY", "")}
30
+ YAML
31
+ )
32
+ end
33
+
34
+ def create_config
35
+ template "config/appsignal.rb", "config/appsignal.rb"
36
+ end
37
+
38
+ def add_app_env_to_env_example
39
+ return unless File.exist?(".env.example")
40
+ return if File.read(".env.example").include?("APPSIGNAL_APP_ENV")
41
+
42
+ append_to_file ".env.example", "APPSIGNAL_APP_ENV=\n"
43
+ end
44
+
45
+ # A broken initializer or a missing credential raises before Rails can report
46
+ # anything — the error reporter does not exist yet — so the boot is wrapped.
47
+ def report_boot_errors
48
+ return unless File.exist?("config.ru")
49
+ return if File.read("config.ru").include?("Appsignal.send_error")
50
+
51
+ gsub_file "config.ru",
52
+ %r{^require_relative ["']config/environment["']\n},
53
+ boot_error_reporting
54
+ end
55
+
56
+ def create_context_concern
57
+ copy_file "controllers/concerns/appsignal_context.rb",
58
+ "app/controllers/concerns/appsignal_context.rb"
59
+ end
60
+
61
+ def include_context_in_application_controller
62
+ return if File.read("app/controllers/application_controller.rb").include?("AppsignalContext")
63
+
64
+ inject_into_class "app/controllers/application_controller.rb",
65
+ "ApplicationController",
66
+ " include AppsignalContext\n"
67
+ run "rubocop --autocorrect app/controllers/application_controller.rb", abort_on_failure: true
68
+ end
69
+
70
+ def include_context_in_trek_controllers
71
+ copy_file "initializers/appsignal_context.rb",
72
+ "config/initializers/appsignal_context.rb"
73
+ end
74
+
75
+ def final_instructions
76
+ say ""
77
+ say "AppSignal is configured for production. Check the setup with:"
78
+ say ""
79
+ say " bundle exec appsignal diagnose"
80
+ say ""
81
+ end
82
+
83
+ private
84
+
85
+ def boot_error_reporting
86
+ <<~RUBY
87
+ begin
88
+ require_relative "config/environment"
89
+ rescue Exception => error # rubocop:disable Lint/RescueException
90
+ # Errors raised while booting (a broken initializer, a missing credential)
91
+ # happen before the Rails error reporter exists — report them directly.
92
+ # The gem itself is loaded by Bundler.require, so a failure earlier than
93
+ # that (config/boot.rb, the Gemfile) has no Appsignal to report with.
94
+ Appsignal.send_error(error) if defined?(Appsignal)
95
+ raise
96
+ end
97
+ RUBY
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,64 @@
1
+ require "rails/generators/actions"
2
+ require "generators/trek/helpers"
3
+
4
+ module Trek
5
+ module Generators
6
+ module Install
7
+ class ModelSpecsGenerator < Rails::Generators::Base
8
+ include Trek::Generators::Helpers
9
+
10
+ desc "Add specs for the models Trek installs (Page, User…), with their Oaken seeds."
11
+
12
+ source_root File.expand_path("../templates", __dir__)
13
+
14
+ # In every group: `db/seeds.rb` calls Oaken, and `db:seed` runs in every environment.
15
+ def add_gem
16
+ return if in_gemfile?("oaken")
17
+
18
+ gem "oaken", "~> 1.0"
19
+ bundle_install
20
+ end
21
+
22
+ def load_oaken_in_seeds
23
+ seeds = File.join(destination_root, "db/seeds.rb")
24
+ return if File.exist?(seeds) && File.read(seeds).include?("Oaken.seed")
25
+
26
+ create_file "db/seeds.rb" unless File.exist?(seeds)
27
+ append_to_file "db/seeds.rb", <<~RUBY
28
+
29
+ # Oaken (https://github.com/kaspth/oaken) loads db/seeds/setup (defaults and helpers) first.
30
+ # Pass directories to seed them too, e.g. `Oaken.seed :data`; specs load their own cases with
31
+ # `seed "cases/…"`.
32
+ Oaken.seed
33
+ RUBY
34
+ end
35
+
36
+ def load_oaken_in_specs
37
+ rails_helper = File.join(destination_root, "spec/rails_helper.rb")
38
+ return unless File.exist?(rails_helper)
39
+ return if File.read(rails_helper).include?("oaken/rspec_setup")
40
+
41
+ # After rspec-rails, which Oaken's setup configures (transactional examples, seeding the
42
+ # database before the suite).
43
+ inject_into_file "spec/rails_helper.rb",
44
+ "require \"oaken/rspec_setup\"\n",
45
+ after: %r{^require ["']rspec/rails["'].*\n}
46
+ end
47
+
48
+ # Every file under the templates' db/seeds, spec/models and spec/requests is installed, so a new
49
+ # template needs no change here.
50
+ def copy_seeds
51
+ directory "db/seeds", "db/seeds"
52
+ end
53
+
54
+ def copy_specs
55
+ directory "spec/models", "spec/models"
56
+ directory "spec/requests", "spec/requests"
57
+ directory "spec/fixtures", "spec/fixtures"
58
+ # The tag policies' specs come with them, from `trek:taxonomies`.
59
+ directory "spec/policies", "spec/policies", exclude_pattern: /tag(_category)?_policy_spec/
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -58,6 +58,9 @@ module Trek
58
58
 
59
59
  def log_and_abort(error)
60
60
  Rails.logger.info(error)
61
+ # Reported as handled (the delivery is deliberately abandoned, not a failure) and
62
+ # through Rails.error, so the app code stays independent of the monitoring vendor.
63
+ Rails.error.report(error, handled: true)
61
64
  end
62
65
  RUBY
63
66
  end
@@ -5,7 +5,7 @@ module Trek
5
5
  module Generators
6
6
  module Install
7
7
  class RubyGenerator < Rails::Generators::Base
8
- RUBY_VERSION = "4.0.6"
8
+ RUBY_VERSION = "4.0.7"
9
9
 
10
10
  include Trek::Generators::Helpers
11
11
 
@@ -5,7 +5,7 @@ module Trek
5
5
  module Generators
6
6
  module Install
7
7
  class ScalingoGenerator < Rails::Generators::Base
8
- BUILDPACK_URL = "https://github.com/etaminstudio/trek-buildpack#v0.1.0".freeze
8
+ BUILDPACK_URL = "https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz".freeze
9
9
 
10
10
  include Trek::Generators::Helpers
11
11
 
@@ -19,6 +19,8 @@ module Trek
19
19
  "app/uploaders/model_image_uploader.rb"
20
20
  template "record_image_uploader.rb",
21
21
  "app/uploaders/record_image_uploader.rb"
22
+ template "audio_uploader.rb",
23
+ "app/uploaders/audio_uploader.rb"
22
24
  end
23
25
 
24
26
  def uncomment_route
@@ -76,6 +76,10 @@ module Trek
76
76
  generate "trek:install:mailer"
77
77
  end
78
78
 
79
+ def install_monitoring
80
+ generate "trek:install:appsignal"
81
+ end
82
+
79
83
  def install_user
80
84
  generate "trek:install:user"
81
85
  end
@@ -111,6 +115,7 @@ module Trek
111
115
  generate "trek:install:fragment"
112
116
  generate "trek:install:menu_node"
113
117
  generate "trek:install:external_link"
118
+ generate "trek:install:model_specs"
114
119
  end
115
120
 
116
121
  def install_ci
@@ -44,6 +44,13 @@ module Trek
44
44
  "app/policies/admin/tag_category_policy.rb"
45
45
  end
46
46
 
47
+ def copy_policy_specs
48
+ template "spec/policies/admin/tag_policy_spec.rb",
49
+ "spec/policies/admin/tag_policy_spec.rb"
50
+ template "spec/policies/admin/tag_category_policy_spec.rb",
51
+ "spec/policies/admin/tag_category_policy_spec.rb"
52
+ end
53
+
47
54
  def generate_crud
48
55
  generate("trek:scaffold", "TagCategory name:string key:string")
49
56
  generate("trek:scaffold", "Tag name:string key:string category:references")
@@ -20,7 +20,7 @@ stages:
20
20
 
21
21
  # Defaults
22
22
  default:
23
- image: registry.git.etamin.studio/devops/ci-ruby/node-24:4.0.6
23
+ image: registry.git.etamin.studio/devops/ci-ruby/node-24:4.0.7
24
24
 
25
25
  services:
26
26
  - postgres:16
@@ -0,0 +1,25 @@
1
+ # AppSignal's Ruby config file, loaded by the gem's Railtie (not a Rails initializer).
2
+ #
3
+ # The push API key lives in the Rails credentials (the installer injects it from
4
+ # APPSIGNAL_PUSH_API_KEY). APPSIGNAL_* environment variables are read before this
5
+ # block runs, so they keep precedence: the credentials only fill in what is unset,
6
+ # which leaves per-environment overrides (e.g. APPSIGNAL_APP_ENV) possible.
7
+ Appsignal.configure do |config|
8
+ config.name = "<%= application_name.dasherize %>"
9
+
10
+ config.push_api_key = Rails.application.credentials.dig(:appsignal, :push_api_key) if config.push_api_key.blank?
11
+
12
+ # The agent activates itself only when APPSIGNAL_PUSH_API_KEY is set in the
13
+ # environment; with the key in the credentials, activation has to be explicit.
14
+ # Production is monitored as soon as a key exists, the other environments never are.
15
+ config.active = config.push_api_key.present? if Rails.env.production?
16
+
17
+ # The slug is read-only on Scalingo, as on any host with a read-only filesystem, so
18
+ # the agent cannot write its own diagnostic log to log/: it falls back to /tmp on its
19
+ # own and announces it on every boot and every one-off container. Say it upfront.
20
+ config.log_path = "/tmp" if Rails.env.production?
21
+
22
+ # Trek mounts rails-healthcheck at /healthcheck — the platform polls it constantly
23
+ # and it would dominate the throughput graphs. (Rails' own /up is ignored by default.)
24
+ config.ignore_actions += ["Healthcheck::HealthchecksController#check"]
25
+ end
@@ -9,6 +9,10 @@ module Admin
9
9
  set_rack_response ImageUploader.upload_response(:cache, request.env)
10
10
  end
11
11
 
12
+ def audio
13
+ set_rack_response AudioUploader.upload_response(:cache, request.env)
14
+ end
15
+
12
16
  private
13
17
 
14
18
  def set_rack_response((status, headers, body))
@@ -29,6 +29,13 @@ module Admin
29
29
  end
30
30
 
31
31
  def update
32
+ # has_secure_password ignores a blank password: refuse it, rather than report a change that
33
+ # didn't happen.
34
+ if params[:password].blank?
35
+ @user.errors.add(:password, :blank)
36
+ return redirect_to edit_admin_user_password_reset_path(@token), alert: @user.errors.full_messages.join(", ")
37
+ end
38
+
32
39
  @user.update!(params.permit(:password))
33
40
  @user.user_sessions.destroy_all
34
41
  redirect_to new_admin_user_session_path, notice: t(".success")
@@ -15,7 +15,10 @@ module Admin
15
15
  def theme
16
16
  value = params[:theme].to_s
17
17
  Current.user.update!(theme: value) if User.themes.key?(value)
18
- head :no_content
18
+ respond_to do |format|
19
+ format.turbo_stream
20
+ format.html { head :no_content }
21
+ end
19
22
  end
20
23
 
21
24
  private
@@ -0,0 +1,37 @@
1
+ # Tags every AppSignal transaction with the area it belongs to and the acting user,
2
+ # so errors and slow requests can be filtered per namespace and per actor.
3
+ #
4
+ # Included in ApplicationController (front-office and Admin::BaseController through
5
+ # it) and, from config/initializers/appsignal_context.rb, in Trek::BaseController.
6
+ #
7
+ # Free of charge when AppSignal is inactive (no push API key, i.e. every environment
8
+ # but production): each Appsignal call below returns early.
9
+ module AppsignalContext
10
+ extend ActiveSupport::Concern
11
+
12
+ ADMIN_CONTROLLER_PATHS = ["admin/", "trek/"].freeze
13
+
14
+ included do
15
+ before_action :set_appsignal_context
16
+ end
17
+
18
+ private
19
+
20
+ def set_appsignal_context
21
+ Appsignal.set_namespace(appsignal_namespace)
22
+ Appsignal.add_tags(user_id: appsignal_user_id) if appsignal_user_id
23
+ end
24
+
25
+ def appsignal_namespace
26
+ controller_path.start_with?(*ADMIN_CONTROLLER_PATHS) ? "admin" : "web"
27
+ end
28
+
29
+ def appsignal_user_id
30
+ # Trek::Authentication resolves the session in its own before_action, which may
31
+ # run after this one, so resume it explicitly. Front-office controllers don't
32
+ # include the concern and have no session to resume.
33
+ resume_session if respond_to?(:resume_session, true) && Current.session.nil?
34
+
35
+ Current.user&.id
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ # Makes `fragments` available to seeds and specs: Oaken only defines a model's method once a seed
2
+ # file has used it.
3
+ register Fragment
@@ -0,0 +1,6 @@
1
+ # Defaults for `users.create` and `users.new` (see https://github.com/kaspth/oaken): a unique email and
2
+ # a password passing Trek's validations, so a seed or a spec only sets what it cares about. Specs
3
+ # sign in with `users.password`.
4
+ def users.password = "correct horse battery staple"
5
+
6
+ users.defaults email: -> { "user-#{SecureRandom.hex(4)}@example.com" }, password: users.password
@@ -0,0 +1,5 @@
1
+ # A small page tree for specs, loaded with `seed "cases/page_tree"`: the home page (the tree's root,
2
+ # which has no slug), a section, and a page inside it.
3
+ home = pages.create :home, key: "root", title: "Home"
4
+ company = pages.create :company, title: "Company", parent: home
5
+ pages.create :team, title: "Our team", parent: company
@@ -0,0 +1,5 @@
1
+ # Trek::BaseController is an engine class, so the concern is mixed in on every code
2
+ # reload rather than from a class body the application owns.
3
+ Rails.application.config.to_prepare do
4
+ Trek::BaseController.include(AppsignalContext)
5
+ end
@@ -8,5 +8,5 @@ en:
8
8
  attributes:
9
9
  <%= file_path %>:
10
10
  <%- attributes.each do |attr| -%>
11
- <%= attr.name %>: <%= attr.name.humanize -%>
11
+ <%= attr.name %>: <%= attr.name.humanize %>
12
12
  <%- end -%>
@@ -5,7 +5,7 @@ class TrekFragments < <%= migration_class_name %>
5
5
  t.string :namespace, index: true
6
6
  t.integer :position, null: false
7
7
  t.jsonb :title, default: {}
8
- t.jsonb :content
8
+ t.jsonb :content, default: {}
9
9
 
10
10
  t.timestamps
11
11
 
@@ -18,4 +18,3 @@ class TrekMenuNodes < <%= migration_class_name %>
18
18
  end
19
19
  end
20
20
  end
21
-
@@ -1,6 +1,6 @@
1
1
  class TrekTagCategories < <%= migration_class_name %>
2
2
  def change
3
- create_table "tag_categories" do |t|
3
+ create_table "tag_categories" do |t|
4
4
  t.string :key, index: { unique: true }
5
5
  t.string :name
6
6
  t.integer :position, null: false
@@ -1,6 +1,6 @@
1
1
  class TrekTaggings < <%= migration_class_name %>
2
2
  def change
3
- create_table :taggings do |t|
3
+ create_table :taggings do |t|
4
4
  t.references :taggable, null: false, polymorphic: true
5
5
  t.references :tag, null: false, foreign_key: true
6
6
 
@@ -4,7 +4,8 @@ class TrekUsers < <%= migration_class_name %>
4
4
  add_column :<%= tableized_model_class %>, :theme, :integer, default: 0, null: false
5
5
  add_column :<%= tableized_model_class %>, :locale, :string
6
6
  add_column :<%= tableized_model_class %>, :role, :integer, default: 1, null: false
7
- add_column :<%= tableized_model_class %>, :password_digest, :string, null: false
7
+ # The table is created empty by the trek_create_users migration, so no default is needed.
8
+ add_column :<%= tableized_model_class %>, :password_digest, :string, null: false # rubocop:disable Rails/NotNullColumn
8
9
  add_column :<%= tableized_model_class %>, :last_activity_at, :datetime
9
10
  end
10
11
  end
@@ -6,7 +6,6 @@ class Tag < ApplicationRecord
6
6
  optional: true
7
7
 
8
8
  has_many :taggings, dependent: :destroy
9
- has_many :taggables, through: :taggings
10
9
 
11
10
  validates :name, :key, presence: true
12
11
  validates :key, uniqueness: { scope: :category }
@@ -19,6 +18,12 @@ class Tag < ApplicationRecord
19
18
  scope :in_categories, ->(keys) { TagCategory.where(key: keys).ordered.map(&:tags).reduce(:+) }
20
19
  scope :in_category, ->(key) { TagCategory.find_by(key:)&.tags }
21
20
 
21
+ # The tagged records, of any model. Not a `has_many :through`: Rails can't go through the
22
+ # polymorphic `Tagging#taggable`.
23
+ def taggables
24
+ taggings.includes(:taggable).map(&:taggable)
25
+ end
26
+
22
27
  def to_s
23
28
  name
24
29
  end
@@ -13,7 +13,7 @@ module Admin
13
13
  end
14
14
 
15
15
  def destroy?
16
- user.privileged? && record.taggables.empty?
16
+ user.privileged? && record.taggings.empty?
17
17
  end
18
18
 
19
19
  def reorder?
@@ -16,7 +16,7 @@ module Admin
16
16
  relation_scope do |relation|
17
17
  next relation if user.privileged?
18
18
 
19
- relation.where(user:)
19
+ relation.none
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,24 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Page do
4
+ before { seed "cases/page_tree" }
5
+
6
+ it "slugs the title" do
7
+ expect(pages.team.slug).to eq("our-team")
8
+ end
9
+
10
+ it "builds the full path from the slugs of its ancestors, root excluded" do
11
+ expect(pages.team.full_path).to eq("company/our-team")
12
+ end
13
+
14
+ it "is found at its full path" do
15
+ expect(described_class.find_at_full_path("company/our-team")).to eq(pages.team)
16
+ end
17
+
18
+ it "keeps its former path when moved, so old URLs can be redirected" do
19
+ team = pages.team
20
+ team.update!(parent: pages.home)
21
+
22
+ expect(team.reload.paths.map(&:path)).to eq(["our-team", "company/our-team"])
23
+ end
24
+ end
@@ -0,0 +1,109 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe User do
4
+ subject(:user) { users.new }
5
+
6
+ it "is valid with an email and a password" do
7
+ expect(user).to be_valid
8
+ end
9
+
10
+ describe "validations" do
11
+ it "requires an email" do
12
+ user.email = nil
13
+
14
+ expect(user).to be_invalid
15
+ end
16
+
17
+ it "requires a well-formed email" do
18
+ user.email = "not an email"
19
+
20
+ expect(user).to be_invalid
21
+ end
22
+
23
+ it "requires a unique email" do
24
+ users.create(email: user.email)
25
+
26
+ expect(user).to be_invalid
27
+ end
28
+
29
+ it "requires a unique email whatever its case" do
30
+ users.create(email: "ada@example.com")
31
+ user.email = "Ada@Example.com"
32
+
33
+ expect(user).to be_invalid
34
+ end
35
+
36
+ %w[ada..lovelace@example.com .ada@example.com ada.@example.com ada@example..com ada@example].each do |email|
37
+ it "refuses an email mail providers would reject: #{email}" do
38
+ user.email = email
39
+
40
+ expect(user).to be_invalid
41
+ end
42
+ end
43
+
44
+ it "refuses a password made of spaces only" do
45
+ user.password = " " * 8
46
+
47
+ expect(user).to be_invalid
48
+ end
49
+
50
+ it "requires a password of at least 8 characters on creation" do
51
+ user.password = "short"
52
+
53
+ expect(user).to be_invalid
54
+ end
55
+
56
+ it "requires a password of at least 8 characters when changing it" do
57
+ user = users.create
58
+ user.password = "short"
59
+
60
+ expect(user).to be_invalid
61
+ end
62
+
63
+ it "does not require the password when changing something else" do
64
+ user = described_class.find(users.create.id)
65
+ user.name = "Ada"
66
+
67
+ expect(user).to be_valid
68
+ end
69
+ end
70
+
71
+ describe "email" do
72
+ it "is stored in lower case, without surrounding spaces" do
73
+ expect(users.create(email: " Ada@Example.com ").email).to eq("ada@example.com")
74
+ end
75
+
76
+ it "finds the user whatever its case" do
77
+ ada = users.create(email: "ada@example.com")
78
+
79
+ expect(described_class.find_by(email: "ADA@example.com")).to eq(ada)
80
+ end
81
+ end
82
+
83
+ describe "roles" do
84
+ it "defaults to guest" do
85
+ expect(user.role).to eq("guest")
86
+ end
87
+
88
+ it "treats admins and editors as privileged" do
89
+ expect(%w[admin editor guest].map { |role| users.new(role:).privileged? }).to eq([true, true, false])
90
+ end
91
+
92
+ it "scopes privileged users" do
93
+ admin, editor = users.create(role: :admin), users.create(role: :editor)
94
+ users.create(role: :reader)
95
+
96
+ expect(described_class.privileged).to contain_exactly(admin, editor)
97
+ end
98
+ end
99
+
100
+ describe "#to_s" do
101
+ it "reads the name" do
102
+ expect(users.new(name: "Ada").to_s).to eq("Ada")
103
+ end
104
+
105
+ it "falls back to the email" do
106
+ expect(users.new(email: "ada@example.com").to_s).to eq("ada@example.com")
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,17 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe Admin::ExternalLinkPolicy do
4
+ before { ExternalLink.create!(url: "https://example.com") }
5
+
6
+ it "shows privileged users every external link" do
7
+ policy = described_class.new(user: users.new(role: :editor))
8
+
9
+ expect(policy.apply_scope(ExternalLink.all, type: :active_record_relation)).to match_array(ExternalLink.all)
10
+ end
11
+
12
+ it "shows other roles no external link" do
13
+ policy = described_class.new(user: users.new(role: :reader))
14
+
15
+ expect(policy.apply_scope(ExternalLink.all, type: :active_record_relation)).to be_empty
16
+ end
17
+ end