alchemy_cms 2.1.12 → 2.2.rc1

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 (405) hide show
  1. data/.travis.yml +1 -1
  2. data/Gemfile +20 -16
  3. data/README.md +11 -7
  4. data/alchemy_cms.gemspec +31 -29
  5. data/app/assets/javascripts/alchemy/alchemy.base.js +234 -233
  6. data/app/assets/javascripts/alchemy/alchemy.buttons.js +26 -26
  7. data/app/assets/javascripts/alchemy/alchemy.datepicker.js +32 -32
  8. data/app/assets/javascripts/alchemy/alchemy.dirty.js +78 -78
  9. data/app/assets/javascripts/alchemy/alchemy.dragndrop.js +103 -103
  10. data/app/assets/javascripts/alchemy/alchemy.element_editor_selector.js +84 -84
  11. data/app/assets/javascripts/alchemy/alchemy.elements_window.js +88 -88
  12. data/app/assets/javascripts/alchemy/alchemy.file_progress.js +92 -0
  13. data/app/assets/javascripts/alchemy/alchemy.growler.js +23 -23
  14. data/app/assets/javascripts/alchemy/alchemy.html5.js +23 -0
  15. data/app/assets/javascripts/alchemy/alchemy.image_cropper.js +53 -52
  16. data/app/assets/javascripts/alchemy/alchemy.jquery_loader.js +33 -30
  17. data/app/assets/javascripts/alchemy/alchemy.js +2 -0
  18. data/app/assets/javascripts/alchemy/alchemy.js_extensions.js +14 -14
  19. data/app/assets/javascripts/alchemy/alchemy.link_overlay.js.coffee +181 -0
  20. data/app/assets/javascripts/alchemy/alchemy.menubar.js +48 -48
  21. data/app/assets/javascripts/alchemy/alchemy.page_sorter.js +39 -39
  22. data/app/assets/javascripts/alchemy/alchemy.preview.js +81 -83
  23. data/app/assets/javascripts/alchemy/alchemy.preview_window.js +86 -86
  24. data/app/assets/javascripts/alchemy/alchemy.routes.js.erb +23 -23
  25. data/app/assets/javascripts/alchemy/alchemy.swf_upload.js +208 -353
  26. data/app/assets/javascripts/alchemy/alchemy.uploader.js +279 -0
  27. data/app/assets/javascripts/alchemy/alchemy.windows.js +268 -268
  28. data/app/assets/javascripts/tiny_mce/plugins/alchemy_link/editor_plugin.js +42 -42
  29. data/app/assets/stylesheets/alchemy/_defaults.scss +89 -89
  30. data/app/assets/stylesheets/alchemy/base.css.scss +790 -804
  31. data/app/assets/stylesheets/alchemy/buttons.css.scss +263 -264
  32. data/app/assets/stylesheets/alchemy/dashboard.css.scss +66 -66
  33. data/app/assets/stylesheets/alchemy/elements.css.scss +575 -564
  34. data/app/assets/stylesheets/alchemy/flash.css.scss +45 -45
  35. data/app/assets/stylesheets/alchemy/frame.css.scss +262 -262
  36. data/app/assets/stylesheets/alchemy/icons.css.scss +86 -86
  37. data/app/assets/stylesheets/alchemy/jquery-ui.alchemy.css.scss +706 -707
  38. data/app/assets/stylesheets/alchemy/jquery.Jcrop.css.scss +46 -27
  39. data/app/assets/stylesheets/alchemy/jquery.sb.css.scss +107 -107
  40. data/app/assets/stylesheets/alchemy/login.css.scss +40 -38
  41. data/app/assets/stylesheets/alchemy/menubar.css.scss +84 -82
  42. data/app/assets/stylesheets/alchemy/modules.css.scss +9 -9
  43. data/app/assets/stylesheets/alchemy/pagination.css.scss +49 -49
  44. data/app/assets/stylesheets/alchemy/print.css +7 -7
  45. data/app/assets/stylesheets/alchemy/sitemap.css.scss +185 -154
  46. data/app/assets/stylesheets/alchemy/standard_set.css +205 -206
  47. data/app/assets/stylesheets/alchemy/tables.css.scss +94 -60
  48. data/app/assets/stylesheets/alchemy/tinymce_content.css.scss +34 -34
  49. data/app/assets/stylesheets/alchemy/tinymce_dialog.css.scss +177 -177
  50. data/app/assets/stylesheets/alchemy/upload.css.scss +68 -45
  51. data/app/assets/stylesheets/tiny_mce/plugins/inlinepopups/skins/alchemy/window.css.scss +222 -221
  52. data/app/controllers/alchemy/admin/attachments_controller.rb +100 -100
  53. data/app/controllers/alchemy/admin/base_controller.rb +116 -116
  54. data/app/controllers/alchemy/admin/clipboard_controller.rb +44 -44
  55. data/app/controllers/alchemy/admin/contents_controller.rb +55 -55
  56. data/app/controllers/alchemy/admin/dashboard_controller.rb +12 -11
  57. data/app/controllers/alchemy/admin/elements_controller.rb +109 -109
  58. data/app/controllers/alchemy/admin/essence_audios_controller.rb +8 -8
  59. data/app/controllers/alchemy/admin/essence_files_controller.rb +20 -20
  60. data/app/controllers/alchemy/admin/essence_flashes_controller.rb +8 -8
  61. data/app/controllers/alchemy/admin/essence_pictures_controller.rb +81 -81
  62. data/app/controllers/alchemy/admin/essence_videos_controller.rb +8 -8
  63. data/app/controllers/alchemy/admin/languages_controller.rb +16 -4
  64. data/app/controllers/alchemy/admin/layoutpages_controller.rb +9 -9
  65. data/app/controllers/alchemy/admin/pages_controller.rb +249 -248
  66. data/app/controllers/alchemy/admin/pictures_controller.rb +105 -105
  67. data/app/controllers/alchemy/admin/resources_controller.rb +111 -166
  68. data/app/controllers/alchemy/admin/trash_controller.rb +18 -18
  69. data/app/controllers/alchemy/admin/users_controller.rb +69 -69
  70. data/app/controllers/alchemy/attachments_controller.rb +27 -27
  71. data/app/controllers/alchemy/base_controller.rb +159 -155
  72. data/app/controllers/alchemy/elements_controller.rb +16 -16
  73. data/app/controllers/alchemy/messages_controller.rb +59 -59
  74. data/app/controllers/alchemy/pages_controller.rb +194 -198
  75. data/app/controllers/alchemy/pictures_controller.rb +57 -57
  76. data/app/controllers/alchemy/user_sessions_controller.rb +70 -70
  77. data/app/helpers/alchemy/admin/attachments_helper.rb +7 -7
  78. data/app/helpers/alchemy/admin/base_helper.rb +425 -425
  79. data/app/helpers/alchemy/admin/contents_helper.rb +72 -72
  80. data/app/helpers/alchemy/admin/elements_helper.rb +108 -107
  81. data/app/helpers/alchemy/admin/essences_helper.rb +108 -108
  82. data/app/helpers/alchemy/admin/pages_helper.rb +42 -42
  83. data/app/helpers/alchemy/admin/pictures_helper.rb +26 -26
  84. data/app/helpers/alchemy/base_helper.rb +90 -90
  85. data/app/helpers/alchemy/elements_helper.rb +167 -168
  86. data/app/helpers/alchemy/essences_helper.rb +133 -133
  87. data/app/helpers/alchemy/pages_helper.rb +515 -526
  88. data/app/helpers/alchemy/pictures_helper.rb +19 -0
  89. data/app/mailers/alchemy/messages.rb +13 -13
  90. data/app/mailers/alchemy/notifications.rb +19 -19
  91. data/app/models/alchemy.rb +3 -3
  92. data/app/models/alchemy/attachment.rb +52 -78
  93. data/app/models/alchemy/cell.rb +48 -48
  94. data/app/models/alchemy/content.rb +196 -196
  95. data/app/models/alchemy/element.rb +500 -490
  96. data/app/models/alchemy/essence_audio.rb +7 -7
  97. data/app/models/alchemy/essence_date.rb +10 -10
  98. data/app/models/alchemy/essence_file.rb +7 -7
  99. data/app/models/alchemy/essence_flash.rb +6 -6
  100. data/app/models/alchemy/essence_html.rb +9 -9
  101. data/app/models/alchemy/essence_picture.rb +17 -17
  102. data/app/models/alchemy/essence_richtext.rb +53 -53
  103. data/app/models/alchemy/essence_text.rb +21 -21
  104. data/app/models/alchemy/essence_video.rb +7 -7
  105. data/app/models/alchemy/folded_page.rb +3 -3
  106. data/app/models/alchemy/language.rb +91 -91
  107. data/app/models/alchemy/language/code.rb +14 -14
  108. data/app/models/alchemy/message.rb +48 -45
  109. data/app/models/alchemy/page.rb +555 -558
  110. data/app/models/alchemy/picture.rb +100 -99
  111. data/app/models/alchemy/user.rb +76 -74
  112. data/app/models/alchemy/user_session.rb +8 -8
  113. data/app/sweepers/alchemy/content_sweeper.rb +15 -15
  114. data/app/sweepers/alchemy/pages_sweeper.rb +50 -50
  115. data/app/sweepers/alchemy/pictures_sweeper.rb +14 -14
  116. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +23 -23
  117. data/app/views/alchemy/admin/attachments/_attachment.html.erb +56 -56
  118. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +15 -15
  119. data/app/views/alchemy/admin/attachments/_files_list.html.erb +16 -16
  120. data/app/views/alchemy/admin/attachments/create.js.erb +2 -2
  121. data/app/views/alchemy/admin/attachments/destroy.js.erb +3 -3
  122. data/app/views/alchemy/admin/attachments/edit.html.erb +10 -10
  123. data/app/views/alchemy/admin/attachments/index.html.erb +14 -14
  124. data/app/views/alchemy/admin/attachments/new.html.erb +9 -9
  125. data/app/views/alchemy/admin/clipboard/index.html.erb +21 -21
  126. data/app/views/alchemy/admin/clipboard/insert.js.erb +5 -5
  127. data/app/views/alchemy/admin/contents/_missing.html.erb +1 -1
  128. data/app/views/alchemy/admin/contents/create.js.erb +33 -33
  129. data/app/views/alchemy/admin/contents/destroy.js.erb +5 -5
  130. data/app/views/alchemy/admin/contents/new.html.erb +13 -13
  131. data/app/views/alchemy/admin/contents/order.js.erb +2 -2
  132. data/app/views/alchemy/admin/dashboard/index.html.erb +96 -92
  133. data/app/views/alchemy/admin/elements/_add_content.html.erb +12 -12
  134. data/app/views/alchemy/admin/elements/_element.html.erb +3 -3
  135. data/app/views/alchemy/admin/elements/_new_element_form.html.erb +28 -28
  136. data/app/views/alchemy/admin/elements/_picture_editor.html.erb +21 -21
  137. data/app/views/alchemy/admin/elements/create.js.erb +32 -32
  138. data/app/views/alchemy/admin/elements/index.html.erb +2 -2
  139. data/app/views/alchemy/admin/elements/new.html.erb +31 -31
  140. data/app/views/alchemy/admin/elements/order.js.erb +3 -3
  141. data/app/views/alchemy/admin/elements/trash.js.erb +13 -13
  142. data/app/views/alchemy/admin/elements/update.js.erb +6 -6
  143. data/app/views/alchemy/admin/essence_files/assign.js.erb +6 -6
  144. data/app/views/alchemy/admin/essence_pictures/crop.html.erb +32 -32
  145. data/app/views/alchemy/admin/essence_pictures/destroy.js.erb +1 -1
  146. data/app/views/alchemy/admin/essence_pictures/edit.html.erb +1 -1
  147. data/app/views/alchemy/admin/essence_pictures/update.js.erb +11 -11
  148. data/app/views/alchemy/admin/languages/_form.html.erb +51 -45
  149. data/app/views/alchemy/admin/languages/_language.html.erb +45 -45
  150. data/app/views/alchemy/admin/languages/_table.html.erb +25 -25
  151. data/app/views/alchemy/admin/layoutpages/index.html.erb +35 -35
  152. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +42 -42
  153. data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
  154. data/app/views/alchemy/admin/pages/_new_page_form.html.erb +27 -27
  155. data/app/views/alchemy/admin/pages/_page.html.erb +112 -112
  156. data/app/views/alchemy/admin/pages/_sitemap.html.erb +1 -1
  157. data/app/views/alchemy/admin/pages/configure.html.erb +71 -69
  158. data/app/views/alchemy/admin/pages/configure_external.html.erb +38 -38
  159. data/app/views/alchemy/admin/pages/destroy.js.erb +10 -10
  160. data/app/views/alchemy/admin/pages/edit.html.erb +165 -165
  161. data/app/views/alchemy/admin/pages/fold.js.erb +2 -2
  162. data/app/views/alchemy/admin/pages/index.html.erb +66 -66
  163. data/app/views/alchemy/admin/pages/locked.html.erb +1 -1
  164. data/app/views/alchemy/admin/pages/new.html.erb +40 -40
  165. data/app/views/alchemy/admin/pages/sort.js.erb +4 -4
  166. data/app/views/alchemy/admin/pages/unlock.js.erb +6 -6
  167. data/app/views/alchemy/admin/pages/update.js.erb +19 -19
  168. data/app/views/alchemy/admin/partials/_flash.html.erb +2 -2
  169. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  170. data/app/views/alchemy/admin/partials/_flash_upload.html.erb +38 -0
  171. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +15 -15
  172. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +8 -8
  173. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +35 -35
  174. data/app/views/alchemy/admin/partials/_search_form.html.erb +14 -14
  175. data/app/views/alchemy/admin/partials/_sub_navigation_tab.html.erb +6 -6
  176. data/app/views/alchemy/admin/partials/_upload_form.html.erb +56 -118
  177. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +11 -11
  178. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +71 -71
  179. data/app/views/alchemy/admin/pictures/_picture.html.erb +32 -32
  180. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +26 -26
  181. data/app/views/alchemy/admin/pictures/_pictures_list.html.erb +6 -6
  182. data/app/views/alchemy/admin/pictures/create.js.erb +7 -7
  183. data/app/views/alchemy/admin/pictures/index.html.erb +67 -67
  184. data/app/views/alchemy/admin/pictures/show_in_window.html.erb +5 -4
  185. data/app/views/alchemy/admin/resources/_form.html.erb +25 -19
  186. data/app/views/alchemy/admin/resources/_resource.html.erb +37 -36
  187. data/app/views/alchemy/admin/resources/_table.html.erb +17 -17
  188. data/app/views/alchemy/admin/resources/destroy.js.erb +1 -1
  189. data/app/views/alchemy/admin/resources/index.html.erb +14 -14
  190. data/app/views/alchemy/admin/trash/index.html.erb +20 -20
  191. data/app/views/alchemy/admin/users/_table.html.erb +52 -52
  192. data/app/views/alchemy/admin/users/_user.html.erb +35 -36
  193. data/app/views/alchemy/admin/users/index.html.erb +26 -27
  194. data/app/views/alchemy/base/error_notice.html.erb +2 -2
  195. data/app/views/alchemy/base/update.js.erb +1 -1
  196. data/app/views/alchemy/elements/_contactform_view.html.erb +76 -76
  197. data/app/views/alchemy/elements/_download_view.html.erb +1 -1
  198. data/app/views/alchemy/elements/_editor_not_found.html.erb +3 -3
  199. data/app/views/alchemy/elements/_searchresult_view.html.erb +2 -2
  200. data/app/views/alchemy/elements/_sitemap_editor.html.erb +1 -1
  201. data/app/views/alchemy/essences/_essence_audio_view.html.erb +13 -13
  202. data/app/views/alchemy/essences/_essence_date_editor.html.erb +10 -10
  203. data/app/views/alchemy/essences/_essence_file_editor.html.erb +62 -62
  204. data/app/views/alchemy/essences/_essence_file_view.html.erb +8 -8
  205. data/app/views/alchemy/essences/_essence_flash_view.html.erb +19 -19
  206. data/app/views/alchemy/essences/_essence_html_editor.html.erb +6 -6
  207. data/app/views/alchemy/essences/_essence_picture_editor.html.erb +73 -73
  208. data/app/views/alchemy/essences/_essence_picture_view.html.erb +34 -34
  209. data/app/views/alchemy/essences/_essence_richtext_editor.html.erb +7 -7
  210. data/app/views/alchemy/essences/_essence_text_editor.html.erb +8 -8
  211. data/app/views/alchemy/navigation/_image_link.html.erb +12 -12
  212. data/app/views/alchemy/navigation/_link.html.erb +13 -13
  213. data/app/views/alchemy/navigation/_renderer.html.erb +17 -17
  214. data/app/views/alchemy/page_layouts/_layout_footer.html.erb +1 -1
  215. data/app/views/alchemy/page_layouts/_layout_header.html.erb +1 -1
  216. data/app/views/alchemy/pages/show.rss.builder +14 -14
  217. data/app/views/alchemy/search/_result.html.erb +7 -7
  218. data/app/views/alchemy/user_sessions/leave.html.erb +9 -9
  219. data/app/views/alchemy/user_sessions/login.html.erb +39 -39
  220. data/app/views/alchemy/user_sessions/logout.html.erb +1 -1
  221. data/app/views/layouts/alchemy/admin.html.erb +81 -81
  222. data/app/views/layouts/alchemy/login.html.erb +36 -36
  223. data/app/views/layouts/alchemy/pages.html.erb +48 -27
  224. data/bin/alchemy +136 -134
  225. data/config/alchemy/config.yml +13 -9
  226. data/config/locales/alchemy.de.yml +24 -30
  227. data/config/locales/alchemy.en.yml +11 -28
  228. data/config/routes.rb +146 -146
  229. data/db/migrate/20100607193638_create_essence_pictures.rb +3 -3
  230. data/db/migrate/20101109150312_alter_pages_visible_column_default.rb +1 -1
  231. data/db/migrate/20101109151812_create_languages.rb +3 -3
  232. data/db/migrate/20110115123343_remove_css_class_default_from_essence_pictures.rb +3 -3
  233. data/db/migrate/20110224105120_change_pages_visible_default.rb +3 -3
  234. data/db/migrate/20110228182659_remove_default_page_layout_from_pages.rb +2 -2
  235. data/db/migrate/20110414163140_remove_display_name_from_elements.rb +3 -3
  236. data/db/migrate/20110511100516_rename_essence_texts_title_to_link_title.rb +2 -2
  237. data/db/migrate/20110529130500_add_cell_id_to_elements.rb +3 -3
  238. data/db/migrate/20110530102804_change_pages_page_layout_column.rb +3 -3
  239. data/db/migrate/20110711142057_change_open_link_in_new_window_to_link_target.rb +1 -1
  240. data/db/migrate/20111116125112_namespace_alchemy_models.rb +21 -21
  241. data/lib/alchemy/auth_engine.rb +5 -5
  242. data/lib/alchemy/capistrano.rb +64 -64
  243. data/lib/alchemy/config.rb +47 -44
  244. data/lib/alchemy/engine.rb +41 -41
  245. data/lib/alchemy/essence.rb +50 -51
  246. data/lib/alchemy/i18n.rb +48 -48
  247. data/lib/alchemy/modules.rb +30 -30
  248. data/lib/alchemy/mount_point.rb +6 -6
  249. data/lib/alchemy/page_layout.rb +94 -93
  250. data/lib/alchemy/resource.rb +82 -0
  251. data/lib/alchemy/resources_helper.rb +56 -0
  252. data/lib/alchemy/scoped_pagination_url_helper.rb +8 -8
  253. data/lib/alchemy/seeder.rb +88 -88
  254. data/lib/alchemy/tinymce.rb +49 -49
  255. data/lib/alchemy/upgrader.rb +103 -103
  256. data/lib/alchemy/version.rb +1 -1
  257. data/lib/alchemy_cms.rb +26 -27
  258. data/lib/extensions/action_view.rb +1 -1
  259. data/lib/extensions/array.rb +3 -3
  260. data/lib/extensions/hash.rb +3 -3
  261. data/lib/middleware/flash_session_cookie.rb +1 -1
  262. data/lib/rails/generators/alchemy/deploy_script/deploy_script_generator.rb +24 -24
  263. data/lib/rails/generators/alchemy/elements/elements_generator.rb +7 -7
  264. data/lib/rails/generators/alchemy/elements/templates/view.html.erb +17 -17
  265. data/lib/rails/generators/alchemy/page_layouts/page_layouts_generator.rb +6 -6
  266. data/lib/rails/generators/alchemy/plugin/templates/authorization_rules.rb +8 -8
  267. data/lib/rails/generators/alchemy/scaffold/files/pages.html.erb +7 -7
  268. data/lib/rails/generators/alchemy/scaffold/scaffold_generator.rb +36 -36
  269. data/lib/rails/templates/alchemy.rb +1 -1
  270. data/lib/tasks/ferret.rake +2 -2
  271. data/lib/tasks/fleximage.rake +32 -32
  272. data/lib/tasks/install.rake +12 -12
  273. data/lib/tasks/upgrade.rake +4 -4
  274. data/spec/controllers/admin/clipboard_controller_spec.rb +43 -43
  275. data/spec/controllers/admin/contents_controller_spec.rb +22 -22
  276. data/spec/controllers/admin/elements_controller_spec.rb +13 -13
  277. data/spec/controllers/admin/languages_controller_spec.rb +31 -0
  278. data/spec/controllers/admin/pages_controller_spec.rb +10 -41
  279. data/spec/controllers/admin/resources_controller_spec.rb +19 -0
  280. data/spec/controllers/admin/trash_controller_spec.rb +11 -11
  281. data/spec/controllers/base_controller_spec.rb +46 -46
  282. data/spec/controllers/pages_controller_spec.rb +102 -92
  283. data/spec/dummy/app/controllers/admin/events_controller.rb +3 -0
  284. data/spec/dummy/app/controllers/errors_controller.rb +5 -0
  285. data/spec/dummy/app/models/event.rb +3 -0
  286. data/spec/dummy/config.ru +1 -1
  287. data/spec/dummy/config/application.rb +3 -0
  288. data/spec/dummy/config/authorization_rules.rb +7 -0
  289. data/spec/dummy/config/environments/development.rb +8 -1
  290. data/spec/dummy/config/environments/production.rb +1 -1
  291. data/spec/dummy/config/environments/test.rb +5 -2
  292. data/spec/dummy/config/routes.rb +9 -1
  293. data/spec/dummy/db/schema.rb +47 -35
  294. data/spec/dummy/public/404.html +19 -7
  295. data/spec/dummy/public/404.html.bak +26 -0
  296. data/spec/dummy/public/422.html +19 -7
  297. data/spec/dummy/public/500.html +19 -7
  298. data/spec/dummy/script/rails +2 -2
  299. data/spec/factories.rb +76 -76
  300. data/spec/helpers/admin/base_helper_spec.rb +21 -21
  301. data/spec/helpers/admin/contents_helper_spec.rb +14 -14
  302. data/spec/helpers/admin/elements_helper_spec.rb +40 -40
  303. data/spec/helpers/admin/essences_helper_spec.rb +7 -7
  304. data/spec/helpers/elements_helper_spec.rb +154 -154
  305. data/spec/helpers/essences_helper_spec.rb +10 -10
  306. data/spec/helpers/pages_helper_spec.rb +369 -372
  307. data/spec/helpers/pictures_helper_spec.rb +14 -0
  308. data/spec/helpers/url_helpers_spec.rb +29 -34
  309. data/spec/integration/admin/pages_controller_spec.rb +50 -50
  310. data/spec/integration/admin/resources_integration_spec.rb +100 -0
  311. data/spec/integration/navigation_spec.rb +1 -1
  312. data/spec/integration/pages_controller_spec.rb +205 -207
  313. data/spec/integration/security_spec.rb +47 -47
  314. data/spec/javascripts/alchemy/link_overlay_spec.js.coffee +42 -0
  315. data/spec/javascripts/alchemy_integration_spec.coffee +12 -0
  316. data/spec/javascripts/fixtures/select.html +3 -0
  317. data/spec/javascripts/helpers/.gitkeep +0 -0
  318. data/spec/javascripts/helpers/jasmine-jquery-1.3.1.js +289 -0
  319. data/spec/javascripts/helpers/mock-ajax.js +207 -0
  320. data/spec/javascripts/helpers/test_responses/test_responses.js +1 -0
  321. data/spec/javascripts/spec.css +3 -0
  322. data/spec/javascripts/spec.js +2 -0
  323. data/spec/libraries/resource_spec.rb +122 -0
  324. data/spec/libraries/resources_helper_spec.rb +105 -0
  325. data/spec/models/content_spec.rb +67 -67
  326. data/spec/models/element_spec.rb +133 -133
  327. data/spec/models/essence_picture_spec.rb +5 -5
  328. data/spec/models/essence_richtext_spec.rb +4 -4
  329. data/spec/models/language_spec.rb +89 -89
  330. data/spec/models/page_spec.rb +436 -506
  331. data/spec/models/picture_spec.rb +34 -34
  332. data/spec/models/user_spec.rb +2 -2
  333. data/spec/page_layout_spec.rb +24 -20
  334. data/spec/routing_spec.rb +253 -253
  335. data/spec/spec_helper.rb +51 -50
  336. data/spec/support/alchemy/controller_hacks.rb +25 -25
  337. data/spec/support/alchemy/specs_helpers.rb +25 -25
  338. data/spec/support/integration_spec_helper.rb +24 -0
  339. data/vendor/assets/javascripts/jquery_plugins/jquery.html5uploader.js +271 -0
  340. data/vendor/assets/javascripts/jquery_plugins/jquery.in-place-edit.js +86 -85
  341. data/vendor/assets/javascripts/jquery_plugins/jquery.ui.nestedSortable.js +380 -377
  342. data/vendor/assets/javascripts/jquery_plugins/jquery.ui.tabspaging.js +285 -279
  343. data/vendor/assets/javascripts/swfupload/SWFObject License.txt +3 -3
  344. data/vendor/assets/javascripts/swfupload/swfupload.cookies.js +47 -47
  345. data/vendor/assets/javascripts/swfupload/swfupload.js +612 -612
  346. data/vendor/assets/javascripts/swfupload/swfupload.queue.js +93 -93
  347. data/vendor/assets/javascripts/swfupload/swfupload.speed.js +337 -337
  348. data/vendor/assets/javascripts/swfupload/swfupload.swfobject.js +706 -102
  349. data/vendor/assets/javascripts/tiny_mce/license.txt +6 -6
  350. data/vendor/assets/javascripts/tiny_mce/plugins/fullscreen/fullscreen.htm +97 -97
  351. data/vendor/assets/javascripts/tiny_mce/plugins/inlinepopups/template.htm +386 -376
  352. data/vendor/assets/javascripts/tiny_mce/plugins/paste/js/pastetext.js +30 -30
  353. data/vendor/assets/javascripts/tiny_mce/plugins/paste/js/pasteword.js +45 -45
  354. data/vendor/assets/javascripts/tiny_mce/plugins/paste/pastetext.htm +20 -17
  355. data/vendor/assets/javascripts/tiny_mce/plugins/paste/pasteword.htm +12 -12
  356. data/vendor/assets/javascripts/tiny_mce/plugins/table/cell.htm +183 -173
  357. data/vendor/assets/javascripts/tiny_mce/plugins/table/css/cell.css +4 -4
  358. data/vendor/assets/javascripts/tiny_mce/plugins/table/css/row.css +7 -7
  359. data/vendor/assets/javascripts/tiny_mce/plugins/table/css/table.css +3 -3
  360. data/vendor/assets/javascripts/tiny_mce/plugins/table/js/cell.js +282 -280
  361. data/vendor/assets/javascripts/tiny_mce/plugins/table/js/merge_cells.js +15 -15
  362. data/vendor/assets/javascripts/tiny_mce/plugins/table/js/row.js +204 -204
  363. data/vendor/assets/javascripts/tiny_mce/plugins/table/js/table.js +435 -435
  364. data/vendor/assets/javascripts/tiny_mce/plugins/table/merge_cells.htm +24 -22
  365. data/vendor/assets/javascripts/tiny_mce/plugins/table/row.htm +144 -136
  366. data/vendor/assets/javascripts/tiny_mce/plugins/table/table.htm +184 -168
  367. data/vendor/assets/javascripts/tiny_mce/themes/advanced/about.htm +62 -46
  368. data/vendor/assets/javascripts/tiny_mce/themes/advanced/anchor.htm +17 -16
  369. data/vendor/assets/javascripts/tiny_mce/themes/advanced/charmap.htm +56 -47
  370. data/vendor/assets/javascripts/tiny_mce/themes/advanced/color_picker.htm +67 -54
  371. data/vendor/assets/javascripts/tiny_mce/themes/advanced/editor_template.js +852 -1
  372. data/vendor/assets/javascripts/tiny_mce/themes/advanced/image.htm +79 -69
  373. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/about.js +48 -48
  374. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/anchor.js +28 -28
  375. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/charmap.js +317 -317
  376. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/color_picker.js +248 -246
  377. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/image.js +246 -246
  378. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/link.js +146 -146
  379. data/vendor/assets/javascripts/tiny_mce/themes/advanced/js/source_editor.js +31 -31
  380. data/vendor/assets/javascripts/tiny_mce/themes/advanced/langs/de.js +1 -1
  381. data/vendor/assets/javascripts/tiny_mce/themes/advanced/langs/de_dlg.js +1 -1
  382. data/vendor/assets/javascripts/tiny_mce/themes/advanced/langs/en.js +1 -1
  383. data/vendor/assets/javascripts/tiny_mce/themes/advanced/langs/en_dlg.js +1 -1
  384. data/vendor/assets/javascripts/tiny_mce/themes/advanced/link.htm +53 -46
  385. data/vendor/assets/javascripts/tiny_mce/themes/advanced/shortcuts.htm +57 -45
  386. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/default/content.css +182 -47
  387. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/default/dialog.css +399 -92
  388. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/default/ui.css +891 -187
  389. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/highcontrast/content.css +102 -24
  390. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/highcontrast/dialog.css +377 -78
  391. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/highcontrast/ui.css +452 -77
  392. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/o2k7/content.css +167 -45
  393. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/o2k7/dialog.css +399 -92
  394. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/o2k7/ui.css +890 -190
  395. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/o2k7/ui_black.css +33 -7
  396. data/vendor/assets/javascripts/tiny_mce/themes/advanced/skins/o2k7/ui_silver.css +15 -4
  397. data/vendor/assets/javascripts/tiny_mce/themes/advanced/source_editor.htm +18 -15
  398. data/vendor/assets/javascripts/tiny_mce/utils/editable_selects.js +46 -46
  399. data/vendor/assets/javascripts/tiny_mce/utils/form_utils.js +124 -124
  400. data/vendor/assets/javascripts/tiny_mce/utils/mctabs.js +112 -112
  401. data/vendor/assets/javascripts/tiny_mce/utils/validate.js +213 -213
  402. metadata +102 -115
  403. data/app/assets/javascripts/alchemy/alchemy.link_overlay.js +0 -216
  404. data/lib/tasks/routes.rake +0 -29
  405. data/spec/integration/admin/resources_spec.rb +0 -19
