forest_cms 0.98.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +204 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/config/forest_manifest.js +2 -0
  6. data/app/assets/fonts/FontAwesome.otf +0 -0
  7. data/app/assets/fonts/fontawesome-webfont.eot +0 -0
  8. data/app/assets/fonts/fontawesome-webfont.svg +2671 -0
  9. data/app/assets/fonts/fontawesome-webfont.ttf +0 -0
  10. data/app/assets/fonts/fontawesome-webfont.woff +0 -0
  11. data/app/assets/fonts/fontawesome-webfont.woff2 +0 -0
  12. data/app/assets/images/forest/favicons/android-chrome-192x192.png +0 -0
  13. data/app/assets/images/forest/favicons/android-chrome-512x512.png +0 -0
  14. data/app/assets/images/forest/favicons/apple-touch-icon.png +0 -0
  15. data/app/assets/images/forest/favicons/browserconfig.xml +9 -0
  16. data/app/assets/images/forest/favicons/favicon-16x16.png +0 -0
  17. data/app/assets/images/forest/favicons/favicon-32x32.png +0 -0
  18. data/app/assets/images/forest/favicons/favicon.ico +0 -0
  19. data/app/assets/images/forest/favicons/manifest.json.erb +18 -0
  20. data/app/assets/images/forest/favicons/mstile-150x150.png +0 -0
  21. data/app/assets/images/forest/favicons/safari-pinned-tab.svg +21 -0
  22. data/app/assets/images/forest/forest-low.jpg +0 -0
  23. data/app/assets/images/forest/forest.jpg +0 -0
  24. data/app/assets/javascripts/forest/admin/lib/jquery_plugins.js +32 -0
  25. data/app/assets/javascripts/forest/admin/main.js +59 -0
  26. data/app/assets/javascripts/forest/admin/partials/add_block.js +22 -0
  27. data/app/assets/javascripts/forest/admin/partials/block_collapser.js +49 -0
  28. data/app/assets/javascripts/forest/admin/partials/block_errors.js +11 -0
  29. data/app/assets/javascripts/forest/admin/partials/block_selector_inputs.js +61 -0
  30. data/app/assets/javascripts/forest/admin/partials/cocoon_events.js +3 -0
  31. data/app/assets/javascripts/forest/admin/partials/cocoon_sortable_field_sets.js +55 -0
  32. data/app/assets/javascripts/forest/admin/partials/collage_inputs.js +286 -0
  33. data/app/assets/javascripts/forest/admin/partials/color_inputs.js +24 -0
  34. data/app/assets/javascripts/forest/admin/partials/datepicker_init.js +70 -0
  35. data/app/assets/javascripts/forest/admin/partials/documentation_sidebar_affix.js +22 -0
  36. data/app/assets/javascripts/forest/admin/partials/dropdown_block_selector.js +106 -0
  37. data/app/assets/javascripts/forest/admin/partials/edit_header.js +22 -0
  38. data/app/assets/javascripts/forest/admin/partials/file_upload_init.js +111 -0
  39. data/app/assets/javascripts/forest/admin/partials/forest_tables.js +42 -0
  40. data/app/assets/javascripts/forest/admin/partials/infinite_loader.js +89 -0
  41. data/app/assets/javascripts/forest/admin/partials/markdown_text_editor_init.js +89 -0
  42. data/app/assets/javascripts/forest/admin/partials/media_item_chooser_modal.js.erb +259 -0
  43. data/app/assets/javascripts/forest/admin/partials/menus.js +79 -0
  44. data/app/assets/javascripts/forest/admin/partials/multi_checkbox_selector.js +27 -0
  45. data/app/assets/javascripts/forest/admin/partials/persisted_value_warnings.js +38 -0
  46. data/app/assets/javascripts/forest/admin/partials/repeater_inputs.js +24 -0
  47. data/app/assets/javascripts/forest/admin/partials/select2_init.js +165 -0
  48. data/app/assets/javascripts/forest/admin/partials/select_tag_form_submitter.js +40 -0
  49. data/app/assets/javascripts/forest/admin/partials/sortable_gallery.js +45 -0
  50. data/app/assets/javascripts/forest/admin/partials/sortable_page_block_position_setter.js +37 -0
  51. data/app/assets/javascripts/forest/admin/partials/status_scheduler.js +16 -0
  52. data/app/assets/javascripts/forest/admin/partials/tab_templates.js +93 -0
  53. data/app/assets/javascripts/forest/admin/partials/tables.js +7 -0
  54. data/app/assets/javascripts/forest/admin/partials/tooltips.js +7 -0
  55. data/app/assets/javascripts/forest/admin/partials/turboform_form_submitter.js +15 -0
  56. data/app/assets/javascripts/forest/admin/vendor/imagesloaded.pkgd.min.js +7 -0
  57. data/app/assets/javascripts/forest/admin/vendor/jquery-ui-timepicker-addon.js +2291 -0
  58. data/app/assets/javascripts/forest/admin/vendor/jquery.ba-throttle-debounce.min.js +9 -0
  59. data/app/assets/javascripts/forest/admin/vendor/jquery.nestable.js +756 -0
  60. data/app/assets/javascripts/forest/admin/vendor/select2.full.min.js +3 -0
  61. data/app/assets/javascripts/forest/admin/vendor/simplemde.min.js +15 -0
  62. data/app/assets/javascripts/forest/application_admin.js +29 -0
  63. data/app/assets/javascripts/forest/application_admin_from_host_app.js +1 -0
  64. data/app/assets/javascripts/forest/application_public.js +19 -0
  65. data/app/assets/javascripts/forest/cable.js +13 -0
  66. data/app/assets/javascripts/forest/lib/jquery-3.3.1.min.js +2 -0
  67. data/app/assets/javascripts/forest/main.js +59 -0
  68. data/app/assets/javascripts/forest/public/partials/analytics.js +16 -0
  69. data/app/assets/stylesheets/forest/admin/_bootstrap-variables.scss +877 -0
  70. data/app/assets/stylesheets/forest/admin/_variables.scss +13 -0
  71. data/app/assets/stylesheets/forest/admin/base/_foundation.scss +17 -0
  72. data/app/assets/stylesheets/forest/admin/mixins/_breakpoints.scss +26 -0
  73. data/app/assets/stylesheets/forest/admin/mixins/_glyphicons.scss +278 -0
  74. data/app/assets/stylesheets/forest/admin/mixins/_layout.scss +13 -0
  75. data/app/assets/stylesheets/forest/admin/mixins/_links.scss +10 -0
  76. data/app/assets/stylesheets/forest/admin/mixins/_lists.scss +10 -0
  77. data/app/assets/stylesheets/forest/admin/mixins/_mixins.scss +6 -0
  78. data/app/assets/stylesheets/forest/admin/mixins/_triangles.scss +21 -0
  79. data/app/assets/stylesheets/forest/admin/partials/_admin.scss +57 -0
  80. data/app/assets/stylesheets/forest/admin/partials/_affix.scss +5 -0
  81. data/app/assets/stylesheets/forest/admin/partials/_block_errors.scss +15 -0
  82. data/app/assets/stylesheets/forest/admin/partials/_block_layout.scss +48 -0
  83. data/app/assets/stylesheets/forest/admin/partials/_block_type_edit_ui.scss +15 -0
  84. data/app/assets/stylesheets/forest/admin/partials/_bootstrap_customizations.scss +12 -0
  85. data/app/assets/stylesheets/forest/admin/partials/_collage_inputs.scss +122 -0
  86. data/app/assets/stylesheets/forest/admin/partials/_documentation.scss +7 -0
  87. data/app/assets/stylesheets/forest/admin/partials/_dropdown_block_selector.scss +19 -0
  88. data/app/assets/stylesheets/forest/admin/partials/_edit_header.scss +16 -0
  89. data/app/assets/stylesheets/forest/admin/partials/_file_upload.scss +3 -0
  90. data/app/assets/stylesheets/forest/admin/partials/_forest_tables.scss +20 -0
  91. data/app/assets/stylesheets/forest/admin/partials/_forms_and_inputs.scss +72 -0
  92. data/app/assets/stylesheets/forest/admin/partials/_helper_classes.scss +24 -0
  93. data/app/assets/stylesheets/forest/admin/partials/_infinite_loader.scss +8 -0
  94. data/app/assets/stylesheets/forest/admin/partials/_markdown_editor.scss +12 -0
  95. data/app/assets/stylesheets/forest/admin/partials/_media_item_chooser.scss +168 -0
  96. data/app/assets/stylesheets/forest/admin/partials/_media_library.scss +109 -0
  97. data/app/assets/stylesheets/forest/admin/partials/_pages.scss +6 -0
  98. data/app/assets/stylesheets/forest/admin/partials/_pagination.scss +5 -0
  99. data/app/assets/stylesheets/forest/admin/partials/_repeater_inputs.scss +57 -0
  100. data/app/assets/stylesheets/forest/admin/partials/_resizable_ui.scss +1 -0
  101. data/app/assets/stylesheets/forest/admin/partials/_select2_custom.scss +20 -0
  102. data/app/assets/stylesheets/forest/admin/partials/_sessions.scss +13 -0
  103. data/app/assets/stylesheets/forest/admin/partials/_slug_attributes.scss +9 -0
  104. data/app/assets/stylesheets/forest/admin/partials/_sortable_field_sets.scss +22 -0
  105. data/app/assets/stylesheets/forest/admin/partials/_sortable_select2.scss +18 -0
  106. data/app/assets/stylesheets/forest/admin/partials/_sortable_ui.scss +15 -0
  107. data/app/assets/stylesheets/forest/admin/partials/_status_indicators.scss +33 -0
  108. data/app/assets/stylesheets/forest/admin/partials/_status_scheduler.scss +9 -0
  109. data/app/assets/stylesheets/forest/admin/partials/_tooltips.scss +5 -0
  110. data/app/assets/stylesheets/forest/admin/partials/_turbolinks.scss +5 -0
  111. data/app/assets/stylesheets/forest/admin/partials/_typography.scss +19 -0
  112. data/app/assets/stylesheets/forest/admin/partials/_ui.scss +32 -0
  113. data/app/assets/stylesheets/forest/admin/sprockets/font-awesome.scss +2337 -0
  114. data/app/assets/stylesheets/forest/admin/sprockets/main_bootstrap.scss +3 -0
  115. data/app/assets/stylesheets/forest/admin/vendor/_jquery-ui-timepicker-addon.scss +30 -0
  116. data/app/assets/stylesheets/forest/admin/vendor/_nestable.scss +347 -0
  117. data/app/assets/stylesheets/forest/admin/vendor/_select2_1_core.scss +484 -0
  118. data/app/assets/stylesheets/forest/admin/vendor/_select2_2_bootstrap.scss +612 -0
  119. data/app/assets/stylesheets/forest/admin/vendor/_simplemde.min.scss +7 -0
  120. data/app/assets/stylesheets/forest/admin_overrides.scss +1 -0
  121. data/app/assets/stylesheets/forest/application_admin.css +23 -0
  122. data/app/assets/stylesheets/forest/application_public.css +16 -0
  123. data/app/assets/stylesheets/forest/main_admin.scss +16 -0
  124. data/app/assets/stylesheets/forest/main_public.scss +4 -0
  125. data/app/assets/stylesheets/forest/public/base/_admin_error_messages.scss +15 -0
  126. data/app/assets/stylesheets/forest/public/base/_foundation.scss +24 -0
  127. data/app/assets/stylesheets/forest/public/base/_images.scss +14 -0
  128. data/app/assets/stylesheets/forest/scaffolds.scss +87 -0
  129. data/app/assets/stylesheets/jquery.fileupload.scss +37 -0
  130. data/app/classes/forest/block_set.rb +26 -0
  131. data/app/classes/forest/error.rb +15 -0
  132. data/app/classes/forest/markdown_renderer.rb +31 -0
  133. data/app/classes/search_index_manager.rb +83 -0
  134. data/app/controllers/admin/block_kinds_controller.rb +31 -0
  135. data/app/controllers/admin/block_layouts_controller.rb +47 -0
  136. data/app/controllers/admin/cache_purge_controller.rb +11 -0
  137. data/app/controllers/admin/dashboard_controller.rb +6 -0
  138. data/app/controllers/admin/documentation_controller.rb +5 -0
  139. data/app/controllers/admin/errors_controller.rb +9 -0
  140. data/app/controllers/admin/forest_controller.rb +96 -0
  141. data/app/controllers/admin/imports_controller.rb +34 -0
  142. data/app/controllers/admin/media_items_controller.rb +107 -0
  143. data/app/controllers/admin/menus_controller.rb +69 -0
  144. data/app/controllers/admin/pages_controller.rb +105 -0
  145. data/app/controllers/admin/settings_controller.rb +66 -0
  146. data/app/controllers/admin/translations_controller.rb +58 -0
  147. data/app/controllers/admin/user_groups_controller.rb +69 -0
  148. data/app/controllers/admin/users_controller.rb +88 -0
  149. data/app/controllers/application_controller.rb +2 -0
  150. data/app/controllers/errors_controller.rb +26 -0
  151. data/app/controllers/forest_controller.rb +39 -0
  152. data/app/controllers/media_items_controller.rb +12 -0
  153. data/app/controllers/pages_controller.rb +39 -0
  154. data/app/controllers/public_controller.rb +5 -0
  155. data/app/helpers/admin_helper.rb +95 -0
  156. data/app/helpers/admin_info_helper.rb +12 -0
  157. data/app/helpers/admin_settings_helper.rb +16 -0
  158. data/app/helpers/application_helper.rb +2 -0
  159. data/app/helpers/block_helper.rb +25 -0
  160. data/app/helpers/cache_helper.rb +23 -0
  161. data/app/helpers/forest_errors_helper.rb +34 -0
  162. data/app/helpers/form_helper.rb +93 -0
  163. data/app/helpers/image_helper.rb +65 -0
  164. data/app/helpers/markdown_helper.rb +25 -0
  165. data/app/helpers/meta_helper.rb +238 -0
  166. data/app/helpers/navigation_helper.rb +18 -0
  167. data/app/helpers/status_indicator_helper.rb +7 -0
  168. data/app/inputs/block_selector_input.rb +16 -0
  169. data/app/inputs/collage_input.rb +63 -0
  170. data/app/inputs/color_input.rb +30 -0
  171. data/app/inputs/currency_input.rb +22 -0
  172. data/app/inputs/datepicker_input.rb +19 -0
  173. data/app/inputs/gallery_input.rb +67 -0
  174. data/app/inputs/grouped_collection_select_input.rb +19 -0
  175. data/app/inputs/image_input.rb +82 -0
  176. data/app/inputs/repeater_input.rb +58 -0
  177. data/app/jobs/forest/application_job.rb +4 -0
  178. data/app/mailers/forest/application_mailer.rb +6 -0
  179. data/app/models/application_record.rb +13 -0
  180. data/app/models/block_kind.rb +54 -0
  181. data/app/models/block_layout.rb +19 -0
  182. data/app/models/block_slot.rb +49 -0
  183. data/app/models/blocks/base_block.rb +134 -0
  184. data/app/models/cache_record.rb +2 -0
  185. data/app/models/concerns/attachable.rb +126 -0
  186. data/app/models/concerns/blockable.rb +65 -0
  187. data/app/models/concerns/filter_controller_scopes.rb +13 -0
  188. data/app/models/concerns/forest/cache_buster.rb +112 -0
  189. data/app/models/concerns/searchable.rb +200 -0
  190. data/app/models/concerns/sluggable.rb +70 -0
  191. data/app/models/concerns/statusable.rb +71 -0
  192. data/app/models/concerns/versionable.rb +13 -0
  193. data/app/models/forest/application_record.rb +111 -0
  194. data/app/models/import.rb +43 -0
  195. data/app/models/media_item.rb +136 -0
  196. data/app/models/menu.rb +65 -0
  197. data/app/models/page.rb +154 -0
  198. data/app/models/setting.rb +142 -0
  199. data/app/models/translation.rb +97 -0
  200. data/app/models/user.rb +45 -0
  201. data/app/models/user_group.rb +17 -0
  202. data/app/policies/application_policy.rb +71 -0
  203. data/app/policies/block_kind_policy.rb +2 -0
  204. data/app/policies/block_layout_policy.rb +2 -0
  205. data/app/policies/block_record_policy.rb +13 -0
  206. data/app/policies/dashboard_policy.rb +19 -0
  207. data/app/policies/media_item_policy.rb +5 -0
  208. data/app/policies/menu_policy.rb +2 -0
  209. data/app/policies/page_policy.rb +9 -0
  210. data/app/policies/setting_policy.rb +2 -0
  211. data/app/policies/translation_policy.rb +2 -0
  212. data/app/policies/user_group_policy.rb +2 -0
  213. data/app/policies/user_policy.rb +17 -0
  214. data/app/views/admin/_file_upload.html.erb +75 -0
  215. data/app/views/admin/_flash_notifications.html.erb +20 -0
  216. data/app/views/admin/_navigation.html.erb +61 -0
  217. data/app/views/admin/api/index/_header.json.jbuilder +2 -0
  218. data/app/views/admin/api/index/_records.json.jbuilder +6 -0
  219. data/app/views/admin/block_kinds/_form.html.erb +34 -0
  220. data/app/views/admin/block_kinds/edit.html.erb +3 -0
  221. data/app/views/admin/block_kinds/index.html.erb +31 -0
  222. data/app/views/admin/block_layouts/_form.html.erb +33 -0
  223. data/app/views/admin/block_layouts/edit.html.erb +3 -0
  224. data/app/views/admin/block_layouts/index.html.erb +29 -0
  225. data/app/views/admin/block_layouts/new.html.erb +3 -0
  226. data/app/views/admin/block_layouts/show.html.erb +24 -0
  227. data/app/views/admin/block_record/_block_layout.html.erb +67 -0
  228. data/app/views/admin/block_record/_block_slot_field_buttons.html.erb +15 -0
  229. data/app/views/admin/block_record/_block_slot_fields.html.erb +37 -0
  230. data/app/views/admin/block_record/_block_slot_fields_template.html.erb +50 -0
  231. data/app/views/admin/dashboard/_panel.html.erb +30 -0
  232. data/app/views/admin/dashboard/index.html.erb +18 -0
  233. data/app/views/admin/documentation/_body.html.erb +9 -0
  234. data/app/views/admin/documentation/_sidebar.html.erb +17 -0
  235. data/app/views/admin/documentation/index.html.erb +87 -0
  236. data/app/views/admin/form_inputs/_slug.html.erb +11 -0
  237. data/app/views/admin/form_inputs/_status.html.erb +20 -0
  238. data/app/views/admin/form_inputs/collage/_media_item_fields.html.erb +41 -0
  239. data/app/views/admin/headers/_edit.html.erb +68 -0
  240. data/app/views/admin/headers/_index.html.erb +61 -0
  241. data/app/views/admin/headers/_new.html.erb +14 -0
  242. data/app/views/admin/headers/_show.html.erb +37 -0
  243. data/app/views/admin/imports/edit.html.erb +60 -0
  244. data/app/views/admin/media_items/_form.html.erb +54 -0
  245. data/app/views/admin/media_items/_media_item_grid_layout.html.erb +23 -0
  246. data/app/views/admin/media_items/_media_item_list_layout.html.erb +32 -0
  247. data/app/views/admin/media_items/_modal.html.erb +62 -0
  248. data/app/views/admin/media_items/edit.html.erb +3 -0
  249. data/app/views/admin/media_items/index.html.erb +58 -0
  250. data/app/views/admin/media_items/index.json.jbuilder +2 -0
  251. data/app/views/admin/media_items/new.html.erb +3 -0
  252. data/app/views/admin/menus/_form.html.erb +92 -0
  253. data/app/views/admin/menus/_list_item.html.erb +39 -0
  254. data/app/views/admin/menus/edit.html.erb +3 -0
  255. data/app/views/admin/menus/index.html.erb +29 -0
  256. data/app/views/admin/menus/new.html.erb +3 -0
  257. data/app/views/admin/menus/show.html.erb +3 -0
  258. data/app/views/admin/navigation/_resources_dropdown.html.erb +12 -0
  259. data/app/views/admin/pages/_additional_inputs.html.erb +1 -0
  260. data/app/views/admin/pages/_form.html.erb +52 -0
  261. data/app/views/admin/pages/_table_row.html.erb +14 -0
  262. data/app/views/admin/pages/edit.html.erb +3 -0
  263. data/app/views/admin/pages/index.html.erb +33 -0
  264. data/app/views/admin/pages/index.json.jbuilder +2 -0
  265. data/app/views/admin/pages/new.html.erb +3 -0
  266. data/app/views/admin/pages/show.html.erb +3 -0
  267. data/app/views/admin/settings/_form.html.erb +25 -0
  268. data/app/views/admin/settings/edit.html.erb +3 -0
  269. data/app/views/admin/settings/index.html.erb +38 -0
  270. data/app/views/admin/settings/new.html.erb +3 -0
  271. data/app/views/admin/settings/show.html.erb +19 -0
  272. data/app/views/admin/settings/types/_boolean.html.erb +1 -0
  273. data/app/views/admin/settings/types/_image.html.erb +10 -0
  274. data/app/views/admin/settings/types/_integer.html.erb +5 -0
  275. data/app/views/admin/settings/types/_string.html.erb +5 -0
  276. data/app/views/admin/settings/types/_text.html.erb +5 -0
  277. data/app/views/admin/shared/_admin_head_additions.html.erb +1 -0
  278. data/app/views/admin/shared/navigation/_navbar.html.erb +8 -0
  279. data/app/views/admin/translations/_form.html.erb +31 -0
  280. data/app/views/admin/translations/edit.html.erb +3 -0
  281. data/app/views/admin/translations/index.html.erb +41 -0
  282. data/app/views/admin/translations/new.html.erb +3 -0
  283. data/app/views/admin/translations/show.html.erb +19 -0
  284. data/app/views/admin/user_groups/_form.html.erb +11 -0
  285. data/app/views/admin/user_groups/edit.html.erb +3 -0
  286. data/app/views/admin/user_groups/index.html.erb +29 -0
  287. data/app/views/admin/user_groups/new.html.erb +3 -0
  288. data/app/views/admin/user_groups/show.html.erb +9 -0
  289. data/app/views/admin/users/_form.html.erb +41 -0
  290. data/app/views/admin/users/edit.html.erb +3 -0
  291. data/app/views/admin/users/index.html.erb +35 -0
  292. data/app/views/admin/users/new.html.erb +3 -0
  293. data/app/views/admin/users/show.html.erb +4 -0
  294. data/app/views/blocks/_show.html.erb +21 -0
  295. data/app/views/devise/confirmations/new.html.erb +28 -0
  296. data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
  297. data/app/views/devise/mailer/password_change.html.erb +3 -0
  298. data/app/views/devise/mailer/reset_password_instructions.html.erb +8 -0
  299. data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
  300. data/app/views/devise/passwords/edit.html.erb +31 -0
  301. data/app/views/devise/passwords/new.html.erb +27 -0
  302. data/app/views/devise/registrations/edit.html.erb +39 -0
  303. data/app/views/devise/registrations/new.html.erb +1 -0
  304. data/app/views/devise/sessions/_default_login.html.erb +25 -0
  305. data/app/views/devise/sessions/_first_time_login.html.erb +22 -0
  306. data/app/views/devise/sessions/new.html.erb +12 -0
  307. data/app/views/devise/shared/_flash_notifications.html.erb +3 -0
  308. data/app/views/devise/shared/_links.html.erb +25 -0
  309. data/app/views/devise/unlocks/new.html.erb +28 -0
  310. data/app/views/errors/internal_server_error.html.erb +2 -0
  311. data/app/views/errors/internal_server_error.json.jbuilder +4 -0
  312. data/app/views/errors/not_found.html.erb +2 -0
  313. data/app/views/errors/not_found.json.jbuilder +4 -0
  314. data/app/views/errors/unprocessable.html.erb +2 -0
  315. data/app/views/errors/unprocessable.json.jbuilder +4 -0
  316. data/app/views/forest/_analytics.html.erb +10 -0
  317. data/app/views/forest/_favicons.html.erb +7 -0
  318. data/app/views/kaminari/_first_page.html.erb +11 -0
  319. data/app/views/kaminari/_gap.html.erb +8 -0
  320. data/app/views/kaminari/_last_page.html.erb +11 -0
  321. data/app/views/kaminari/_next_page.html.erb +11 -0
  322. data/app/views/kaminari/_page.html.erb +12 -0
  323. data/app/views/kaminari/_paginator.html.erb +25 -0
  324. data/app/views/kaminari/_prev_page.html.erb +11 -0
  325. data/app/views/layouts/_shared_head.html.erb +48 -0
  326. data/app/views/layouts/admin.html.erb +19 -0
  327. data/app/views/layouts/application.html.erb +15 -0
  328. data/app/views/layouts/devise.html.erb +12 -0
  329. data/app/views/layouts/mailer.html.erb +13 -0
  330. data/app/views/layouts/mailer.text.erb +1 -0
  331. data/app/views/media_items/show.html.erb +29 -0
  332. data/app/views/pages/show.html.erb +1 -0
  333. data/app/views/public/index.html.erb +2 -0
  334. data/app/views/shared/_footer.html.erb +9 -0
  335. data/app/views/shared/_header.html.erb +9 -0
  336. data/app/views/shared/_header_colophon.html.erb +7 -0
  337. data/app/views/shared/_menu.html.erb +5 -0
  338. data/app/views/shared/_menu_item.html.erb +30 -0
  339. data/app/views/shared/_subnav.html.erb +7 -0
  340. data/config/initializers/aws.rb +17 -0
  341. data/config/initializers/devise.rb +275 -0
  342. data/config/initializers/forest.rb +5 -0
  343. data/config/initializers/paperclip.rb +1 -0
  344. data/config/initializers/rails_extensions.rb +6 -0
  345. data/config/initializers/simple_form.rb +164 -0
  346. data/config/initializers/simple_form/markdown.rb +21 -0
  347. data/config/initializers/simple_form/remote.rb +38 -0
  348. data/config/initializers/simple_form/sortable.rb +28 -0
  349. data/config/initializers/simple_form_bootstrap.rb +155 -0
  350. data/config/locales/devise.en.yml +62 -0
  351. data/config/locales/en.yml +24 -0
  352. data/config/locales/simple_form.en.yml +31 -0
  353. data/config/routes.rb +61 -0
  354. data/db/migrate/20161217191000_devise_create_users.rb +48 -0
  355. data/db/migrate/20161217200300_create_pages.rb +21 -0
  356. data/db/migrate/20161217205353_create_menus.rb +12 -0
  357. data/db/migrate/20161219033747_create_settings.rb +14 -0
  358. data/db/migrate/20161219231741_create_media_items.rb +18 -0
  359. data/db/migrate/20161225212203_create_user_groups.rb +10 -0
  360. data/db/migrate/20161225212258_create_join_table_user_groups_users.rb +8 -0
  361. data/db/migrate/20170109175932_create_block_kinds.rb +14 -0
  362. data/db/migrate/20170109180818_create_block_slots.rb +17 -0
  363. data/db/migrate/20170109185414_create_base_blocks.rb +8 -0
  364. data/db/migrate/20170818025124_create_block_layouts.rb +13 -0
  365. data/db/migrate/20170818025229_add_block_layout_to_block_slots.rb +5 -0
  366. data/db/migrate/20170818025500_remove_layout_from_block_slots.rb +5 -0
  367. data/db/migrate/20170821161755_add_unique_index_to_block_layouts.rb +6 -0
  368. data/db/migrate/20170826173346_create_translations.rb +12 -0
  369. data/db/migrate/20170910204934_index_attachment_content_type_on_media_items.rb +5 -0
  370. data/db/migrate/20180206035546_create_cache_records.rb +14 -0
  371. data/db/migrate/20180415034427_add_redirect_to_pages.rb +5 -0
  372. data/db/migrate/20180618190248_add_index_created_at_to_media_items.rb +5 -0
  373. data/db/migrate/20180625172226_add_blockable_metadata_to_pages.rb +6 -0
  374. data/db/migrate/20180626182949_remove_attachable_from_media_items.rb +7 -0
  375. data/db/migrate/20180712162414_add_media_item_status_to_media_items.rb +6 -0
  376. data/db/migrate/20190223155013_add_type_to_block_kinds.rb +6 -0
  377. data/db/migrate/20190423173909_add_seo_title_to_pages.rb +5 -0
  378. data/lib/forest.rb +9 -0
  379. data/lib/forest/engine.rb +51 -0
  380. data/lib/forest/migrations.rb +86 -0
  381. data/lib/forest/rails/active_record/has_many_ordered.rb +30 -0
  382. data/lib/forest/rails/active_record/has_repeatable.rb +19 -0
  383. data/lib/forest/rails/core_ext/enumerable.rb +17 -0
  384. data/lib/forest/version.rb +3 -0
  385. data/lib/generators/forest/block/USAGE +8 -0
  386. data/lib/generators/forest/block/block_generator.rb +103 -0
  387. data/lib/generators/forest/block/templates/_block.html.erb +2 -0
  388. data/lib/generators/forest/block/templates/_block.scss.erb +5 -0
  389. data/lib/generators/forest/block/templates/_block_edit_fields.html.erb +21 -0
  390. data/lib/generators/forest/block/templates/create_table_migration.rb +31 -0
  391. data/lib/generators/forest/block/templates/model.rb +41 -0
  392. data/lib/generators/forest/install/USAGE +2 -0
  393. data/lib/generators/forest/install/install_generator.rb +72 -0
  394. data/lib/generators/forest/scaffold/USAGE +8 -0
  395. data/lib/generators/forest/scaffold/scaffold_generator.rb +133 -0
  396. data/lib/generators/forest/scaffold/templates/admin_controller.rb +67 -0
  397. data/lib/generators/forest/scaffold/templates/controller.rb +20 -0
  398. data/lib/generators/forest/scaffold/templates/create_table_migration.rb +35 -0
  399. data/lib/generators/forest/scaffold/templates/model.rb +21 -0
  400. data/lib/generators/forest/scaffold/templates/policy.rb +2 -0
  401. data/lib/generators/forest/scaffold/templates/views/admin/_form.html.erb +56 -0
  402. data/lib/generators/forest/scaffold/templates/views/admin/edit.html.erb +3 -0
  403. data/lib/generators/forest/scaffold/templates/views/admin/index.html.erb +58 -0
  404. data/lib/generators/forest/scaffold/templates/views/admin/index.json.jbuilder +2 -0
  405. data/lib/generators/forest/scaffold/templates/views/admin/new.html.erb +3 -0
  406. data/lib/generators/forest/scaffold/templates/views/admin/show.html.erb +12 -0
  407. data/lib/generators/forest/scaffold/templates/views/public/index.html.erb +1 -0
  408. data/lib/generators/forest/scaffold/templates/views/public/show.html.erb +17 -0
  409. data/lib/tasks/forest_db_tasks.rake +176 -0
  410. data/lib/tasks/forest_elasticsearch_tasks.rake +22 -0
  411. metadata +719 -0
