alchemy_cms 5.3.4 → 6.0.0.b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (295) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -14
  3. data/.gitignore +1 -1
  4. data/.hound.yml +1 -1
  5. data/.rubocop.yml +46 -4
  6. data/CHANGELOG.md +80 -69
  7. data/Gemfile +4 -2
  8. data/README.md +5 -2
  9. data/Rakefile +0 -18
  10. data/alchemy_cms.gemspec +78 -65
  11. data/app/assets/javascripts/alchemy/admin.js +2 -2
  12. data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -30
  13. data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -1
  14. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +2 -7
  15. data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -25
  16. data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +1 -1
  17. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +2 -0
  18. data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +1 -1
  19. data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +3 -1
  20. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +1 -1
  21. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +40 -29
  22. data/app/assets/javascripts/alchemy/alchemy.page_sorter.js +24 -0
  23. data/app/assets/javascripts/alchemy/alchemy.sitemap.js.coffee +119 -0
  24. data/app/assets/javascripts/alchemy/page_select.js +8 -13
  25. data/app/assets/javascripts/alchemy/templates/index.js +0 -1
  26. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
  27. data/app/assets/javascripts/alchemy/templates/page.hbs +7 -17
  28. data/app/assets/stylesheets/alchemy/admin.scss +1 -1
  29. data/app/assets/stylesheets/alchemy/archive.scss +4 -8
  30. data/app/assets/stylesheets/alchemy/buttons.scss +0 -4
  31. data/app/assets/stylesheets/alchemy/elements.scss +73 -61
  32. data/app/assets/stylesheets/alchemy/images.scss +8 -0
  33. data/app/assets/stylesheets/alchemy/node-select.scss +4 -3
  34. data/app/assets/stylesheets/alchemy/page-select.scss +5 -29
  35. data/app/assets/stylesheets/alchemy/sitemap.scss +7 -9
  36. data/app/assets/stylesheets/tinymce/skins/alchemy/content.min.css.scss +3 -3
  37. data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +7 -7
  38. data/app/controllers/alchemy/admin/attachments_controller.rb +6 -2
  39. data/app/controllers/alchemy/admin/base_controller.rb +8 -16
  40. data/app/controllers/alchemy/admin/elements_controller.rb +58 -34
  41. data/app/controllers/alchemy/admin/essence_audios_controller.rb +30 -0
  42. data/app/controllers/alchemy/admin/essence_files_controller.rb +0 -14
  43. data/app/controllers/alchemy/admin/essence_pictures_controller.rb +8 -79
  44. data/app/controllers/alchemy/admin/essence_videos_controller.rb +33 -0
  45. data/app/controllers/alchemy/admin/ingredients_controller.rb +30 -0
  46. data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -1
  47. data/app/controllers/alchemy/admin/pages_controller.rb +19 -23
  48. data/app/controllers/alchemy/admin/pictures_controller.rb +35 -9
  49. data/app/controllers/alchemy/api/elements_controller.rb +10 -5
  50. data/app/controllers/alchemy/api/pages_controller.rb +3 -15
  51. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +13 -3
  52. data/app/controllers/concerns/alchemy/admin/crop_action.rb +26 -0
  53. data/app/decorators/alchemy/element_editor.rb +23 -1
  54. data/app/decorators/alchemy/ingredient_editor.rb +154 -0
  55. data/app/helpers/alchemy/admin/elements_helper.rb +1 -0
  56. data/app/helpers/alchemy/admin/essences_helper.rb +1 -1
  57. data/app/helpers/alchemy/admin/ingredients_helper.rb +42 -0
  58. data/app/helpers/alchemy/elements_block_helper.rb +22 -7
  59. data/app/helpers/alchemy/elements_helper.rb +14 -7
  60. data/app/helpers/alchemy/pages_helper.rb +3 -11
  61. data/app/jobs/alchemy/base_job.rb +11 -0
  62. data/app/jobs/alchemy/publish_page_job.rb +11 -0
  63. data/app/models/alchemy/attachment.rb +1 -1
  64. data/app/models/alchemy/content/factory.rb +23 -27
  65. data/app/models/alchemy/content.rb +1 -6
  66. data/app/models/alchemy/element/definitions.rb +29 -27
  67. data/app/models/alchemy/element/element_contents.rb +131 -122
  68. data/app/models/alchemy/element/element_essences.rb +100 -98
  69. data/app/models/alchemy/element/element_ingredients.rb +176 -0
  70. data/app/models/alchemy/element/presenters.rb +89 -87
  71. data/app/models/alchemy/element.rb +40 -73
  72. data/app/models/alchemy/elements_repository.rb +126 -0
  73. data/app/models/alchemy/essence_audio.rb +12 -0
  74. data/app/models/alchemy/essence_headline.rb +40 -0
  75. data/app/models/alchemy/essence_picture.rb +4 -116
  76. data/app/models/alchemy/essence_richtext.rb +12 -0
  77. data/app/models/alchemy/essence_video.rb +12 -0
  78. data/app/models/alchemy/image_cropper_settings.rb +87 -0
  79. data/app/models/alchemy/ingredient.rb +219 -0
  80. data/app/models/alchemy/ingredient_validator.rb +97 -0
  81. data/app/models/alchemy/ingredients/audio.rb +29 -0
  82. data/app/models/alchemy/ingredients/boolean.rb +21 -0
  83. data/app/models/alchemy/ingredients/datetime.rb +20 -0
  84. data/app/models/alchemy/ingredients/file.rb +30 -0
  85. data/app/models/alchemy/ingredients/headline.rb +42 -0
  86. data/app/models/alchemy/ingredients/html.rb +19 -0
  87. data/app/models/alchemy/ingredients/link.rb +16 -0
  88. data/app/models/alchemy/ingredients/node.rb +23 -0
  89. data/app/models/alchemy/ingredients/page.rb +23 -0
  90. data/app/models/alchemy/ingredients/picture.rb +41 -0
  91. data/app/models/alchemy/ingredients/richtext.rb +57 -0
  92. data/app/models/alchemy/ingredients/select.rb +10 -0
  93. data/app/models/alchemy/ingredients/text.rb +17 -0
  94. data/app/models/alchemy/ingredients/video.rb +33 -0
  95. data/app/models/alchemy/language.rb +0 -11
  96. data/app/models/alchemy/node.rb +1 -1
  97. data/app/models/alchemy/page/fixed_attributes.rb +53 -51
  98. data/app/models/alchemy/page/page_elements.rb +186 -205
  99. data/app/models/alchemy/page/page_naming.rb +66 -64
  100. data/app/models/alchemy/page/page_natures.rb +139 -142
  101. data/app/models/alchemy/page/page_scopes.rb +113 -102
  102. data/app/models/alchemy/page/publisher.rb +50 -0
  103. data/app/models/alchemy/page/url_path.rb +1 -1
  104. data/app/models/alchemy/page.rb +69 -36
  105. data/app/models/alchemy/page_version.rb +58 -0
  106. data/app/models/alchemy/picture/calculations.rb +2 -8
  107. data/app/models/alchemy/picture/preprocessor.rb +2 -0
  108. data/app/models/alchemy/picture/transformations.rb +21 -93
  109. data/app/models/alchemy/picture.rb +4 -2
  110. data/app/models/concerns/alchemy/picture_thumbnails.rb +181 -0
  111. data/app/models/concerns/alchemy/touch_elements.rb +2 -2
  112. data/app/presenters/alchemy/picture_view.rb +88 -0
  113. data/app/serializers/alchemy/element_serializer.rb +5 -0
  114. data/app/serializers/alchemy/page_serializer.rb +1 -7
  115. data/app/serializers/alchemy/page_tree_serializer.rb +6 -5
  116. data/app/services/alchemy/delete_elements.rb +44 -0
  117. data/app/services/alchemy/duplicate_element.rb +56 -0
  118. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +1 -2
  119. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  120. data/app/views/alchemy/admin/attachments/assign.js.erb +11 -0
  121. data/app/views/alchemy/admin/crop.html.erb +36 -0
  122. data/app/views/alchemy/admin/elements/_element.html.erb +14 -10
  123. data/app/views/alchemy/admin/elements/{_element_footer.html.erb → _footer.html.erb} +0 -0
  124. data/app/views/alchemy/admin/elements/{_new_element_form.html.erb → _form.html.erb} +1 -1
  125. data/app/views/alchemy/admin/elements/{_element_header.html.erb → _header.html.erb} +1 -1
  126. data/app/views/alchemy/admin/elements/{_element_toolbar.html.erb → _toolbar.html.erb} +5 -6
  127. data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +1 -3
  128. data/app/views/alchemy/admin/elements/new.html.erb +3 -3
  129. data/app/views/alchemy/admin/elements/order.js.erb +0 -17
  130. data/app/views/alchemy/admin/elements/update.js.erb +3 -2
  131. data/app/views/alchemy/admin/essence_audios/edit.html.erb +7 -0
  132. data/app/views/alchemy/admin/essence_pictures/update.js.erb +0 -1
  133. data/app/views/alchemy/admin/essence_videos/edit.html.erb +11 -0
  134. data/app/views/alchemy/admin/ingredients/_audio_fields.html.erb +4 -0
  135. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +18 -0
  136. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +25 -0
  137. data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +8 -0
  138. data/app/views/alchemy/admin/ingredients/edit.html.erb +4 -0
  139. data/app/views/alchemy/admin/layoutpages/edit.html.erb +0 -5
  140. data/app/views/alchemy/admin/nodes/_node.html.erb +2 -2
  141. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +1 -1
  142. data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
  143. data/app/views/alchemy/admin/pages/_file_link.html.erb +1 -1
  144. data/app/views/alchemy/admin/pages/_form.html.erb +0 -25
  145. data/app/views/alchemy/admin/pages/_internal_link.html.erb +1 -1
  146. data/app/views/alchemy/admin/pages/_new_page_form.html.erb +5 -16
  147. data/app/views/alchemy/admin/pages/_page.html.erb +133 -111
  148. data/app/views/alchemy/admin/pages/_sitemap.html.erb +16 -10
  149. data/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb +5 -2
  150. data/app/views/alchemy/admin/pages/_toolbar.html.erb +12 -0
  151. data/app/views/alchemy/admin/pages/edit.html.erb +37 -25
  152. data/app/views/alchemy/admin/pages/fold.js.erb +2 -0
  153. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  154. data/app/views/alchemy/admin/pages/sort.html.erb +19 -0
  155. data/app/views/alchemy/admin/pages/update.js.erb +0 -7
  156. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -4
  157. data/app/views/alchemy/admin/partials/_routes.html.erb +8 -19
  158. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +4 -8
  159. data/app/views/alchemy/admin/pictures/_infos.html.erb +0 -1
  160. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +4 -4
  161. data/app/views/alchemy/admin/pictures/assign.js.erb +10 -0
  162. data/app/views/alchemy/admin/resources/_form.html.erb +1 -0
  163. data/app/views/alchemy/essences/_essence_audio_editor.html.erb +4 -0
  164. data/app/views/alchemy/essences/_essence_audio_view.html.erb +15 -0
  165. data/app/views/alchemy/essences/_essence_file_editor.html.erb +15 -6
  166. data/app/views/alchemy/essences/_essence_headline_editor.html.erb +36 -0
  167. data/app/views/alchemy/essences/_essence_headline_view.html.erb +10 -0
  168. data/app/views/alchemy/essences/_essence_link_editor.html.erb +8 -4
  169. data/app/views/alchemy/essences/_essence_page_editor.html.erb +1 -1
  170. data/app/views/alchemy/essences/_essence_picture_editor.html.erb +27 -12
  171. data/app/views/alchemy/essences/_essence_text_editor.html.erb +12 -4
  172. data/app/views/alchemy/essences/_essence_video_editor.html.erb +4 -0
  173. data/app/views/alchemy/essences/_essence_video_view.html.erb +18 -0
  174. data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +21 -16
  175. data/app/views/alchemy/essences/shared/_linkable_essence_tools.html.erb +2 -2
  176. data/app/views/alchemy/ingredients/_audio_editor.html.erb +5 -0
  177. data/app/views/alchemy/ingredients/_audio_view.html.erb +14 -0
  178. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +11 -0
  179. data/app/views/alchemy/ingredients/_boolean_view.html.erb +1 -0
  180. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +17 -0
  181. data/app/views/alchemy/ingredients/_datetime_view.html.erb +9 -0
  182. data/app/views/alchemy/ingredients/_file_editor.html.erb +50 -0
  183. data/app/views/alchemy/ingredients/_file_view.html.erb +17 -0
  184. data/app/views/alchemy/ingredients/_headline_editor.html.erb +30 -0
  185. data/app/views/alchemy/ingredients/_headline_view.html.erb +9 -0
  186. data/app/views/alchemy/ingredients/_html_editor.html.erb +8 -0
  187. data/app/views/alchemy/ingredients/_html_view.html.erb +1 -0
  188. data/app/views/alchemy/ingredients/_link_editor.html.erb +24 -0
  189. data/app/views/alchemy/ingredients/_link_view.html.erb +9 -0
  190. data/app/views/alchemy/ingredients/_node_editor.html.erb +25 -0
  191. data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
  192. data/app/views/alchemy/ingredients/_page_editor.html.erb +24 -0
  193. data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
  194. data/app/views/alchemy/ingredients/_picture_editor.html.erb +59 -0
  195. data/app/views/alchemy/ingredients/_picture_view.html.erb +5 -0
  196. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +12 -0
  197. data/app/views/alchemy/ingredients/_richtext_view.html.erb +3 -0
  198. data/app/views/alchemy/ingredients/_select_editor.html.erb +29 -0
  199. data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
  200. data/app/views/alchemy/ingredients/_text_editor.html.erb +19 -0
  201. data/app/views/alchemy/ingredients/_text_view.html.erb +16 -0
  202. data/app/views/alchemy/ingredients/_video_editor.html.erb +5 -0
  203. data/app/views/alchemy/ingredients/_video_view.html.erb +17 -0
  204. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +20 -0
  205. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +57 -0
  206. data/config/brakeman.ignore +66 -159
  207. data/config/initializers/dragonfly.rb +2 -0
  208. data/config/locales/alchemy.en.yml +26 -15
  209. data/config/routes.rb +19 -26
  210. data/db/migrate/20201207131309_create_page_versions.rb +19 -0
  211. data/db/migrate/20201207135820_add_page_version_id_to_alchemy_elements.rb +76 -0
  212. data/db/migrate/20210205143548_rename_public_on_and_public_until_on_alchemy_pages.rb +10 -0
  213. data/db/migrate/20210326105046_add_sanitized_body_to_alchemy_essence_richtexts.rb +7 -0
  214. data/db/migrate/20210406093436_add_alchemy_essence_headlines.rb +12 -0
  215. data/db/migrate/20210506135919_create_essence_audios.rb +19 -0
  216. data/db/migrate/20210506140258_create_essence_videos.rb +23 -0
  217. data/db/migrate/20210508091432_create_alchemy_ingredients.rb +22 -0
  218. data/lib/alchemy/admin/preview_url.rb +2 -0
  219. data/lib/alchemy/deprecation.rb +1 -1
  220. data/lib/alchemy/dragonfly/processors/auto_orient.rb +18 -0
  221. data/lib/alchemy/elements_finder.rb +14 -60
  222. data/lib/alchemy/engine.rb +1 -8
  223. data/lib/alchemy/essence.rb +1 -2
  224. data/lib/alchemy/hints.rb +8 -4
  225. data/lib/alchemy/page_layout.rb +0 -13
  226. data/lib/alchemy/permissions.rb +31 -29
  227. data/lib/alchemy/resource.rb +13 -3
  228. data/lib/alchemy/tasks/tidy.rb +29 -0
  229. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -1
  230. data/lib/alchemy/test_support/factories/element_factory.rb +8 -8
  231. data/lib/alchemy/test_support/factories/essence_audio_factory.rb +7 -0
  232. data/lib/alchemy/test_support/factories/essence_video_factory.rb +7 -0
  233. data/lib/alchemy/test_support/factories/ingredient_factory.rb +25 -0
  234. data/lib/alchemy/test_support/factories/page_factory.rb +20 -1
  235. data/lib/alchemy/test_support/factories/page_version_factory.rb +23 -0
  236. data/lib/alchemy/test_support/having_crop_action_examples.rb +170 -0
  237. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +646 -0
  238. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +21 -0
  239. data/lib/alchemy/test_support/shared_ingredient_examples.rb +57 -0
  240. data/lib/alchemy/test_support.rb +2 -11
  241. data/lib/alchemy/tinymce.rb +17 -0
  242. data/lib/alchemy/upgrader/five_point_zero.rb +0 -32
  243. data/lib/alchemy/upgrader/six_point_zero.rb +21 -0
  244. data/lib/alchemy/upgrader/tasks/add_page_versions.rb +33 -0
  245. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +51 -0
  246. data/lib/alchemy/version.rb +1 -1
  247. data/lib/alchemy_cms.rb +0 -1
  248. data/lib/generators/alchemy/elements/elements_generator.rb +1 -0
  249. data/lib/generators/alchemy/elements/templates/view.html.erb +9 -0
  250. data/lib/generators/alchemy/elements/templates/view.html.haml +9 -0
  251. data/lib/generators/alchemy/elements/templates/view.html.slim +9 -0
  252. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +38 -0
  253. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +14 -0
  254. data/lib/generators/alchemy/ingredient/templates/model.rb.tt +13 -0
  255. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -0
  256. data/lib/generators/alchemy/install/install_generator.rb +1 -2
  257. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +1 -1
  258. data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
  259. data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
  260. data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
  261. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  262. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  263. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  264. data/lib/tasks/alchemy/tidy.rake +12 -0
  265. data/lib/tasks/alchemy/upgrade.rake +21 -15
  266. data/package/admin.js +8 -4
  267. data/package/src/file_editors.js +28 -0
  268. data/package/src/image_cropper.js +103 -0
  269. data/package/src/image_loader.js +58 -0
  270. data/package/src/node_tree.js +8 -8
  271. data/package/src/picture_editors.js +169 -0
  272. data/package/src/utils/__tests__/ajax.spec.js +16 -52
  273. data/package/src/utils/ajax.js +0 -12
  274. data/package.json +3 -2
  275. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
  276. data/vendor/assets/javascripts/jquery_plugins/jquery.ui.nestedSortable.js +434 -0
  277. data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
  278. metadata +291 -53
  279. data/.rspec +0 -1
  280. data/app/assets/javascripts/alchemy/alchemy.image_cropper.js.coffee +0 -44
  281. data/app/assets/javascripts/alchemy/alchemy.trash_window.js.coffee +0 -30
  282. data/app/assets/javascripts/alchemy/templates/page_folder.hbs +0 -3
  283. data/app/assets/stylesheets/alchemy/trash.scss +0 -8
  284. data/app/controllers/alchemy/admin/trash_controller.rb +0 -44
  285. data/app/views/alchemy/admin/essence_files/assign.js.erb +0 -3
  286. data/app/views/alchemy/admin/essence_pictures/assign.js.erb +0 -4
  287. data/app/views/alchemy/admin/essence_pictures/crop.html.erb +0 -48
  288. data/app/views/alchemy/admin/trash/clear.js.erb +0 -4
  289. data/app/views/alchemy/admin/trash/index.html.erb +0 -31
  290. data/lib/alchemy/error_tracking/airbrake_handler.rb +0 -13
  291. data/lib/alchemy/error_tracking.rb +0 -14
  292. data/lib/alchemy/test_support/factories.rb +0 -20
  293. data/package/src/page_publication_fields.js +0 -27
  294. data/package/src/page_sorter.js +0 -62
  295. data/package/src/sitemap.js +0 -148
