trek 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.slim-lint.yml +0 -3
- data/AGENTS.md +8 -1
- data/CHANGELOG.md +62 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +20 -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 +66 -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 +29 -0
- data/app/components/trek/form/group_component/group_component.css +34 -0
- data/app/components/trek/header_component/header_component.css +10 -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 +20 -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/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 +1 -1
- 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/customize.md +18 -2
- data/docs/deploy.md +7 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +1 -1
- data/docs/reference/components.md +15 -0
- data/docs/reference/concerns/phonable.md +27 -1
- 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 +4 -2
- 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/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 +76 -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 +71 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +45 -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 +3 -0
- data/package.json +1 -1
- data/postcss.config.js +36 -0
- data/trek.gemspec +5 -0
- metadata +70 -3
- data/app/assets/icons/trek/theme.svg +0 -11
|
@@ -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,76 @@
|
|
|
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 password of at least 8 characters on creation" do
|
|
30
|
+
user.password = "short"
|
|
31
|
+
|
|
32
|
+
expect(user).to be_invalid
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "requires a password of at least 8 characters when changing it" do
|
|
36
|
+
user = users.create
|
|
37
|
+
user.password = "short"
|
|
38
|
+
|
|
39
|
+
expect(user).to be_invalid
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "does not require the password when changing something else" do
|
|
43
|
+
user = described_class.find(users.create.id)
|
|
44
|
+
user.name = "Ada"
|
|
45
|
+
|
|
46
|
+
expect(user).to be_valid
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "roles" do
|
|
51
|
+
it "defaults to guest" do
|
|
52
|
+
expect(user.role).to eq("guest")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "treats admins and editors as privileged" do
|
|
56
|
+
expect(%w[admin editor guest].map { |role| users.new(role:).privileged? }).to eq([true, true, false])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "scopes privileged users" do
|
|
60
|
+
admin, editor = users.create(role: :admin), users.create(role: :editor)
|
|
61
|
+
users.create(role: :reader)
|
|
62
|
+
|
|
63
|
+
expect(described_class.privileged).to contain_exactly(admin, editor)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe "#to_s" do
|
|
68
|
+
it "reads the name" do
|
|
69
|
+
expect(users.new(name: "Ada").to_s).to eq("Ada")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "falls back to the email" do
|
|
73
|
+
expect(users.new(email: "ada@example.com").to_s).to eq("ada@example.com")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::FragmentPolicy do
|
|
4
|
+
def policy(role, record = Fragment)
|
|
5
|
+
described_class.new(record, user: users.new(role:))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
%w[admin editor].each do |role|
|
|
9
|
+
it "lets #{role}s list, create, edit and delete fragments" do
|
|
10
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
%w[user reader guest].each do |role|
|
|
15
|
+
it "keeps #{role}s away from fragments" do
|
|
16
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "lets privileged users edit a fragment" do
|
|
21
|
+
expect(policy("editor").permitted_attributes).to contain_exactly(:content, :key, :namespace, :title)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "lets other roles edit nothing" do
|
|
25
|
+
expect(policy("reader").permitted_attributes).to be_empty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "shows other roles no fragment" do
|
|
29
|
+
Fragment.create!(key: "footer", namespace: "layout", title: "Footer")
|
|
30
|
+
|
|
31
|
+
expect(policy("reader").apply_scope(Fragment.all, type: :active_record_relation)).to be_empty
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::PagePolicy do
|
|
4
|
+
def policy(role, record = Page)
|
|
5
|
+
described_class.new(record, user: users.new(role:))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
%w[admin editor].each do |role|
|
|
9
|
+
it "lets #{role}s list, create, edit and delete pages" do
|
|
10
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
%w[user reader guest].each do |role|
|
|
15
|
+
it "keeps #{role}s away from pages" do
|
|
16
|
+
expect(%i[index? create? update? destroy?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "lets privileged users edit a page's content" do
|
|
21
|
+
expect(policy("editor").permitted_attributes).to include(:title, :slug, :description, :content)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "lets other roles edit nothing" do
|
|
25
|
+
expect(policy("reader").permitted_attributes).to be_empty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "shows privileged users every page" do
|
|
29
|
+
seed "cases/page_tree"
|
|
30
|
+
|
|
31
|
+
expect(policy("editor").apply_scope(Page.all, type: :active_record_relation)).to match_array(Page.all)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "shows other roles no page" do
|
|
35
|
+
seed "cases/page_tree"
|
|
36
|
+
|
|
37
|
+
expect(policy("reader").apply_scope(Page.all, type: :active_record_relation)).to be_empty
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::TagCategoryPolicy do
|
|
4
|
+
let(:category) { TagCategory.create!(name: "Topics", key: "topics") }
|
|
5
|
+
|
|
6
|
+
def policy(role, record = TagCategory)
|
|
7
|
+
described_class.new(record, user: users.new(role:))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
%w[admin editor].each do |role|
|
|
11
|
+
it "lets #{role}s list, create and edit tag categories" do
|
|
12
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
%w[user reader guest].each do |role|
|
|
17
|
+
it "keeps #{role}s away from tag categories" do
|
|
18
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "lets privileged users delete an empty category" do
|
|
23
|
+
expect(policy("editor", category).apply(:destroy?)).to be(true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "keeps a category with tags from being deleted" do
|
|
27
|
+
Tag.create!(name: "News", key: "news", category:)
|
|
28
|
+
|
|
29
|
+
expect(policy("admin", category.reload).apply(:destroy?)).to be(false)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::TagPolicy do
|
|
4
|
+
let(:tag) { Tag.create!(name: "News", key: "news") }
|
|
5
|
+
|
|
6
|
+
def policy(role, record = Tag)
|
|
7
|
+
described_class.new(record, user: users.new(role:))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
%w[admin editor].each do |role|
|
|
11
|
+
it "lets #{role}s list, create and edit tags" do
|
|
12
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(true))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
%w[user reader guest].each do |role|
|
|
17
|
+
it "keeps #{role}s away from tags" do
|
|
18
|
+
expect(%i[index? create? update?].map { |rule| policy(role).apply(rule) }).to all(be(false))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "lets privileged users delete an unused tag" do
|
|
23
|
+
expect(policy("editor", tag).apply(:destroy?)).to be(true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "keeps a tag in use from being deleted" do
|
|
27
|
+
allow(tag).to receive(:taggings).and_return([Tagging.new])
|
|
28
|
+
|
|
29
|
+
expect(policy("admin", tag).apply(:destroy?)).to be(false)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe Admin::UserPolicy do
|
|
4
|
+
let(:admin) { users.create(role: :admin) }
|
|
5
|
+
let(:editor) { users.create(role: :editor) }
|
|
6
|
+
|
|
7
|
+
def policy(user, record = User)
|
|
8
|
+
described_class.new(record, user:)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "lets admins list, create and delete users" do
|
|
12
|
+
expect(%i[index? create? destroy?].map { |rule| policy(admin).apply(rule) }).to all(be(true))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "lets admins edit any user" do
|
|
16
|
+
expect(policy(admin, editor).apply(:update?)).to be(true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "keeps editors away from the users list" do
|
|
20
|
+
expect(%i[index? create? destroy?].map { |rule| policy(editor).apply(rule) }).to all(be(false))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "lets editors see and edit themselves" do
|
|
24
|
+
expect(%i[show? update?].map { |rule| policy(editor, editor).apply(rule) }).to all(be(true))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "keeps editors away from other users" do
|
|
28
|
+
expect(%i[show? update?].map { |rule| policy(editor, admin).apply(rule) }).to all(be(false))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "lets admins change a user's role" do
|
|
32
|
+
expect(policy(admin, editor).permitted_attributes).to include(:role)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "lets admins invite the users they create" do
|
|
36
|
+
expect(policy(admin, User).permitted_attributes).to include(:send_invite)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "lets editors change their own details but not their role" do
|
|
40
|
+
expect(policy(editor, editor).permitted_attributes).to contain_exactly(:name, :email, :locale, :theme)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "lets editors change nothing on another user" do
|
|
44
|
+
expect(policy(editor, admin).permitted_attributes).to be_empty
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "shows admins the privileged users only" do
|
|
48
|
+
reader = users.create(role: :reader)
|
|
49
|
+
|
|
50
|
+
expect(described_class.new(user: admin).apply_scope(User.all, type: :active_record_relation)).not_to include(reader)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "shows editors no user" do
|
|
54
|
+
admin
|
|
55
|
+
|
|
56
|
+
expect(described_class.new(user: editor).apply_scope(User.all, type: :active_record_relation)).to be_empty
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin fragments", type: :request do
|
|
4
|
+
let!(:footer) { fragments.create(key: "footer", namespace: "layout", title: "Footer") }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
editor = users.create(role: :editor)
|
|
8
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "lists the fragments" do
|
|
12
|
+
get admin_fragments_path
|
|
13
|
+
|
|
14
|
+
expect(response.body).to include("footer")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "renders the new fragment form" do
|
|
18
|
+
get new_admin_fragment_path
|
|
19
|
+
|
|
20
|
+
expect(response).to have_http_status(:ok)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "creates a fragment" do
|
|
24
|
+
params = { fragment: { key: "tagline", namespace: "home", title: "Tagline" } }
|
|
25
|
+
|
|
26
|
+
expect { post admin_fragments_path, params: }.to change(Fragment, :count).by(1)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "refuses a key already used in the namespace" do
|
|
30
|
+
post admin_fragments_path, params: { fragment: { key: "footer", namespace: "layout", title: "Another footer" } }
|
|
31
|
+
|
|
32
|
+
expect(response).to have_http_status(:unprocessable_entity)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "renders the edit form" do
|
|
36
|
+
get edit_admin_fragment_path(footer)
|
|
37
|
+
|
|
38
|
+
expect(response).to have_http_status(:ok)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "updates a fragment" do
|
|
42
|
+
patch admin_fragment_path(footer), params: { fragment: { title: "Site footer" } }
|
|
43
|
+
|
|
44
|
+
expect(footer.reload.title).to eq("Site footer")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "deletes a fragment" do
|
|
48
|
+
expect { delete admin_fragment_path(footer) }.to change(Fragment, :count).by(-1)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Admin pages", type: :request do
|
|
4
|
+
before do
|
|
5
|
+
seed "cases/page_tree"
|
|
6
|
+
editor = users.create(role: :editor)
|
|
7
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "lists the pages" do
|
|
11
|
+
get admin_pages_path
|
|
12
|
+
|
|
13
|
+
expect(response.body).to include("Company").and include("Our team")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "renders the new page form" do
|
|
17
|
+
get new_admin_page_path
|
|
18
|
+
|
|
19
|
+
expect(response).to have_http_status(:ok)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "creates a page" do
|
|
23
|
+
expect { post admin_pages_path, params: { page: { title: "Contact" } } }.to change(Page, :count).by(1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "refuses a page without a title" do
|
|
27
|
+
post admin_pages_path, params: { page: { title: "" } }
|
|
28
|
+
|
|
29
|
+
expect(response).to have_http_status(:unprocessable_entity)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "renders the edit form" do
|
|
33
|
+
get edit_admin_page_path(pages.team)
|
|
34
|
+
|
|
35
|
+
expect(response).to have_http_status(:ok)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "updates a page" do
|
|
39
|
+
patch admin_page_path(pages.team), params: { page: { title: "The team" } }
|
|
40
|
+
|
|
41
|
+
expect(pages.team.title).to eq("The team")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "deletes a page" do
|
|
45
|
+
expect { delete admin_page_path(pages.team) }.to change(Page, :count).by(-1)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "rails_helper"
|
|
2
|
+
|
|
3
|
+
# The endpoints the admin's image and sound fields upload to (`Trek::Form::ImageFieldComponent`,
|
|
4
|
+
# `Trek::Form::SoundFieldComponent`).
|
|
5
|
+
RSpec.describe "Admin uploads", type: :request do
|
|
6
|
+
before do
|
|
7
|
+
editor = users.create(role: :editor)
|
|
8
|
+
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "stores an uploaded image in the cache and returns its data" do
|
|
12
|
+
post "/admin/upload/image", params: { file: fixture_file_upload("image.png", "image/png") }
|
|
13
|
+
|
|
14
|
+
expect(response.parsed_body["data"]).to include("id", "storage" => "cache")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "stores an uploaded sound in the cache and returns its data" do
|
|
18
|
+
post "/admin/upload/audio", params: { file: fixture_file_upload("sound.mp3", "audio/mpeg") }
|
|
19
|
+
|
|
20
|
+
expect(response.parsed_body["data"]).to include("id", "storage" => "cache")
|
|
21
|
+
end
|
|
22
|
+
end
|