plum-cms 0.1.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 +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/images/plum-mark.svg +7 -0
- data/app/assets/images/table-needs-logo.svg +83 -0
- data/app/assets/javascripts/plum/application.js +15 -0
- data/app/assets/stylesheets/application.css +10 -0
- data/app/assets/stylesheets/plum/control_panel.css +2 -0
- data/app/assets/tailwind/application.css +47 -0
- data/app/controllers/plum/application_controller.rb +34 -0
- data/app/controllers/plum/cp/assets_controller.rb +84 -0
- data/app/controllers/plum/cp/base_controller.rb +45 -0
- data/app/controllers/plum/cp/content_types_controller.rb +59 -0
- data/app/controllers/plum/cp/dashboard_controller.rb +10 -0
- data/app/controllers/plum/cp/entries_controller.rb +240 -0
- data/app/controllers/plum/cp/form_definitions_controller.rb +105 -0
- data/app/controllers/plum/cp/form_submissions_controller.rb +26 -0
- data/app/controllers/plum/cp/globals_controller.rb +84 -0
- data/app/controllers/plum/cp/nav_items_controller.rb +68 -0
- data/app/controllers/plum/cp/nav_menus_controller.rb +56 -0
- data/app/controllers/plum/cp/site_settings_controller.rb +87 -0
- data/app/controllers/plum/cp/taxonomies_controller.rb +56 -0
- data/app/controllers/plum/cp/terms_controller.rb +55 -0
- data/app/controllers/plum/cp/theme_previews_controller.rb +27 -0
- data/app/controllers/plum/cp/themes_controller.rb +98 -0
- data/app/controllers/plum/form_submissions_controller.rb +44 -0
- data/app/controllers/plum/pages_controller.rb +161 -0
- data/app/controllers/plum/sessions_controller.rb +25 -0
- data/app/controllers/plum/theme_assets_controller.rb +14 -0
- data/app/helpers/application_helper.rb +2 -0
- data/app/helpers/markdown_helper.rb +21 -0
- data/app/helpers/plum/cp/assets_helper.rb +21 -0
- data/app/helpers/plum/cp/theme_settings_helper.rb +26 -0
- data/app/javascript/application.js +18 -0
- data/app/javascript/controllers/application.js +9 -0
- data/app/javascript/controllers/index.js +4 -0
- data/app/javascript/controllers/plum/blocks_editor_controller.js +275 -0
- data/app/javascript/controllers/plum/blueprint_controller.js +120 -0
- data/app/javascript/controllers/plum/form_fields_controller.js +84 -0
- data/app/javascript/controllers/plum/image_picker_controller.js +143 -0
- data/app/javascript/controllers/plum/theme_settings_controller.js +17 -0
- data/app/models/plum/application_record.rb +5 -0
- data/app/models/plum/asset.rb +69 -0
- data/app/models/plum/content_type.rb +35 -0
- data/app/models/plum/entry.rb +67 -0
- data/app/models/plum/entry_term.rb +8 -0
- data/app/models/plum/form_definition.rb +68 -0
- data/app/models/plum/form_submission.rb +76 -0
- data/app/models/plum/global.rb +24 -0
- data/app/models/plum/nav_item.rb +58 -0
- data/app/models/plum/nav_menu.rb +24 -0
- data/app/models/plum/site.rb +112 -0
- data/app/models/plum/site_scoped.rb +17 -0
- data/app/models/plum/site_setting.rb +35 -0
- data/app/models/plum/taxonomy.rb +24 -0
- data/app/models/plum/term.rb +23 -0
- data/app/models/plum/user.rb +16 -0
- data/app/services/plum/base_blocks.rb +161 -0
- data/app/services/plum/block_editor_config.rb +65 -0
- data/app/services/plum/block_library.rb +31 -0
- data/app/services/plum/builder_renderer.rb +63 -0
- data/app/services/plum/field_expander.rb +78 -0
- data/app/services/plum/form_renderer.rb +110 -0
- data/app/services/plum/liquid_context.rb +335 -0
- data/app/services/plum/liquid_filters.rb +58 -0
- data/app/services/plum/liquid_renderer.rb +49 -0
- data/app/services/plum/theme.rb +160 -0
- data/app/services/plum/theme_asset_path.rb +31 -0
- data/app/services/plum/theme_package_installer.rb +287 -0
- data/app/services/plum/theme_registry.rb +63 -0
- data/app/services/plum/theme_resolver.rb +55 -0
- data/app/services/plum/theme_settings_params.rb +54 -0
- data/app/themes/bagel-boy/assets/screenshot.svg +92 -0
- data/app/themes/bagel-boy/assets/theme.css +184 -0
- data/app/themes/bagel-boy/blocks/hours.liquid +4 -0
- data/app/themes/bagel-boy/blocks/menu_item.liquid +7 -0
- data/app/themes/bagel-boy/layouts/base.liquid +64 -0
- data/app/themes/bagel-boy/templates/collections/posts.liquid +32 -0
- data/app/themes/bagel-boy/templates/entries/_default.liquid +13 -0
- data/app/themes/bagel-boy/templates/entries/landing.liquid +4 -0
- data/app/themes/bagel-boy/templates/entries/pages.liquid +18 -0
- data/app/themes/bagel-boy/templates/entries/posts.liquid +8 -0
- data/app/themes/bagel-boy/templates/index.liquid +10 -0
- data/app/themes/bagel-boy/templates/search.liquid +28 -0
- data/app/themes/bagel-boy/theme.yml +47 -0
- data/app/themes/bagel-shop/assets/screenshot.svg +28 -0
- data/app/themes/bagel-shop/assets/theme.css +7 -0
- data/app/themes/bagel-shop/layouts/base.liquid +507 -0
- data/app/themes/bagel-shop/templates/entries/_default.liquid +15 -0
- data/app/themes/bagel-shop/templates/entries/pages.liquid +12 -0
- data/app/themes/bagel-shop/templates/entries/posts.liquid +19 -0
- data/app/themes/bagel-shop/templates/index.liquid +55 -0
- data/app/themes/bagel-shop/theme.yml +30 -0
- data/app/themes/default/assets/screenshot.svg +24 -0
- data/app/themes/default/assets/theme.css +89 -0
- data/app/themes/default/layouts/base.liquid +117 -0
- data/app/themes/default/templates/collections/_default.liquid +16 -0
- data/app/themes/default/templates/collections/posts.liquid +27 -0
- data/app/themes/default/templates/entries/_default.liquid +26 -0
- data/app/themes/default/templates/entries/landing.liquid +5 -0
- data/app/themes/default/templates/entries/pages.liquid +17 -0
- data/app/themes/default/templates/entries/posts.liquid +20 -0
- data/app/themes/default/templates/index.liquid +22 -0
- data/app/themes/default/templates/search.liquid +28 -0
- data/app/themes/default/templates/taxonomies/_default.liquid +25 -0
- data/app/themes/default/templates/taxonomies/_default_index.liquid +14 -0
- data/app/themes/default/theme.yml +23 -0
- data/app/views/layouts/plum/cp.html.erb +138 -0
- data/app/views/layouts/plum/session.html.erb +43 -0
- data/app/views/plum/cp/assets/_form.html.erb +44 -0
- data/app/views/plum/cp/assets/edit.html.erb +14 -0
- data/app/views/plum/cp/assets/index.html.erb +52 -0
- data/app/views/plum/cp/assets/new.html.erb +9 -0
- data/app/views/plum/cp/content_types/_form.html.erb +113 -0
- data/app/views/plum/cp/content_types/edit.html.erb +8 -0
- data/app/views/plum/cp/content_types/index.html.erb +48 -0
- data/app/views/plum/cp/content_types/new.html.erb +5 -0
- data/app/views/plum/cp/content_types/show.html.erb +59 -0
- data/app/views/plum/cp/dashboard/show.html.erb +50 -0
- data/app/views/plum/cp/entries/_form.html.erb +196 -0
- data/app/views/plum/cp/entries/edit.html.erb +9 -0
- data/app/views/plum/cp/entries/index.html.erb +51 -0
- data/app/views/plum/cp/entries/new.html.erb +5 -0
- data/app/views/plum/cp/form_definitions/_form.html.erb +90 -0
- data/app/views/plum/cp/form_definitions/edit.html.erb +11 -0
- data/app/views/plum/cp/form_definitions/index.html.erb +43 -0
- data/app/views/plum/cp/form_definitions/new.html.erb +5 -0
- data/app/views/plum/cp/form_definitions/show.html.erb +70 -0
- data/app/views/plum/cp/form_submissions/show.html.erb +20 -0
- data/app/views/plum/cp/globals/_form.html.erb +44 -0
- data/app/views/plum/cp/globals/edit.html.erb +11 -0
- data/app/views/plum/cp/globals/index.html.erb +41 -0
- data/app/views/plum/cp/globals/new.html.erb +5 -0
- data/app/views/plum/cp/globals/show.html.erb +14 -0
- data/app/views/plum/cp/nav_items/_form.html.erb +56 -0
- data/app/views/plum/cp/nav_items/edit.html.erb +11 -0
- data/app/views/plum/cp/nav_items/new.html.erb +6 -0
- data/app/views/plum/cp/nav_menus/_form.html.erb +33 -0
- data/app/views/plum/cp/nav_menus/_nav_item.html.erb +24 -0
- data/app/views/plum/cp/nav_menus/edit.html.erb +11 -0
- data/app/views/plum/cp/nav_menus/index.html.erb +41 -0
- data/app/views/plum/cp/nav_menus/new.html.erb +5 -0
- data/app/views/plum/cp/nav_menus/show.html.erb +24 -0
- data/app/views/plum/cp/shared/_icon.html.erb +39 -0
- data/app/views/plum/cp/shared/_image_picker.html.erb +38 -0
- data/app/views/plum/cp/site_settings/edit.html.erb +145 -0
- data/app/views/plum/cp/site_settings/show.html.erb +62 -0
- data/app/views/plum/cp/taxonomies/_form.html.erb +38 -0
- data/app/views/plum/cp/taxonomies/edit.html.erb +2 -0
- data/app/views/plum/cp/taxonomies/index.html.erb +38 -0
- data/app/views/plum/cp/taxonomies/new.html.erb +2 -0
- data/app/views/plum/cp/taxonomies/show.html.erb +46 -0
- data/app/views/plum/cp/terms/_form.html.erb +30 -0
- data/app/views/plum/cp/terms/edit.html.erb +2 -0
- data/app/views/plum/cp/terms/new.html.erb +2 -0
- data/app/views/plum/cp/themes/index.html.erb +160 -0
- data/app/views/plum/form_mailer/submission_notification.html.erb +13 -0
- data/app/views/plum/form_mailer/submission_notification.text.erb +7 -0
- data/app/views/plum/sessions/new.html.erb +20 -0
- data/compat/plum-cms.rb +1 -0
- data/config/locales/en.yml +31 -0
- data/config/plum_importmap.rb +4 -0
- data/config/plum_routes.rb +34 -0
- data/db/engine_migrate/20260101000001_create_plum_tables.rb +154 -0
- data/lib/generators/plum/install/install_generator.rb +54 -0
- data/lib/generators/plum/install/templates/plum_initializer.rb +37 -0
- data/lib/plum/configuration.rb +88 -0
- data/lib/plum/content_source.rb +45 -0
- data/lib/plum/content_source_context.rb +30 -0
- data/lib/plum/content_source_registry.rb +120 -0
- data/lib/plum/engine.rb +30 -0
- data/lib/plum/liquid_tags/form_tag.rb +17 -0
- data/lib/plum/version.rb +7 -0
- data/lib/plum.rb +11 -0
- data/vendor/javascript/lexxy.js +11859 -0
- metadata +405 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class AssetsController < BaseController
|
|
4
|
+
include Plum::Cp::AssetsHelper
|
|
5
|
+
|
|
6
|
+
before_action :set_asset, only: [ :edit, :update, :destroy ]
|
|
7
|
+
before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy ]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@assets = asset_scope.order(created_at: :desc)
|
|
11
|
+
@asset = asset_scope.build
|
|
12
|
+
|
|
13
|
+
respond_to do |format|
|
|
14
|
+
format.html
|
|
15
|
+
format.json { render json: @assets.map { |asset| asset_json(asset) } }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def new
|
|
20
|
+
@asset = asset_scope.build
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create
|
|
24
|
+
@asset = asset_scope.build(asset_params)
|
|
25
|
+
|
|
26
|
+
if @asset.save
|
|
27
|
+
respond_to do |format|
|
|
28
|
+
format.html { redirect_to cp_assets_path, notice: "Asset uploaded" }
|
|
29
|
+
format.json { render json: asset_json(@asset), status: :created }
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
respond_to do |format|
|
|
33
|
+
format.html do
|
|
34
|
+
@assets = asset_scope.order(created_at: :desc)
|
|
35
|
+
render :index, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
format.json { render json: { errors: @asset.errors.full_messages }, status: :unprocessable_entity }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def edit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update
|
|
46
|
+
if @asset.update(asset_params)
|
|
47
|
+
redirect_to cp_assets_path, notice: "Asset updated"
|
|
48
|
+
else
|
|
49
|
+
render :edit, status: :unprocessable_entity
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def destroy
|
|
54
|
+
@asset.destroy
|
|
55
|
+
redirect_to cp_assets_path, notice: "Asset deleted"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def asset_scope
|
|
61
|
+
current_site.assets.with_attached_file
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def set_asset
|
|
65
|
+
@asset = asset_scope.find(params[:id])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def asset_params
|
|
69
|
+
params.require(:asset).permit(:file, :alt_text, :caption, :folder)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def asset_json(asset)
|
|
73
|
+
{
|
|
74
|
+
id: asset.id,
|
|
75
|
+
url: asset.url,
|
|
76
|
+
filename: asset.filename,
|
|
77
|
+
folder: asset.folder.to_s,
|
|
78
|
+
alt_text: asset.alt_text.to_s,
|
|
79
|
+
label: plum_asset_label(asset)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class BaseController < Plum::ApplicationController
|
|
4
|
+
layout "plum/cp"
|
|
5
|
+
helper AssetsHelper
|
|
6
|
+
helper ThemeSettingsHelper
|
|
7
|
+
|
|
8
|
+
before_action :require_login
|
|
9
|
+
helper_method :cp_prefix
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def cp_prefix
|
|
14
|
+
@cp_prefix ||= cp_root_path.chomp("/")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def require_login
|
|
18
|
+
return if Plum.authorized?(self)
|
|
19
|
+
|
|
20
|
+
if Plum.configuration.authorize_with == :plum
|
|
21
|
+
redirect_to login_path, alert: "Please log in to continue"
|
|
22
|
+
else
|
|
23
|
+
head :forbidden
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def require_role(*roles)
|
|
28
|
+
return unless Plum.configuration.authorize_with == :plum
|
|
29
|
+
|
|
30
|
+
user = Plum.current_user(self)
|
|
31
|
+
return if user&.respond_to?(:role) && roles.map(&:to_s).include?(user.role.to_s)
|
|
32
|
+
|
|
33
|
+
redirect_to cp_root_path, alert: "You don't have permission to do that"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def require_editor
|
|
37
|
+
require_role(:editor, :admin)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def require_admin
|
|
41
|
+
require_role(:admin)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class ContentTypesController < BaseController
|
|
4
|
+
before_action :set_content_type, only: [ :show, :edit, :update, :destroy ]
|
|
5
|
+
before_action :require_admin, only: [ :new, :create, :edit, :update, :destroy ]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@content_types = ContentType.for_site(current_site)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
@entries = @content_type.entries.order(updated_at: :desc)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@content_type = current_site.content_types.build
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@content_type = current_site.content_types.build(content_type_params)
|
|
21
|
+
if @content_type.save
|
|
22
|
+
redirect_to cp_content_type_path(@content_type), notice: "Content type created"
|
|
23
|
+
else
|
|
24
|
+
render :new, status: :unprocessable_entity
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def edit
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def update
|
|
32
|
+
if @content_type.update(content_type_params)
|
|
33
|
+
redirect_to cp_content_type_path(@content_type), notice: "Content type updated"
|
|
34
|
+
else
|
|
35
|
+
render :edit, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def destroy
|
|
40
|
+
@content_type.destroy
|
|
41
|
+
redirect_to cp_content_types_path, notice: "Content type deleted"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def set_content_type
|
|
47
|
+
@content_type = current_site.content_types.find(params[:id])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def content_type_params
|
|
51
|
+
permitted = params.require(:content_type).permit(:name, :handle, :singleton, :icon, :blueprint)
|
|
52
|
+
if permitted[:blueprint].is_a?(String)
|
|
53
|
+
permitted[:blueprint] = JSON.parse(permitted[:blueprint]) rescue {}
|
|
54
|
+
end
|
|
55
|
+
permitted
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class DashboardController < BaseController
|
|
4
|
+
def show
|
|
5
|
+
@content_types = ContentType.for_site(current_site)
|
|
6
|
+
@recent_entries = Entry.for_site(current_site).includes(:content_type).order(updated_at: :desc).limit(10)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class EntriesController < BaseController
|
|
4
|
+
ALLOWED_RICH_TEXT_TAGS = %w[
|
|
5
|
+
a action-text-attachment blockquote br code em figcaption figure
|
|
6
|
+
h1 h2 h3 hr img li ol p pre s strong table tbody td th thead tr ul
|
|
7
|
+
].freeze
|
|
8
|
+
ALLOWED_RICH_TEXT_ATTRIBUTES = %w[
|
|
9
|
+
alt caption content-type filename filesize height href presentation
|
|
10
|
+
rel sgid src target title url width
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
before_action :set_content_type
|
|
14
|
+
before_action :set_entry, only: [ :edit, :update, :destroy ]
|
|
15
|
+
before_action :set_form_collections, only: [ :new, :create, :edit, :update ]
|
|
16
|
+
before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy ]
|
|
17
|
+
|
|
18
|
+
def index
|
|
19
|
+
@entries = @content_type.entries.order(updated_at: :desc)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def new
|
|
23
|
+
@entry = @content_type.entries.build(status: :draft, site: current_site)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create
|
|
27
|
+
@entry = @content_type.entries.build(entry_params)
|
|
28
|
+
@entry.site = current_site
|
|
29
|
+
@entry.author = current_user if current_user.is_a?(Plum::User)
|
|
30
|
+
|
|
31
|
+
if save_entry(@entry)
|
|
32
|
+
redirect_to edit_cp_content_type_entry_path(@content_type, @entry), notice: "Entry created"
|
|
33
|
+
else
|
|
34
|
+
render :new, status: :unprocessable_entity
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def edit
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update
|
|
42
|
+
@entry.assign_attributes(entry_params)
|
|
43
|
+
|
|
44
|
+
if save_entry(@entry)
|
|
45
|
+
redirect_to edit_cp_content_type_entry_path(@content_type, @entry), notice: "Entry updated"
|
|
46
|
+
else
|
|
47
|
+
render :edit, status: :unprocessable_entity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def destroy
|
|
52
|
+
@entry.destroy
|
|
53
|
+
redirect_to cp_content_type_entries_path(@content_type), notice: "Entry deleted"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def set_content_type
|
|
59
|
+
@content_type = current_site.content_types.find(params[:content_type_id])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_entry
|
|
63
|
+
@entry = @content_type.entries.find(params[:id])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def set_form_collections
|
|
67
|
+
@assets = current_site.assets.with_attached_file.order(created_at: :desc)
|
|
68
|
+
@theme = current_site.theme
|
|
69
|
+
@relationship_entries_by_field = relationship_fields.each_with_object({}) do |field, entries_by_field|
|
|
70
|
+
entries_by_field[field["handle"].to_s] = relationship_entry_scope(@entry, field).order(:title)
|
|
71
|
+
end
|
|
72
|
+
@taxonomy_terms_by_field = taxonomy_fields.each_with_object({}) do |field, terms_by_field|
|
|
73
|
+
taxonomy = current_site.taxonomies.find_by(handle: field["taxonomy"].to_s)
|
|
74
|
+
terms_by_field[field["handle"].to_s] = taxonomy ? taxonomy.terms.ordered : []
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def entry_params
|
|
79
|
+
permitted = params.require(:entry).permit(:title, :slug, :status, :published_at)
|
|
80
|
+
if params[:entry][:data].is_a?(ActionController::Parameters)
|
|
81
|
+
permitted[:data] = params[:entry][:data].permit!.to_h
|
|
82
|
+
end
|
|
83
|
+
permitted
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def save_entry(entry)
|
|
87
|
+
return false unless entry.valid?
|
|
88
|
+
return false unless attach_uploaded_image_fields(entry)
|
|
89
|
+
|
|
90
|
+
entry.save.tap { |saved| sync_entry_terms(entry) if saved }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def sync_entry_terms(entry)
|
|
94
|
+
submitted_ids = Array(params.dig(:entry, :term_ids)).map(&:to_i).select(&:positive?)
|
|
95
|
+
valid_ids = Plum::Term.where(id: submitted_ids, site: current_site).pluck(:id)
|
|
96
|
+
entry.term_ids = valid_ids
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attach_uploaded_image_fields(entry)
|
|
100
|
+
image_uploads = params.dig(:entry, :image_uploads)
|
|
101
|
+
unless image_uploads.respond_to?(:[])
|
|
102
|
+
return normalize_entry_data_fields(entry)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
image_fields.each do |field|
|
|
106
|
+
handle = field["handle"].to_s
|
|
107
|
+
upload = image_uploads[handle]
|
|
108
|
+
next if upload.blank?
|
|
109
|
+
|
|
110
|
+
asset = current_site.assets.build(
|
|
111
|
+
file: upload,
|
|
112
|
+
alt_text: default_asset_alt_text(entry, field),
|
|
113
|
+
folder: field["folder"].presence || @content_type.handle
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
unless asset.save
|
|
117
|
+
entry.errors.add(:base, "#{field_label(field)} #{asset.errors.full_messages.to_sentence}")
|
|
118
|
+
return false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
entry.data ||= {}
|
|
122
|
+
entry.data[handle] = asset.id
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
normalize_entry_data_fields(entry)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def normalize_entry_data_fields(entry)
|
|
129
|
+
entry.data ||= {}
|
|
130
|
+
|
|
131
|
+
@content_type.fields.each do |field|
|
|
132
|
+
handle = field["handle"].to_s
|
|
133
|
+
next if handle.blank?
|
|
134
|
+
|
|
135
|
+
value = entry.data[handle]
|
|
136
|
+
entry.data[handle] = normalized_field_value(entry, field, value)
|
|
137
|
+
return false if entry.errors.any?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def normalized_field_value(entry, field, value)
|
|
144
|
+
case field["type"]
|
|
145
|
+
when "boolean"
|
|
146
|
+
ActiveModel::Type::Boolean.new.cast(value)
|
|
147
|
+
when "checkboxes"
|
|
148
|
+
Array(value).select(&:present?)
|
|
149
|
+
when "image"
|
|
150
|
+
value.present? ? value.to_i : nil
|
|
151
|
+
when "rich_text"
|
|
152
|
+
value
|
|
153
|
+
when "relationship"
|
|
154
|
+
normalized_relationship_value(entry, field, value)
|
|
155
|
+
when "blocks"
|
|
156
|
+
normalized_blocks_value(value)
|
|
157
|
+
else
|
|
158
|
+
value
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def normalized_relationship_value(entry, field, value)
|
|
163
|
+
return nil if value.blank?
|
|
164
|
+
|
|
165
|
+
related_entry = relationship_entry_scope(entry, field).find_by(id: value.to_i)
|
|
166
|
+
return related_entry.id if related_entry
|
|
167
|
+
|
|
168
|
+
entry.errors.add(:base, "#{field_label(field)} is not a valid entry")
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# A blocks field is submitted as a JSON string from the block editor. We
|
|
173
|
+
# parse it and normalize each block instance to { id, type, fields }.
|
|
174
|
+
def normalized_blocks_value(value)
|
|
175
|
+
parsed = value.is_a?(String) ? safe_parse_blocks(value) : value
|
|
176
|
+
return [] unless parsed.is_a?(Array)
|
|
177
|
+
|
|
178
|
+
parsed.filter_map do |block|
|
|
179
|
+
block = block.to_h if block.respond_to?(:to_h)
|
|
180
|
+
next unless block.is_a?(Hash)
|
|
181
|
+
|
|
182
|
+
type = block["type"].to_s
|
|
183
|
+
next if type.blank?
|
|
184
|
+
|
|
185
|
+
fields = block["fields"]
|
|
186
|
+
fields = fields.to_h if fields.respond_to?(:to_h)
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
"id" => block["id"].presence || SecureRandom.uuid,
|
|
190
|
+
"type" => type,
|
|
191
|
+
"fields" => fields.is_a?(Hash) ? fields : {}
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def safe_parse_blocks(value)
|
|
197
|
+
JSON.parse(value)
|
|
198
|
+
rescue JSON::ParserError
|
|
199
|
+
[]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def sanitized_rich_text(value)
|
|
203
|
+
Rails::HTML5::SafeListSanitizer.new.sanitize(
|
|
204
|
+
value.to_s,
|
|
205
|
+
tags: ALLOWED_RICH_TEXT_TAGS,
|
|
206
|
+
attributes: ALLOWED_RICH_TEXT_ATTRIBUTES
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def image_fields
|
|
211
|
+
@content_type.fields.select { |field| field["type"] == "image" && field["handle"].present? }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def relationship_fields
|
|
215
|
+
@content_type.fields.select { |field| field["type"] == "relationship" && field["handle"].present? }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def taxonomy_fields
|
|
219
|
+
@content_type.fields.select { |field| field["type"] == "taxonomy" && field["handle"].present? }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def relationship_entry_scope(entry, field)
|
|
223
|
+
site = entry&.site || current_site
|
|
224
|
+
scope = Entry.for_site(site).includes(:content_type)
|
|
225
|
+
target_handle = field["content_type"].to_s.strip.presence
|
|
226
|
+
return scope unless target_handle
|
|
227
|
+
|
|
228
|
+
scope.joins(:content_type).where(ContentType.table_name => { handle: target_handle })
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def field_label(field)
|
|
232
|
+
field["label"].presence || field["handle"].to_s.humanize
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def default_asset_alt_text(entry, field)
|
|
236
|
+
field["label"].present? ? "#{entry.title} #{field['label']}" : entry.title
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class FormDefinitionsController < BaseController
|
|
4
|
+
helper_method :form_fields_json
|
|
5
|
+
|
|
6
|
+
before_action :set_form_definition, only: [ :show, :edit, :update, :destroy ]
|
|
7
|
+
before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy ]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@form_definitions = current_site.form_definitions.includes(:form_submissions).order(:name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
@submissions = @form_definition.form_submissions.order(created_at: :desc)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def new
|
|
18
|
+
@form_definition = current_site.form_definitions.build(fields: [])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
@form_definition = current_site.form_definitions.build(form_definition_params)
|
|
23
|
+
|
|
24
|
+
if assign_fields(@form_definition) && @form_definition.save
|
|
25
|
+
redirect_to cp_form_definition_path(@form_definition), notice: "Form created"
|
|
26
|
+
else
|
|
27
|
+
render :new, status: :unprocessable_entity
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def edit
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update
|
|
35
|
+
@form_definition.assign_attributes(form_definition_params)
|
|
36
|
+
|
|
37
|
+
if assign_fields(@form_definition) && @form_definition.save
|
|
38
|
+
redirect_to cp_form_definition_path(@form_definition), notice: "Form updated"
|
|
39
|
+
else
|
|
40
|
+
render :edit, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def destroy
|
|
45
|
+
@form_definition.destroy
|
|
46
|
+
redirect_to cp_form_definitions_path, notice: "Form deleted"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def set_form_definition
|
|
52
|
+
@form_definition = current_site.form_definitions.find(params[:id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def form_definition_params
|
|
56
|
+
params.require(:form_definition).permit(:name, :handle, :notification_email)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def assign_fields(form_definition)
|
|
60
|
+
@fields_json = params.dig(:form_definition, :fields_json)
|
|
61
|
+
form_definition.fields = normalized_fields(@fields_json)
|
|
62
|
+
true
|
|
63
|
+
rescue JSON::ParserError
|
|
64
|
+
form_definition.errors.add(:fields, "must be valid JSON")
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def normalized_fields(raw_json)
|
|
69
|
+
parsed = JSON.parse(raw_json.presence || '{"fields":[]}')
|
|
70
|
+
fields = parsed.is_a?(Hash) ? parsed["fields"] : parsed
|
|
71
|
+
Array(fields).filter_map { |field| normalized_field(field) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def normalized_field(field)
|
|
75
|
+
return unless field.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
handle = field["handle"].to_s.strip
|
|
78
|
+
return if handle.blank?
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
"handle" => handle,
|
|
82
|
+
"type" => normalized_field_type(field["type"]),
|
|
83
|
+
"label" => field["label"].to_s.strip.presence || handle.humanize,
|
|
84
|
+
"required" => ActiveModel::Type::Boolean.new.cast(field["required"]),
|
|
85
|
+
"options" => normalized_options(field["options"])
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalized_field_type(type)
|
|
90
|
+
type = type.to_s
|
|
91
|
+
FormDefinition::FIELD_TYPES.include?(type) ? type : "text"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalized_options(options)
|
|
95
|
+
Array(options).flat_map { |value| value.to_s.split(",") }.map(&:strip).reject(&:blank?)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def form_fields_json(form_definition)
|
|
99
|
+
return @fields_json if defined?(@fields_json) && @fields_json.present?
|
|
100
|
+
|
|
101
|
+
JSON.pretty_generate({ fields: form_definition.form_fields })
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class FormSubmissionsController < BaseController
|
|
4
|
+
before_action :set_form_definition
|
|
5
|
+
before_action :set_form_submission
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def destroy
|
|
11
|
+
@form_submission.destroy
|
|
12
|
+
redirect_to cp_form_definition_path(@form_definition), notice: "Submission deleted"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_form_definition
|
|
18
|
+
@form_definition = current_site.form_definitions.find(params[:form_definition_id])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_form_submission
|
|
22
|
+
@form_submission = @form_definition.form_submissions.find(params[:id])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
module Cp
|
|
3
|
+
class GlobalsController < BaseController
|
|
4
|
+
helper_method :global_data_json
|
|
5
|
+
|
|
6
|
+
before_action :set_global, only: [ :show, :edit, :update, :destroy ]
|
|
7
|
+
before_action :require_editor, only: [ :new, :create, :edit, :update, :destroy ]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@globals = current_site.globals.order(:name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new
|
|
17
|
+
@global = current_site.globals.build(data: {})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@global = current_site.globals.build(global_params)
|
|
22
|
+
|
|
23
|
+
if assign_data(@global) && @global.save
|
|
24
|
+
redirect_to cp_global_path(@global), notice: "Global created"
|
|
25
|
+
else
|
|
26
|
+
render :new, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def edit
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update
|
|
34
|
+
@global.assign_attributes(global_params)
|
|
35
|
+
|
|
36
|
+
if assign_data(@global) && @global.save
|
|
37
|
+
redirect_to cp_global_path(@global), notice: "Global updated"
|
|
38
|
+
else
|
|
39
|
+
render :edit, status: :unprocessable_entity
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy
|
|
44
|
+
@global.destroy
|
|
45
|
+
redirect_to cp_globals_path, notice: "Global deleted"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def set_global
|
|
51
|
+
@global = current_site.globals.find(params[:id])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def global_params
|
|
55
|
+
params.require(:global).permit(:name, :handle)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def assign_data(global)
|
|
59
|
+
@data_json = params.dig(:global, :data_json)
|
|
60
|
+
global.data = parse_data_json(@data_json)
|
|
61
|
+
true
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
global.errors.add(:data, "must be valid JSON")
|
|
64
|
+
false
|
|
65
|
+
rescue TypeError
|
|
66
|
+
global.errors.add(:data, "must be a JSON object")
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_data_json(raw_json)
|
|
71
|
+
data = JSON.parse(raw_json.presence || "{}")
|
|
72
|
+
raise TypeError unless data.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
data
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def global_data_json(global)
|
|
78
|
+
return @data_json if defined?(@data_json) && @data_json.present?
|
|
79
|
+
|
|
80
|
+
JSON.pretty_generate(global.data)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|