panda-cms 0.7.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.
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