panda-cms 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (233) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +73 -0
  3. data/Rakefile +7 -0
  4. data/app/assets/builds/panda.cms.css +2808 -0
  5. data/app/assets/config/panda_cms_manifest.js +4 -0
  6. data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
  7. data/app/assets/stylesheets/panda/cms/editor.css +120 -0
  8. data/app/builders/panda/cms/form_builder.rb +234 -0
  9. data/app/components/panda/cms/admin/button_component.rb +70 -0
  10. data/app/components/panda/cms/admin/container_component.html.erb +13 -0
  11. data/app/components/panda/cms/admin/container_component.rb +13 -0
  12. data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
  13. data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
  14. data/app/components/panda/cms/admin/heading_component.rb +45 -0
  15. data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
  16. data/app/components/panda/cms/admin/panel_component.rb +13 -0
  17. data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
  18. data/app/components/panda/cms/admin/slideover_component.rb +15 -0
  19. data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
  20. data/app/components/panda/cms/admin/statistics_component.rb +17 -0
  21. data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
  22. data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
  23. data/app/components/panda/cms/admin/table_component.html.erb +29 -0
  24. data/app/components/panda/cms/admin/table_component.rb +46 -0
  25. data/app/components/panda/cms/admin/tag_component.rb +35 -0
  26. data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
  27. data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
  28. data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
  29. data/app/components/panda/cms/admin/user_display_component.rb +21 -0
  30. data/app/components/panda/cms/code_component.rb +64 -0
  31. data/app/components/panda/cms/grid_component.html.erb +6 -0
  32. data/app/components/panda/cms/grid_component.rb +15 -0
  33. data/app/components/panda/cms/menu_component.html.erb +6 -0
  34. data/app/components/panda/cms/menu_component.rb +58 -0
  35. data/app/components/panda/cms/page_menu_component.html.erb +21 -0
  36. data/app/components/panda/cms/page_menu_component.rb +38 -0
  37. data/app/components/panda/cms/rich_text_component.html.erb +6 -0
  38. data/app/components/panda/cms/rich_text_component.rb +84 -0
  39. data/app/components/panda/cms/text_component.rb +72 -0
  40. data/app/constraints/panda/cms/admin_constraint.rb +18 -0
  41. data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
  42. data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
  43. data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
  44. data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
  45. data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
  46. data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
  47. data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
  48. data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
  49. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
  50. data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
  51. data/app/controllers/panda/cms/application_controller.rb +57 -0
  52. data/app/controllers/panda/cms/errors_controller.rb +33 -0
  53. data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
  54. data/app/controllers/panda/cms/pages_controller.rb +72 -0
  55. data/app/controllers/panda/cms/posts_controller.rb +13 -0
  56. data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
  57. data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
  58. data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
  59. data/app/helpers/panda/cms/application_helper.rb +120 -0
  60. data/app/helpers/panda/cms/pages_helper.rb +6 -0
  61. data/app/helpers/panda/cms/theme_helper.rb +18 -0
  62. data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
  63. data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
  64. data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
  65. data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
  66. data/app/javascript/panda/cms/application_panda_cms.js +39 -0
  67. data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
  68. data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
  69. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
  70. data/app/javascript/panda/cms/controllers/index.js +48 -0
  71. data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
  72. data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
  73. data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
  74. data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
  75. data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
  76. data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
  77. data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
  78. data/app/jobs/panda/cms/application_job.rb +6 -0
  79. data/app/jobs/panda/cms/record_visit_job.rb +31 -0
  80. data/app/mailers/panda/cms/application_mailer.rb +8 -0
  81. data/app/mailers/panda/cms/form_mailer.rb +21 -0
  82. data/app/models/action_text/rich_text_version.rb +6 -0
  83. data/app/models/panda/cms/application_record.rb +7 -0
  84. data/app/models/panda/cms/block.rb +34 -0
  85. data/app/models/panda/cms/block_content.rb +18 -0
  86. data/app/models/panda/cms/block_content_version.rb +8 -0
  87. data/app/models/panda/cms/breadcrumb.rb +12 -0
  88. data/app/models/panda/cms/current.rb +17 -0
  89. data/app/models/panda/cms/form.rb +9 -0
  90. data/app/models/panda/cms/form_submission.rb +7 -0
  91. data/app/models/panda/cms/menu.rb +52 -0
  92. data/app/models/panda/cms/menu_item.rb +58 -0
  93. data/app/models/panda/cms/page.rb +96 -0
  94. data/app/models/panda/cms/page_version.rb +8 -0
  95. data/app/models/panda/cms/post.rb +60 -0
  96. data/app/models/panda/cms/post_version.rb +8 -0
  97. data/app/models/panda/cms/redirect.rb +11 -0
  98. data/app/models/panda/cms/template.rb +124 -0
  99. data/app/models/panda/cms/template_version.rb +8 -0
  100. data/app/models/panda/cms/user.rb +31 -0
  101. data/app/models/panda/cms/version.rb +8 -0
  102. data/app/models/panda/cms/visit.rb +9 -0
  103. data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
  104. data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
  105. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  106. data/app/views/layouts/panda/cms/application.html.erb +41 -0
  107. data/app/views/layouts/panda/cms/public.html.erb +3 -0
  108. data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -0
  109. data/app/views/panda/cms/admin/files/index.html.erb +124 -0
  110. data/app/views/panda/cms/admin/files/show.html.erb +2 -0
  111. data/app/views/panda/cms/admin/forms/edit.html.erb +0 -0
  112. data/app/views/panda/cms/admin/forms/index.html.erb +13 -0
  113. data/app/views/panda/cms/admin/forms/new.html.erb +15 -0
  114. data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
  115. data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
  116. data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
  117. data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
  118. data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
  119. data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
  120. data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
  121. data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
  122. data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
  123. data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
  124. data/app/views/panda/cms/admin/sessions/new.html.erb +17 -0
  125. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +68 -0
  126. data/app/views/panda/cms/admin/settings/index.html.erb +21 -0
  127. data/app/views/panda/cms/admin/settings/insta.html +4 -0
  128. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +28 -0
  129. data/app/views/panda/cms/admin/shared/_flash.html.erb +5 -0
  130. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +41 -0
  131. data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
  132. data/app/views/panda/cms/shared/_editor.html.erb +0 -0
  133. data/app/views/panda/cms/shared/_favicons.html.erb +9 -0
  134. data/app/views/panda/cms/shared/_footer.html.erb +2 -0
  135. data/app/views/panda/cms/shared/_header.html.erb +15 -0
  136. data/app/views/panda/cms/shared/_importmap.html.erb +33 -0
  137. data/config/importmap.rb +13 -0
  138. data/config/initializers/inflections.rb +3 -0
  139. data/config/initializers/panda/cms/form_errors.rb +38 -0
  140. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +11 -0
  141. data/config/initializers/panda/cms/paper_trail.rb +7 -0
  142. data/config/initializers/panda/cms.rb +10 -0
  143. data/config/initializers/zeitwork.rb +3 -0
  144. data/config/locales/en.yml +49 -0
  145. data/config/puma/test.rb +9 -0
  146. data/config/routes.rb +48 -0
  147. data/config/tailwind.config.js +37 -0
  148. data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
  149. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
  150. data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
  151. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
  152. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
  153. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
  154. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
  155. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
  156. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
  157. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
  158. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +82 -0
  159. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
  160. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
  161. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
  162. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
  163. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
  164. data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
  165. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
  166. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
  167. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
  168. data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
  169. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
  170. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
  171. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  172. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
  173. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
  174. data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
  175. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
  176. data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
  177. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
  178. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
  179. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
  180. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
  181. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
  182. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
  183. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
  184. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
  185. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
  186. data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
  187. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
  188. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
  189. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
  190. data/db/migrate/migrate +1 -0
  191. data/db/seeds.rb +5 -0
  192. data/lib/generators/panda/cms/install_generator.rb +29 -0
  193. data/lib/panda/cms/bulk_editor.rb +171 -0
  194. data/lib/panda/cms/demo_site_generator.rb +67 -0
  195. data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
  196. data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
  197. data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
  198. data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
  199. data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
  200. data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
  201. data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
  202. data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
  203. data/lib/panda/cms/editor_js/renderer.rb +124 -0
  204. data/lib/panda/cms/editor_js.rb +16 -0
  205. data/lib/panda/cms/editor_js_content.rb +21 -0
  206. data/lib/panda/cms/engine.rb +257 -0
  207. data/lib/panda/cms/exceptions_app.rb +26 -0
  208. data/lib/panda/cms/railtie.rb +11 -0
  209. data/lib/panda/cms/slug.rb +24 -0
  210. data/lib/panda/cms.rb +0 -0
  211. data/lib/panda-cms/version.rb +5 -0
  212. data/lib/panda-cms.rb +81 -0
  213. data/lib/tasks/panda_cms.rake +54 -0
  214. data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
  215. data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
  216. data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
  217. data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
  218. data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
  219. data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
  220. data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
  221. data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
  222. data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
  223. data/public/panda-cms-assets/favicons/browserconfig.xml +9 -0
  224. data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
  225. data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
  226. data/public/panda-cms-assets/favicons/favicon.ico +0 -0
  227. data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
  228. data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +61 -0
  229. data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
  230. data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
  231. data/public/panda-cms-assets/panda-nav.png +0 -0
  232. data/public/panda-cms-assets/rich_text_editor.css +568 -0
  233. metadata +654 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ # Text component
