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,67 @@
1
+ <% module_namespacing do -%>
2
+ class Admin::<%= plural_name.camelize %>Controller < Admin::ForestController
3
+ before_action :set_<%= singular_name %>, only: [:show, :edit, :update, :destroy]
4
+
5
+ def index
6
+ @<%= plural_name %> = apply_scopes(<%= name %>).by_id.page(params[:page])
7
+ end
8
+
9
+ def show
10
+ authorize @<%= singular_name %>
11
+ end
12
+
13
+ def new
14
+ @<%= singular_name %> = <%= name %>.new
15
+ authorize @<%= singular_name %>
16
+ end
17
+
18
+ def edit
19
+ authorize @<%= singular_name %>
20
+ end
21
+
22
+ def create
23
+ @<%= singular_name %> = <%= name %>.new(<%= singular_name %>_params)
24
+ authorize @<%= singular_name %>
25
+
26
+ if @<%= singular_name %>.save
27
+ redirect_to edit_admin_<%= singular_name %>_path(@<%= singular_name %>), notice: '<%= name %> was successfully created.'
28
+ else
29
+ render :new
30
+ end
31
+ end
32
+
33
+ def update
34
+ authorize @<%= singular_name %>
35
+
36
+ if @<%= singular_name %>.update(<%= singular_name %>_params)
37
+ redirect_to edit_admin_<%= singular_name %>_path(@<%= singular_name %>), notice: '<%= name %> was successfully updated.'
38
+ else
39
+ render :edit
40
+ end
41
+ end
42
+
43
+ def destroy
44
+ authorize @<%= singular_name %>
45
+ @<%= singular_name %>.destroy
46
+ redirect_to admin_<%= plural_name %>_url, notice: '<%= name %> was successfully destroyed.'
47
+ end
48
+
49
+ private
50
+
51
+ def <%= singular_name %>_params
52
+ # Add blockable params to the permitted attributes if this record is blockable `**BlockSlot.blockable_params`
53
+ <%
54
+ additional_attributes = []
55
+ additional_attributes.prepend(':status') unless options.skip_statusable?
56
+ additional_attributes.prepend(':slug') unless options.skip_sluggable?
57
+ additional_attributes = additional_attributes.join(', ')
58
+ additional_attributes = additional_attributes << ', ' if additional_attributes.present?
59
+ -%>
60
+ params.require(:<%= singular_name %>).permit(<%= additional_attributes -%><%= attributes.collect { |a| ":#{a.column_name}" }.join(', ') %>)
61
+ end
62
+
63
+ def set_<%= singular_name %>
64
+ @<%= singular_name %> = <%= name %>.find(params[:id])
65
+ end
66
+ end
67
+ <% end -%>
@@ -0,0 +1,20 @@
1
+ <% module_namespacing do -%>
2
+ class <%= plural_name.camelize %>Controller < ForestController
3
+ before_action :set_<%= singular_name %>, only: [:show]
4
+
5
+ def index
6
+ # TODO: published/statusable scope
7
+ @<%= plural_name %> = apply_scopes(<%= name %>)
8
+ end
9
+
10
+ def show
11
+ authorize @<%= singular_name %>
12
+ end
13
+
14
+ private
15
+
16
+ def set_<%= singular_name %>
17
+ @<%= singular_name %> = <%= name %>.find_by!(slug: params[:id])
18
+ end
19
+ end
20
+ <% end -%>
@@ -0,0 +1,35 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ <% attributes.each do |attribute| -%>
5
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
6
+ <% end -%>
7
+ <% unless options.skip_sluggable? -%>
8
+ t.string :slug
9
+ <% end -%>
10
+ <% unless options.skip_statusable? -%>
11
+ t.integer :status, default: 1, null: false
12
+ <% end -%>
13
+ <% unless options.skip_blockable? -%>
14
+ t.jsonb :blockable_metadata, default: {}
15
+ <% end -%>
16
+
17
+ t.timestamps
18
+ end
19
+ <% attributes.select(&:token?).each do |attribute| -%>
20
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
21
+ <% end -%>
22
+ <% attributes_with_index.each do |attribute| -%>
23
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
24
+ <% end -%>
25
+ <% unless options.skip_sluggable? -%>
26
+ add_index :<%= table_name %>, :slug, unique: true
27
+ <% end -%>
28
+ <% unless options.skip_statusable? -%>
29
+ add_index :<%= table_name %>, :status
30
+ <% end -%>
31
+ <% unless options.skip_blockable? -%>
32
+ add_index :<%= table_name %>, :blockable_metadata, using: :gin
33
+ <% end -%>
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %> < Forest::ApplicationRecord
3
+ <% unless options.skip_blockable? -%>
4
+ include Blockable
5
+ <% end -%>
6
+ <% unless options.skip_sluggable? -%>
7
+ include Sluggable
8
+ <% end -%>
9
+ <% unless options.skip_statusable? -%>
10
+ include Statusable
11
+ <% end -%>
12
+
13
+ <% attributes.each do |attribute| %><% if attribute.reference? -%>
14
+ belongs_to :<%= attribute.name %>
15
+
16
+ <% end -%><% end -%>
17
+ # def self.resource_description
18
+ # 'Briefly describe this resource.'
19
+ # end
20
+ end
21
+ <% end -%>
@@ -0,0 +1,2 @@
1
+ class <%= name %>Policy < BlockRecordPolicy
2
+ end
@@ -0,0 +1,56 @@
1
+ <%%= simple_form_for([:admin, @<%= singular_table_name %>]) do |f| %>
2
+ <%%= f.error_notification %>
3
+ <div class="row">
4
+ <div class="col-sm-12">
5
+ <div class="panel panel-default" data-collapse-parent>
6
+ <div class="panel-heading">
7
+ <h3 class="panel-title">
8
+ Metadata
9
+ <button type="button" class="btn btn-default btn-xs pull-right" data-collapse-trigger>
10
+ <div class="collapsable-icon glyphicon glyphicon-chevron-up"></div>
11
+ </button>
12
+ </h3>
13
+ </div>
14
+ <div class="panel-body" data-collapse-body>
15
+ <div class="form-inputs">
16
+ <div class="row">
17
+ <div class="col-sm-6">
18
+ <% unless options.skip_sluggable? -%>
19
+ <%%= render 'admin/form_inputs/slug', f: f %>
20
+ <% end -%>
21
+ <%- for attribute in attributes -%>
22
+ <%- if attribute.reference? -%>
23
+ <%- if attribute.name =~ /media_item/ -%>
24
+ <%%= f.association :<%= attribute.name %>, as: :image %>
25
+ <%- else -%>
26
+ <%%= f.association :<%= attribute.name %> %>
27
+ <%- end -%>
28
+ <%- else -%>
29
+ <%- if attribute.name =~ /date/ -%>
30
+ <%%= f.input :<%= attribute.name %>, as: :datepicker %>
31
+ <%- elsif attribute.name =~ /text/ -%>
32
+ <%%= f.input :<%= attribute.name %>, markdown: true %>
33
+ <%- else -%>
34
+ <%%= f.input :<%= attribute.name %> %>
35
+ <%- end -%>
36
+ <%- end -%>
37
+ <%- end -%>
38
+ <%%= f.button :submit, class: 'btn-success' %>
39
+ </div>
40
+ <div class="col-sm-6">
41
+ <% unless options.skip_statusable? -%>
42
+ <%%= render 'admin/form_inputs/status', f: f, record: @<%= singular_table_name %> %>
43
+ <% end -%>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <% unless options.skip_blockable? -%>
50
+ <%# Uncomment this if the record is blockable. Optionally pass the block_layout or block_kinds to the partial. %>
51
+ <%%#= render 'admin/block_record/block_layout', f: f, block_layout: @block_layout, block_kinds: @block_kinds %>
52
+ <%%#= f.button :submit, class: 'btn-success' %>
53
+ <% end %>
54
+ </div>
55
+ </div>
56
+ <%% end %>
@@ -0,0 +1,3 @@
1
+ <%%= render 'admin/headers/edit', record: @<%= singular_name %> %>
2
+
3
+ <%%= render 'form', record: @<%= singular_name %> %>
@@ -0,0 +1,58 @@
1
+ <%%= render 'admin/headers/index', records: @<%= plural_name %>, model_name: '<%= name %>' %>
2
+
3
+ <div class="table-responsive">
4
+ <table class="forest-table table table-striped">
5
+ <thead>
6
+ <tr>
7
+ <%% if <%= name %>.statusable? %>
8
+ <th></th>
9
+ <%% end %>
10
+ <th><%%= table_sorter title: 'ID', path: :admin_<%= plural_name %>_path, scope: :by_id, default_order: :desc %></th>
11
+ <%- unless options.skip_sluggable? -%>
12
+ <th><%%= table_sorter title: 'Slug', path: :admin_<%= plural_name %>_path, scope: :by_slug, default_order: :asc %></th>
13
+ <% end -%>
14
+ <%- for attribute in attributes -%>
15
+ <th><%= attribute.human_name.titleize %></th>
16
+ <% end -%>
17
+ <th><%%= table_sorter title: 'Created&nbsp;at'.html_safe, path: :admin_<%= plural_name %>_path, scope: :by_created_at, default_order: :desc %></th>
18
+ <th><%%= table_sorter title: 'Updated&nbsp;at'.html_safe, path: :admin_<%= plural_name %>_path, scope: :by_updated_at, default_order: :desc %></th>
19
+ <th colspan="3"></th>
20
+ </tr>
21
+ </thead>
22
+
23
+ <tbody>
24
+ <%% @<%= plural_name %>.each do |<%= singular_name %>| %>
25
+ <tr>
26
+ <%% if <%= name %>.statusable? %>
27
+ <td><%%= status_indicator <%= singular_name %> %></td>
28
+ <%% end %>
29
+ <td><%%= <%= singular_name %>.id %></td>
30
+ <%- unless options.skip_sluggable? -%>
31
+ <td><%%= <%= singular_name %>.slug %></td>
32
+ <% end -%>
33
+ <%- for attribute in attributes -%>
34
+ <td><%%= <%= singular_name %>.<%= attribute.column_name %> %></td>
35
+ <% end -%>
36
+ <td><%%= forest_date <%= singular_name %>.created_at %></td>
37
+ <td><%%= forest_date <%= singular_name %>.updated_at %></td>
38
+ <td width="1"><%%= link_to 'Edit', edit_admin_<%= singular_name %>_path(<%= singular_name %>), class: 'btn btn-xs btn-primary', role: 'button' %></td>
39
+ <td width="1">
40
+ <%% if try('<%= singular_name %>_path', <%= singular_name %>) %>
41
+ <%% if <%= singular_name %>.respond_to?(:to_friendly_param) %>
42
+ <%% param = <%= singular_name %>.to_friendly_param %>
43
+ <%% else %>
44
+ <%% param = <%= singular_name %> %>
45
+ <%% end %>
46
+ <%%= link_to 'Show', <%= singular_name %>_path(param), class: 'btn btn-xs btn-info', role: 'button', data: { turbolinks: false } %>
47
+ <%% else %>
48
+ <%%= link_to 'Show', admin_<%= singular_name %>_path(<%= singular_name %>), class: 'btn btn-xs btn-info', role: 'button', data: { turbolinks: false } %>
49
+ <%% end %>
50
+ </td>
51
+ <td width="1"><%%= link_to 'Destroy', [:admin, <%= singular_name %>], method: :delete, class: 'btn btn-xs btn-danger', role: 'button', data: { confirm: 'Are you sure you want to destroy this <%= singular_name.titleize %>?' } %></td>
52
+ </tr>
53
+ <%% end %>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+
58
+ <%%= paginate @<%= plural_name %> %>
@@ -0,0 +1,2 @@
1
+ json.partial! 'admin/api/index/header', records: @<%= plural_name %>
2
+ json.partial! 'admin/api/index/records', records: @<%= plural_name %>
@@ -0,0 +1,3 @@
1
+ <%%= render 'admin/headers/new', record: @<%= singular_name %> %>
2
+
3
+ <%%= render 'form', <%= singular_name %>: @<%= singular_name %> %>
@@ -0,0 +1,12 @@
1
+ <p>Find me in app/views/forest/<%= plural_name %>/show.html.erb</p>
2
+
3
+ <%%# content_for :page_description, @<%= singular_name %>.description if @<%= singular_name %>.try(:description).present? %>
4
+
5
+ <%- for attribute in attributes -%>
6
+ <p>
7
+ <b><%= attribute.name.titleize %></b>
8
+ <%%= @<%= singular_name %>.<%= attribute.name %> %>
9
+ </p>
10
+ <%- end -%>
11
+
12
+ <%%= render_blocks @<%= singular_name %> %>
@@ -0,0 +1 @@
1
+ <p>Find me in app/views/forest/<%= plural_name %>/index.html.erb</p>
@@ -0,0 +1,17 @@
1
+ <p>Find me in app/views/forest/<%= plural_name %>/show.html.erb</p>
2
+
3
+ <%%# content_for :page_description, @<%= singular_name %>.description if @<%= singular_name %>.try(:description).present? %>
4
+
5
+ <%- for attribute in attributes -%>
6
+ <p>
7
+ <b><%= attribute.name.titleize %></b>
8
+ <%%= @<%= singular_name %>.<%= attribute.name %> %>
9
+ </p>
10
+ <%- end -%>
11
+
12
+ <p>
13
+ <b>Status:</b>
14
+ <%%= @<%= singular_name %>.try(:status) %>
15
+ </p>
16
+
17
+ <%%= render_blocks @<%= singular_name %> %>
@@ -0,0 +1,176 @@
1
+ require 'open-uri'
2
+
3
+ namespace :forest do
4
+ desc "Download and replace this database with a current capture from heroku."
5
+ task 'db:capture' => :environment do
6
+ db = Rails.application.config.database_configuration[Rails.env]
7
+ user = db['username']
8
+ database = db['database']
9
+ puts "[Forest] This command captures a snapshot of the Heroku database, downloads it, drops the local database and recreates it from the Heroku snapshot."
10
+ puts "[Forest] Please run:"
11
+ puts "heroku pg:backups:capture && heroku pg:backups:download && bin/rails db:drop DISABLE_DATABASE_ENVIRONMENT_CHECK=1 && bin/rails db:create && pg_restore --verbose --clean --no-acl --no-owner -h localhost #{if user then "-U #{user}" end} -d #{database} latest.dump; rm latest.dump"
12
+ end
13
+
14
+ # https://gist.github.com/hopsoft/56ba6f55fe48ad7f8b90
15
+ desc "Dumps the database to db/APP_NAME.dump"
16
+ task 'db:dump' => :environment do
17
+ with_config do |app, db, user|
18
+ puts "[Forest] This task dumps the database to #{Rails.root}/db/#{app}.dump"
19
+ puts "[Forest] Please run:"
20
+ puts "pg_dump --host localhost #{if user then "-U #{user}" end} --verbose --clean --no-owner --no-acl --format=c #{db} > #{Rails.root}/db/#{app}.dump"
21
+ end
22
+ end
23
+
24
+ # https://gist.github.com/hopsoft/56ba6f55fe48ad7f8b90
25
+ desc "Restores the database dump at db/APP_NAME.dump"
26
+ task 'db:restore' => :environment do
27
+ with_config do |app, db, user|
28
+ puts "[Forest] This task restores the database from #{Rails.root}/db/#{app}.dump"
29
+ puts "[Forest] Please run:"
30
+ puts 'bin/rails db:drop DISABLE_DATABASE_ENVIRONMENT_CHECK=1 && bin/rails db:create'
31
+ puts "pg_restore --verbose --host localhost #{if user then "-U #{user}" end} --clean --no-owner --no-acl --dbname #{db} #{Rails.root}/db/#{app}.dump"
32
+ end
33
+ end
34
+
35
+ desc "Import the database to Heroku."
36
+ task 'db:import_to_heroku' => :environment do
37
+ with_config do |app, db, user|
38
+ check_for_s3_env_variables!
39
+
40
+ timestamp = Time.now.to_i
41
+
42
+ object_key = "forest/forest_db_dumps/#{app}_#{timestamp}.dump"
43
+ if s3_bucket.object(object_key).upload_file("#{Rails.root}/db/#{app}.dump")
44
+ s3.client.put_object_acl({
45
+ acl: 'public-read',
46
+ bucket: s3_bucket.name,
47
+ key: object_key
48
+ })
49
+
50
+ puts "[Forest] Careful! You just uploaded a publicly accessible db dump to the #{s3_bucket.name} bucket."
51
+ puts "[Forest] To import that db dump to Heroku, please run:"
52
+
53
+ if aws_region == 'us-east-1'
54
+ puts "heroku pg:backups:restore 'https://s3.amazonaws.com/#{s3_bucket_name}/#{object_key}' DATABASE_URL"
55
+ else
56
+ puts "heroku pg:backups:restore 'https://#{s3_bucket_name}.s3.amazonaws.com/#{object_key}' DATABASE_URL"
57
+ end
58
+
59
+ puts "\n"
60
+ puts "[Forest] ** After importing to Heroku, run this command to delete the public db dump from Amazon S3. Don't leave the db dump publicly accessible! **"
61
+ puts "bin/rails forest:db:destroy_s3_dump object_key=#{object_key}"
62
+ else
63
+ puts "[Forest] Error: unable to upload object to S3."
64
+ end
65
+ end
66
+ end
67
+
68
+ desc "Destroy a database dump from Amazon S3."
69
+ task 'db:destroy_s3_dump' => :environment do
70
+ check_for_s3_env_variables!
71
+
72
+ if s3_bucket.object(ENV['object_key']).delete
73
+ puts "[Forest] #{ENV['object_key']} destroyed"
74
+ remaining_objects = s3_bucket.objects(prefix: 'forest/forest_db_dumps/').collect(&:key).reject { |k| k == 'forest/forest_db_dumps/' }
75
+ if remaining_objects.present?
76
+ puts "[Forest] Warning: There are still files in the forest/forest_db_dumps directory (this directory should be empty!). Please delete these publicly accessible files."
77
+ puts "[Forest] Run the following commands to delete the objects:"
78
+ remaining_objects.each do |object_key|
79
+ puts "bin/rails forest:db:destroy_s3_dump object_key=#{object_key}"
80
+ end
81
+ end
82
+ else
83
+ puts "[Forest] Error: unable to destroy #{ENV['object_key']}. Log in to Amazon S3 and destroy the object manually."
84
+ end
85
+ end
86
+
87
+ desc "Upload the latest Heroku database to S3 for archival purposes. This assumes you have already enabled daily pg backups on Heroku."
88
+ task 'db:archive_to_s3' => :environment do
89
+ # TODO: This won't work as is because the heroku CLI isn't available by default on
90
+ # heroku. It may be better to use https://github.com/kbaum/heroku-database-backups.
91
+ # TODO: add configuration that allows setting a maximum number of database backups.
92
+ # The price class could also be adjusted, for example setting S3 One Zone-IA Storage
93
+ # for very old files to save $$.
94
+
95
+ check_for_s3_env_variables!
96
+
97
+ url = `heroku pg:backups public-url`
98
+ url.sub!(/\n$/, '')
99
+
100
+ db_uri = URI.parse(url)
101
+
102
+ file_name = db_uri.path.split('/').reject(&:blank?).join('_')
103
+ object_key = "forest/forest_db_archives/#{database_name}/#{file_name}.gz"
104
+ object = s3_bucket.object(object_key)
105
+
106
+ abort("[Forest] Database already exists on S3, aborting #{s3_bucket_name} #{object_key}") if object.exists?
107
+
108
+ file = Tempfile.new(["#{file_name}", '.sql'])
109
+
110
+ puts "[Forest] Downloading database from Heroku"
111
+ open(url) do |f|
112
+ IO.copy_stream(f, file)
113
+ end
114
+
115
+ puts "[Forest] Compressing database file"
116
+
117
+ zipped_file = Tempfile.new(["#{file_name}", '.gz'])
118
+
119
+ Zlib::GzipWriter.open(zipped_file) do |gz|
120
+ gz.mtime = File.mtime(file)
121
+ gz.orig_name = "#{file_name}.sql"
122
+ File.open(file) do |file|
123
+ while chunk = file.read(16*1024) do
124
+ gz.write(chunk)
125
+ end
126
+ end
127
+ end
128
+
129
+ puts "[Forest] Uploading database file to S3"
130
+ if object.upload_file(zipped_file)
131
+ s3.client.put_object_acl({
132
+ acl: 'private',
133
+ bucket: s3_bucket.name,
134
+ key: object_key
135
+ })
136
+ else
137
+ puts "[Forest] Error: unable to upload object to S3."
138
+ end
139
+
140
+ file.delete
141
+ zipped_file.delete
142
+ end
143
+
144
+ private
145
+
146
+ def with_config
147
+ yield Rails.application.class.parent_name.underscore,
148
+ database_name,
149
+ ActiveRecord::Base.connection_config[:username]
150
+ end
151
+
152
+ def s3
153
+ @s3 ||= Aws::S3::Resource.new(region: aws_region)
154
+ end
155
+
156
+ def s3_bucket
157
+ @s3_bucket ||= s3.bucket(s3_bucket_name)
158
+ end
159
+
160
+ def check_for_s3_env_variables!
161
+ abort('[Forest] Error: Please specify an AWS_REGION environment variable') if aws_region.blank?
162
+ abort('[Forest] Error: Please specify an S3_BUCKET_NAME environment variable') if s3_bucket_name.blank?
163
+ end
164
+
165
+ def aws_region
166
+ @aws_region ||= ENV['AWS_REGION'].presence || Rails.application.credentials&.dig(:aws, :aws_region)
167
+ end
168
+
169
+ def s3_bucket_name
170
+ @s3_bucket_name ||= ENV['S3_BUCKET_NAME'].presence || Rails.application.credentials&.dig(:aws, :s3_bucket_name)
171
+ end
172
+
173
+ def database_name
174
+ @database_name ||= ActiveRecord::Base.connection_config[:database]
175
+ end
176
+ end