@@ -0,0 +1,43 @@
1
+ require 'csv'
2
+
3
+ class Import
4
+ attr_reader :klass, :file
5
+
6
+ def initialize(klass, file)
7
+ @klass = klass
8
+ @file = file
9
+
10
+ validate_import
11
+
12
+ # TODO: initialize import job instead of doing import here
13
+ perform!
14
+ end
15
+
16
+ def perform!
17
+ # TODO: this creates a new object per CSV row. It may be worth optimizing further to use
18
+ # something like this approach to do only a single insert.
19
+ # https://gist.github.com/jackrg/76ade1724bd816292e4e
20
+ CSV.foreach(file.tempfile,
21
+ headers: true,
22
+ encoding:'iso-8859-1:utf-8',
23
+ skip_blanks: true,
24
+ skip_lines: /^(?:,\s*)+$/,
25
+ header_converters: -> (f) { f.strip },
26
+ converters: -> (f) { f ? f.strip : nil } ) do |row|
27
+ attrs = row.to_h.compact
28
+ klass.find_or_create_by!(attrs)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def validate_import
35
+ unless file.content_type == 'text/csv'
36
+ raise "Import must be a .csv file."
37
+ end
38
+
39
+ unless klass.present?
40
+ raise "A valid model must be supplied to the import."
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,136 @@
1
+ class MediaItem < Forest::ApplicationRecord
2
+ include Rails.application.routes.url_helpers
3
+ include Attachable
4
+ include Forest::CacheBuster
5
+ include Sluggable
6
+
7
+ DATE_FILTER_CACHE_KEY = 'forest_media_item_dates_for_filter'
8
+ CONTENT_TYPE_CACHE_KEY = 'forest_media_item_content_types_for_filter'
9
+
10
+ validates_attachment_presence :attachment
11
+
12
+ before_validation :set_default_metadata
13
+
14
+ after_commit :expire_cache
15
+
16
+ has_many :pages, foreign_key: :featured_image_id
17
+
18
+ enum media_item_status: {
19
+ not_hidden: 0,
20
+ hidden: 1
21
+ }
22
+
23
+ scope :by_date, -> (date) {
24
+ begin
25
+ date = Date.parse(date)
26
+ where('media_items.created_at >= ? AND media_items.created_at <= ?', date.beginning_of_month, date.end_of_month)
27
+ rescue ArgumentError => e
28
+ date = nil
29
+ end
30
+ }
31
+
32
+ def self.resource_description
33
+ 'Media items consist of image, video and other file uploads.'
34
+ end
35
+
36
+ def self.dates_for_filter
37
+ Rails.cache.fetch DATE_FILTER_CACHE_KEY do
38
+ self.grouped_by_year_month.collect { |x| [x.created_at.strftime('%B %Y'), x.created_at.strftime('%d-%m-%Y')] }.reverse
39
+ end
40
+ end
41
+
42
+ def self.content_types_for_filter
43
+ Rails.cache.fetch CONTENT_TYPE_CACHE_KEY do
44
+ self.grouped_by_content_type.collect { |x| x.attachment_content_type }
45
+ end
46
+ end
47
+
48
+ def self.expire_cache!
49
+ Rails.cache.delete self::DATE_FILTER_CACHE_KEY
50
+ Rails.cache.delete self::CONTENT_TYPE_CACHE_KEY
51
+ end
52
+
53
+ def generate_slug
54
+ if self.slug.blank? || changed.include?('slug')
55
+ if title.present?
56
+ slug_attribute = title
57
+ elsif attachment_file_name.present?
58
+ slug_attribute = attachment_file_name
59
+ else
60
+ slug_attribute = SecureRandom.uuid
61
+ end
62
+
63
+ slug_attribute = slug_attribute.parameterize
64
+
65
+ if MediaItem.where(slug: slug_attribute).present?
66
+ slug_attribute = slug_attribute + '-' + SecureRandom.uuid
67
+ end
68
+
69
+ self.slug = slug_attribute
70
+ end
71
+ end
72
+
73
+ def glyphicon
74
+ if image?
75
+ 'glyphicon-picture'
76
+ elsif video?
77
+ 'glyphicon-play'
78
+ elsif attachment_content_type == 'application/zip'
79
+ 'glyphicon-folder-close'
80
+ else
81
+ 'glyphicon-file'
82
+ end
83
+ end
84
+
85
+ def to_jq_upload
86
+ {
87
+ 'id': self.id,
88
+ 'name': read_attribute(:title),
89
+ 'file_name': attachment_file_name,
90
+ 'is_image': image?,
91
+ 'glyphicon': glyphicon,
92
+ 'size': attachment.size,
93
+ 'url': edit_admin_media_item_path(self),
94
+ 'thumbnail_url': attachment.url(:medium),
95
+ 'delete_url': admin_media_item_path(id: id),
96
+ 'delete_type': 'DELETE'
97
+ }
98
+ end
99
+
100
+ # Portrait images have a lower aspect ratio
101
+ def aspect_ratio
102
+ dimensions[:width].to_f / dimensions[:height].to_f
103
+ end
104
+
105
+ def landscape?(ratio = 1)
106
+ aspect_ratio > ratio
107
+ end
108
+
109
+ def portrait?(ratio = 1)
110
+ !landscape?(ratio)
111
+ end
112
+
113
+ def to_select2_response
114
+ img_tag = "<img src='#{attachment.url(:thumb)}' style='height: 50px;'> " if image? && attachment.present?
115
+ "#{img_tag}<span class='select2-response__id'>#{id}</span> #{to_label}"
116
+ end
117
+
118
+ def to_select2_selection
119
+ img_tag = "<img src='#{attachment.url(:thumb)}' style='height: 20px; display: inline-block; vertical-align: top;'> " if image? && attachment.present?
120
+ "#{img_tag}<span class='select2-response__id'>#{id}</span> #{to_label}"
121
+ end
122
+
123
+ private
124
+
125
+ def self.grouped_by_year_month
126
+ self.select("DISTINCT ON (DATE_TRUNC('month', media_items.created_at)) *")
127
+ end
128
+
129
+ def self.grouped_by_content_type
130
+ self.select("DISTINCT ON (media_items.attachment_content_type) *")
131
+ end
132
+
133
+ def expire_cache
134
+ self.class.expire_cache!
135
+ end
136
+ end
@@ -0,0 +1,65 @@
1
+ class Menu < Forest::ApplicationRecord
2
+ include Sluggable
3
+
4
+ CACHE_KEY = 'forest_menus'
5
+
6
+ after_commit :expire_cache
7
+
8
+ validates :title, presence: true
9
+
10
+ def self.for(slug)
11
+ self.menus.select { |menu| menu.slug == slug.to_s }.first
12
+ end
13
+
14
+ def self.expire_cache!
15
+ Rails.cache.delete CACHE_KEY
16
+ end
17
+
18
+ def self.resource_description
19
+ "Menus are where you edit the navigation structure of your website. Menu items are composed of custom links or associations with pages."
20
+ end
21
+
22
+ def structure_as_json
23
+ JSON.parse(structure.presence || '[]')
24
+ end
25
+
26
+ def nokogiri
27
+ @nokogiri ||= begin
28
+ fragment = Nokogiri::HTML.fragment('<ul></ul>')
29
+ structure_as_json.each do |menu_item|
30
+ create_nokogiri_children(menu_item, fragment.at_css('ul'))
31
+ end
32
+ fragment
33
+ end
34
+ end
35
+
36
+ def render
37
+ nokogiri.to_html.html_safe
38
+ end
39
+
40
+ private
41
+
42
+ def self.menus
43
+ @memo ||= Rails.cache.fetch CACHE_KEY do
44
+ self.all.to_a
45
+ end
46
+ end
47
+
48
+ def self.reset_method_cache!
49
+ @memo = nil
50
+ end
51
+
52
+ def expire_cache
53
+ self.class.expire_cache!
54
+ end
55
+
56
+ def create_nokogiri_children(menu_item, node)
57
+ node.add_child("<li><a href='#{menu_item['url']}'>#{menu_item['name']}</a></li>")
58
+ if menu_item['children'].present?
59
+ node.css('> li').last.add_child('<ul></ul>')
60
+ menu_item['children'].each do |menu_item_child|
61
+ create_nokogiri_children(menu_item_child, node.css('> li').last.css('> ul').last)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,154 @@
1
+ class Page < Forest::ApplicationRecord
2
+ include Blockable
3
+ include Statusable
4
+ # include Versionable
5
+
6
+ before_validation :generate_slug
7
+ before_validation :generate_path, if: :hierarchy_changed?
8
+
9
+ after_save :touch_associated_pages, if: :hierarchy_changed?
10
+ after_save :expire_menu_cache
11
+ after_save :touch_self
12
+
13
+ after_destroy :remove_page_hierarchy!
14
+
15
+ validates_presence_of :title
16
+ validates_presence_of :slug
17
+ validates_presence_of :path
18
+ validates_uniqueness_of :slug, scope: :parent_page_id
19
+ validates_uniqueness_of :path
20
+ validate :parent_page_is_not_self
21
+
22
+ has_many :immediate_children, -> { by_title }, class_name: 'Page', foreign_key: 'parent_page_id'
23
+
24
+ belongs_to :featured_image, class_name: 'MediaItem', optional: true
25
+ belongs_to :parent_page, class_name: 'Page', optional: true
26
+
27
+ scope :by_parent_page, -> (orderer = :desc) { order("pages.parent_page_id #{orderer} NULLS #{orderer == :desc ? 'LAST' : 'FIRST'}, pages.title #{orderer}, pages.id ASC") }
28
+ scope :title_like, -> (string) { where('pages.title ILIKE ?', "%#{string}%") }
29
+ scope :parent_pages, -> { where(parent_page_id: nil).order(:title, :id) }
30
+ scope :non_parent_pages, -> { where('pages.parent_page_id IS NOT NULL').order(:title, :id) }
31
+
32
+ def self.resource_description
33
+ "Pages offer a flexible and modular way to present information."
34
+ end
35
+
36
+ def generate_slug
37
+ self.slug = title.parameterize if self.slug.blank?
38
+ end
39
+
40
+ def to_friendly_param
41
+ path
42
+ end
43
+
44
+ def page_ancestors
45
+ # TODO: these recursive function aren't performant and should be cached in the view
46
+ @_page_ancestors ||= begin
47
+ ancestors ||= []
48
+ page = self
49
+ parent = page.parent_page unless page.parent_page == page
50
+ while parent
51
+ ancestors << parent
52
+ page = parent
53
+ parent = page.try :parent_page
54
+ end
55
+ ancestors
56
+ end
57
+ end
58
+
59
+ def page_descendents
60
+ # TODO: these recursive function aren't performant and should be cached in the view
61
+ @_page_descendents ||= begin
62
+ descendents = []
63
+ children = self.immediate_children
64
+
65
+ while children.present?
66
+ descendents.concat children
67
+ children = children.collect(&:immediate_children).reject(&:blank?).flatten
68
+ end
69
+
70
+ descendents
71
+ end
72
+ end
73
+
74
+ def page_root
75
+ @_page_root ||= page_ancestors.find { |p| p.parent_page_id.nil? }
76
+ end
77
+
78
+ def descendent_of?(page_or_page_slug)
79
+ if page_or_page_slug.is_a?(Page)
80
+ page_ancestors.any? { |p| p == page_or_page_slug }
81
+ else
82
+ page_ancestors.any? { |p| p.slug == page_or_page_slug }
83
+ end
84
+ end
85
+
86
+ def all_associated_pages
87
+ @_all_associated_pages ||= page_ancestors.concat(page_ancestors.each.collect(&:page_descendents)).flatten
88
+ end
89
+
90
+ def remove_page_hierarchy!
91
+ pages_to_touch = Page.where(id: [self.id, *self.page_descendents.collect(&:id)])
92
+ self.immediate_children.update_all(parent_page_id: nil)
93
+ pages_to_touch.update_all(updated_at: DateTime.now)
94
+ end
95
+
96
+ def select2_format
97
+ ((page_ancestors.length + 1).times.collect{}.join('&mdash; ') + title).as_json
98
+ end
99
+
100
+ # Returns a collection of media items that represent the basic images on the page. Override this
101
+ # method in your host app if your pages often contain images stored under other attribute names.
102
+ def typical_media_items
103
+ media_item_ids = []
104
+ media_item_ids.push(featured_image_id) if featured_image_id.present?
105
+
106
+ blocks.each do |b|
107
+ media_item_ids.push(b.media_item_id) if b.try(:media_item_id).present?
108
+ media_item_ids.concat(b.media_item_ids) if b.try(:media_item_ids).present?
109
+ end
110
+
111
+ if media_item_ids.present?
112
+ MediaItem.where(id: media_item_ids.uniq)
113
+ else
114
+ MediaItem.none
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def touch_associated_pages
121
+ Page.where(id: all_associated_pages.collect(&:id)).update_all(updated_at: DateTime.now)
122
+ end
123
+
124
+ def current_or_previous_changes
125
+ self.changed.presence || self.previous_changes.keys
126
+ end
127
+
128
+ def hierarchy_changed?
129
+ (current_or_previous_changes & %w(parent_page_id slug)).any?
130
+ end
131
+
132
+ def generate_path
133
+ if page_ancestors.any?
134
+ generated_path = "#{page_ancestors.reverse.collect(&:slug).join('/')}/#{self.slug}"
135
+ else
136
+ generated_path = self.slug
137
+ end
138
+ self.path = generated_path
139
+ end
140
+
141
+ def parent_page_is_not_self
142
+ if parent_page == self
143
+ errors.add :parent_page, "a parent page can't be assigned to itself."
144
+ end
145
+ end
146
+
147
+ def expire_menu_cache
148
+ Menu.expire_cache!
149
+ end
150
+
151
+ def touch_self
152
+ self.touch unless self.new_record?
153
+ end
154
+ end
@@ -0,0 +1,142 @@
1
+ class Setting < Forest::ApplicationRecord
2
+ include Sluggable
3
+
4
+ CACHE_KEY = 'forest_settings'
5
+ APPLICATION_CACHE_KEY = 'forest_application_cache_key'
6
+ DEFAULT_SETTINGS = %i(site_title description featured_image)
7
+ VALID_VALUE_TYPES = %w(text boolean image integer string)
8
+
9
+ after_commit :touch_associations
10
+ after_commit :expire_cache
11
+
12
+ def self.for(key)
13
+ self.get(key).try(:value)
14
+ end
15
+
16
+ def self.get(key)
17
+ self.settings.select { |setting| setting.slug == key.to_s }.first
18
+ end
19
+
20
+ def self.expire_cache!
21
+ Rails.cache.delete CACHE_KEY
22
+ end
23
+
24
+ def self.expire_application_cache_key!
25
+ Rails.cache.delete APPLICATION_CACHE_KEY
26
+ end
27
+
28
+ def self.application_cache_key
29
+ Rails.cache.fetch APPLICATION_CACHE_KEY do
30
+ SecureRandom.uuid
31
+ end
32
+ end
33
+
34
+ # Define setting values, value types, and descriptions in your i18n file using the following pattern:
35
+ # en:
36
+ # forest:
37
+ # settings:
38
+ # banner_text:
39
+ # forest_setting_value: true
40
+ # forest_setting_value_type: boolean
41
+ # forest_setting_description: 'Enable a site-wide banner'
42
+ def self.initialize_from_i18n
43
+ I18n.backend.send(:init_translations) unless I18n.backend.initialized?
44
+ settings_from_i18n = I18n.backend.send(:translations).dig(:en, :forest, :settings).presence || {}
45
+
46
+ # Destroy any settings that are no longer in the i18n initialization or default settings array, and have matching updated and created at timestamps.
47
+ # This means any settings that are added directly from the database will be deleted.
48
+ Setting.all.reject { |setting|
49
+ Array(Setting::DEFAULT_SETTINGS).concat(settings_from_i18n.keys).include?(setting.slug.to_sym)
50
+ }.each { |setting|
51
+ if setting.has_not_been_updated?
52
+ logger.info { "[Forest][Setting] Destroying obsolete setting for #{setting.slug}" }
53
+ setting.destroy
54
+ end
55
+ }
56
+
57
+ settings_from_i18n.each do |setting|
58
+ k = setting[0].to_s
59
+ v = setting[1]
60
+
61
+ if v.is_a?(Hash) && v.keys.include?(:forest_setting_value)
62
+ v = v[:forest_setting_value]
63
+ value_type_from_i18n = setting[1][:forest_setting_value_type] unless setting[1].try(:[], :forest_setting_value_type).nil?
64
+ description_from_i18n = setting[1][:forest_setting_description] unless setting[1].try(:[], :forest_setting_description).nil?
65
+
66
+ if VALID_VALUE_TYPES.none?(value_type_from_i18n)
67
+ logger.warn { "[Forest][Setting] Warning: invalid value type '#{value_type_from_i18n}' for #{setting}. Setting value type to text." }
68
+ value_type_from_i18n = 'text'
69
+ end
70
+ end
71
+
72
+ if value_type_from_i18n == 'boolean'
73
+ if [true, 'true'].any?(v)
74
+ v = 1
75
+ else
76
+ v = 0
77
+ end
78
+ end
79
+
80
+ if [k, v].all?(&:present?)
81
+ s = Setting.get(k)
82
+
83
+ if s.blank?
84
+ s = Setting.new(title: k.titleize, slug: k)
85
+ end
86
+
87
+ s.value = v
88
+ s.value_type = value_type_from_i18n unless value_type_from_i18n.nil?
89
+ s.description = description_from_i18n unless description_from_i18n.nil?
90
+
91
+ if s.new_record?
92
+ logger.info { "[Forest][Setting] Creating new setting for #{k}" }
93
+ s.save
94
+ elsif s.has_not_been_updated?
95
+ if s.changed?
96
+ logger.info { "[Forest][Setting] Updating value for setting key #{k}" }
97
+ s.update_columns(title: k.titleize, slug: k, value: v)
98
+ s.update_column(:value_type, value_type_from_i18n) unless value_type_from_i18n.nil?
99
+ s.update_column(:description, description_from_i18n) unless description_from_i18n.nil?
100
+ end
101
+ end
102
+ else
103
+ logger.warn { "[Forest][Setting] Warning: unable to create setting for #{setting}. Key or value is blank." }
104
+ end
105
+ end
106
+
107
+ expire_cache!
108
+ end
109
+
110
+ def self.resource_description
111
+ "Settings are where you define static values that are used throughout the site. For example the title of your website, the description, and other values."
112
+ end
113
+
114
+ def slug_as_key?
115
+ true
116
+ end
117
+
118
+ def has_not_been_updated?
119
+ updated_at == created_at
120
+ end
121
+
122
+ private
123
+
124
+ def self.settings
125
+ @memo ||= Rails.cache.fetch CACHE_KEY do
126
+ self.all.to_a
127
+ end
128
+ end
129
+
130
+ def self.reset_method_cache!
131
+ @memo = nil
132
+ end
133
+
134
+ def expire_cache
135
+ self.class.expire_cache!
136
+ end
137
+
138
+ # Pages may depend on settings and should be updated each time a setting is changed
139
+ def touch_associations
140
+ Page.update_all(updated_at: Time.now)
141
+ end
142
+ end