dm_event 4.2.1.5

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 (337) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +23 -0
  3. data/README.md +33 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/images/dm_event/payment_logos/sofort/at/100x38.png +0 -0
  6. data/app/assets/images/dm_event/payment_logos/sofort/at/200x75.png +0 -0
  7. data/app/assets/images/dm_event/payment_logos/sofort/at/250x94.png +0 -0
  8. data/app/assets/images/dm_event/payment_logos/sofort/at/28x7.png +0 -0
  9. data/app/assets/images/dm_event/payment_logos/sofort/at/320x120.png +0 -0
  10. data/app/assets/images/dm_event/payment_logos/sofort/at/42x11.png +0 -0
  11. data/app/assets/images/dm_event/payment_logos/sofort/at/75x28.png +0 -0
  12. data/app/assets/images/dm_event/payment_logos/sofort/at/icon-32x22.png +0 -0
  13. data/app/assets/images/dm_event/payment_logos/sofort/at/icon-32x24.png +0 -0
  14. data/app/assets/images/dm_event/payment_logos/sofort/at/icon-48x32.png +0 -0
  15. data/app/assets/images/dm_event/payment_logos/sofort/at/icon-48x34.png +0 -0
  16. data/app/assets/images/dm_event/payment_logos/sofort/at/icon-64x42.png +0 -0
  17. data/app/assets/images/dm_event/payment_logos/sofort/at/icon-64x44.png +0 -0
  18. data/app/assets/images/dm_event/payment_logos/sofort/be/100x38.png +0 -0
  19. data/app/assets/images/dm_event/payment_logos/sofort/be/200x75.png +0 -0
  20. data/app/assets/images/dm_event/payment_logos/sofort/be/250x94.png +0 -0
  21. data/app/assets/images/dm_event/payment_logos/sofort/be/28x7.png +0 -0
  22. data/app/assets/images/dm_event/payment_logos/sofort/be/320x120.png +0 -0
  23. data/app/assets/images/dm_event/payment_logos/sofort/be/42x11.png +0 -0
  24. data/app/assets/images/dm_event/payment_logos/sofort/be/75x28.png +0 -0
  25. data/app/assets/images/dm_event/payment_logos/sofort/be/icon-32x22.png +0 -0
  26. data/app/assets/images/dm_event/payment_logos/sofort/be/icon-32x24.png +0 -0
  27. data/app/assets/images/dm_event/payment_logos/sofort/be/icon-48x32.png +0 -0
  28. data/app/assets/images/dm_event/payment_logos/sofort/be/icon-48x34.png +0 -0
  29. data/app/assets/images/dm_event/payment_logos/sofort/be/icon-64x42.png +0 -0
  30. data/app/assets/images/dm_event/payment_logos/sofort/be/icon-64x44.png +0 -0
  31. data/app/assets/images/dm_event/payment_logos/sofort/ch/100x38.png +0 -0
  32. data/app/assets/images/dm_event/payment_logos/sofort/ch/200x75.png +0 -0
  33. data/app/assets/images/dm_event/payment_logos/sofort/ch/250x94.png +0 -0
  34. data/app/assets/images/dm_event/payment_logos/sofort/ch/28x7.png +0 -0
  35. data/app/assets/images/dm_event/payment_logos/sofort/ch/320x120.png +0 -0
  36. data/app/assets/images/dm_event/payment_logos/sofort/ch/42x11.png +0 -0
  37. data/app/assets/images/dm_event/payment_logos/sofort/ch/75x28.png +0 -0
  38. data/app/assets/images/dm_event/payment_logos/sofort/ch/icon-32x22.png +0 -0
  39. data/app/assets/images/dm_event/payment_logos/sofort/ch/icon-32x24.png +0 -0
  40. data/app/assets/images/dm_event/payment_logos/sofort/ch/icon-48x32.png +0 -0
  41. data/app/assets/images/dm_event/payment_logos/sofort/ch/icon-48x34.png +0 -0
  42. data/app/assets/images/dm_event/payment_logos/sofort/ch/icon-64x42.png +0 -0
  43. data/app/assets/images/dm_event/payment_logos/sofort/ch/icon-64x44.png +0 -0
  44. data/app/assets/images/dm_event/payment_logos/sofort/de/100x38.png +0 -0
  45. data/app/assets/images/dm_event/payment_logos/sofort/de/200x75.png +0 -0
  46. data/app/assets/images/dm_event/payment_logos/sofort/de/250x94.png +0 -0
  47. data/app/assets/images/dm_event/payment_logos/sofort/de/28x7.png +0 -0
  48. data/app/assets/images/dm_event/payment_logos/sofort/de/320x120.png +0 -0
  49. data/app/assets/images/dm_event/payment_logos/sofort/de/42x11.png +0 -0
  50. data/app/assets/images/dm_event/payment_logos/sofort/de/75x28.png +0 -0
  51. data/app/assets/images/dm_event/payment_logos/sofort/de/icon-32x22.png +0 -0
  52. data/app/assets/images/dm_event/payment_logos/sofort/de/icon-32x24.png +0 -0
  53. data/app/assets/images/dm_event/payment_logos/sofort/de/icon-48x32.png +0 -0
  54. data/app/assets/images/dm_event/payment_logos/sofort/de/icon-48x34.png +0 -0
  55. data/app/assets/images/dm_event/payment_logos/sofort/de/icon-64x42.png +0 -0
  56. data/app/assets/images/dm_event/payment_logos/sofort/de/icon-64x44.png +0 -0
  57. data/app/assets/images/dm_event/payment_logos/sofort/es/100x38.png +0 -0
  58. data/app/assets/images/dm_event/payment_logos/sofort/es/200x75.png +0 -0
  59. data/app/assets/images/dm_event/payment_logos/sofort/es/250x94.png +0 -0
  60. data/app/assets/images/dm_event/payment_logos/sofort/es/28x7.png +0 -0
  61. data/app/assets/images/dm_event/payment_logos/sofort/es/320x120.png +0 -0
  62. data/app/assets/images/dm_event/payment_logos/sofort/es/42x11.png +0 -0
  63. data/app/assets/images/dm_event/payment_logos/sofort/es/75x28.png +0 -0
  64. data/app/assets/images/dm_event/payment_logos/sofort/es/icon-32x22.png +0 -0
  65. data/app/assets/images/dm_event/payment_logos/sofort/es/icon-32x24.png +0 -0
  66. data/app/assets/images/dm_event/payment_logos/sofort/es/icon-48x32.png +0 -0
  67. data/app/assets/images/dm_event/payment_logos/sofort/es/icon-48x34.png +0 -0
  68. data/app/assets/images/dm_event/payment_logos/sofort/es/icon-64x42.png +0 -0
  69. data/app/assets/images/dm_event/payment_logos/sofort/es/icon-64x44.png +0 -0
  70. data/app/assets/images/dm_event/payment_logos/sofort/fr/100x38.png +0 -0
  71. data/app/assets/images/dm_event/payment_logos/sofort/fr/200x75.png +0 -0
  72. data/app/assets/images/dm_event/payment_logos/sofort/fr/250x94.png +0 -0
  73. data/app/assets/images/dm_event/payment_logos/sofort/fr/28x7.png +0 -0
  74. data/app/assets/images/dm_event/payment_logos/sofort/fr/320x120.png +0 -0
  75. data/app/assets/images/dm_event/payment_logos/sofort/fr/42x11.png +0 -0
  76. data/app/assets/images/dm_event/payment_logos/sofort/fr/75x28.png +0 -0
  77. data/app/assets/images/dm_event/payment_logos/sofort/fr/icon-32x22.png +0 -0
  78. data/app/assets/images/dm_event/payment_logos/sofort/fr/icon-32x24.png +0 -0
  79. data/app/assets/images/dm_event/payment_logos/sofort/fr/icon-48x32.png +0 -0
  80. data/app/assets/images/dm_event/payment_logos/sofort/fr/icon-48x34.png +0 -0
  81. data/app/assets/images/dm_event/payment_logos/sofort/fr/icon-64x42.png +0 -0
  82. data/app/assets/images/dm_event/payment_logos/sofort/fr/icon-64x44.png +0 -0
  83. data/app/assets/images/dm_event/payment_logos/sofort/it/100x38.png +0 -0
  84. data/app/assets/images/dm_event/payment_logos/sofort/it/200x75.png +0 -0
  85. data/app/assets/images/dm_event/payment_logos/sofort/it/250x94.png +0 -0
  86. data/app/assets/images/dm_event/payment_logos/sofort/it/28x7.png +0 -0
  87. data/app/assets/images/dm_event/payment_logos/sofort/it/320x120.png +0 -0
  88. data/app/assets/images/dm_event/payment_logos/sofort/it/42x11.png +0 -0
  89. data/app/assets/images/dm_event/payment_logos/sofort/it/75x28.png +0 -0
  90. data/app/assets/images/dm_event/payment_logos/sofort/it/icon-32x22.png +0 -0
  91. data/app/assets/images/dm_event/payment_logos/sofort/it/icon-32x24.png +0 -0
  92. data/app/assets/images/dm_event/payment_logos/sofort/it/icon-48x32.png +0 -0
  93. data/app/assets/images/dm_event/payment_logos/sofort/it/icon-48x34.png +0 -0
  94. data/app/assets/images/dm_event/payment_logos/sofort/it/icon-64x42.png +0 -0
  95. data/app/assets/images/dm_event/payment_logos/sofort/it/icon-64x44.png +0 -0
  96. data/app/assets/images/dm_event/payment_logos/sofort/nl/100x38.png +0 -0
  97. data/app/assets/images/dm_event/payment_logos/sofort/nl/200x75.png +0 -0
  98. data/app/assets/images/dm_event/payment_logos/sofort/nl/250x94.png +0 -0
  99. data/app/assets/images/dm_event/payment_logos/sofort/nl/28x7.png +0 -0
  100. data/app/assets/images/dm_event/payment_logos/sofort/nl/320x120.png +0 -0
  101. data/app/assets/images/dm_event/payment_logos/sofort/nl/42x11.png +0 -0
  102. data/app/assets/images/dm_event/payment_logos/sofort/nl/75x28.png +0 -0
  103. data/app/assets/images/dm_event/payment_logos/sofort/nl/icon-32x22.png +0 -0
  104. data/app/assets/images/dm_event/payment_logos/sofort/nl/icon-32x24.png +0 -0
  105. data/app/assets/images/dm_event/payment_logos/sofort/nl/icon-48x32.png +0 -0
  106. data/app/assets/images/dm_event/payment_logos/sofort/nl/icon-48x34.png +0 -0
  107. data/app/assets/images/dm_event/payment_logos/sofort/nl/icon-64x42.png +0 -0
  108. data/app/assets/images/dm_event/payment_logos/sofort/nl/icon-64x44.png +0 -0
  109. data/app/assets/images/dm_event/payment_logos/sofort/pl/100x38.png +0 -0
  110. data/app/assets/images/dm_event/payment_logos/sofort/pl/200x75.png +0 -0
  111. data/app/assets/images/dm_event/payment_logos/sofort/pl/250x94.png +0 -0
  112. data/app/assets/images/dm_event/payment_logos/sofort/pl/28x7.png +0 -0
  113. data/app/assets/images/dm_event/payment_logos/sofort/pl/320x120.png +0 -0
  114. data/app/assets/images/dm_event/payment_logos/sofort/pl/42x11.png +0 -0
  115. data/app/assets/images/dm_event/payment_logos/sofort/pl/75x28.png +0 -0
  116. data/app/assets/images/dm_event/payment_logos/sofort/pl/icon-32x22.png +0 -0
  117. data/app/assets/images/dm_event/payment_logos/sofort/pl/icon-32x24.png +0 -0
  118. data/app/assets/images/dm_event/payment_logos/sofort/pl/icon-48x32.png +0 -0
  119. data/app/assets/images/dm_event/payment_logos/sofort/pl/icon-48x34.png +0 -0
  120. data/app/assets/images/dm_event/payment_logos/sofort/pl/icon-64x42.png +0 -0
  121. data/app/assets/images/dm_event/payment_logos/sofort/pl/icon-64x44.png +0 -0
  122. data/app/assets/images/dm_event/payment_logos/sofort/uk/100x38.png +0 -0
  123. data/app/assets/images/dm_event/payment_logos/sofort/uk/200x75.png +0 -0
  124. data/app/assets/images/dm_event/payment_logos/sofort/uk/250x94.png +0 -0
  125. data/app/assets/images/dm_event/payment_logos/sofort/uk/28x7.png +0 -0
  126. data/app/assets/images/dm_event/payment_logos/sofort/uk/320x120.png +0 -0
  127. data/app/assets/images/dm_event/payment_logos/sofort/uk/42x11.png +0 -0
  128. data/app/assets/images/dm_event/payment_logos/sofort/uk/75x28.png +0 -0
  129. data/app/assets/images/dm_event/payment_logos/sofort/uk/icon-32x22.png +0 -0
  130. data/app/assets/images/dm_event/payment_logos/sofort/uk/icon-32x24.png +0 -0
  131. data/app/assets/images/dm_event/payment_logos/sofort/uk/icon-48x32.png +0 -0
  132. data/app/assets/images/dm_event/payment_logos/sofort/uk/icon-48x34.png +0 -0
  133. data/app/assets/images/dm_event/payment_logos/sofort/uk/icon-64x42.png +0 -0
  134. data/app/assets/images/dm_event/payment_logos/sofort/uk/icon-64x44.png +0 -0
  135. data/app/assets/javascripts/dm_event/admin.js +94 -0
  136. data/app/assets/stylesheets/dm_event/admin.css +152 -0
  137. data/app/assets/stylesheets/dm_event/application.css +65 -0
  138. data/app/controllers/dm_event/admin/admin_controller.rb +13 -0
  139. data/app/controllers/dm_event/admin/registrations_controller.rb +411 -0
  140. data/app/controllers/dm_event/admin/workshop_prices_controller.rb +73 -0
  141. data/app/controllers/dm_event/admin/workshops_controller.rb +147 -0
  142. data/app/controllers/dm_event/application_controller.rb +23 -0
  143. data/app/controllers/dm_event/payments_controller.rb +83 -0
  144. data/app/controllers/dm_event/registrations_controller.rb +368 -0
  145. data/app/datatables/registration_datatable.rb +117 -0
  146. data/app/helpers/dm_event/application_helper.rb +4 -0
  147. data/app/helpers/dm_event/registrations_helper.rb +14 -0
  148. data/app/helpers/dm_event/workshops_helper.rb +41 -0
  149. data/app/mailers/payment_reminder_mailer.rb +25 -0
  150. data/app/mailers/registration_notify_mailer.rb +31 -0
  151. data/app/models/dm_event/concerns/ability.rb +38 -0
  152. data/app/models/dm_event/concerns/registration_state_email.rb +80 -0
  153. data/app/models/dm_event/concerns/registration_state_machine.rb +218 -0
  154. data/app/models/dm_event/concerns/user_profile.rb +20 -0
  155. data/app/models/dm_event/model_decorators.rb +6 -0
  156. data/app/models/dm_event/permitted_params.rb +35 -0
  157. data/app/models/payment.rb +48 -0
  158. data/app/models/registration.rb +297 -0
  159. data/app/models/workshop.rb +247 -0
  160. data/app/models/workshop_price.rb +118 -0
  161. data/app/presenters/event_common_presenter.rb +31 -0
  162. data/app/presenters/registration_presenter.rb +34 -0
  163. data/app/presenters/workshop_presenter.rb +22 -0
  164. data/app/views/dm_event/admin/registrations/_form.html.erb +297 -0
  165. data/app/views/dm_event/admin/registrations/action_state.js.erb +1 -0
  166. data/app/views/dm_event/admin/registrations/edit.html.erb +1 -0
  167. data/app/views/dm_event/admin/registrations/index.html.erb +0 -0
  168. data/app/views/dm_event/admin/workshop_prices/_form.html.erb +62 -0
  169. data/app/views/dm_event/admin/workshop_prices/edit.html.erb +2 -0
  170. data/app/views/dm_event/admin/workshop_prices/index.html.erb +31 -0
  171. data/app/views/dm_event/admin/workshop_prices/new.html.erb +2 -0
  172. data/app/views/dm_event/admin/workshops/_form.html.erb +61 -0
  173. data/app/views/dm_event/admin/workshops/_header_menu.html.erb +21 -0
  174. data/app/views/dm_event/admin/workshops/_registration_stats.html.erb +58 -0
  175. data/app/views/dm_event/admin/workshops/additional_configuration.html.erb +47 -0
  176. data/app/views/dm_event/admin/workshops/edit.html.erb +2 -0
  177. data/app/views/dm_event/admin/workshops/edit_system_email.html.erb +75 -0
  178. data/app/views/dm_event/admin/workshops/financials.html.erb +179 -0
  179. data/app/views/dm_event/admin/workshops/index.html.erb +100 -0
  180. data/app/views/dm_event/admin/workshops/lost_users.html.erb +44 -0
  181. data/app/views/dm_event/admin/workshops/new.html.erb +2 -0
  182. data/app/views/dm_event/admin/workshops/show.html.erb +47 -0
  183. data/app/views/dm_event/admin/workshops/user_outstanding_balances.html.erb +29 -0
  184. data/app/views/dm_event/liquid_tags/_funding_project_status.html.erb +27 -0
  185. data/app/views/dm_event/payments/_registrations_paypal_standard.html.erb +23 -0
  186. data/app/views/dm_event/payments/_registrations_sofort.html.erb +18 -0
  187. data/app/views/dm_event/registrations/_section_address.html.erb +30 -0
  188. data/app/views/dm_event/registrations/_section_custom_fields.html.erb +7 -0
  189. data/app/views/dm_event/registrations/_section_prices.html.erb +33 -0
  190. data/app/views/dm_event/registrations/_workshop_price.html.erb +18 -0
  191. data/app/views/dm_event/registrations/choose_payment.html.erb +5 -0
  192. data/app/views/dm_event/registrations/closed.html.erb +14 -0
  193. data/app/views/dm_event/registrations/new.html.erb +30 -0
  194. data/app/views/dm_event/registrations/success.html.erb +15 -0
  195. data/app/views/layouts/email_templates/dm_event_email_layout.html.erb +318 -0
  196. data/app/views/layouts/email_templates/dm_event_email_layout.text.erb +1 -0
  197. data/app/views/layouts/email_templates/dm_event_payment_reminder.html.erb +22 -0
  198. data/app/views/layouts/email_templates/dm_event_payment_reminder.text.erb +19 -0
  199. data/app/views/layouts/email_templates/dm_event_registration_notify.html.erb +21 -0
  200. data/app/views/layouts/email_templates/dm_event_registration_notify.text.erb +13 -0
  201. data/config/initializers/dm_event.rb +1 -0
  202. data/config/initializers/liquid_init.rb +2 -0
  203. data/config/locales/ems.cs.yml +51 -0
  204. data/config/locales/ems.de.yml +51 -0
  205. data/config/locales/ems.en.yml +51 -0
  206. data/config/locales/ems.fi.yml +51 -0
  207. data/config/locales/ems.ja.yml +51 -0
  208. data/config/routes.rb +40 -0
  209. data/db/migrate/20130510155617_create_events.rb +98 -0
  210. data/db/migrate/20130516124327_add_locale_to_registration.rb +5 -0
  211. data/db/migrate/20130516204454_add_user_profile_to_registration.rb +11 -0
  212. data/db/migrate/20130518092324_add_require_address.rb +7 -0
  213. data/db/migrate/20130609145812_add_workshop_published.rb +5 -0
  214. data/db/migrate/20130624134506_add_amount_currency.rb +16 -0
  215. data/db/migrate/20130703131515_add_workshop_currency.rb +5 -0
  216. data/db/migrate/20130913083529_add_event_funding_type.rb +6 -0
  217. data/db/migrate/20130914105736_add_enable_payments.rb +5 -0
  218. data/db/migrate/20130916160033_add_sidebar_to_workshop.rb +5 -0
  219. data/db/migrate/20130917161700_add_show_address.rb +5 -0
  220. data/db/migrate/20131116180719_add_default_funding_goal.rb +8 -0
  221. data/db/migrate/20131127165445_add_payment_reminder.rb +5 -0
  222. data/db/migrate/20140122204702_add_event_image.rb +5 -0
  223. data/db/migrate/20140523165342_add_workshop_summary.rb +5 -0
  224. data/db/migrate/20140605052208_add_uuid_to_registration.rb +10 -0
  225. data/db/migrate/20140707152124_add_registration_comment_count.rb +5 -0
  226. data/db/migrate/20140708142312_add_payment_comment.rb +20 -0
  227. data/db/migrate/20150709125105_add_payment_reminder_history.rb +16 -0
  228. data/db/migrate/20160810081542_add_bcc_contact_email.rb +5 -0
  229. data/db/migrate/20160821150120_index_foreign_keys_in_ems_registrations.rb +9 -0
  230. data/db/migrate/20160821150121_index_foreign_keys_in_ems_workshop_price_translations.rb +5 -0
  231. data/db/migrate/20160821150122_index_foreign_keys_in_ems_workshop_prices.rb +6 -0
  232. data/db/migrate/20160821150123_index_foreign_keys_in_ems_workshop_translations.rb +5 -0
  233. data/db/migrate/20160821150124_index_foreign_keys_in_ems_workshops.rb +6 -0
  234. data/lib/dm_event/engine.rb +20 -0
  235. data/lib/dm_event/liquid/tags/funding_project_status.rb +55 -0
  236. data/lib/dm_event.rb +4 -0
  237. data/lib/tasks/dm_event_tasks.rake +4 -0
  238. data/spec/controllers/registrations_controller_spec.rb +152 -0
  239. data/spec/dummy/README.rdoc +28 -0
  240. data/spec/dummy/Rakefile +6 -0
  241. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  242. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  243. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  244. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  245. data/spec/dummy/app/models/ability.rb +10 -0
  246. data/spec/dummy/app/models/user.rb +6 -0
  247. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  248. data/spec/dummy/bin/bundle +3 -0
  249. data/spec/dummy/bin/rails +4 -0
  250. data/spec/dummy/bin/rake +4 -0
  251. data/spec/dummy/config/application.rb +26 -0
  252. data/spec/dummy/config/boot.rb +5 -0
  253. data/spec/dummy/config/database.yml +25 -0
  254. data/spec/dummy/config/environment.rb +5 -0
  255. data/spec/dummy/config/environments/development.rb +37 -0
  256. data/spec/dummy/config/environments/production.rb +82 -0
  257. data/spec/dummy/config/environments/test.rb +39 -0
  258. data/spec/dummy/config/initializers/assets.rb +8 -0
  259. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  260. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  261. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  262. data/spec/dummy/config/initializers/inflections.rb +16 -0
  263. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  264. data/spec/dummy/config/initializers/session_store.rb +3 -0
  265. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  266. data/spec/dummy/config/locales/en.yml +23 -0
  267. data/spec/dummy/config/routes.rb +11 -0
  268. data/spec/dummy/config/secrets.yml +22 -0
  269. data/spec/dummy/config.ru +4 -0
  270. data/spec/dummy/db/development.sqlite3 +0 -0
  271. data/spec/dummy/db/migrate/20141114170927_add_globalize_countries.dm_core.rb +50 -0
  272. data/spec/dummy/db/migrate/20141114170928_devise_create_users.dm_core.rb +46 -0
  273. data/spec/dummy/db/migrate/20141114170929_add_user_fields.dm_core.rb +14 -0
  274. data/spec/dummy/db/migrate/20141114170930_rolify_create_roles.dm_core.rb +20 -0
  275. data/spec/dummy/db/migrate/20141114170931_add_last_access.dm_core.rb +10 -0
  276. data/spec/dummy/db/migrate/20141114170932_create_versions.dm_core.rb +19 -0
  277. data/spec/dummy/db/migrate/20141114170933_add_object_changes_column_to_versions.dm_core.rb +12 -0
  278. data/spec/dummy/db/migrate/20141114170934_create_dm_core_accounts.dm_core.rb +13 -0
  279. data/spec/dummy/db/migrate/20141114170935_add_account_to_users.dm_core.rb +9 -0
  280. data/spec/dummy/db/migrate/20141114170936_create_preferences.dm_core.rb +13 -0
  281. data/spec/dummy/db/migrate/20141114170937_create_comments.dm_core.rb +22 -0
  282. data/spec/dummy/db/migrate/20141114170938_add_activity.dm_core.rb +21 -0
  283. data/spec/dummy/db/migrate/20141114170939_add_type_to_comments.dm_core.rb +9 -0
  284. data/spec/dummy/db/migrate/20141114170940_add_category.dm_core.rb +28 -0
  285. data/spec/dummy/db/migrate/20141114170941_create_email_table.dm_core.rb +26 -0
  286. data/spec/dummy/db/migrate/20141114170942_add_user_profile.dm_core.rb +46 -0
  287. data/spec/dummy/db/migrate/20141114170943_add_profile_email.dm_core.rb +14 -0
  288. data/spec/dummy/db/migrate/20141114170944_create_payment_history.dm_core.rb +37 -0
  289. data/spec/dummy/db/migrate/20141114170945_change_anchor_field.dm_core.rb +10 -0
  290. data/spec/dummy/db/migrate/20141114170946_create_user_site_profile.dm_core.rb +27 -0
  291. data/spec/dummy/db/migrate/20141114170947_add_avatar.dm_core.rb +12 -0
  292. data/spec/dummy/db/migrate/20141114170948_add_notify_to_payment_history.dm_core.rb +8 -0
  293. data/spec/dummy/db/migrate/20141114170949_acts_as_votable_migration.dm_core.rb +28 -0
  294. data/spec/dummy/db/migrate/20141114170950_add_user_site_profile_uuid.dm_core.rb +19 -0
  295. data/spec/dummy/db/migrate/20141114170951_add_invoice_id.dm_core.rb +7 -0
  296. data/spec/dummy/db/migrate/20141114170952_acts_as_follower_migration.dm_core.rb +18 -0
  297. data/spec/dummy/db/migrate/20141114170953_rename_invoice_id.dm_core.rb +12 -0
  298. data/spec/dummy/db/migrate/20141114170954_add_core_addresses.dm_core.rb +18 -0
  299. data/spec/dummy/db/migrate/20141114170955_papertrail_increase_column.dm_core.rb +9 -0
  300. data/spec/dummy/db/migrate/20141114170956_acts_as_taggable_on_migration.dm_core.rb +32 -0
  301. data/spec/dummy/db/migrate/20141114170957_add_missing_unique_indices.dm_core.rb +23 -0
  302. data/spec/dummy/db/migrate/20141114170958_add_taggings_counter_cache_to_tags.dm_core.rb +16 -0
  303. data/spec/dummy/db/migrate/20141114170959_create_custom_fields.dm_core.rb +40 -0
  304. data/spec/dummy/db/migrate/20141114170960_add_missing_taggable_index.dm_core.rb +11 -0
  305. data/spec/dummy/db/migrate/20141119103740_create_cms.dm_cms.rb +92 -0
  306. data/spec/dummy/db/migrate/20141119103741_add_account_to_cms.dm_cms.rb +11 -0
  307. data/spec/dummy/db/migrate/20141119103742_create_blog.dm_cms.rb +62 -0
  308. data/spec/dummy/db/migrate/20141119103743_add_notification_sent.dm_cms.rb +6 -0
  309. data/spec/dummy/db/migrate/20141119103744_add_blog_comment.dm_cms.rb +8 -0
  310. data/spec/dummy/db/migrate/20141119103745_add_blog_image.dm_cms.rb +6 -0
  311. data/spec/dummy/db/migrate/20141119103746_rename_snippet_slug.dm_cms.rb +14 -0
  312. data/spec/dummy/db/migrate/20141119103747_add_requires_subscription_blog.dm_cms.rb +8 -0
  313. data/spec/dummy/db/migrate/20141119103748_add_pages_ranked_model.dm_cms.rb +23 -0
  314. data/spec/dummy/db/migrate/20141119103749_add_blog_owner.dm_cms.rb +7 -0
  315. data/spec/dummy/db/migrate/20141119103750_create_media_files.dm_cms.rb +30 -0
  316. data/spec/dummy/db/migrate/20141119103751_add_cmspage_summary.dm_cms.rb +7 -0
  317. data/spec/dummy/db/migrate/20141119103752_add_blog_image_email_header.dm_cms.rb +6 -0
  318. data/spec/dummy/db/migrate/20141119103753_add_header_image.dm_cms.rb +10 -0
  319. data/spec/dummy/db/migrate/20160128154135_add_favored_locale.dm_core.rb +17 -0
  320. data/spec/dummy/db/migrate/20160128154136_update_papertrail_v4.dm_core.rb +41 -0
  321. data/spec/dummy/db/schema.rb +677 -0
  322. data/spec/dummy/public/404.html +67 -0
  323. data/spec/dummy/public/422.html +67 -0
  324. data/spec/dummy/public/500.html +66 -0
  325. data/spec/dummy/public/favicon.ico +0 -0
  326. data/spec/factories/account_factory.rb +7 -0
  327. data/spec/factories/registration_factory.rb +13 -0
  328. data/spec/factories/workshop_factory.rb +25 -0
  329. data/spec/factories/workshop_price_factory.rb +19 -0
  330. data/spec/models/registration_spec.rb +83 -0
  331. data/spec/models/workshop_spec.rb +26 -0
  332. data/spec/rails_helper.rb +70 -0
  333. data/spec/spec_helper.rb +85 -0
  334. data/spec/support/accounts.rb +17 -0
  335. data/spec/support/devise.rb +44 -0
  336. data/spec/support/fix_locale.rb +57 -0
  337. metadata +520 -0
