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.
- checksums.yaml +4 -4
- data/.slim-lint.yml +0 -3
- data/AGENTS.md +9 -2
- data/CHANGELOG.md +73 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +23 -3
- data/Makefile +11 -9
- data/README.md +1 -1
- data/app/assets/icons/trek/chevron-down.svg +1 -0
- data/app/assets/icons/trek/chevron-up.svg +1 -0
- data/app/assets/icons/trek/external.svg +1 -0
- data/app/assets/icons/trek/theme-auto.svg +1 -0
- data/app/assets/icons/trek/theme-dark.svg +1 -0
- data/app/assets/icons/trek/theme-light.svg +1 -0
- data/app/assets/stylesheets/trek/base.css +13 -0
- data/app/components/trek/button_component/button_component.css +9 -0
- data/app/components/trek/button_component.rb +13 -1
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +1 -1
- data/app/components/trek/dashboard/tile_component.rb +14 -0
- data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
- data/app/components/trek/dialog_component/dialog_component.js +62 -2
- data/app/components/trek/dropdown_component/dropdown_component.css +69 -0
- data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
- data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
- data/app/components/trek/dropdown_component.rb +34 -0
- data/app/components/trek/form/group_component/group_component.css +34 -0
- data/app/components/trek/header_component/header_component.css +9 -4
- data/app/components/trek/header_component/header_component.html.slim +17 -10
- data/app/components/trek/header_component/header_component.js +29 -5
- data/app/components/trek/header_component.rb +24 -3
- data/app/components/trek/menu_component/menu_component.css +6 -0
- data/app/components/trek/menu_component.rb +29 -4
- data/app/components/trek/toaster/toast_component.rb +1 -1
- data/app/controllers/concerns/trek/authorization.rb +12 -0
- data/app/controllers/concerns/trek/filters.rb +18 -1
- data/app/helpers/trek/page_path_helper.rb +4 -3
- data/app/helpers/trek/theme_helper.rb +16 -1
- data/app/models/concerns/trek/phonable.rb +15 -3
- data/app/models/concerns/trek/sectionable.rb +5 -0
- data/app/models/concerns/trek/users/validations.rb +15 -6
- data/app/uploaders/audio_uploader.rb +11 -0
- data/app/uploaders/record_image_uploader.rb +3 -0
- data/config/locales/trek.en.yml +3 -2
- data/config/locales/trek.fr.yml +1 -0
- data/docs/.vitepress/config.mjs +20 -1
- data/docs/.vitepress/theme/style.css +36 -8
- data/docs/changelog.md +9 -0
- data/docs/customize.md +18 -2
- data/docs/deploy.md +7 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +8 -8
- data/docs/public/apple-touch-icon.png +0 -0
- data/docs/public/favicon.ico +0 -0
- data/docs/public/favicon.png +0 -0
- data/docs/public/trek-mark.webp +0 -0
- data/docs/reference/components.md +15 -0
- data/docs/reference/concerns/phonable.md +27 -1
- data/docs/reference/controllers.md +8 -3
- data/docs/reference/environment-variables.md +2 -0
- data/docs/reference/forms.md +2 -0
- data/docs/reference/generators/install.md +4 -1
- data/docs/reference/icons.md +1 -1
- data/docs/reference/models/tag.md +1 -1
- data/docs/reference/models/user.md +6 -4
- data/docs/reference/uploaders.md +31 -6
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
- data/lib/generators/trek/install/appsignal_generator.rb +102 -0
- data/lib/generators/trek/install/model_specs_generator.rb +64 -0
- data/lib/generators/trek/install/postmark_generator.rb +3 -0
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +1 -1
- data/lib/generators/trek/install/uploaders_generator.rb +2 -0
- data/lib/generators/trek/install_generator.rb +5 -0
- data/lib/generators/trek/taxonomies_generator.rb +7 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
- data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
- data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +7 -0
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
- data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
- data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
- data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
- data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
- data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
- data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
- data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
- data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
- data/lib/generators/trek/templates/models/tag.rb +6 -1
- data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
- data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
- data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
- data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
- data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
- data/lib/generators/trek/templates/spec/models/user_spec.rb +109 -0
- data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
- data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
- data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
- data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
- data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
- data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
- data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
- data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +78 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +51 -0
- data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
- data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
- data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
- data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
- data/lib/tasks/trek/attachments.rake +18 -0
- data/lib/trek/cached_attachments.rb +53 -0
- data/lib/trek/saved_attachments_reload.rb +44 -0
- data/lib/trek/version.rb +1 -1
- data/lib/trek.rb +4 -0
- data/package.json +1 -1
- data/postcss.config.js +36 -0
- data/trek.gemspec +6 -0
- metadata +89 -3
- 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 ScalingoGenerator < Rails::Generators::Base
|
|
8
|
-
BUILDPACK_URL = "https://github.com/etaminstudio/trek-buildpack
|
|
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
|
|
|
@@ -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")
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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,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
|
|
@@ -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
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|