@@ -1,492 +1,502 @@
1
1
  module Alchemy
2
- class Element < ActiveRecord::Base
3
-
4
- # All Elements inside a cell are a list. All Elements not in cell are in the cell_id.nil list.
5
- acts_as_list :scope => [:page_id, :cell_id]
6
- stampable :stamper_class_name => :user
7
-
8
- has_many :contents, :order => :position, :dependent => :destroy
9
- belongs_to :cell
10
- belongs_to :page
11
- has_and_belongs_to_many :to_be_sweeped_pages, :class_name => 'Alchemy::Page', :uniq => true, :join_table => 'alchemy_elements_alchemy_pages'
12
-
13
- validates_uniqueness_of :position, :scope => [:page_id, :cell_id]
14
- validates_presence_of :name, :on => :create
15
-
16
- attr_accessor :create_contents_after_create
17
-
18
- after_create :create_contents, :unless => Proc.new { |m| m.create_contents_after_create == false }
19
-
20
- # TODO: add a trashed column to elements table
21
- scope :trashed, where(:page_id => nil).order('updated_at DESC')
22
- scope :not_trashed, where('`alchemy_elements`.`page_id` IS NOT NULL')
23
- scope :published, where(:public => true)
24
- scope :named, lambda { |names| where(arel_table[:name].in(names)) }
25
- scope :excluded, lambda { |names| where(arel_table[:name].not_in(names)) }
26
- scope :not_in_cell, where(:cell_id => nil)
27
-
28
- # Returns next Element on self.page or nil. Pass a Element.name to get next of this kind.
29
- def next(name = nil)
30
- if name.nil?
31
- find_conditions = ["public = 1 AND page_id = ? AND position > ?", self.page.id, self.position]
32
- else
33
- find_conditions = ["public = 1 AND page_id = ? AND name = ? AND position > ?", self.page.id, name, self.position]
34
- end
35
- self.class.where(find_conditions).order("position ASC").limit(1)
36
- end
37
-
38
- # Returns previous Element on self.page or nil. Pass a Element.name to get previous of this kind.
39
- def prev(name = nil)
40
- if name.nil?
41
- find_conditions = ["public = 1 AND page_id = ? AND position < ?", self.page.id, self.position]
42
- else
43
- find_conditions = ["public = 1 AND page_id = ? AND name = ? AND position < ?", self.page.id, name, self.position]
44
- end
45
- self.class.where(find_conditions).order("position DESC").limit(1)
46
- end
47
-
48
- # Stores the page into `to_be_sweeped_pages` (Pages that have to be sweeped after updating element).
49
- def store_page(page)
50
- return true if page.nil?
51
- unless self.to_be_sweeped_pages.include? page
52
- self.to_be_sweeped_pages << page
53
- self.save
54
- end
55
- end
56
-
57
- # Nullifies the page_id and cell_id, fold the element, set it to unpublic and removes its position.
58
- def trash
59
- self.attributes = {
60
- :page_id => nil,
61
- :cell_id => nil,
62
- :folded => true,
63
- :public => false
64
- }
65
- self.remove_from_list
66
- end
67
-
68
- def trashed?
69
- page_id.nil?
70
- end
71
-
72
- def content_by_name(name)
73
- self.contents.find_by_name(name)
74
- end
75
-
76
- def content_by_type(essence_type)
77
- self.contents.find_by_essence_type(Content.normalize_essence_type(essence_type))
78
- end
79
-
80
- def all_contents_by_name(name)
81
- self.contents.where(:name => name)
82
- end
83
-
84
- def all_contents_by_type(essence_type)
85
- self.contents.where(:essence_type => Content.normalize_essence_type(essence_type))
86
- end
87
-
88
- # Returns the content that is marked as rss title.
89
- #
90
- # Mark a content as rss title in your +elements.yml+ file:
91
- #
92
- # - name: news
93
- # contents:
94
- # - name: headline
95
- # type: EssenceText
96
- # rss_title: true
97
- #
98
- def content_for_rss_title
99
- rss_title = content_descriptions.detect { |c| c['rss_title'] }
100
- contents.find_by_name(rss_title['name'])
101
- end
102
-
103
- # Returns the content that is marked as rss description.
104
- #
105
- # Mark a content as rss description in your +elements.yml+ file:
106
- #
107
- # - name: news
108
- # contents:
109
- # - name: body
110
- # type: EssenceRichtext
111
- # rss_description: true
112
- #
113
- def content_for_rss_description
114
- rss_title = content_descriptions.detect { |c| c['rss_description'] }
115
- contents.find_by_name(rss_title['name'])
116
- end
117
-
118
- # Inits a new element for page as described in /config/alchemy/elements.yml from element_name
119
- def self.new_from_scratch(attributes)
120
- attributes.stringify_keys!
121
- return Element.new if attributes['name'].blank?
122
- element_descriptions = Element.descriptions
123
- return if element_descriptions.blank?
124
- element_scratch = element_descriptions.select{ |m| m["name"] == attributes['name'].split('#').first }.first
125
- element = Element.new(
126
- element_scratch.except('contents', 'available_contents', 'display_name').merge({
127
- :page_id => attributes['page_id']
128
- })
129
- )
130
- element
131
- end
132
-
133
- # Inits a new element for page as described in /config/alchemy/elements.yml from element_name and saves it
134
- def self.create_from_scratch(attributes)
135
- element = Element.new_from_scratch(attributes)
136
- element.save if element
137
- return element
138
- end
139
-
140
- # pastes a element from the clipboard in the session to page
141
- def self.paste_from_clipboard(page_id, element, method, position)
142
- copy = self.copy(element, :page_id => page_id)
143
- copy.insert_at(position)
144
- if method == "move" && copy.valid?
145
- element.destroy
146
- end
147
- copy
148
- end
149
-
150
- # Returns the descriptions from elements.yml file.
151
- # Alchemy comes with its own elements.yml file. As so called standard set.
152
- # Place a elements.yml file inside your apps config/alchemy folder to define
153
- # your own set of elements
154
- def self.descriptions
155
- if ::File.exists? "#{::Rails.root}/config/alchemy/elements.yml"
156
- element_definitions = ::YAML.load_file( "#{::Rails.root}/config/alchemy/elements.yml" )
157
- end
158
- if !element_definitions
159
- if ::File.exists?(::File.join(::File.dirname(__FILE__), "../../../config/alchemy/elements.yml"))
160
- element_definitions = ::YAML.load_file( ::File.join(::File.dirname(__FILE__), "../../../config/alchemy/elements.yml") )
161
- end
162
- end
163
- if !element_definitions
164
- raise LoadError, "Could not find elements.yml file! Please run: rails generate alchemy:scaffold"
165
- end
166
- element_definitions
167
- end
168
-
169
- # List all elements for page_layout
170
- def self.all_for_page(page)
171
- raise TypeError if page.class.name != "Alchemy::Page"
172
- # if page_layout has cells, collect elements from cells and group them by cellname
173
- page_layout = PageLayout.get(page.page_layout)
174
- if page_layout.blank?
175
- logger.warn "\n++++++\nWARNING! Could not find page_layout description for page: #{page.name}\n++++++++\n"
176
- return []
177
- end
178
- elements_for_layout = []
179
- elements_for_layout += all_definitions_for(page_layout['elements'])
180
- return [] if elements_for_layout.blank?
181
- # all unique elements from this layout
182
- unique_elements = elements_for_layout.select{ |m| m["unique"] == true }
183
- elements_already_on_the_page = page.elements
184
- # delete all elements from the elements that could be placed that are unique and already and the page
185
- unique_elements.each do |unique_element|
186
- elements_already_on_the_page.each do |already_placed_element|
187
- if already_placed_element.name == unique_element["name"]
188
- elements_for_layout.delete(unique_element)
189
- end
190
- end
191
- end
192
- return elements_for_layout
193
- end
194
-
195
- def self.all_definitions_for(element_names)
196
- return [] if element_names.blank?
197
- if element_names.to_s == "all"
198
- return element_descriptions
199
- else
200
- return definitions.select { |e| element_names.include? e['name'] }
201
- end
202
- end
203
-
204
- # This methods does a copy of source and all depending contents and all of their depending essences.
205
- #
206
- # == Options
207
- #
208
- # You can pass a differences Hash as second option to update attributes for the copy.
209
- #
210
- # == Example
211
- #
212
- # @copy = Alchemy::Element.copy(@element, {:public => false})
213
- # @copy.public? # => false
214
- #
215
- def self.copy(source, differences = {})
216
- attributes = source.attributes.except("id").merge(differences)
217
- element = self.create!(attributes.merge(:create_contents_after_create => false, :id => nil, :position => nil))
218
- source.contents.each do |content|
219
- new_content = Content.copy(content, :element_id => element.id)
220
- new_content.move_to_bottom
221
- end
222
- element
223
- end
224
-
225
- def self.definitions
226
- self.descriptions
227
- end
228
-
229
- # Returns the array with the hashes for all element contents in the elements.yml file
230
- def content_descriptions
231
- return nil if description.blank?
232
- description['contents']
233
- end
234
-
235
- # Returns the array with the hashes for all element available_contents in the elements.yml file
236
- def available_content_descriptions
237
- return nil if description.blank?
238
- description['available_contents']
239
- end
240
-
241
- # Returns the description for given content_name
242
- def content_description_for(content_name)
243
- if content_descriptions.blank?
244
- logger.warn("\n+++++++++++ Warning: Element #{self.name} is missing the content description for #{content_name}\n")
245
- return nil
246
- else
247
- content_descriptions.detect { |d| d['name'] == content_name }
248
- end
249
- end
250
-
251
- # Returns the description for given content_name inside the available_contents
252
- def available_content_description_for(content_name)
253
- return nil if available_content_descriptions.blank?
254
- available_content_descriptions.detect { |d| d['name'] == content_name }
255
- end
256
-
257
- # returns the description of the element with my name in element.yml
258
- def description
259
- self.class.descriptions.detect{ |d| d['name'] == self.name }
260
- end
261
- alias_method :definition, :description
262
-
263
- # Human name for displaying in selectboxes and element editor views.
264
- # The name is beeing translated from elements name value as described in config/alchemy/elements.yml
265
- #
266
- # Translate the name in your config/locales language file. Example:
267
- #
268
- # de:
269
- # element_names:
270
- # contactform: 'Kontakt Formular'
271
- #
272
- # If no translation is found the capitalized name is used!
273
- #
274
- def display_name
275
- return name.capitalize if description.blank?
276
- I18n.t(description['name'], :scope => :element_names)
277
- end
278
-
279
- # Gets the preview text from the first Content found in the +elements.yml+ Element description file.
280
- # You can flag a Content as +take_me_for_preview+ to take this as preview.
281
- def preview_text(maxlength = 30)
282
- return "" if description.blank?
283
- my_contents = description["contents"]
284
- return "" if my_contents.blank?
285
- content_flagged_as_preview = my_contents.select{ |a| a["take_me_for_preview"] }.first
286
- if content_flagged_as_preview.blank?
287
- content_to_take_as_preview = my_contents.first
288
- else
289
- content_to_take_as_preview = content_flagged_as_preview
290
- end
291
- preview_content = self.contents.select{ |content| content.name == content_to_take_as_preview["name"] }.first
292
- return "" if preview_content.blank? || preview_content.essence.blank?
293
- text = preview_content.essence.preview_text(maxlength)
294
- text.size > maxlength ? "#{text[0..maxlength]}..." : text
295
- end
296
-
297
- # Generates a preview text containing Element#display_name and Element#preview_text.
298
- # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view.
299
- #
300
- # === Example
301
- #
302
- # A Element described as:
303
- #
304
- # - name: funky_element
305
- # display_name: Funky Element
306
- # contents:
307
- # - name: headline
308
- # type: EssenceText
309
- # - name: text
310
- # type EssenceRichtext
311
- # take_me_for_preview: true
312
- #
313
- # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
314
- #
315
- # Funky Element: I want to tell ...
316
- #
317
- # Options:
318
- #
319
- # maxlength(integer). [Default 30] : Length of characters after the text will be cut off.
320
- #
321
- def display_name_with_preview_text(maxlength = 30)
322
- "#{display_name}: #{preview_text(maxlength)}"
323
- end
324
-
325
- def dom_id
326
- "#{name}_#{id}"
327
- end
328
-
329
- # List all elements by from page_layout
330
- def self.elements_for_layout(layout)
331
- element_descriptions = Element.descriptions
332
- elements = []
333
- page_layout = PageLayout.get(layout)
334
- layout_elements = page_layout["elements"]
335
- return element_descriptions if layout_elements == "all"
336
- element_descriptions.each do |element|
337
- if layout_elements.include?(element["name"])
338
- elements << element
339
- end
340
- end
341
- return elements
342
- end
343
-
344
- def self.get_from_clipboard(clipboard)
345
- return nil if clipboard.blank?
346
- self.find_by_id(clipboard[:element_id])
347
- end
348
-
349
- def self.all_from_clipboard(clipboard)
350
- return [] if clipboard.nil?
351
- self.find_all_by_id(clipboard.collect { |i| i[:id] })
352
- end
353
-
354
- def self.all_from_clipboard_for_page(clipboard, page)
355
- return [] if clipboard.nil? || page.nil?
356
- allowed_elements = self.all_for_page(page)
357
- clipboard_elements = self.all_from_clipboard(clipboard)
358
- allowed_element_names = allowed_elements.collect { |e| e['name'] }
359
- clipboard_elements.select { |ce| allowed_element_names.include?(ce.name) }
360
- end
361
-
362
- # returns the collection of available essence_types that can be created for this element depending on its description in elements.yml
363
- def available_contents
364
- description['available_contents']
365
- end
366
-
367
- # Returns the contents ingredient for passed content name.
368
- def ingredient(name)
369
- content = content_by_name(name)
370
- return nil if content.blank?
371
- content.ingredient
372
- end
373
-
374
- def has_ingredient?(name)
375
- self.ingredient(name).present?
376
- end
377
-
378
- def save_contents(params)
379
- contents.each do |content|
380
- unless content.update_essence(params[:contents]["content_#{content.id}"])
381
- errors.add(:base, :essence_validation_failed)
382
- end
383
- end
384
- return errors.blank?
385
- end
386
-
387
- def essences
388
- return [] if contents.blank?
389
- contents.collect(&:essence)
390
- end
391
-
392
- # Returns all essence_errors in the format:
393
- #
394
- # {
395
- # essence.content.name => [error_message_for_validation_1, error_message_for_validation_2]
396
- # }
397
- #
398
- # Get translated error messages with Element#essence_error_messages
399
- #
400
- def essence_errors
401
- essence_errors = {}
402
- essences.each do |essence|
403
- unless essence.errors.blank?
404
- essence_errors[essence.content.name] = essence.validation_errors
405
- end
406
- end
407
- essence_errors
408
- end
409
-
410
- # Essence validation errors
411
- #
412
- # Messages are translated via I18n.
413
- #
414
- # Inside your translation file add translations like:
415
- #
416
- # alchemy:
417
- # content_validations:
418
- # name_of_the_element:
419
- # name_of_the_content:
420
- # validation_error_type: Error Message
421
- #
422
- # validation_error_type has to be one of:
423
- #
424
- # * blank
425
- # * taken
426
- # * wrong_format
427
- #
428
- # === Example:
429
- #
430
- # de:
431
- # alchemy:
432
- # content_validations:
433
- # contactform:
434
- # email:
435
- # wrong_format: 'Die Email hat nicht das richtige Format'
436
- #
437
- def essence_error_messages
438
- messages = []
439
- essence_errors.each do |content_name, errors|
440
- errors.each do |error|
441
- messages << I18n.t(error,
442
- :scope => [:content_validations, self.name, content_name],
443
- :default => [
444
- "alchemy.content_validations.fields.#{content_name}.#{error}".to_sym,
445
- "alchemy.content_validations.errors.#{error}".to_sym
446
- ],
447
- :field => Content.translated_label_for(content_name)
448
- )
449
- end
450
- end
451
- messages
452
- end
453
-
454
- def contents_with_errors
455
- contents.select(&:essence_validation_failed?)
456
- end
457
-
458
- def has_validations?
459
- !contents.detect(&:has_validations?).blank?
460
- end
461
-
462
- def rtf_contents
463
- contents.select { |content| content.essence_type == 'Alchemy::EssenceRichtext' }
464
- end
465
- alias_method :richtext_contents, :rtf_contents
466
-
467
- # The name of the cell the element could be placed in.
468
- def belonging_cellnames(page)
469
- cellnames = Cell.names_for_element(name)
470
- if cellnames.blank? || !page.has_cells?
471
- return ['for_other_elements']
472
- else
473
- return cellnames
474
- end
475
- end
476
-
477
- private
478
-
479
- # creates the contents for this element as described in the elements.yml
480
- def create_contents
481
- contents = []
482
- if description["contents"].blank?
483
- logger.warn "\n++++++\nWARNING! Could not find any content descriptions for element: #{self.name}\n++++++++\n"
484
- else
485
- description["contents"].each do |content_hash|
486
- contents << Content.create_from_scratch(self, content_hash.symbolize_keys)
487
- end
488
- end
489
- end
490
-
491
- end
2
+ class Element < ActiveRecord::Base
3
+
4
+ # All Elements inside a cell are a list. All Elements not in cell are in the cell_id.nil list.
5
+ acts_as_list :scope => [:page_id, :cell_id]
6
+ stampable :stamper_class_name => :user
7
+
8
+ has_many :contents, :order => :position, :dependent => :destroy
9
+ belongs_to :cell
10
+ belongs_to :page
11
+ has_and_belongs_to_many :to_be_sweeped_pages, :class_name => 'Alchemy::Page', :uniq => true, :join_table => 'alchemy_elements_alchemy_pages'
12
+
13
+ validates_uniqueness_of :position, :scope => [:page_id, :cell_id]
14
+ validates_presence_of :name, :on => :create, :message => '^' + I18n.t(:choose_element)
15
+
16
+ attr_accessor :create_contents_after_create
17
+
18
+ after_create :create_contents, :unless => Proc.new { |m| m.create_contents_after_create == false }
19
+
20
+ # TODO: add a trashed column to elements table
21
+ scope :trashed, where(:page_id => nil).order('updated_at DESC')
22
+ scope :not_trashed, where('`alchemy_elements`.`page_id` IS NOT NULL')
23
+ scope :published, where(:public => true)
24
+ scope :named, lambda { |names| where(arel_table[:name].in(names)) }
25
+ scope :excluded, lambda { |names| where(arel_table[:name].not_in(names)) }
26
+ scope :not_in_cell, where(:cell_id => nil)
27
+
28
+ # Returns next Element on self.page or nil. Pass a Element.name to get next of this kind.
29
+ def next(name = nil)
30
+ if name.nil?
31
+ find_conditions = ["public = 1 AND page_id = ? AND position > ?", self.page.id, self.position]
32
+ else
33
+ find_conditions = ["public = 1 AND page_id = ? AND name = ? AND position > ?", self.page.id, name, self.position]
34
+ end
35
+ self.class.where(find_conditions).order("position ASC").limit(1)
36
+ end
37
+
38
+ # Returns previous Element on self.page or nil. Pass a Element.name to get previous of this kind.
39
+ def prev(name = nil)
40
+ if name.nil?
41
+ find_conditions = ["public = 1 AND page_id = ? AND position < ?", self.page.id, self.position]
42
+ else
43
+ find_conditions = ["public = 1 AND page_id = ? AND name = ? AND position < ?", self.page.id, name, self.position]
44
+ end
45
+ self.class.where(find_conditions).order("position DESC").limit(1)
46
+ end
47
+
48
+ # Stores the page into `to_be_sweeped_pages` (Pages that have to be sweeped after updating element).
49
+ def store_page(page)
50
+ return true if page.nil?
51
+ unless self.to_be_sweeped_pages.include? page
52
+ self.to_be_sweeped_pages << page
53
+ self.save
54
+ end
55
+ end
56
+
57
+ # Nullifies the page_id and cell_id, fold the element, set it to unpublic and removes its position.
58
+ def trash
59
+ self.attributes = {
60
+ :page_id => nil,
61
+ :cell_id => nil,
62
+ :folded => true,
63
+ :public => false
64
+ }
65
+ self.remove_from_list
66
+ end
67
+
68
+ def trashed?
69
+ page_id.nil?
70
+ end
71
+
72
+ def content_by_name(name)
73
+ self.contents.find_by_name(name)
74
+ end
75
+
76
+ def content_by_type(essence_type)
77
+ self.contents.find_by_essence_type(Content.normalize_essence_type(essence_type))
78
+ end
79
+
80
+ def all_contents_by_name(name)
81
+ self.contents.where(:name => name)
82
+ end
83
+
84
+ def all_contents_by_type(essence_type)
85
+ self.contents.where(:essence_type => Content.normalize_essence_type(essence_type))
86
+ end
87
+
88
+ # Returns the content that is marked as rss title.
89
+ #
90
+ # Mark a content as rss title in your +elements.yml+ file:
91
+ #
92
+ # - name: news
93
+ # contents:
94
+ # - name: headline
95
+ # type: EssenceText
96
+ # rss_title: true
97
+ #
98
+ def content_for_rss_title
99
+ rss_title = content_descriptions.detect { |c| c['rss_title'] }
100
+ contents.find_by_name(rss_title['name'])
101
+ end
102
+
103
+ # Returns the content that is marked as rss description.
104
+ #
105
+ # Mark a content as rss description in your +elements.yml+ file:
106
+ #
107
+ # - name: news
108
+ # contents:
109
+ # - name: body
110
+ # type: EssenceRichtext
111
+ # rss_description: true
112
+ #
113
+ def content_for_rss_description
114
+ rss_title = content_descriptions.detect { |c| c['rss_description'] }
115
+ contents.find_by_name(rss_title['name'])
116
+ end
117
+
118
+ # Inits a new element for page as described in /config/alchemy/elements.yml from element_name
119
+ def self.new_from_scratch(attributes)
120
+ attributes.stringify_keys!
121
+ return Element.new if attributes['name'].blank?
122
+ element_descriptions = Element.descriptions
123
+ return if element_descriptions.blank?
124
+ element_scratch = element_descriptions.select { |m| m["name"] == attributes['name'].split('#').first }.first
125
+ element = Element.new(
126
+ element_scratch.except('contents', 'available_contents', 'display_name').merge({
127
+ :page_id => attributes['page_id']
128
+ })
129
+ )
130
+ element
131
+ end
132
+
133
+ # Inits a new element for page as described in /config/alchemy/elements.yml from element_name and saves it
134
+ def self.create_from_scratch(attributes)
135
+ element = Element.new_from_scratch(attributes)
136
+ element.save if element
137
+ return element
138
+ end
139
+
140
+ # pastes a element from the clipboard in the session to page
141
+ def self.paste_from_clipboard(page_id, element, method, position)
142
+ copy = self.copy(element, :page_id => page_id)
143
+ copy.insert_at(position)
144
+ if method == "move" && copy.valid?
145
+ element.destroy
146
+ end
147
+ copy
148
+ end
149
+
150
+ # Returns the descriptions from elements.yml file.
151
+ # Alchemy comes with its own elements.yml file. As so called standard set.
152
+ # Place a elements.yml file inside your apps config/alchemy folder to define
153
+ # your own set of elements
154
+ def self.descriptions
155
+ if ::File.exists? "#{::Rails.root}/config/alchemy/elements.yml"
156
+ element_definitions = ::YAML.load_file("#{::Rails.root}/config/alchemy/elements.yml")
157
+ end
158
+ if !element_definitions
159
+ if ::File.exists?(::File.join(::File.dirname(__FILE__), "../../../config/alchemy/elements.yml"))
160
+ element_definitions = ::YAML.load_file(::File.join(::File.dirname(__FILE__), "../../../config/alchemy/elements.yml"))
161
+ end
162
+ end
163
+ if !element_definitions
164
+ raise LoadError, "Could not find elements.yml file! Please run: rails generate alchemy:scaffold"
165
+ end
166
+ element_definitions
167
+ end
168
+
169
+ # List all elements for page_layout
170
+ def self.all_for_page(page)
171
+ raise TypeError if page.class.name != "Alchemy::Page"
172
+ # if page_layout has cells, collect elements from cells and group them by cellname
173
+ page_layout = PageLayout.get(page.page_layout)
174
+ if page_layout.blank?
175
+ logger.warn "\n++++++\nWARNING! Could not find page_layout description for page: #{page.name}\n++++++++\n"
176
+ return []
177
+ end
178
+ elements_for_layout = []
179
+ elements_for_layout += all_definitions_for(page_layout['elements'])
180
+ return [] if elements_for_layout.blank?
181
+ # all unique elements from this layout
182
+ unique_elements = elements_for_layout.select { |m| m["unique"] == true }
183
+ elements_already_on_the_page = page.elements
184
+ # delete all elements from the elements that could be placed that are unique and already and the page
185
+ unique_elements.each do |unique_element|
186
+ elements_already_on_the_page.each do |already_placed_element|
187
+ if already_placed_element.name == unique_element["name"]
188
+ elements_for_layout.delete(unique_element)
189
+ end
190
+ end
191
+ end
192
+ return elements_for_layout
193
+ end
194
+
195
+ def self.all_definitions_for(element_names)
196
+ return [] if element_names.blank?
197
+ if element_names.to_s == "all"
198
+ return element_descriptions
199
+ else
200
+ return definitions.select { |e| element_names.include? e['name'] }
201
+ end
202
+ end
203
+
204
+ # This methods does a copy of source and all depending contents and all of their depending essences.
205
+ #
206
+ # == Options
207
+ #
208
+ # You can pass a differences Hash as second option to update attributes for the copy.
209
+ #
210
+ # == Example
211
+ #
212
+ # @copy = Alchemy::Element.copy(@element, {:public => false})
213
+ # @copy.public? # => false
214
+ #
215
+ def self.copy(source, differences = {})
216
+ attributes = source.attributes.except("id").merge(differences)
217
+ element = self.create!(attributes.merge(:create_contents_after_create => false, :id => nil, :position => nil))
218
+ source.contents.each do |content|
219
+ new_content = Content.copy(content, :element_id => element.id)
220
+ new_content.move_to_bottom
221
+ end
222
+ element
223
+ end
224
+
225
+ def self.definitions
226
+ self.descriptions
227
+ end
228
+
229
+ # Returns the array with the hashes for all element contents in the elements.yml file
230
+ def content_descriptions
231
+ return nil if description.blank?
232
+ description['contents']
233
+ end
234
+
235
+ # Returns the array with the hashes for all element available_contents in the elements.yml file
236
+ def available_content_descriptions
237
+ return nil if description.blank?
238
+ description['available_contents']
239
+ end
240
+
241
+ # Returns the description for given content_name
242
+ def content_description_for(content_name)
243
+ if content_descriptions.blank?
244
+ logger.warn("\n+++++++++++ Warning: Element #{self.name} is missing the content description for #{content_name}\n")
245
+ return nil
246
+ else
247
+ content_descriptions.detect { |d| d['name'] == content_name }
248
+ end
249
+ end
250
+
251
+ # Returns the description for given content_name inside the available_contents
252
+ def available_content_description_for(content_name)
253
+ return nil if available_content_descriptions.blank?
254
+ available_content_descriptions.detect { |d| d['name'] == content_name }
255
+ end
256
+
257
+ # returns the description of the element with my name in element.yml
258
+ def description
259
+ self.class.descriptions.detect { |d| d['name'] == self.name }
260
+ end
261
+
262
+ alias_method :definition, :description
263
+
264
+ # Human name for displaying in selectboxes and element editor views.
265
+ # The name is beeing translated from elements name value as described in config/alchemy/elements.yml
266
+ #
267
+ # Translate the name in your config/locales language file. Example:
268
+ #
269
+ # de:
270
+ # element_names:
271
+ # contactform: 'Kontakt Formular'
272
+ #
273
+ # If no translation is found the capitalized name is used!
274
+ #
275
+ def display_name
276
+ return name.capitalize if description.blank?
277
+ I18n.t(description['name'], :scope => :element_names)
278
+ end
279
+
280
+ # Gets the preview text from the first Content found in the +elements.yml+ Element description file.
281
+ # You can flag a Content as +take_me_for_preview+ to take this as preview.
282
+ def preview_text(maxlength = 30)
283
+ return "" if description.blank?
284
+ my_contents = description["contents"]
285
+ return "" if my_contents.blank?
286
+ content_flagged_as_preview = my_contents.select { |a| a["take_me_for_preview"] }.first
287
+ if content_flagged_as_preview.blank?
288
+ content_to_take_as_preview = my_contents.first
289
+ else
290
+ content_to_take_as_preview = content_flagged_as_preview
291
+ end
292
+ preview_content = self.contents.select { |content| content.name == content_to_take_as_preview["name"] }.first
293
+ return "" if preview_content.blank? || preview_content.essence.blank?
294
+ text = preview_content.essence.preview_text(maxlength)
295
+ text.size > maxlength ? "#{text[0..maxlength]}..." : text
296
+ end
297
+
298
+ # Generates a preview text containing Element#display_name and Element#preview_text.
299
+ # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view.
300
+ #
301
+ # === Example
302
+ #
303
+ # A Element described as:
304
+ #
305
+ # - name: funky_element
306
+ # display_name: Funky Element
307
+ # contents:
308
+ # - name: headline
309
+ # type: EssenceText
310
+ # - name: text
311
+ # type EssenceRichtext
312
+ # take_me_for_preview: true
313
+ #
314
+ # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
315
+ #
316
+ # Funky Element: I want to tell ...
317
+ #
318
+ # Options:
319
+ #
320
+ # maxlength(integer). [Default 30] : Length of characters after the text will be cut off.
321
+ #
322
+ def display_name_with_preview_text(maxlength = 30)
323
+ "#{display_name}: #{preview_text(maxlength)}"
324
+ end
325
+
326
+ def dom_id
327
+ "#{name}_#{id}"
328
+ end
329
+
330
+ # List all elements by from page_layout
331
+ def self.elements_for_layout(layout)
332
+ element_descriptions = Element.descriptions
333
+ elements = []
334
+ page_layout = PageLayout.get(layout)
335
+ layout_elements = page_layout["elements"]
336
+ return element_descriptions if layout_elements == "all"
337
+ element_descriptions.each do |element|
338
+ if layout_elements.include?(element["name"])
339
+ elements << element
340
+ end
341
+ end
342
+ return elements
343
+ end
344
+
345
+ def self.get_from_clipboard(clipboard)
346
+ return nil if clipboard.blank?
347
+ self.find_by_id(clipboard[:element_id])
348
+ end
349
+
350
+ def self.all_from_clipboard(clipboard)
351
+ return [] if clipboard.nil?
352
+ self.find_all_by_id(clipboard.collect { |i| i[:id] })
353
+ end
354
+
355
+ def self.all_from_clipboard_for_page(clipboard, page)
356
+ return [] if clipboard.nil? || page.nil?
357
+ allowed_elements = self.all_for_page(page)
358
+ clipboard_elements = self.all_from_clipboard(clipboard)
359
+ allowed_element_names = allowed_elements.collect { |e| e['name'] }
360
+ clipboard_elements.select { |ce| allowed_element_names.include?(ce.name) }
361
+ end
362
+
363
+ # returns the collection of available essence_types that can be created for this element depending on its description in elements.yml
364
+ def available_contents
365
+ description['available_contents']
366
+ end
367
+
368
+ # Returns the contents ingredient for passed content name.
369
+ def ingredient(name)
370
+ content = content_by_name(name)
371
+ return nil if content.blank?
372
+ content.ingredient
373
+ end
374
+
375
+ def has_ingredient?(name)
376
+ self.ingredient(name).present?
377
+ end
378
+
379
+ def save_contents(params)
380
+ contents.each do |content|
381
+ unless content.update_essence(params[:contents]["content_#{content.id}"])
382
+ errors.add(:base, :essence_validation_failed)
383
+ end
384
+ end
385
+ return errors.blank?
386
+ end
387
+
388
+ def essences
389
+ return [] if contents.blank?
390
+ contents.collect(&:essence)
391
+ end
392
+
393
+ # Returns all essence_errors in the format:
394
+ #
395
+ # {
396
+ # essence.content.name => [error_message_for_validation_1, error_message_for_validation_2]
397
+ # }
398
+ #
399
+ # Get translated error messages with Element#essence_error_messages
400
+ #
401
+ def essence_errors
402
+ essence_errors = {}
403
+ essences.each do |essence|
404
+ unless essence.errors.blank?
405
+ essence_errors[essence.content.name] = essence.validation_errors
406
+ end
407
+ end
408
+ essence_errors
409
+ end
410
+
411
+ # Essence validation errors
412
+ #
413
+ # Messages are translated via I18n.
414
+ #
415
+ # Inside your translation file add translations like:
416
+ #
417
+ # alchemy:
418
+ # content_validations:
419
+ # name_of_the_element:
420
+ # name_of_the_content:
421
+ # validation_error_type: Error Message
422
+ #
423
+ # validation_error_type has to be one of:
424
+ #
425
+ # * blank
426
+ # * taken
427
+ # * wrong_format
428
+ #
429
+ # === Example:
430
+ #
431
+ # de:
432
+ # alchemy:
433
+ # content_validations:
434
+ # contactform:
435
+ # email:
436
+ # wrong_format: 'Die Email hat nicht das richtige Format'
437
+ #
438
+ def essence_error_messages
439
+ messages = []
440
+ essence_errors.each do |content_name, errors|
441
+ errors.each do |error|
442
+ messages << I18n.t(error,
443
+ :scope => [:content_validations, self.name, content_name],
444
+ :default => [
445
+ "alchemy.content_validations.fields.#{content_name}.#{error}".to_sym,
446
+ "alchemy.content_validations.errors.#{error}".to_sym
447
+ ],
448
+ :field => Content.translated_label_for(content_name)
449
+ )
450
+ end
451
+ end
452
+ messages
453
+ end
454
+
455
+ def contents_with_errors
456
+ contents.select(&:essence_validation_failed?)
457
+ end
458
+
459
+ def has_validations?
460
+ !contents.detect(&:has_validations?).blank?
461
+ end
462
+
463
+ def rtf_contents
464
+ contents.select { |content| content.essence_type == 'Alchemy::EssenceRichtext' }
465
+ end
466
+
467
+ alias_method :richtext_contents, :rtf_contents
468
+
469
+ # The name of the cell the element could be placed in.
470
+ def belonging_cellnames(page)
471
+ cellnames = Cell.names_for_element(name)
472
+ if cellnames.blank? || !page.has_cells?
473
+ return ['for_other_elements']
474
+ else
475
+ return cellnames
476
+ end
477
+ end
478
+
479
+ def which_instance
480
+ 'original'
481
+ end
482
+
483
+ def self.which_class
484
+ 'original'
485
+ end
486
+
487
+ private
488
+
489
+ # creates the contents for this element as described in the elements.yml
490
+ def create_contents
491
+ contents = []
492
+ if description["contents"].blank?
493
+ logger.warn "\n++++++\nWARNING! Could not find any content descriptions for element: #{self.name}\n++++++++\n"
494
+ else
495
+ description["contents"].each do |content_hash|
496
+ contents << Content.create_from_scratch(self, content_hash.symbolize_keys)
497
+ end
498
+ end
499
+ end
500
+
501
+ end
492
502
  end