6
+ # @param key [Symbol] The key to use for the text component
7
+ # @param text [String] The text to display
8
+ # @param editable [Boolean] If the text is editable or not (defaults to true)
9
+ # @param options [Hash] The options to pass to the content_tag
10
+ class RichTextComponent < ViewComponent::Base
11
+ class ComponentError < StandardError; end
12
+
13
+ KIND = "rich_text"
14
+
15
+ attr_accessor :editable
16
+ attr_accessor :content
17
+ attr_accessor :options
18
+
19
+ def initialize(key: :text_component, text: "Lorem ipsum...", editable: true, **options)
20
+ @key = key
21
+ @text = text
22
+ @options = options || {}
23
+ @editable = editable
24
+ end
25
+
26
+ # Check if the element is editable and set up the content
27
+ def before_render
28
+ @editable &&= params[:embed_id].present? && params[:embed_id] == Current.page.id && Current.user.admin?
29
+
30
+ block = Panda::CMS::Block.find_by(kind: "rich_text", key: @key, panda_cms_template_id: Current.page.panda_cms_template_id)
31
+ raise ComponentError, "Block not found for key: #{@key}" unless block
32
+
33
+ block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
34
+ if block_content.nil?
35
+ block_content = Panda::CMS::BlockContent.create!(
36
+ block: block,
37
+ panda_cms_page_id: Current.page.id,
38
+ content: ""
39
+ )
40
+ end
41
+
42
+ @content = block_content.cached_content || block_content.content
43
+ raise ComponentError, "No content found for block: #{block.id}" if @content.nil?
44
+
45
+ @options[:id] = block_content.id
46
+
47
+ if @editable
48
+ @options[:data] = {
49
+ page_id: Current.page.id,
50
+ mode: "rich_text"
51
+ }
52
+
53
+ # Convert HTML content to EditorJS format if needed
54
+ begin
55
+ editor_content = Panda::CMS::HtmlToEditorJsConverter.convert(@content)
56
+ @content = editor_content
57
+ rescue Panda::CMS::HtmlToEditorJsConverter::ConversionError => e
58
+ raise ComponentError, "Failed to convert content: #{e.message}"
59
+ end
60
+ elsif @content.is_a?(Hash)
61
+ begin
62
+ renderer = Panda::CMS::EditorJs::Renderer.new(@content)
63
+ @content = renderer.render
64
+ rescue => e
65
+ raise ComponentError, "Failed to render content: #{e.message}"
66
+ end
67
+ else
68
+ @content = @content.html_safe
69
+ end
70
+ rescue ActiveRecord::RecordNotFound => e
71
+ raise ComponentError, "Database record not found: #{e.message}"
72
+ rescue ActiveRecord::RecordInvalid => e
73
+ raise ComponentError, "Invalid record: #{e.message}"
74
+ rescue => e
75
+ raise ComponentError, "Component error: #{e.message}"
76
+ end
77
+
78
+ # Only render the component if there is some content set, or if the component is editable
79
+ def render?
80
+ @content.present? || @editable
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ # Text component
6
+ # @param key [Symbol] The key to use for the text component
7
+ # @param text [String] The text to display
8
+ # @param editable [Boolean] If the text is editable or not (defaults to true)
9
+ # @param options [Hash] The options to pass to the content_tag
10
+ class TextComponent < ViewComponent::Base
11
+ KIND = "plain_text"
12
+
13
+ # Allows accessing the plain text of the component directly
14
+ attr_accessor :plain_text
15
+
16
+ def initialize(key: :text_component, text: "Lorem ipsum...", editable: true, **options)
17
+ @key = key
18
+ @text = text
19
+ @options = options || {}
20
+ @options[:id] ||= "text-#{key.to_s.dasherize}"
21
+ @editable = editable
22
+ end
23
+
24
+ def call
25
+ content_tag(:span, @content, @options, false) # Don't escape the content
26
+ rescue
27
+ if !Rails.env.production? || is_defined?(Sentry)
28
+ raise Panda::CMS::MissingBlockError.new("Block with key #{@key} not found for page #{Current.page.title}")
29
+ else
30
+ false
31
+ end
32
+ end
33
+
34
+ #
35
+ # Prepares content for display
36
+ #
37
+ # @usage Do not use this when rendering editable content
38
+ def prepare_content_for_display(content)
39
+ # Replace \n characters with <br> tags
40
+ content.gsub("\n", "<br>")
41
+ end
42
+
43
+ # Check if the element is editable
44
+ # TODO: Check user permissions
45
+ def before_render
46
+ @editable &&= params[:embed_id].present? && params[:embed_id] == Current.page.id
47
+
48
+ block = Panda::CMS::Block.find_by(kind: KIND, key: @key, panda_cms_template_id: Current.page.panda_cms_template_id)
49
+
50
+ if block.nil?
51
+ return false
52
+ end
53
+
54
+ block_content = block.block_contents.find_by(panda_cms_page_id: Current.page.id)
55
+ plain_text = block_content&.content.to_s
56
+ if @editable
57
+ @options[:contenteditable] = "plaintext-only"
58
+ @options[:data] = {
59
+ "editable-kind": "plain_text",
60
+ "editable-page-id": Current.page.id,
61
+ "editable-block-content-id": block_content&.id
62
+ }
63
+
64
+ @options[:id] = "editor-#{block_content&.id}"
65
+ @content = plain_text
66
+ else
67
+ @content = prepare_content_for_display(plain_text)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ module Panda
2
+ module CMS
3
+ class AdminConstraint
4
+ def initialize(&block)
5
+ @block = block
6
+ end
7
+
8
+ def matches?(request)
9
+ user = current_user(request)
10
+ user.present? && user.admin? && @block&.call(user)
11
+ end
12
+
13
+ def current_user(request)
14
+ User.find_by(id: request.session[:user_id])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class BlockContentsController < ApplicationController
7
+ before_action :set_page, only: %i[update]
8
+ before_action :set_block_content, only: %i[update]
9
+ before_action :set_paper_trail_whodunnit, only: %i[update]
10
+ before_action :authenticate_admin_user!
11
+
12
+ # @type PATCH/PUT
13
+ # @return
14
+ def update
15
+ Rails.logger.debug "Content params: #{params.inspect}"
16
+ Rails.logger.debug "Raw content: #{request.raw_post}"
17
+
18
+ if @block_content.update!(content: params.dig(:content))
19
+ @block_content.page.touch
20
+ render json: @block_content, status: :ok
21
+ else
22
+ render json: @block_content.errors, status: :unprocessable_entity
23
+ end
24
+ rescue => e
25
+ Rails.logger.error "Error updating block content: #{e.message}"
26
+ render json: {error: e.message}, status: :unprocessable_entity
27
+ end
28
+
29
+ private
30
+
31
+ # @type private
32
+ # @return Panda::CMS::Page
33
+ def set_page
34
+ @page = Panda::CMS::Page.find(params[:page_id])
35
+ end
36
+
37
+ # @type private
38
+ # @return Panda::CMS::BlockContent
39
+ def set_block_content
40
+ @block_content = Panda::CMS::BlockContent.find(params[:id])
41
+ end
42
+
43
+ # Only allow a list of trusted parameters through.
44
+ # @type private
45
+ # @return ActionController::StrongParameters
46
+ def block_content_params
47
+ params.require(:block_content).permit(:content)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ require "groupdate"
2
+
3
+ module Panda
4
+ module CMS
5
+ class Admin::DashboardController < ApplicationController
6
+ before_action :set_initial_breadcrumb, only: %i[show]
7
+ before_action :authenticate_admin_user!
8
+
9
+ # GET /admin
10
+ def show
11
+ end
12
+
13
+ private
14
+
15
+ def set_initial_breadcrumb
16
+ add_breadcrumb "Dashboard", Panda::CMS.route_namespace
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ module Panda
2
+ module CMS
3
+ class Admin::FilesController < ApplicationController
4
+ before_action :set_initial_breadcrumb, only: %i[index show]
5
+ before_action :authenticate_admin_user!
6
+
7
+ def index
8
+ redirect_to admin_dashboard_path
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ private
15
+
16
+ def set_initial_breadcrumb
17
+ add_breadcrumb "Media", admin_files_path
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class FormsController < ApplicationController
7
+ before_action :set_initial_breadcrumb, only: %i[index show]
8
+ # before_action :set_paper_trail_whodunnit, only: %i[create update]
9
+ before_action :authenticate_admin_user!
10
+
11
+ # Lists all forms
12
+ # @type GET
13
+ # @return ActiveRecord::Collection A list of all forms
14
+ def index
15
+ forms = Panda::CMS::Form.order(:name)
16
+ render :index, locals: {forms: forms}
17
+ end
18
+
19
+ def show
20
+ form = Panda::CMS::Form.find(params[:id])
21
+
22
+ add_breadcrumb form.name, admin_form_path(form)
23
+ submissions = form.form_submissions.order(created_at: :desc)
24
+ # TODO: Set a whitelist of fields we allow to be submitted for the form, shown in this view
25
+ # and a formatting array of how to display them... eventually?
26
+
27
+ fields = if submissions.last
28
+ submissions.last.data.keys.reverse.map { |field| [field, field.titleize] }
29
+ else
30
+ []
31
+ end
32
+
33
+ render :show, locals: {form: form, submissions: submissions, fields: fields}
34
+ end
35
+
36
+ private
37
+
38
+ def set_initial_breadcrumb
39
+ add_breadcrumb "Forms", admin_forms_path
40
+ end
41
+
42
+ private
43
+
44
+ # Only allow a list of trusted parameters through
45
+ # @type private
46
+ # @return ActionController::StrongParameters
47
+ def form_params
48
+ params.require(:form).permit(:name, :completion_path)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class MenusController < ApplicationController
7
+ before_action :set_initial_breadcrumb, only: %i[index]
8
+ before_action :authenticate_admin_user!
9
+
10
+ # Lists all menus which can be managed by the administrator
11
+ # @type GET
12
+ # @return ActiveRecord::Collection An array of all menus
13
+ def index
14
+ menus = Panda::CMS::Menu.order(:name)
15
+ render :index, locals: {menus: menus}
16
+ end
17
+
18
+ private
19
+
20
+ def menu
21
+ @menu ||= Panda::CMS::Menu.find(params[:id])
22
+ end
23
+
24
+ def set_initial_breadcrumb
25
+ add_breadcrumb "Menus", admin_menus_path
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class PagesController < ApplicationController
7
+ before_action :set_initial_breadcrumb, only: %i[index edit new create update]
8
+ before_action :set_paper_trail_whodunnit, only: %i[create update]
9
+ before_action :authenticate_admin_user!
10
+
11
+ # Lists all pages which can be managed by the administrator
12
+ # @type GET
13
+ # @return ActiveRecord::Collection A list of all pages
14
+ def index
15
+ homepage = Panda::CMS::Page.find_by(path: "/")
16
+ render :index, locals: {root_page: homepage}
17
+ end
18
+
19
+ # Loads the add page form
20
+ # @type GET
21
+ def new
22
+ locals = setup_new_page_form(page: page)
23
+ render :new, locals: locals
24
+ end
25
+
26
+ # Loads the page editor
27
+ # @type GET
28
+ def edit
29
+ add_breadcrumb page.title, edit_admin_page_path(page)
30
+
31
+ render :edit, locals: {page: page, template: page.template}
32
+ end
33
+
34
+ # POST /admin/pages
35
+ def create
36
+ page = Panda::CMS::Page.new(page_params)
37
+ if page.save
38
+ page.update(path: page.parent.path + page.path) unless page.parent.path == "/"
39
+ redirect_to edit_admin_page_path(page), notice: "The page was successfully created."
40
+ else
41
+ flash[:error] = "There was an error creating the page."
42
+ locals = setup_new_page_form(page: page)
43
+ render :new, locals: locals, status: :unprocessable_entity
44
+ end
45
+ end
46
+
47
+ # @type PATCH/PUT
48
+ # @return
49
+ def update
50
+ if page.update(page_params)
51
+ redirect_to edit_admin_page_path(page),
52
+ status: :see_other,
53
+ flash: {success: "This page was successfully updated!"}
54
+ else
55
+ flash[:error] = "There was an error updating the page."
56
+ render :edit, status: :unprocessable_entity
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Get the page from the ID
63
+ # @type private
64
+ # @return Panda::CMS::Page
65
+ def page
66
+ @page ||= if params[:id]
67
+ Panda::CMS::Page.find(params[:id])
68
+ else
69
+ Panda::CMS::Page.new(template: Panda::CMS::Template.default)
70
+ end
71
+ end
72
+
73
+ def set_initial_breadcrumb
74
+ add_breadcrumb "Pages", admin_pages_path
75
+ end
76
+
77
+ def setup_new_page_form(page:)
78
+ add_breadcrumb "Add Page", new_admin_page_path
79
+ {page: page}
80
+ end
81
+
82
+ # Only allow a list of trusted parameters through.
83
+ # @type private
84
+ # @return ActionController::StrongParameters
85
+ def page_params
86
+ params.require(:page).permit(:title, :path, :panda_cms_template_id, :parent_id, :status)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Panda
6
+ module CMS
7
+ module Admin
8
+ class PostsController < ApplicationController
9
+ before_action :set_initial_breadcrumb, only: %i[index new edit create update]
10
+ before_action :set_paper_trail_whodunnit, only: %i[create update]
11
+ before_action :authenticate_admin_user!
12
+
13
+ # Get all posts
14
+ # @type GET
15
+ # @return ActiveRecord::Collection A list of all posts
16
+ def index
17
+ posts = Panda::CMS::Post.with_user.ordered
18
+ render :index, locals: {posts: posts}
19
+ end
20
+
21
+ # Loads the add post form
22
+ # @type GET
23
+ def new
24
+ locals = setup_new_post_form
25
+ render :new, locals: locals
26
+ end
27
+
28
+ # Loads the post editor
29
+ # @type GET
30
+ def edit
31
+ add_breadcrumb post.title, edit_admin_post_path(post.admin_param)
32
+
33
+ # Get the latest version's content or fall back to post's content
34
+ preserved_content = if post.versions.exists?
35
+ reified_post = post.versions.last.reify
36
+ reified_post&.content || post.content
37
+ else
38
+ post.content
39
+ end
40
+
41
+ render :edit, locals: {
42
+ post: post,
43
+ url: admin_post_path(post.admin_param),
44
+ preserved_content: preserved_content
45
+ }
46
+ end
47
+
48
+ # POST /admin/posts
49
+ def create
50
+ @post = Panda::CMS::Post.new(post_params)
51
+
52
+ begin
53
+ @post.content = JSON.parse(post_params[:content])
54
+ rescue
55
+ @post.content = post_params[:content]
56
+ end
57
+
58
+ if @post.save
59
+ Rails.logger.debug "Post saved successfully"
60
+ redirect_to edit_admin_post_path(@post.admin_param), notice: "Post was successfully created."
61
+ else
62
+ Rails.logger.debug "Post save failed: #{@post.errors.full_messages.inspect}"
63
+ flash.now[:error] = @post.errors.full_messages.join(", ")
64
+ locals = setup_new_post_form(post: @post, preserved_content: post_params[:content])
65
+ render :new, locals: locals, status: :unprocessable_entity
66
+ end
67
+ end
68
+
69
+ # @type PATCH/PUT
70
+ # @return
71
+ def update
72
+ Rails.logger.debug "Updating post with params: #{post_params.inspect}"
73
+ Rails.logger.debug "Current content: #{post.content.inspect}"
74
+ Rails.logger.debug "New content from params: #{post_params[:content].inspect}"
75
+
76
+ if post.update(post_params)
77
+ Rails.logger.debug "Post updated successfully"
78
+ add_breadcrumb post.title, edit_admin_post_path(post.admin_param)
79
+ redirect_to edit_admin_post_path(post.admin_param),
80
+ status: :see_other,
81
+ flash: {success: "The post was successfully updated!"}
82
+ else
83
+ Rails.logger.debug "Post update failed: #{post.errors.full_messages.inspect}"
84
+ Rails.logger.debug "Preserving content: #{post_params[:content].inspect}"
85
+ flash[:error] = post.errors.full_messages.join(", ")
86
+ add_breadcrumb post.title.presence || "Edit Post", edit_admin_post_path(post.admin_param)
87
+ render :edit, locals: {
88
+ post: post,
89
+ url: admin_post_path(post.admin_param),
90
+ preserved_content: post_params[:content]
91
+ }, status: :unprocessable_entity
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Get the post from the ID
98
+ # @type private
99
+ # @return Panda::CMS::Post
100
+ def post
101
+ @post ||= if params[:id]
102
+ Panda::CMS::Post.find(params[:id])
103
+ else
104
+ Panda::CMS::Post.new(
105
+ status: "active",
106
+ published_at: Time.zone.now
107
+ )
108
+ end
109
+ end
110
+
111
+ def set_initial_breadcrumb
112
+ add_breadcrumb "Posts", admin_posts_path
113
+ end
114
+
115
+ def setup_new_post_form(post: nil, preserved_content: nil)
116
+ add_breadcrumb "Add Post", new_admin_post_path
117
+
118
+ post ||= Panda::CMS::Post.new(
119
+ status: "active",
120
+ published_at: Time.zone.now
121
+ )
122
+
123
+ {
124
+ post: post,
125
+ url: admin_posts_path,
126
+ preserved_content: preserved_content
127
+ }
128
+ end
129
+
130
+ # Only allow a list of trusted parameters through.
131
+ # @type private
132
+ # @return ActionController::StrongParameters
133
+ def post_params
134
+ params.require(:post).permit(
135
+ :title,
136
+ :slug,
137
+ :status,
138
+ :published_at,
139
+ :user_id,
140
+ :content
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class SessionsController < ApplicationController
7
+ layout "panda/cms/public"
8
+
9
+ def new
10
+ @providers = Panda::CMS.config.authentication.select { |_, v| v[:enabled] && !v[:hidden] }.keys
11
+ end
12
+
13
+ def create
14
+ user_info = request.env.dig("omniauth.auth", "info")
15
+ provider = params[:provider].to_sym
16
+
17
+ unless Panda::CMS.config.authentication.dig(provider, :enabled)
18
+ Rails.logger.error "Authentication provider '#{provider}' is not enabled"
19
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
20
+ return
21
+ end
22
+
23
+ user = Panda::CMS::User.find_by(email: user_info["email"])
24
+
25
+ if !user && Panda::CMS.config.authentication.dig(provider, :create_account_on_first_login)
26
+ create_as_admin = Panda::CMS.config.authentication.dig(provider, :create_as_admin)
27
+
28
+ # Always create the first user as admin, regardless of what our settings look like
29
+ # else we can't ever really login. :)
30
+ if !create_as_admin
31
+ create_as_admin = true if !create_as_admin && Panda::CMS::User.count.zero?
32
+ end
33
+
34
+ if user_info["first_name"] && user_info["last_name"]
35
+ firstname = user_info["first_name"]
36
+ lastname = user_info["last_name"]
37
+ elsif user_info["name"]
38
+ firstname, lastname = user_info["name"].split(" ", 2)
39
+ end
40
+
41
+ user = User.find_or_create_by(
42
+ email: user_info["email"]
43
+ ) do |u|
44
+ u.firstname = firstname
45
+ u.lastname = lastname
46
+ u.admin = create_as_admin
47
+ u.image_url = user_info["image"]
48
+ end
49
+ end
50
+
51
+ if user.nil?
52
+ # User can't be found with this email address
53
+ Rails.logger.error "User does not exist: #{user_info["email"]}"
54
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
55
+ return
56
+ end
57
+
58
+ if !user.admin?
59
+ # User can't be found with this email address or can't login
60
+ Rails.logger.error "User ID #{user.id} attempted admin login, is not admin." if user && !user.admin
61
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
62
+ return
63
+ end
64
+
65
+ session[:user_id] = user.id
66
+ Panda::CMS::Current.user = user
67
+
68
+ redirect_path = request.env["omniauth.origin"] || admin_dashboard_path
69
+ redirect_to redirect_path, flash: {success: t("panda.cms.admin.sessions.create.success")}
70
+ rescue ::OmniAuth::Strategies::OAuth2::CallbackError => e
71
+ Rails.logger.error "OAuth2 login callback error: #{e.message}"
72
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
73
+ rescue ::OAuth2::Error => e
74
+ Rails.logger.error "OAuth2 login error: #{e.message}"
75
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
76
+ rescue => e
77
+ Rails.logger.error "Unknown login error: #{e.message}"
78
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
79
+ end
80
+
81
+ def failure
82
+ Rails.logger.error "Login failure: #{params[:message]} from #{params[:origin]} using #{params[:strategy]}"
83
+ redirect_to admin_login_path, flash: {error: t("panda.cms.admin.sessions.create.error")}
84
+ end
85
+
86
+ def destroy
87
+ Panda::CMS::Current.user = nil
88
+ session[:user_id] = nil
89
+ redirect_to admin_login_path, flash: {success: t("panda.cms.admin.sessions.destroy.success")}
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end