@@ -0,0 +1,297 @@
1
+ # Important: one reason to link off of the user_profile instead of the user
2
+ # is so that we can support registrations without requiring an account.
3
+ # We can create a "userless" profile, that has all the necessary information.
4
+ # This is instead of duplicating all those fields in the registration table.
5
+ #------------------------------------------------------------------------------
6
+ class Registration < ActiveRecord::Base
7
+ include DmEvent::Concerns::RegistrationStateMachine
8
+ include DmEvent::Concerns::RegistrationStateEmail
9
+ include DmCore::Concerns::HasCustomFields
10
+ include ActiveMerchant::Billing::Integrations
11
+
12
+ self.table_name = 'ems_registrations'
13
+
14
+ belongs_to :workshop, counter_cache: true
15
+ belongs_to :workshop_price
16
+ belongs_to :user_profile
17
+ belongs_to :account
18
+ has_many :payment_histories, as: :owner, dependent: :destroy
19
+ belongs_to :payment_comment, class_name: 'Comment'
20
+ preference :payment_reminder_hold_until, :date
21
+ serialize :payment_reminder_history, Array
22
+ attr_accessor :payment_comment_text
23
+ acts_as_commentable :private
24
+
25
+ accepts_nested_attributes_for :user_profile
26
+
27
+ monetize :amount_paid_cents, with_model_currency: :amount_paid_currency, allow_nil: true
28
+
29
+ default_scope { where(account_id: Account.current.id) }
30
+ scope :attending, -> { where("(aasm_state = 'accepted' OR aasm_state = 'paid') AND archived_on IS NULL") }
31
+ scope :accepted, -> { where("aasm_state = 'accepted' AND archived_on IS NULL") }
32
+ scope :paid, -> { where("aasm_state = 'paid' AND archived_on IS NULL") }
33
+ scope :unpaid, -> { where("aasm_state = 'accepted' AND archived_on IS NULL") } # same as accepted
34
+ scope :discounted,-> { where("discount_value > 0") } # use like registrations.attending.discounted
35
+
36
+ after_initialize :create_uuid
37
+ before_create :set_currency
38
+ after_create :set_receipt_code
39
+
40
+ validates_uniqueness_of :uuid
41
+ validates_presence_of :workshop_price_id, if: Proc.new { |reg| reg.workshop.workshop_prices.size > 0}
42
+ validates_presence_of :workshop_price_id, if: Proc.new { |reg| reg.workshop.workshop_prices.size > 0}
43
+ validates_numericality_of :discount_value, allow_nil: true
44
+ validates_length_of :payment_comment, maximum: 255
45
+
46
+ delegate :first_name, :last_name, :full_name, :email, :address, :address2,
47
+ :city, :state, :country, :zipcode, :phone, to: :user_profile
48
+
49
+ private
50
+
51
+ # the uuid is used to provide a private url to a customer so that they can access
52
+ # their registration if not logged in. This is particularly important when
53
+ # a customer registers without having a user account.
54
+ #------------------------------------------------------------------------------
55
+ def create_uuid
56
+ self.uuid = SecureRandom.uuid if self.new_record?
57
+ end
58
+
59
+ # The amount_paid currency should match the workshop base currency
60
+ #------------------------------------------------------------------------------
61
+ def set_currency
62
+ self[:amount_paid_currency] = workshop.base_currency
63
+ end
64
+
65
+ # Receipt code: (workshop.id)-(registration.id). eg. 003-101
66
+ #------------------------------------------------------------------------------
67
+ def set_receipt_code
68
+ receipt_code = ("%03d" % workshop.id) + '-' + ("%03d" % self[:id])
69
+ update_attribute(:receipt_code, receipt_code)
70
+ end
71
+
72
+ public
73
+
74
+ # receipt code is simply the record id + 1100
75
+ #------------------------------------------------------------------------------
76
+ def self.receiptcode_to_id(receiptcode)
77
+ return receipt_code.split('-')[1].to_i
78
+ end
79
+
80
+ # Price of this registration (without discount)
81
+ #------------------------------------------------------------------------------
82
+ def price
83
+ (workshop_price && workshop_price.price) ? workshop_price.price : Money.new(0, workshop.base_currency)
84
+ end
85
+
86
+ # Price with discount
87
+ #------------------------------------------------------------------------------
88
+ def discounted_price
89
+ price - discount
90
+ end
91
+
92
+ #------------------------------------------------------------------------------
93
+ def discount
94
+ return Money.new(0, workshop.base_currency) if workshop_price.nil? || workshop_price.price.nil?
95
+
96
+ unless discount_value.blank?
97
+ cents = (discount_use_percent ? (workshop_price.price.cents * discount_value / 100) : (discount_value * 100))
98
+ else
99
+ cents = 0
100
+ end
101
+ Money.new(cents, workshop_price.price.currency)
102
+ end
103
+
104
+ # Return the amount still owed, based on the current payments made.
105
+ # balance_owed is positive if payment is still required. Negative if there
106
+ # has been an overpayment
107
+ #------------------------------------------------------------------------------
108
+ def balance_owed
109
+ discounted_price - amount_paid
110
+ end
111
+
112
+ # suggested amount of next payment.
113
+ # if a payment is within 20 of the balance_owed, then they should pay the balance
114
+ #------------------------------------------------------------------------------
115
+ def payment_owed
116
+ if workshop_price
117
+ amount_20 = Money.new(2000, workshop_price.price.currency)
118
+ (workshop_price.payment_price + amount_20) > balance_owed ? balance_owed : workshop_price.payment_price
119
+ else
120
+ balance_owed
121
+ end
122
+ end
123
+
124
+ # Return the number of items specified, in particular the number of items in
125
+ # a particular state
126
+ #------------------------------------------------------------------------------
127
+ def self.number_of(state, options = {})
128
+ query = (options[:only_confirmed] ? where.not(confirmed_on: nil) : all)
129
+ case state
130
+ when :attending
131
+ attending.count
132
+ when :unpaid
133
+ #--- the number of unpaid is the same as the number of accepted
134
+ number_of(:accepted)
135
+ when :checkedin
136
+ query.where.not(checkin_at: 0).where(archived_on: nil).count
137
+ when :archived
138
+ query.where.not(archived_on: nil).count
139
+ when :registrations
140
+ #--- don't count any canceled
141
+ query.where(archived_on: nil).where.not(aasm_state: 'canceled').where.not(aasm_state: 'refunded').count
142
+ when :at_price
143
+ #--- number of registrations for a particular price
144
+ query.where(archived_on: nil).where("(aasm_state = 'paid' OR aasm_state = 'accepted')").where(workshop_price_id: options[:price_id]).count
145
+ when :for_all_prices
146
+ #--- array of counts per price
147
+ query.where(archived_on: nil).where("(aasm_state = 'paid' OR aasm_state = 'accepted')").group(:workshop_price_id).count
148
+ when :discounted
149
+ attending.discounted.count
150
+ when :discounted_total
151
+ total = attending.discounted.to_a.sum(&:discount)
152
+ (total == 0) ? Money.new(0) : total
153
+ when :user_updated
154
+ #--- how many users updated their record
155
+ query.where(archived_on: nil).where("(aasm_state = 'paid' OR aasm_state = 'accepted')").where.not(user_updated_at: nil).count
156
+ when :confirmed
157
+ #--- how many users confirmed their attendance
158
+ where.not(confirmed_on: nil).where(archived_on: nil).where("(aasm_state = 'paid' OR aasm_state = 'accepted')").count
159
+ else
160
+ #--- must be wanting to count the process states
161
+ query.where(archived_on: nil, aasm_state: state).count
162
+ end
163
+ end
164
+
165
+ # check if the regsitration is unpaid
166
+ #------------------------------------------------------------------------------
167
+ def unpaid?
168
+ self.accepted? && self.archived_on == nil
169
+ end
170
+
171
+ # Is it time to send a payment reminder?
172
+ # Due first 7 days after inital registration. Then every 14 days after that
173
+ #------------------------------------------------------------------------------
174
+ def payment_reminder_due?
175
+ if preferred_payment_reminder_hold_until.nil? || preferred_payment_reminder_hold_until < Time.now
176
+ time_period = self.payment_reminder_sent_on.nil? ? (self.created_at + 7.days) : (self.payment_reminder_sent_on + 14.days)
177
+ self.balance_owed > Money.new(0, workshop.base_currency) && time_period < Time.now
178
+ end
179
+
180
+ # if recurring_period since last paymnet
181
+ # if 7 days after registration and no payment
182
+ # if 14 days since last reminder and no payment
183
+ # if resume_reminders is past
184
+ # last_payment = payment_histories.order('created_on').last
185
+ # if workshop_price.recurring?
186
+ # if
187
+ end
188
+
189
+ # Setup the columns for exporting data as csv.
190
+ #------------------------------------------------------------------------------
191
+ def self.csv_columns(workshop)
192
+ column_definitions = []
193
+ column_definitions << ["Receipt Code", "'R-' + item.receipt_code", 75] # 'R-' makes sure Numbers treats as a String, not a Date
194
+ column_definitions << ['Process State', 'item.aasm_state', 100]
195
+ column_definitions << ["Full Name", "item.full_name", 100]
196
+ column_definitions << ["Last Name", "item.last_name.capitalize", 100]
197
+ column_definitions << ["First Name", "item.first_name.capitalize", 100]
198
+ column_definitions << ["Email", "item.email.downcase", 150]
199
+ column_definitions << ["Address", "item.address", 150]
200
+ column_definitions << ["Address2", "item.address2"]
201
+ column_definitions << ["City", "item.city.capitalize", 100]
202
+ column_definitions << ["State", "item.state.capitalize"]
203
+ column_definitions << ["Zipcode", "item.zipcode"]
204
+ column_definitions << ["Country", "item.country.code"]
205
+
206
+ column_definitions << ['Registered on', 'item.created_at.to_date', 75, {type: 'DateTime', numberformat: 'd mmm, yyyy'}]
207
+
208
+ column_definitions << ["Price", "item.workshop_price.price.to_f", nil, {type: 'Number', numberformat: '#,##0.00'}]
209
+ column_definitions << ["Price Description", "item.workshop_price.price_description"]
210
+ column_definitions << ["Price Sub Descr", "item.workshop_price.sub_description"]
211
+ column_definitions << ["Discount", "item.discount.to_f", nil, {type: 'Number', numberformat: '#,##0.00'}]
212
+ column_definitions << ["Paid", "item.amount_paid.to_f", nil, {type: 'Number', numberformat: '#,##0.00'}]
213
+ column_definitions << ["Balance", "item.balance_owed.to_f", nil, {type: 'Number', numberformat: '#,##0.00'}]
214
+
215
+ # ---- add the extra fields defined in the workshop record
216
+ workshop.custom_field_defs.each_with_index do | x, index |
217
+ case x.field_type
218
+ when 'check_box_collection'
219
+ column_definitions << [ "#{x.column_name}", "(z = item.custom_fields.detect { |y| y.custom_field_def_id == #{x.id} }) ? z.value : ''", nil, {type: 'list', custom_field: true}]
220
+ when 'divider'
221
+ else
222
+ column_definitions << [ "#{x.column_name}", "(z = item.custom_fields.detect { |y| y.custom_field_def_id == #{x.id} }) ? z.value : ''", nil, {custom_field: true}]
223
+ end
224
+ end
225
+ return column_definitions
226
+ end
227
+
228
+ # Payment was entered manually, create the history record. You can tell it's
229
+ # a manual entry if the user_profile is filled in - means a human did it.
230
+ #------------------------------------------------------------------------------
231
+ def manual_payment(payment_history, cost, total_currency, user_profile,
232
+ options = { item_ref: '', payment_method: 'cash', bill_to_name: '', payment_date: Time.now,
233
+ notify_data: nil, transaction_id: nil, status: '' } )
234
+ amount = Monetize.parse(cost, total_currency)
235
+
236
+ if payment_history
237
+ new_amount_paid = self.amount_paid - self.workshop_price.to_base_currency(payment_history.total) + self.workshop_price.to_base_currency(amount)
238
+ payment_history.update_attributes(
239
+ item_ref: options[:item_ref],
240
+ cost: cost,
241
+ total_cents: amount.cents,
242
+ total_currency: amount.currency.iso_code,
243
+ payment_method: options[:payment_method],
244
+ bill_to_name: options[:bill_to_name],
245
+ payment_date: options[:payment_date],
246
+ user_profile_id: user_profile.id)
247
+ else
248
+ new_amount_paid = self.amount_paid + self.workshop_price.to_base_currency(amount)
249
+ payment_history = self.payment_histories.create(
250
+ anchor_id: receipt_code,
251
+ item_ref: options[:item_ref],
252
+ cost: cost,
253
+ quantity: 1,
254
+ discount: 0,
255
+ total_cents: amount.cents,
256
+ total_currency: amount.currency.iso_code,
257
+ payment_method: options[:payment_method],
258
+ bill_to_name: options[:bill_to_name],
259
+ payment_date: options[:payment_date],
260
+ user_profile_id: (user_profile ? user_profile.id : nil),
261
+ notify_data: options[:notify_data],
262
+ transaction_id: options[:transaction_id],
263
+ status: (user_profile ? "Completed" : options[:status])
264
+ )
265
+ end
266
+
267
+ if payment_history.errors.empty?
268
+ self.update_attribute(:amount_paid_cents, new_amount_paid.cents)
269
+ self.reload
270
+ self.send('paid!') if balance_owed.cents <= 0 && self.accepted?
271
+ else
272
+ logger.error("===> Error: Registration.manual_payment: #{payment_history.errors.inspect}")
273
+ end
274
+ return payment_history
275
+ end
276
+
277
+ # delete a payment and update the registrations total amount paid
278
+ #------------------------------------------------------------------------------
279
+ def delete_payment(payment_id)
280
+ payment = PaymentHistory.find(payment_id)
281
+ if payment
282
+ self.update_attribute(:amount_paid_cents, (self.amount_paid - self.workshop_price.to_base_currency(payment.total)).cents)
283
+ payment.destroy
284
+ suppress_transition_email
285
+ self.send('accept!') if balance_owed.positive? && self.paid?
286
+ return true
287
+ end
288
+ return false
289
+ end
290
+
291
+ # Return the payment page url, so that it can be used in emails
292
+ #------------------------------------------------------------------------------
293
+ def payment_url
294
+ DmEvent::Engine.routes.url_helpers.register_choose_payment_url(self.uuid, host: Account.current.url_host, locale: I18n.locale)
295
+ end
296
+
297
+ end
@@ -0,0 +1,247 @@
1
+ class Workshop < ActiveRecord::Base
2
+ include DmCore::Concerns::DefinesCustomFields
3
+
4
+ self.table_name = 'ems_workshops'
5
+
6
+ belongs_to :country, class_name: 'DmCore::Country'
7
+ has_many :registrations, dependent: :destroy
8
+ has_many :workshop_prices, dependent: :destroy
9
+ has_many :system_emails, {as: :emailable, dependent: :destroy}
10
+ has_one :pending_email, -> { where("email_type LIKE 'pending'") }, class_name: 'SystemEmail', as: :emailable
11
+ has_one :accepted_email, -> { where("email_type LIKE 'accepted'") }, class_name: 'SystemEmail', as: :emailable
12
+ has_one :rejected_email, -> { where("email_type LIKE 'rejected'") }, class_name: 'SystemEmail', as: :emailable
13
+ has_one :paid_email, -> { where("email_type LIKE 'paid'") }, class_name: 'SystemEmail', as: :emailable
14
+ has_one :waitlisted_email, -> { where("email_type LIKE 'waitlisted'") }, class_name: 'SystemEmail', as: :emailable
15
+ has_one :reviewing_email, -> { where("email_type LIKE 'reviewing'") }, class_name: 'SystemEmail', as: :emailable
16
+ has_one :canceled_email, -> { where("email_type LIKE 'canceled'") }, class_name: 'SystemEmail', as: :emailable
17
+ has_one :refunded_email, -> { where("email_type LIKE 'refunded'") }, class_name: 'SystemEmail', as: :emailable
18
+ has_one :noshow_email, -> { where("email_type LIKE 'noshow'") }, class_name: 'SystemEmail', as: :emailable
19
+ has_one :cms_blog, as: :owner
20
+ has_one :forum, as: :owner
21
+
22
+ # --- globalize
23
+ translates :title, :description, :summary, :sidebar, fallbacks_for_empty_translations: true
24
+ globalize_accessors locales: DmCore::Language.language_array
25
+
26
+ # --- FriendlyId
27
+ extend FriendlyId
28
+ include DmCore::Concerns::FriendlyId
29
+
30
+ resourcify
31
+
32
+ preference :show_social_buttons, :boolean, default: false
33
+ preference :header_accent_color, :string
34
+
35
+ # --- validations
36
+ validates_presence_of :country_id
37
+ validates_presence_of :base_currency
38
+ validates_presence_of :starting_on
39
+ validates_presence_of :ending_on
40
+ validates_presence_of :contact_email
41
+ validates_presence_of :event_style
42
+ validates :title, presence_default_locale: true
43
+ validates :description, liquid: { locales: true }, presence_default_locale: true
44
+ validates :sidebar, liquid: { locales: true }
45
+
46
+ # validates_presence_of :deadline_on
47
+
48
+ default_scope { where(account_id: Account.current.id) }
49
+
50
+ #--- upcoming and past are used in the admin, so should be published and non-published
51
+ scope :upcoming, -> { where('ending_on > ? AND archived_on IS NULL', (Date.today - 1).to_s).order('starting_on DESC').includes(:translations) }
52
+ scope :past, -> { where('ending_on <= ? AND archived_on IS NULL', (Date.today - 1).to_s).order('starting_on DESC').includes(:translations) }
53
+
54
+ #--- available is list of published and registration open and not ended
55
+ scope :available, -> { where(published: true).where('ending_on > ? AND deadline_on > ? AND archived_on IS NULL',
56
+ (Date.today - 1).to_s, (Date.today - 1).to_s).order('starting_on ASC') }
57
+
58
+ scope :published, -> { where(published: true).where('archived_on IS NULL') }
59
+
60
+ #--- don't use allow_nil, as this will erase the base_currency field if no funding_goal is set
61
+ monetize :funding_goal_cents, with_model_currency: :base_currency
62
+
63
+ EVENT_STYLES = [['Workshop', 'workshop'], ['Crowdfunding', 'crowdfunding']]
64
+
65
+ #------------------------------------------------------------------------------
66
+ def model_slug
67
+ send("title_#{Account.current.preferred_default_locale}")
68
+ end
69
+
70
+ # If the total_available is nil, then there are unlimited tickets to be sold.
71
+ # Otherwise, check if we have sold out
72
+ #------------------------------------------------------------------------------
73
+ def price_sold_out?(workshop_price)
74
+ # p.sold_out?(@workshop.event_registration.number_of(:registrations_by_paymenttype, payment_id: p.id)
75
+ false # TODO
76
+ end
77
+
78
+ # Is this workshop in the past?
79
+ #------------------------------------------------------------------------------
80
+ def past?
81
+ ending_on < Time.now
82
+ end
83
+
84
+ #------------------------------------------------------------------------------
85
+ def show_social_buttons?
86
+ preferred_show_social_buttons?
87
+ end
88
+
89
+ # Is the registration closed? If deadline is null, then registration is open ended
90
+ #------------------------------------------------------------------------------
91
+ def registration_closed?
92
+ !published? || (deadline_on ? (deadline_on < Time.now.to_date) : false)
93
+ end
94
+
95
+ #------------------------------------------------------------------------------
96
+ def published?
97
+ published
98
+ end
99
+
100
+ # toggle the archive state of the workshop
101
+ #------------------------------------------------------------------------------
102
+ def toggle_archive
103
+ archived_on ? update_attribute(:archived_on, nil) : update_attribute(:archived_on, Time.now)
104
+ end
105
+
106
+ #------------------------------------------------------------------------------
107
+ def archived?
108
+ self.archived_on ? true : false
109
+ end
110
+
111
+ #------------------------------------------------------------------------------
112
+ def crowdfunding?
113
+ self.event_style == 'crowdfunding'
114
+ end
115
+
116
+ # Return financial summary and details
117
+ # level => :summary or :detail
118
+ #------------------------------------------------------------------------------
119
+ def financial_details(level = :detail)
120
+ #--- pick currency of first price
121
+ financials = {summary: { total_possible: Money.new(0, base_currency), total_possible_worst: Money.new(0, base_currency),
122
+ total_paid: Money.new(0, base_currency), total_outstanding: Money.new(0, base_currency),
123
+ total_outstanding_worst: Money.new(0, base_currency), total_discounts: Money.new(0, base_currency),
124
+ total_paid_percent: 0},
125
+ collected: {},
126
+ collected_monthly: {},
127
+ payment_type: {},
128
+ projected: {}
129
+ }
130
+
131
+ registrations.attending.includes(:workshop_price).each do |registration|
132
+ if registration.workshop_price
133
+ #--- Calculate the summary values
134
+ financials[:summary][:total_possible] += registration.discounted_price
135
+ financials[:summary][:total_paid] += registration.amount_paid.nil? ? Money.new(0, base_currency) : registration.amount_paid
136
+ financials[:summary][:total_outstanding] += registration.balance_owed
137
+ financials[:summary][:total_discoutns] += registration.discount
138
+ end
139
+ end
140
+
141
+ if level == :detail
142
+ registrations.attending.includes(:workshop_price, :payment_histories).each do |registration|
143
+ if registration.workshop_price
144
+ #--- Calculate what has been collected, by payment method
145
+ registration.payment_histories.each do |payment_history|
146
+ payment_method = payment_history.payment_method.titlecase
147
+ financials[:collected]["#{payment_method}"] = Money.new(0, base_currency) if financials[:collected]["#{payment_method}"].nil?
148
+ financials[:collected]["#{payment_method}"] += payment_history.total
149
+
150
+ month = payment_history.payment_date.beginning_of_month
151
+ financials[:collected_monthly][month] = Money.new(0, base_currency) if financials[:collected_monthly][month].nil?
152
+ financials[:collected_monthly][month] += payment_history.total
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ #--- give a worst case value - reduce by 20%
159
+ financials[:summary][:total_possible_worst] = financials[:summary][:total_possible] * 0.80
160
+ financials[:summary][:total_outstanding_worst] = financials[:summary][:total_outstanding] * 0.80
161
+ financials[:summary][:total_paid_percent] = (100 * financials[:summary][:total_paid] / financials[:summary][:total_possible]).round if financials[:summary][:total_possible].positive?
162
+ return financials
163
+ end
164
+
165
+ #------------------------------------------------------------------------------
166
+ def total_paid
167
+ total_paid = Money.new(0, base_currency)
168
+ registrations.attending.each do |registration|
169
+ if registration.workshop_price
170
+ total_paid += registration.amount_paid.nil? ? Money.new(0, base_currency) : registration.amount_paid
171
+ end
172
+ end
173
+ return total_paid
174
+ end
175
+
176
+ # Send out payment reminder emails to unpaid attendees, or to a specific one.
177
+ # if a specific registration, then always send out the email
178
+ #------------------------------------------------------------------------------
179
+ def send_payment_reminder_emails(registration_id = 'all')
180
+ success = failed = 0
181
+ unpaid_list = ( registration_id == 'all' ? registrations.unpaid : registrations.unpaid.where(id: registration_id) )
182
+ unpaid_list.each do |registration|
183
+ if registration.payment_reminder_due? || registration_id != 'all'
184
+ email = PaymentReminderMailer.payment_reminder(registration).deliver_now
185
+ if email
186
+ registration.update_attribute(:payment_reminder_sent_on, Time.now)
187
+ registration.update_attribute(:payment_reminder_history, [Time.now] + registration.payment_reminder_history)
188
+ success += 1
189
+ else
190
+ failed += 1
191
+ end
192
+ end
193
+ end
194
+ return {success: success, failed: failed}
195
+ end
196
+
197
+
198
+ # is the passed in user attending? Used in some deep level authorization checks,
199
+ # which rely on the "member?" method.
200
+ # This does not consider a userless registration as a "member", since there is
201
+ # no way they can login
202
+ #------------------------------------------------------------------------------
203
+ def member?(user)
204
+ self.registrations.attending.where(user_profile_id: user.user_profile.id).count > 0
205
+ end
206
+
207
+ # Provide a list of users that are members (does not include userless registrations)
208
+ #------------------------------------------------------------------------------
209
+ def member_count
210
+ self.registrations.attending.joins(user_profile: [:user]).references(:user_profile).where('user_profiles.user_id IS NOT NULL').count
211
+ end
212
+
213
+ # Return list of Users that are attending (does not include userless registrations)
214
+ #------------------------------------------------------------------------------
215
+ def member_list
216
+ User.includes(:user_profile).references(:user_profile).where(user_profiles: { id: self.registrations.attending.map(&:user_profile_id) } )
217
+ end
218
+
219
+ #------------------------------------------------------------------------------
220
+ def header_image(default = nil)
221
+ self.image || default
222
+ end
223
+
224
+ #------------------------------------------------------------------------------
225
+ def header_accent_color(default = '')
226
+ self.preferred_header_accent_color || default
227
+ end
228
+
229
+ # Find list of newly createdusers that have not registered for any events, between
230
+ # the end of the workshop and up to 60 day before the start of the workshop.
231
+ # Not perfect, since people can register just to access special
232
+ # content. But gives rough idea of people creating an account but not realizing
233
+ # they need to register for the event they want to participate in.
234
+ #------------------------------------------------------------------------------
235
+ def lost_users(days_ago = 10)
236
+ lost = []
237
+ new_users = User.where(created_at: (self.starting_on - days_ago.day)..self.ending_on, account_id: Account.current.id)
238
+ new_users.each do |user|
239
+ if user.user_site_profiles.where(account_id: Account.current.id)
240
+ if user.user_profile.registrations.count == 0
241
+ lost << user
242
+ end
243
+ end
244
+ end
245
+ lost
246
+ end
247
+ end
@@ -0,0 +1,118 @@
1
+ # [todo] re-evaluate whether alternate currencies are really needed. I can't
2
+ # think of a real use case at the moment, and it adds unnecessary complexity.
3
+ #
4
+ # Note: The currency of a WorkshopPrice is the base currency of it's workshop.
5
+ # If they were different, we would need to always have an exchange rate available
6
+ # to convert to/from the workshop and the price currency.
7
+ # With the alternate price/currencies, the exchange rate is directly based on
8
+ # the prices in the two currencies.
9
+ #------------------------------------------------------------------------------
10
+ class WorkshopPrice < ActiveRecord::Base
11
+
12
+ self.table_name = 'ems_workshop_prices'
13
+
14
+ belongs_to :workshop
15
+ has_many :registrations
16
+
17
+ default_scope { where(account_id: Account.current.id).order('row_order ASC') }
18
+
19
+ # --- globalize
20
+ translates :price_description, :sub_description, :payment_details, fallbacks_for_empty_translations: true
21
+ globalize_accessors locales: DmCore::Language.language_array
22
+
23
+ monetize :price_cents, with_model_currency: :price_currency, allow_nil: true
24
+ monetize :alt1_price_cents, with_model_currency: :alt1_price_currency, allow_nil: true
25
+ monetize :alt2_price_cents, with_model_currency: :alt2_price_currency, allow_nil: true
26
+
27
+ include RankedModel
28
+ ranks :row_order, with_same: :workshop_id
29
+
30
+ validates :price_description, presence_default_locale: true
31
+ validates :payment_details, liquid: { locales: true }
32
+ validates_presence_of :price_currency, if: Proc.new { |w| w.price_cents }
33
+ validates_presence_of :alt1_price_currency, if: Proc.new { |w| w.alt1_price_cents }
34
+ validates_presence_of :alt2_price_currency, if: Proc.new { |w| w.alt2_price_cents }
35
+ validates_presence_of :recurring_period, if: Proc.new { |w| w.recurring_number }
36
+ validates_presence_of :recurring_number, if: Proc.new { |w| w.recurring_period }
37
+
38
+ PAYMENT_METHODS = ['Cash', 'Check', 'Credit Card', 'Money Order', 'PayPal', 'Wire Transfer']
39
+
40
+ # For some reason, the initial monetized price gets created with the default
41
+ # Money currency. Need to use the current currency, as the internal fractional
42
+ # value depends on it. For example,
43
+ # "15000".to_money('JPY').cents == 15000
44
+ # "15000".to_money('EUR').cents == 1500000
45
+ # Call this method on the attributes before passing into new() or update_attributes()
46
+ #------------------------------------------------------------------------------
47
+ def self.prepare_prices(attributes = {})
48
+ attributes['price'] = attributes['price'].to_money(attributes['price_currency']) if attributes['price'].present? && attributes['price_currency'].present?
49
+ attributes['alt1_price'] = attributes['alt1_price'].to_money(attributes['alt1_price_currency']) if attributes['alt1_price'].present? && attributes['alt1_price_currency'].present?
50
+ attributes['alt2_price'] = attributes['alt2_price'].to_money(attributes['alt2_price_currency']) if attributes['alt2_price'].present? && attributes['alt2_price_currency'].present?
51
+ return attributes
52
+ end
53
+
54
+ #------------------------------------------------------------------------------
55
+ def visible?
56
+ !(disabled? || (!valid_starting_on.nil? && valid_starting_on > Time.now.to_date) || (!valid_until.nil? && valid_until < Time.now.to_date))
57
+ end
58
+
59
+ # If the total_available is nil, then there are unlimited tickets to be sold.
60
+ # Otherwise, check if we have sold out
61
+ #------------------------------------------------------------------------------
62
+ def sold_out?(num_sold)
63
+ (total_available.blank? or total_available == 0) ? false : (num_sold >= total_available)
64
+ end
65
+
66
+ #------------------------------------------------------------------------------
67
+ def price_formatted
68
+ price.nil? ? '' : price.format(no_cents_if_whole: true, symbol: true)
69
+ end
70
+
71
+ # returns the amount of a payment
72
+ #------------------------------------------------------------------------------
73
+ def payment_price
74
+ recurring_payments? ? (price / recurring_number) : price
75
+ end
76
+
77
+ #------------------------------------------------------------------------------
78
+ def recurring_payments?
79
+ recurring_number.to_i > 1
80
+ end
81
+
82
+ #------------------------------------------------------------------------------
83
+ def currency_list
84
+ list = [[price_currency, price_currency]]
85
+ list << [alt1_price_currency, alt1_price_currency] unless alt1_price_currency.blank?
86
+ list << [alt2_price_currency, alt1_price_currency] unless alt1_price_currency.blank?
87
+ return list
88
+ end
89
+
90
+ # Convert an amount in an alternate currency into the base currency
91
+ #------------------------------------------------------------------------------
92
+ def to_base_currency(money)
93
+ bank.exchange_with(money, price_currency)
94
+ end
95
+
96
+ # return a bank object filled with the exchange rates, based on the prices.
97
+ # then you can do: bank.exchange_with(price, 'USD')
98
+ #------------------------------------------------------------------------------
99
+ def bank
100
+ unless @bank
101
+ @bank = Money::Bank::VariableExchange.new
102
+ unless alt1_price_currency.blank?
103
+ @bank.add_rate(price_currency, alt1_price_currency, alt1_price_cents.to_f / price_cents.to_f)
104
+ @bank.add_rate(alt1_price_currency, price_currency, price_cents.to_f / alt1_price_cents.to_f)
105
+ end
106
+ unless alt2_price_currency.blank?
107
+ @bank.add_rate(price_currency, alt2_price_currency, alt2_price_cents.to_f / price_cents.to_f)
108
+ @bank.add_rate(alt2_price_currency, price_currency, price_cents.to_f / alt2_price_cents.to_f)
109
+ end
110
+ unless alt1_price_currency.blank? && alt2_price_currency.blank?
111
+ @bank.add_rate(alt1_price_currency, alt2_price_currency, alt2_price_cents.to_f / alt1_price_cents.to_f)
112
+ @bank.add_rate(alt2_price_currency, alt1_price_currency, alt1_price_cents.to_f / alt2_price_cents.to_f)
113
+ end
114
+ end
115
+ return @bank
116
+ end
117
+
118
+ end