@@ -0,0 +1,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for "having picture thumbnails" do
4
+ it "should not store negative values for crop values" do
5
+ record.crop_from = "-1x100"
6
+ record.crop_size = "-20x30"
7
+ record.save
8
+ expect(record.crop_from).to eq("0x100")
9
+ expect(record.crop_size).to eq("0x30")
10
+ end
11
+
12
+ it "should not store float values for crop values" do
13
+ record.crop_from = "0.05x104.5"
14
+ record.crop_size = "99.5x203.4"
15
+ record.save
16
+ expect(record.crop_from).to eq("0x105")
17
+ expect(record.crop_size).to eq("100x203")
18
+ end
19
+
20
+ it "should not store empty strings for nil crop values" do
21
+ record.crop_from = nil
22
+ record.crop_size = nil
23
+ record.save
24
+ expect(record.crop_from).to eq(nil)
25
+ expect(record.crop_size).to eq(nil)
26
+ end
27
+
28
+ describe "#picture_url" do
29
+ subject(:picture_url) { record.picture_url(options) }
30
+
31
+ let(:options) { {} }
32
+ let(:picture) { create(:alchemy_picture) }
33
+
34
+ context "with no format in the options" do
35
+ it "includes the image's default render format." do
36
+ expect(picture_url).to match(/\.png/)
37
+ end
38
+ end
39
+
40
+ context "with format in the options" do
41
+ let(:options) { { format: "gif" } }
42
+
43
+ it "takes this as format." do
44
+ expect(picture_url).to match(/\.gif/)
45
+ end
46
+ end
47
+
48
+ context "when crop values are present" do
49
+ before do
50
+ allow(record).to receive(:crop_from) { "10x10" }
51
+ allow(record).to receive(:crop_size) { "200x200" }
52
+ end
53
+
54
+ context "if cropping is enabled" do
55
+ before do
56
+ allow(record).to receive(:settings) { { crop: true } }
57
+ end
58
+
59
+ it "passes these crop values to the picture's url method." do
60
+ expect(picture).to receive(:url).with(
61
+ hash_including(crop_from: "10x10", crop_size: "200x200"),
62
+ )
63
+ picture_url
64
+ end
65
+
66
+ context "but with crop values in the options" do
67
+ let(:options) do
68
+ { crop_from: "30x30", crop_size: "75x75" }
69
+ end
70
+
71
+ it "passes these crop values instead." do
72
+ expect(picture).to receive(:url).with(
73
+ hash_including(crop_from: "30x30", crop_size: "75x75"),
74
+ )
75
+ picture_url
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ context "with other options" do
82
+ let(:options) { { foo: "baz" } }
83
+
84
+ context "and the image does not need to be processed" do
85
+ before do
86
+ allow(record).to receive(:settings) { {} }
87
+ end
88
+
89
+ it "adds them to the url" do
90
+ expect(picture_url).to match(/\?foo=baz/)
91
+ end
92
+ end
93
+ end
94
+
95
+ context "without picture assigned" do
96
+ let(:picture) { nil }
97
+
98
+ it { is_expected.to be_nil }
99
+ end
100
+
101
+ context "if picture.url returns nil" do
102
+ before do
103
+ expect(picture).to receive(:url) { nil }
104
+ end
105
+
106
+ it "returns missing image url" do
107
+ is_expected.to eq "missing-image.png"
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#picture_url_options" do
113
+ subject(:picture_url_options) { record.picture_url_options }
114
+
115
+ let(:picture) { build_stubbed(:alchemy_picture) }
116
+
117
+ it { is_expected.to be_a(HashWithIndifferentAccess) }
118
+
119
+ it "includes the pictures default render format." do
120
+ expect(picture).to receive(:default_render_format) { "img" }
121
+ expect(picture_url_options[:format]).to eq("img")
122
+ end
123
+
124
+ context "with crop values present" do
125
+ before do
126
+ allow(record).to receive(:crop_from) { "10x10" }
127
+ allow(record).to receive(:crop_size) { "200x200" }
128
+ end
129
+
130
+ context "with cropping enabled" do
131
+ before do
132
+ allow(record).to receive(:settings) { { crop: true } }
133
+ end
134
+
135
+ it "includes these crop values.", :aggregate_failures do
136
+ expect(picture_url_options[:crop_from]).to eq "10x10"
137
+ expect(picture_url_options[:crop_size]).to eq "200x200"
138
+ end
139
+ end
140
+
141
+ context "with cropping disabled" do
142
+ before do
143
+ allow(record).to receive(:settings) { { crop: nil } }
144
+ end
145
+
146
+ it "does not include these crop values.", :aggregate_failures do
147
+ expect(picture_url_options[:crop_from]).to be_nil
148
+ expect(picture_url_options[:crop_size]).to be_nil
149
+ end
150
+ end
151
+
152
+ # Regression spec for issue #1279
153
+ context "with crop values being empty strings" do
154
+ before do
155
+ allow(record).to receive(:crop_from) { "" }
156
+ allow(record).to receive(:crop_size) { "" }
157
+ end
158
+
159
+ it "does not include these crop values.", :aggregate_failures do
160
+ expect(picture_url_options[:crop_from]).to be_nil
161
+ expect(picture_url_options[:crop_size]).to be_nil
162
+ end
163
+ end
164
+ end
165
+
166
+ context "having size setting" do
167
+ before do
168
+ allow(record).to receive(:settings) { { size: "30x70" } }
169
+ end
170
+
171
+ it "includes this size." do
172
+ expect(picture_url_options[:size]).to eq "30x70"
173
+ end
174
+ end
175
+
176
+ context "having crop setting" do
177
+ before do
178
+ allow(record).to receive(:settings) { { crop: true } }
179
+ end
180
+
181
+ it "includes this setting" do
182
+ expect(picture_url_options[:crop]).to be true
183
+ end
184
+ end
185
+
186
+ context "without picture assigned" do
187
+ let(:picture) { nil }
188
+
189
+ it { is_expected.to be_a(Hash) }
190
+ end
191
+ end
192
+
193
+ describe "#thumbnail_url" do
194
+ subject(:thumbnail_url) { record.thumbnail_url }
195
+
196
+ let(:settings) do
197
+ {}
198
+ end
199
+
200
+ let(:picture) { create(:alchemy_picture) }
201
+
202
+ before do
203
+ allow(record).to receive(:settings) { settings }
204
+ end
205
+
206
+ it "includes the image's original file format." do
207
+ expect(thumbnail_url).to match(/\.png/)
208
+ end
209
+
210
+ it "flattens the image." do
211
+ expect(picture).to receive(:url).with(hash_including(flatten: true))
212
+ thumbnail_url
213
+ end
214
+
215
+ context "when crop is enabled in the settings" do
216
+ let(:settings) do
217
+ { crop: true }
218
+ end
219
+
220
+ context "and crop sizes are present" do
221
+ before do
222
+ allow(record).to receive(:crop_size).and_return("200x200")
223
+ allow(record).to receive(:crop_from).and_return("10x10")
224
+ end
225
+
226
+ it "passes these crop sizes to the picture's url method." do
227
+ expect(picture).to receive(:url).with(
228
+ hash_including(
229
+ crop_from: "10x10",
230
+ crop_size: "200x200",
231
+ crop: true,
232
+ ),
233
+ )
234
+ thumbnail_url
235
+ end
236
+ end
237
+
238
+ context "when no crop sizes are present" do
239
+ it "it does not pass crop sizes to the picture's url method and enables center cropping." do
240
+ expect(picture).to receive(:url).with(
241
+ hash_including(
242
+ crop_from: nil,
243
+ crop_size: nil,
244
+ crop: true,
245
+ ),
246
+ )
247
+ thumbnail_url
248
+ end
249
+ end
250
+ end
251
+
252
+ context "when cropping is disabled in the settings" do
253
+ let(:settings) do
254
+ { crop: false }
255
+ end
256
+
257
+ context "but crop sizes are present" do
258
+ before do
259
+ allow(record).to receive(:crop_size).and_return("200x200")
260
+ allow(record).to receive(:crop_from).and_return("10x10")
261
+ end
262
+
263
+ it "it disables cropping." do
264
+ expect(picture).to receive(:url).with(
265
+ hash_including(
266
+ crop_size: nil,
267
+ crop_from: nil,
268
+ crop: false,
269
+ ),
270
+ )
271
+ thumbnail_url
272
+ end
273
+ end
274
+ end
275
+
276
+ context "without picture assigned" do
277
+ let(:picture) { nil }
278
+
279
+ it { is_expected.to be_nil }
280
+ end
281
+
282
+ context "if picture.url returns nil" do
283
+ before do
284
+ expect(picture).to receive(:url) { nil }
285
+ end
286
+
287
+ it "returns missing image url" do
288
+ is_expected.to eq "alchemy/missing-image.svg"
289
+ end
290
+ end
291
+ end
292
+
293
+ describe "#thumbnail_url_options" do
294
+ subject(:thumbnail_url_options) { record.thumbnail_url_options }
295
+
296
+ let(:settings) { {} }
297
+ let(:picture) { nil }
298
+
299
+ before do
300
+ allow(record).to receive(:settings) { settings }
301
+ end
302
+
303
+ context "with picture assigned" do
304
+ let(:picture) do
305
+ create(:alchemy_picture)
306
+ end
307
+
308
+ it "includes the image's original file format." do
309
+ expect(thumbnail_url_options[:format]).to eq("png")
310
+ end
311
+
312
+ it "flattens the image." do
313
+ expect(thumbnail_url_options[:flatten]).to be(true)
314
+ end
315
+ end
316
+
317
+ context "when cropping is enabled in settings" do
318
+ let(:settings) do
319
+ { crop: true }
320
+ end
321
+
322
+ context "and crop values are present" do
323
+ before do
324
+ expect(record).to receive(:crop_size).at_least(:once) { "200x200" }
325
+ expect(record).to receive(:crop_from).at_least(:once) { "10x10" }
326
+ end
327
+
328
+ it "includes these crop values" do
329
+ expect(thumbnail_url_options).to match(
330
+ hash_including(
331
+ crop_from: "10x10",
332
+ crop_size: "200x200",
333
+ crop: true,
334
+ )
335
+ )
336
+ end
337
+ end
338
+
339
+ context "and no crop values are present" do
340
+ it "does not include crop values but enables center cropping" do
341
+ expect(thumbnail_url_options).to match(
342
+ hash_including(
343
+ crop_from: nil,
344
+ crop_size: nil,
345
+ crop: true,
346
+ )
347
+ )
348
+ end
349
+ end
350
+ end
351
+
352
+ context "when cropping is disabled in settings" do
353
+ let(:settings) do
354
+ { crop: false }
355
+ end
356
+
357
+ context "but crop values are present" do
358
+ before do
359
+ allow(record).to receive(:crop_size) { "200x200" }
360
+ allow(record).to receive(:crop_from) { "10x10" }
361
+ end
362
+
363
+ it "does not include crop values" do
364
+ expect(thumbnail_url_options).to match(
365
+ hash_including(
366
+ crop_from: nil,
367
+ crop_size: nil,
368
+ crop: false,
369
+ )
370
+ )
371
+ end
372
+ end
373
+ end
374
+
375
+ context "without picture assigned" do
376
+ let(:picture) { nil }
377
+
378
+ it "returns default thumbnail options" do
379
+ is_expected.to eq(
380
+ crop: false,
381
+ crop_from: nil,
382
+ crop_size: nil,
383
+ flatten: true,
384
+ format: "jpg",
385
+ size: "160x120",
386
+ )
387
+ end
388
+ end
389
+ end
390
+
391
+ describe "#image_cropper_settings" do
392
+ let(:picture) { nil }
393
+
394
+ subject { record.image_cropper_settings }
395
+
396
+ context "with no picture assigned" do
397
+ it { is_expected.to eq({}) }
398
+ end
399
+
400
+ context "with picture assigned" do
401
+ let(:picture) { build_stubbed(:alchemy_picture) }
402
+
403
+ let(:default_mask) do
404
+ [
405
+ 0,
406
+ 0,
407
+ 300,
408
+ 250,
409
+ ]
410
+ end
411
+
412
+ let(:settings) { {} }
413
+
414
+ before do
415
+ picture.image_file_width = 300
416
+ picture.image_file_height = 250
417
+ allow(record).to receive(:settings) { settings }
418
+ end
419
+
420
+ context "with no render_size present" do
421
+ before do
422
+ expect(record).to receive(:render_size).at_least(:once).and_return(nil)
423
+ end
424
+
425
+ context "with sizes in settings" do
426
+ let(:settings) do
427
+ { size: "300x250" }
428
+ end
429
+
430
+ it "sets sizes to given values" do
431
+ expect(subject[:min_size]).to eq([300, 250])
432
+ end
433
+ end
434
+
435
+ context "with no sizes in settings" do
436
+ it "sets sizes to zero" do
437
+ expect(subject[:min_size]).to eq([0, 0])
438
+ end
439
+ end
440
+ end
441
+
442
+ context "with render_size present in record" do
443
+ it "sets sizes from these values" do
444
+ expect(record).to receive(:render_size).at_least(:once).and_return("30x25")
445
+ expect(subject[:min_size]).to eq([30, 25])
446
+ end
447
+
448
+ context "when width or height is not fixed" do
449
+ it "infers the height from the image file preserving the aspect ratio" do
450
+ expect(record).to receive(:render_size).at_least(:once).and_return("30x")
451
+ expect(subject[:min_size]).to eq([30, 25])
452
+ end
453
+
454
+ context "and aspect ratio set" do
455
+ let(:settings) do
456
+ { fixed_ratio: "2" }
457
+ end
458
+
459
+ it "does not infer the height from the image file preserving the aspect ratio" do
460
+ expect(record).to receive(:render_size).at_least(:once).and_return("x25")
461
+ expect(subject[:min_size]).to eq([50, 25])
462
+ end
463
+ end
464
+ end
465
+
466
+ context "when width or height is not fixed and an aspect ratio is given" do
467
+ context "and aspect ratio set" do
468
+ let(:settings) do
469
+ { fixed_ratio: "0.5" }
470
+ end
471
+
472
+ it "width is given, it infers the height from width and ratio" do
473
+ expect(record).to receive(:render_size).at_least(:once).and_return("30x")
474
+ expect(subject[:min_size]).to eq([30, 60])
475
+ end
476
+ end
477
+
478
+ it "infers the height from the image file preserving the aspect ratio" do
479
+ expect(record).to receive(:render_size).at_least(:once).and_return("x25")
480
+ expect(subject[:min_size]).to eq([30, 25])
481
+ end
482
+ end
483
+ end
484
+
485
+ context "no crop sizes present in record" do
486
+ it "assigns default mask boxes" do
487
+ expect(subject[:default_box]).to eq(default_mask)
488
+ end
489
+ end
490
+
491
+ context "crop sizes present in record" do
492
+ let(:mask) { [0, 0, 120, 160] }
493
+
494
+ before do
495
+ allow(record).to receive(:crop_from).and_return("0x0")
496
+ allow(record).to receive(:crop_size).and_return("120x160")
497
+ end
498
+
499
+ it "assigns cropping boxes" do
500
+ expect(subject[:default_box]).to eq(default_mask)
501
+ end
502
+ end
503
+
504
+ context "with fixed_ratio set to false" do
505
+ let(:settings) do
506
+ { fixed_ratio: false }
507
+ end
508
+
509
+ it "sets ratio to false" do
510
+ expect(subject[:ratio]).to eq(false)
511
+ end
512
+ end
513
+
514
+ context "with fixed_ratio set to a non float string" do
515
+ let(:settings) do
516
+ { fixed_ratio: "123,45" }
517
+ end
518
+
519
+ it "raises an error" do
520
+ expect { subject }.to raise_exception(ArgumentError)
521
+ end
522
+ end
523
+
524
+ context "with no fixed_ratio set" do
525
+ let(:settings) do
526
+ { size: "80x60" }
527
+ end
528
+
529
+ it "sets a fixed ratio from sizes" do
530
+ expect(subject[:ratio]).to eq(80.0 / 60.0)
531
+ end
532
+ end
533
+
534
+ context "with size set to different values" do
535
+ let(:settings) { { crop: true, size: size } }
536
+
537
+ before do
538
+ picture.image_file_width = 200
539
+ picture.image_file_height = 100
540
+ end
541
+
542
+ context "size 200x50" do
543
+ let(:size) { "200x50" }
544
+
545
+ it "default box should be [0, 25, 200, 75]" do
546
+ expect(subject[:default_box]).to eq([0, 25, 200, 75])
547
+ end
548
+ end
549
+
550
+ context "size 0x0" do
551
+ let(:size) { "0x0" }
552
+
553
+ it "it should not crop the picture" do
554
+ expect(subject[:default_box]).to eq([0, 0, 200, 100])
555
+ end
556
+ end
557
+
558
+ context "size 50x100" do
559
+ let(:size) { "50x100" }
560
+
561
+ it "the hash should be {x1: 75, y1: 0, x2: 125, y2: 100}" do
562
+ expect(subject[:default_box]).to eq([75, 0, 125, 100])
563
+ end
564
+ end
565
+
566
+ context "size 50x50" do
567
+ let(:size) { "50x50" }
568
+
569
+ it "the hash should be {x1: 50, y1: 0, x2: 150, y2: 100}" do
570
+ expect(subject[:default_box]).to eq([50, 0, 150, 100])
571
+ end
572
+ end
573
+
574
+ context "size 400x200" do
575
+ let(:size) { "400x200" }
576
+
577
+ it "the hash should be {x1: 0, y1: 0, x2: 200, y2: 100}" do
578
+ expect(subject[:default_box]).to eq([0, 0, 200, 100])
579
+ end
580
+ end
581
+
582
+ context "size 400x100" do
583
+ let(:size) { "400x100" }
584
+
585
+ it "the hash should be {x1: 0, y1: 25, x2: 200, y2: 75}" do
586
+ expect(subject[:default_box]).to eq([0, 25, 200, 75])
587
+ end
588
+ end
589
+
590
+ context "size 200x200" do
591
+ let(:size) { "200x200" }
592
+
593
+ it "the hash should be {x1: 50, y1: 0, x2: 150, y2: 100}" do
594
+ expect(subject[:default_box]).to eq([50, 0, 150, 100])
595
+ end
596
+ end
597
+ end
598
+ end
599
+ end
600
+
601
+ describe "#allow_image_cropping?" do
602
+ let(:picture) do
603
+ stub_model(Alchemy::Picture, image_file_width: 400, image_file_height: 300)
604
+ end
605
+
606
+ subject { record.allow_image_cropping? }
607
+
608
+ it { is_expected.to be_falsy }
609
+
610
+ context "with picture assigned" do
611
+ before do
612
+ allow(record).to receive(:picture) { picture }
613
+ end
614
+
615
+ it { is_expected.to be_falsy }
616
+
617
+ context "and with image larger than crop size" do
618
+ before do
619
+ allow(picture).to receive(:can_be_cropped_to?) { true }
620
+ end
621
+
622
+ it { is_expected.to be_falsy }
623
+
624
+ context "with crop set to true" do
625
+ before do
626
+ allow(record).to receive(:settings) { { crop: true } }
627
+ end
628
+
629
+ context "if picture.image_file is nil" do
630
+ before do
631
+ expect(picture).to receive(:image_file) { nil }
632
+ end
633
+
634
+ it { is_expected.to be_falsy }
635
+ end
636
+
637
+ context "if picture.image_file is present" do
638
+ let(:picture) { build_stubbed(:alchemy_picture) }
639
+
640
+ it { is_expected.to be(true) }
641
+ end
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for "an alchemy ingredient editor" do
4
+ let(:ingredient_editor) { Alchemy::IngredientEditor.new(ingredient) }
5
+
6
+ before do
7
+ view.class.send :include, Alchemy::Admin::BaseHelper
8
+ view.class.send :include, Alchemy::Admin::IngredientsHelper
9
+ allow(element_editor).to receive(:ingredients) { [ingredient_editor] }
10
+ end
11
+
12
+ subject do
13
+ render element_editor
14
+ rendered
15
+ end
16
+
17
+ it "renders a ingredient editor", :aggregate_failures do
18
+ is_expected.to have_css(".ingredient-editor.#{ingredient_editor.partial_name}")
19
+ is_expected.to have_css("[data-ingredient-role]")
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shoulda-matchers"
4
+
5
+ RSpec.shared_examples_for "an alchemy ingredient" do
6
+ let(:element) { build(:alchemy_element, name: "element_with_ingredients") }
7
+
8
+ subject(:ingredient) do
9
+ described_class.new(
10
+ element: element,
11
+ role: "headline",
12
+ )
13
+ end
14
+
15
+ it { is_expected.to belong_to(:element).touch(true).class_name("Alchemy::Element") }
16
+ it { is_expected.to belong_to(:related_object).optional }
17
+ it { is_expected.to validate_presence_of(:role) }
18
+ it { is_expected.to validate_presence_of(:type) }
19
+ it { expect(subject.data).to eq({}) }
20
+
21
+ describe "#settings" do
22
+ subject { ingredient.settings }
23
+
24
+ context "without element" do
25
+ let(:element) { nil }
26
+
27
+ it { is_expected.to eq({}) }
28
+ end
29
+
30
+ context "with element" do
31
+ it { is_expected.to eq({ linkable: true }.with_indifferent_access) }
32
+ end
33
+ end
34
+
35
+ describe "#definition" do
36
+ subject { ingredient.definition }
37
+
38
+ context "without element" do
39
+ let(:element) { nil }
40
+
41
+ it { is_expected.to eq({}) }
42
+ end
43
+
44
+ context "with element" do
45
+ it do
46
+ is_expected.to eq({
47
+ role: "headline",
48
+ type: "Text",
49
+ default: "Hello World",
50
+ settings: {
51
+ linkable: true,
52
+ },
53
+ }.with_indifferent_access)
54
+ end
55
+ end
56
+ end
57
+ end