artfully_ose 1.2.0 → 1.3.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (577) hide show
  1. checksums.yaml +5 -13
  2. data/app/assets/images/actions/open.png +0 -0
  3. data/app/assets/images/actions/pledge.png +0 -0
  4. data/app/assets/images/actions/pledge.psd +0 -0
  5. data/app/assets/images/glyphish/gray/cancel.png +0 -0
  6. data/app/assets/images/loading_gray.gif +0 -0
  7. data/app/assets/javascripts/application.js +30 -4
  8. data/app/assets/javascripts/baseball_card.js.coffee +235 -0
  9. data/app/assets/javascripts/boxoffice.js +12 -49
  10. data/app/assets/javascripts/contributions.js +264 -0
  11. data/app/assets/javascripts/custom/advanced_searches.js +142 -0
  12. data/app/assets/javascripts/custom/endless-scroll.js +1 -1
  13. data/app/assets/javascripts/custom/job-monitor.js +73 -0
  14. data/app/assets/javascripts/custom/kits-config.js +0 -2
  15. data/app/assets/javascripts/custom/mailchimp.js +14 -0
  16. data/app/assets/javascripts/custom/people.js +31 -2
  17. data/app/assets/javascripts/custom/person-finder.js +34 -0
  18. data/app/assets/javascripts/custom/searches.js +12 -0
  19. data/app/assets/javascripts/custom/user-finder.js +111 -0
  20. data/app/assets/javascripts/households.js +12 -5
  21. data/app/assets/javascripts/ical.js +138 -0
  22. data/app/assets/javascripts/jquery-lib/jquery.timeago.js +221 -0
  23. data/app/assets/javascripts/locationselector.js +1 -1
  24. data/app/assets/javascripts/memberships.js +24 -0
  25. data/app/assets/javascripts/relationships.js +10 -0
  26. data/app/assets/javascripts/search.js +22 -0
  27. data/app/assets/javascripts/store/sliding-wizard.js +2 -2
  28. data/app/assets/javascripts/store/store.js +51 -6
  29. data/app/assets/javascripts/tags.js +44 -0
  30. data/app/assets/stylesheets/application.sass +331 -32
  31. data/app/assets/stylesheets/bootstrap-overrides.css +24 -9
  32. data/app/assets/stylesheets/boxoffice.css.scss +0 -4
  33. data/app/assets/stylesheets/sass/_campaigns.sass +43 -0
  34. data/app/assets/stylesheets/sass/_tags.sass +10 -1
  35. data/app/assets/stylesheets/sass/fa-color.css.scss +13 -0
  36. data/app/assets/stylesheets/sass/store.sass +7 -7
  37. data/app/concerns/cart_finder.rb +10 -0
  38. data/app/concerns/oh_noes.rb +1 -1
  39. data/app/controllers/actions_controller.rb +5 -5
  40. data/app/controllers/addresses_controller.rb +1 -1
  41. data/app/controllers/advanced_search_segments_controller.rb +97 -0
  42. data/app/controllers/advanced_searches_controller.rb +118 -0
  43. data/app/controllers/appeals_controller.rb +63 -0
  44. data/app/controllers/artfully_ose_controller.rb +20 -2
  45. data/app/controllers/campaigns_controller.rb +90 -0
  46. data/app/controllers/charts_controller.rb +8 -2
  47. data/app/controllers/console_sales_controller.rb +21 -23
  48. data/app/controllers/contributions_controller.rb +74 -43
  49. data/app/controllers/converts_controller.rb +21 -0
  50. data/app/controllers/delete_ticket_types_controller.rb +35 -0
  51. data/app/controllers/events_pass_types_controller.rb +10 -2
  52. data/app/controllers/export_controller.rb +4 -4
  53. data/app/controllers/households_controller.rb +63 -15
  54. data/app/controllers/imports_controller.rb +13 -10
  55. data/app/controllers/job_monitors_controller.rb +9 -0
  56. data/app/controllers/members/people_controller.rb +7 -5
  57. data/app/controllers/members/sessions_controller.rb +1 -0
  58. data/app/controllers/membership_comps_controller.rb +5 -20
  59. data/app/controllers/memberships_controller.rb +2 -2
  60. data/app/controllers/merges_controller.rb +7 -6
  61. data/app/controllers/mobile/users_controller.rb +18 -6
  62. data/app/controllers/notes_controller.rb +5 -1
  63. data/app/controllers/orders_controller.rb +30 -13
  64. data/app/controllers/organizations_controller.rb +8 -0
  65. data/app/controllers/passes_kits_controller.rb +2 -4
  66. data/app/controllers/people_controller.rb +105 -17
  67. data/app/controllers/pledges_controller.rb +64 -0
  68. data/app/controllers/preview_rows_controller.rb +29 -0
  69. data/app/controllers/regular_donation_kits_controller.rb +11 -6
  70. data/app/controllers/sales_controller.rb +72 -40
  71. data/app/controllers/searches_controller.rb +37 -6
  72. data/app/controllers/segments_controller.rb +33 -2
  73. data/app/controllers/settlements_controller.rb +13 -7
  74. data/app/controllers/shows_controller.rb +58 -24
  75. data/app/controllers/statements_controller.rb +2 -2
  76. data/app/controllers/store/checkouts_controller.rb +43 -26
  77. data/app/controllers/store/donations_controller.rb +27 -11
  78. data/app/controllers/store/memberships_controller.rb +37 -4
  79. data/app/controllers/store/orders_controller.rb +26 -10
  80. data/app/controllers/store/passes_controller.rb +30 -3
  81. data/app/controllers/tags_controller.rb +42 -0
  82. data/app/controllers/ticket_types_controller.rb +9 -2
  83. data/app/controllers/tickets_controller.rb +6 -4
  84. data/app/controllers/unacknowledged_gifts_controller.rb +45 -0
  85. data/app/controllers/user_memberships_controller.rb +47 -8
  86. data/app/helpers/advanced_searches_helper.rb +252 -0
  87. data/app/helpers/artfully_ose_helper.rb +33 -4
  88. data/app/helpers/checkouts_helper.rb +16 -0
  89. data/app/helpers/mailchimp_lists_for_search_helper.rb +21 -0
  90. data/app/helpers/orders_helper.rb +11 -0
  91. data/app/helpers/people_helper.rb +28 -1
  92. data/app/helpers/relationships_helper.rb +6 -0
  93. data/app/helpers/sales_helper.rb +3 -1
  94. data/app/helpers/searches_helper.rb +3 -1
  95. data/app/helpers/segments_helper.rb +3 -0
  96. data/app/mailers/order_mailer.rb +8 -1
  97. data/app/mailers/producer_mailer.rb +39 -0
  98. data/app/mailers/reports_mailer.rb +6 -1
  99. data/app/models/ability.rb +15 -8
  100. data/app/models/action.rb +33 -4
  101. data/app/models/actions/change_action.rb +3 -1
  102. data/app/models/actions/comp_action.rb +3 -1
  103. data/app/models/actions/convert_action.rb +19 -0
  104. data/app/models/actions/do_action.rb +3 -1
  105. data/app/models/actions/exchange_action.rb +3 -1
  106. data/app/models/actions/get_action.rb +3 -1
  107. data/app/models/actions/give_action.rb +8 -2
  108. data/app/models/actions/go_action.rb +3 -1
  109. data/app/models/actions/hear_action.rb +15 -1
  110. data/app/models/actions/join_action.rb +3 -1
  111. data/app/models/actions/mailchimp_hear_action.rb +17 -0
  112. data/app/models/actions/pledge_action.rb +19 -0
  113. data/app/models/actions/refund_action.rb +3 -1
  114. data/app/models/actions/say_action.rb +3 -1
  115. data/app/models/actions/system_action.rb +17 -0
  116. data/app/models/address.rb +76 -31
  117. data/app/models/advanced_search.rb +434 -0
  118. data/app/models/advanced_search_segment.rb +12 -0
  119. data/app/models/appeal.rb +106 -0
  120. data/app/models/box_office.rb +1 -1
  121. data/app/models/budget_restriction.rb +9 -0
  122. data/app/models/campaign.rb +141 -0
  123. data/app/models/cart.rb +39 -4
  124. data/app/models/chart.rb +2 -2
  125. data/app/models/checkout.rb +54 -10
  126. data/app/models/comp.rb +7 -5
  127. data/app/models/company.rb +2 -0
  128. data/app/models/console_sale.rb +26 -0
  129. data/app/models/contribution.rb +327 -62
  130. data/app/models/convert.rb +62 -0
  131. data/app/models/daily_membership_report.rb +1 -1
  132. data/app/models/database_views/item_view.rb +91 -49
  133. data/app/models/discount.rb +1 -1
  134. data/app/models/discounts/discount_type.rb +2 -1
  135. data/app/models/donation.rb +27 -2
  136. data/app/models/donation_search.rb +188 -10
  137. data/app/models/door_list.rb +39 -2
  138. data/app/models/event.rb +31 -15
  139. data/app/models/ext.rb +3 -3
  140. data/app/models/ext/delayed_indexing.rb +41 -1
  141. data/app/models/ext/integrations.rb +6 -0
  142. data/app/models/forwarding_job_monitor.rb +2 -0
  143. data/app/models/household.rb +1 -1
  144. data/app/models/import.rb +39 -8
  145. data/app/models/imports/donations_import.rb +110 -19
  146. data/app/models/imports/events_import.rb +3 -2
  147. data/app/models/imports/mappings.rb +991 -0
  148. data/app/models/imports/memberships_import.rb +218 -20
  149. data/app/models/imports/people_import.rb +9 -1
  150. data/app/models/imports/rollback.rb +9 -0
  151. data/app/models/imports/validations.rb +7 -0
  152. data/app/models/individual.rb +3 -1
  153. data/app/models/item.rb +55 -17
  154. data/app/models/job/acknowledge_job.rb +23 -0
  155. data/app/models/job/checkout_processor.rb +1 -1
  156. data/app/models/job/convert_order_processor.rb +14 -0
  157. data/app/models/job/daily_email_report_job.rb +1 -12
  158. data/app/models/job/destroy_show_job.rb +1 -6
  159. data/app/models/job/destroy_tag_job.rb +18 -0
  160. data/app/models/job/door_list_generator_job.rb +33 -0
  161. data/app/models/job/door_list_mailer_job.rb +15 -0
  162. data/app/models/job/email_organization_sales_report_job.rb +32 -0
  163. data/app/models/job/expire_ticket_job.rb +19 -2
  164. data/app/models/job/export_people_job.rb +47 -0
  165. data/app/models/job/export_settlements_job.rb +24 -0
  166. data/app/models/job/geocode_address_job.rb +2 -7
  167. data/app/models/job/mailchimp_sync_job.rb +61 -3
  168. data/app/models/job/merge_job.rb +19 -0
  169. data/app/models/job/order_export_job.rb +36 -0
  170. data/app/models/job/order_mailer_job.rb +1 -2
  171. data/app/models/job/order_processor.rb +17 -6
  172. data/app/models/job/person_lifetime_value_job.rb +1 -0
  173. data/app/models/job/search_export_job.rb +33 -0
  174. data/app/models/job/suggest_households_by_address_job.rb +29 -0
  175. data/app/models/job/suggest_households_by_spouse_job.rb +22 -0
  176. data/app/models/job_monitor.rb +14 -0
  177. data/app/models/kit.rb +35 -8
  178. data/app/models/kits/campaigns_kit.rb +27 -0
  179. data/app/models/kits/mailchimp_kit.rb +548 -124
  180. data/app/models/kits/membership_kit.rb +14 -6
  181. data/app/models/kits/passes_kit.rb +14 -6
  182. data/app/models/kits/regular_donation_kit.rb +29 -2
  183. data/app/models/{relationships_kit.rb → kits/relationships_kit.rb} +3 -7
  184. data/app/models/kits/scannable_tickets_kit.rb +0 -5
  185. data/app/models/kits/sponsored_donation_kit.rb +2 -12
  186. data/app/models/kits/ticketing_kit.rb +9 -1
  187. data/app/models/linking_job_monitor.rb +2 -0
  188. data/app/models/list_grouping.rb +10 -0
  189. data/app/models/member.rb +35 -23
  190. data/app/models/member_number_generator.rb +2 -0
  191. data/app/models/membership.rb +23 -14
  192. data/app/models/membership_change.rb +17 -1
  193. data/app/models/membership_comp.rb +48 -23
  194. data/app/models/membership_comp_job.rb +7 -0
  195. data/app/models/membership_type.rb +13 -7
  196. data/app/models/monitorable.rb +19 -0
  197. data/app/models/note.rb +10 -1
  198. data/app/models/order.rb +170 -21
  199. data/app/models/order_handler.rb +13 -2
  200. data/app/models/orders/convert_order.rb +11 -0
  201. data/app/models/organization.rb +87 -9
  202. data/app/models/parsed_row.rb +142 -67
  203. data/app/models/pass.rb +13 -7
  204. data/app/models/pass_type.rb +17 -4
  205. data/app/models/passes_report.rb +6 -3
  206. data/app/models/payment.rb +16 -6
  207. data/app/models/payments/cash_payment.rb +1 -0
  208. data/app/models/payments/check_payment.rb +1 -5
  209. data/app/models/payments/comp_payment.rb +0 -1
  210. data/app/models/payments/credit_card_payment.rb +2 -7
  211. data/app/models/permission.rb +28 -0
  212. data/app/models/person.rb +503 -94
  213. data/app/models/phone.rb +3 -0
  214. data/app/models/redis_manager.rb +39 -0
  215. data/app/models/relationship.rb +31 -4
  216. data/app/models/relationship_builder.rb +0 -3
  217. data/app/models/rolling_membership_type.rb +5 -1
  218. data/app/models/s3_coordinator.rb +21 -0
  219. data/app/models/sale.rb +7 -4
  220. data/app/models/scheduled_pledge_payment.rb +69 -0
  221. data/app/models/search.rb +236 -24
  222. data/app/models/section.rb +6 -4
  223. data/app/models/show.rb +21 -12
  224. data/app/models/show_validator.rb +45 -0
  225. data/app/models/soft_credit.rb +126 -0
  226. data/app/models/subscribed_list.rb +52 -0
  227. data/app/models/suggested_household.rb +8 -2
  228. data/app/models/ticket.rb +35 -10
  229. data/app/models/ticket/locker.rb +25 -0
  230. data/app/models/ticket_summary.rb +1 -1
  231. data/app/models/ticket_type.rb +14 -2
  232. data/app/models/user.rb +42 -3
  233. data/app/models/user_membership.rb +57 -3
  234. data/app/models/valuation/lifetime_donations.rb +2 -2
  235. data/app/models/valuation/lifetime_pledges.rb +35 -0
  236. data/app/models/valuation/lifetime_value.rb +5 -1
  237. data/app/models/valuation_query.rb +109 -0
  238. data/app/models/venue.rb +8 -0
  239. data/app/views/actions/_action.html.haml +1 -1
  240. data/app/views/actions/_form.html.haml +2 -2
  241. data/app/views/actions/give/_show.html.haml +1 -1
  242. data/app/views/actions/pledge/_show.html.haml +8 -0
  243. data/app/views/actions/shared/_show.html.haml +1 -1
  244. data/app/views/addresses/_address.html.haml +3 -1
  245. data/app/views/advanced_search_segments/_advanced_searches_results.html.haml +15 -0
  246. data/app/views/advanced_search_segments/index.html.haml +18 -0
  247. data/app/views/advanced_search_segments/show.html.haml +46 -0
  248. data/app/views/advanced_searches/_form.html.haml +61 -0
  249. data/app/views/advanced_searches/_hit.html.haml +10 -0
  250. data/app/views/advanced_searches/_household.html.haml +7 -0
  251. data/app/views/advanced_searches/_new_condition.html.haml +3 -0
  252. data/app/views/advanced_searches/_people_count.html.erb +3 -0
  253. data/app/views/advanced_searches/_people_list.html.haml +5 -0
  254. data/app/views/advanced_searches/filters/_action.html.haml +12 -0
  255. data/app/views/advanced_searches/filters/_birthday.html.haml +9 -0
  256. data/app/views/advanced_searches/filters/_discount_code.html.haml +7 -0
  257. data/app/views/advanced_searches/filters/_donated.html.haml +17 -0
  258. data/app/views/advanced_searches/filters/_email.html.haml +7 -0
  259. data/app/views/advanced_searches/filters/_has_purchased_for.html.haml +9 -0
  260. data/app/views/advanced_searches/filters/_keyword.html.haml +9 -0
  261. data/app/views/advanced_searches/filters/_lifetime_value.html.haml +13 -0
  262. data/app/views/advanced_searches/filters/_limit_results.html.haml +7 -0
  263. data/app/views/advanced_searches/filters/_location.html.haml +11 -0
  264. data/app/views/advanced_searches/filters/_mailchimp_group.html.haml +8 -0
  265. data/app/views/advanced_searches/filters/_mailchimp_list.html.haml +8 -0
  266. data/app/views/advanced_searches/filters/_membership_ended.html.haml +9 -0
  267. data/app/views/advanced_searches/filters/_membership_started.html.haml +9 -0
  268. data/app/views/advanced_searches/filters/_membership_status.html.haml +8 -0
  269. data/app/views/advanced_searches/filters/_membership_type.html.haml +8 -0
  270. data/app/views/advanced_searches/filters/_orders.html.haml +10 -0
  271. data/app/views/advanced_searches/filters/_pass_type.html.haml +9 -0
  272. data/app/views/advanced_searches/filters/_relationships.html.haml +8 -0
  273. data/app/views/advanced_searches/filters/_segment.html.haml +8 -0
  274. data/app/views/advanced_searches/filters/_tagging.html.haml +9 -0
  275. data/app/views/advanced_searches/new.html.haml +12 -0
  276. data/app/views/advanced_searches/show.html.haml +107 -0
  277. data/app/views/advanced_searches/shows_for_org.html.haml +1 -0
  278. data/app/views/advanced_searches/ticket_types_for_org.html.haml +1 -0
  279. data/app/views/advanced_searches/update.js.erb +9 -0
  280. data/app/views/appeals/_form.html.haml +113 -0
  281. data/app/views/appeals/_header.html.haml +6 -0
  282. data/app/views/appeals/_heard_action.html.haml +16 -0
  283. data/app/views/appeals/edit.html.haml +8 -0
  284. data/app/views/appeals/new.html.haml +6 -0
  285. data/app/views/campaigns/_campaign_donations.html.haml +19 -0
  286. data/app/views/campaigns/_campaign_stats.html.haml +28 -0
  287. data/app/views/campaigns/_form.html.haml +48 -0
  288. data/app/views/campaigns/_header.html.haml +6 -0
  289. data/app/views/campaigns/edit.html.haml +6 -0
  290. data/app/views/campaigns/index.html.haml +33 -0
  291. data/app/views/campaigns/new.html.haml +6 -0
  292. data/app/views/campaigns/show.html.haml +132 -0
  293. data/app/views/console_sales/_cart.html.haml +3 -2
  294. data/app/views/console_sales/_payment.html.haml +14 -1
  295. data/app/views/console_sales/new.html.haml +11 -6
  296. data/app/views/contributions/_form.html.haml +139 -37
  297. data/app/views/contributions/_header.html.haml +5 -0
  298. data/app/views/contributions/_match_eligible.html.haml +24 -0
  299. data/app/views/contributions/_pledge_payment.html.haml +22 -0
  300. data/app/views/contributions/_search.html.haml +30 -0
  301. data/app/views/contributions/_soft_credit.html.haml +25 -0
  302. data/app/views/contributions/edit.html.haml +5 -8
  303. data/app/views/contributions/find_person.html.haml +16 -15
  304. data/app/views/contributions/index.html.haml +61 -24
  305. data/app/views/contributions/new.html.haml +5 -5
  306. data/app/views/converts/_grouped_form.html.haml +11 -0
  307. data/app/views/converts/new.html.haml +35 -0
  308. data/app/views/delete_ticket_types/_cannot_destroy_modal.html.haml +11 -0
  309. data/app/views/delete_ticket_types/_cannot_destroy_modal.rb +0 -0
  310. data/app/views/delete_ticket_types/_confirm_destroy_modal.html.haml +13 -0
  311. data/app/views/delete_ticket_types/show.html.haml +4 -0
  312. data/app/views/discounts/_form.html.haml +1 -1
  313. data/app/views/events/_day_date_show.html.haml +4 -2
  314. data/app/views/events/_glance.html.haml +12 -12
  315. data/app/views/events/_header.html.haml +1 -1
  316. data/app/views/events/_list.html.haml +1 -1
  317. data/app/views/events/_menu.html.haml +26 -21
  318. data/app/views/events/_section_fields.html.haml +1 -1
  319. data/app/views/events/_tax_deductible_message.html.haml +3 -0
  320. data/app/views/events/_ticket_type_fields.html.haml +13 -3
  321. data/app/views/events/index.html.haml +2 -1
  322. data/app/views/events/messages.html.haml +12 -10
  323. data/app/views/events/show.html.haml +6 -6
  324. data/app/views/events/temp_discount_form.html.haml +1 -1
  325. data/app/views/events_pass_types/_form.html.haml +1 -1
  326. data/app/views/events_pass_types/new.html.haml +1 -2
  327. data/app/views/households/_edit_modal.html.haml +9 -4
  328. data/app/views/households/_form.html.haml +6 -1
  329. data/app/views/households/_header.html.haml +9 -18
  330. data/app/views/households/confirm.html.haml +43 -0
  331. data/app/views/households/new.html.haml +3 -1
  332. data/app/views/households/show.html.haml +2 -1
  333. data/app/views/households/suggested.html.haml +10 -27
  334. data/app/views/imports/_export_links.html.haml +2 -2
  335. data/app/views/imports/donations/_new.html.haml +49 -6
  336. data/app/views/imports/donations/_pending.html.haml +69 -22
  337. data/app/views/imports/donations/_preview.html.haml +23 -0
  338. data/app/views/imports/events/_new.html.haml +8 -8
  339. data/app/views/imports/index.html.haml +26 -21
  340. data/app/views/imports/memberships/_approved.html.haml +16 -0
  341. data/app/views/imports/memberships/_caching.html.haml +17 -0
  342. data/app/views/imports/memberships/_failed.html.haml +5 -0
  343. data/app/views/imports/memberships/_imported.html.haml +45 -0
  344. data/app/views/imports/memberships/_importing.html.haml +20 -0
  345. data/app/views/imports/memberships/_invalid.html.haml +5 -0
  346. data/app/views/imports/memberships/_new.html.haml +155 -0
  347. data/app/views/imports/memberships/_pending.html.haml +85 -0
  348. data/app/views/imports/memberships/_preview_rows.html.haml +38 -0
  349. data/app/views/imports/people/_new.html.haml +81 -15
  350. data/app/views/imports/shared/_inspect_modal.html.haml +10 -1
  351. data/app/views/imports/shared/_new_sidebar.html.haml +12 -0
  352. data/app/views/imports/show.html.haml +0 -3
  353. data/app/views/index/_action.html.haml +4 -3
  354. data/app/views/index/_recent_activity.html.haml +14 -7
  355. data/app/views/index/dashboard.html.haml +27 -14
  356. data/app/views/job_monitors/_job_monitor_show.html.haml +34 -0
  357. data/app/views/job_monitors/_linking_job_monitor_show.html.haml +31 -0
  358. data/app/views/job_monitors/show.html.haml +1 -0
  359. data/app/views/layouts/_google_analytics.html.haml +9 -11
  360. data/app/views/layouts/_menu.html.haml +5 -3
  361. data/app/views/layouts/application.html.haml +12 -4
  362. data/app/views/layouts/devise_layout.html.haml +1 -1
  363. data/app/views/layouts/storefront.html.haml +7 -10
  364. data/app/views/members/index/index.html.haml +82 -4
  365. data/app/views/members/mailer/_invitation_body.html.haml +2 -2
  366. data/app/views/members/passwords/new.html.haml +13 -0
  367. data/app/views/members/sessions/new.html.haml +1 -1
  368. data/app/views/membership_comps/confirm.html.haml +10 -8
  369. data/app/views/membership_comps/create.html.haml +1 -3
  370. data/app/views/membership_kits/edit.html.haml +6 -0
  371. data/app/views/membership_types/_form.html.haml +1 -1
  372. data/app/views/membership_types/index.html.haml +11 -3
  373. data/app/views/memberships/index.html.haml +122 -109
  374. data/app/views/merges/find_person.html.haml +5 -3
  375. data/app/views/merges/new.html.haml +13 -4
  376. data/app/views/notes/_note.html.haml +3 -3
  377. data/app/views/order_mailer/confirmation_for.html.haml +1 -0
  378. data/app/views/order_mailer/confirmation_for.text.haml +1 -0
  379. data/app/views/orders/_assignable_donations.html.haml +25 -0
  380. data/app/views/orders/_assignable_pledges.html.haml +26 -0
  381. data/app/views/orders/_item_table.haml +20 -8
  382. data/app/views/orders/_order_sidebar.html.haml +37 -7
  383. data/app/views/orders/membership.html.haml +2 -2
  384. data/app/views/orders/passes.html.haml +4 -4
  385. data/app/views/orders/sales.html.haml +5 -5
  386. data/app/views/orders/show.html.haml +26 -7
  387. data/app/views/organizations/_form.html.haml +0 -5
  388. data/app/views/pass_types/index.html.haml +13 -5
  389. data/app/views/passes/index.html.haml +50 -39
  390. data/app/views/passes_kits/edit.html.haml +6 -0
  391. data/app/views/passes_reports/index.html.haml +1 -1
  392. data/app/views/pdfs/member.html.haml +1 -1
  393. data/app/views/pdfs/member_card_generator/blanks_usa_idc6.html.haml +1 -1
  394. data/app/views/pdfs/order.html.haml +5 -3
  395. data/app/views/people/_address_fields.html.haml +40 -0
  396. data/app/views/people/_convert_to_company_modal.html.haml +3 -0
  397. data/app/views/people/_edit_modal.html.haml +121 -38
  398. data/app/views/people/_form.html.haml +8 -14
  399. data/app/views/people/_header.html.haml +11 -16
  400. data/app/views/people/_list.html.haml +3 -9
  401. data/app/views/people/_mailchimp_group.html.haml +3 -0
  402. data/app/views/people/_phone_fields.html.haml +1 -1
  403. data/app/views/people/_relationship_fields.html.haml +10 -5
  404. data/app/views/people/_work_with_menu.html.haml +14 -8
  405. data/app/views/people/index.html.haml +10 -9
  406. data/app/views/people/new.html.haml +18 -4
  407. data/app/views/people/pledges.html.haml +13 -0
  408. data/app/views/people/show.html.haml +96 -45
  409. data/app/views/pledges/_none_scheduled_payment_table.html.haml +17 -0
  410. data/app/views/pledges/_receive_pledge.html.haml +87 -0
  411. data/app/views/pledges/_scheduled_payments_table.html.haml +18 -0
  412. data/app/views/pledges/index.html.haml +47 -0
  413. data/app/views/preview_rows/index.html.haml +1 -0
  414. data/app/views/producer_mailer/donation_notification.html.haml +9 -0
  415. data/app/views/producer_mailer/donation_notification.text.erb +8 -0
  416. data/app/views/producer_mailer/door_list_notification.html.haml +10 -0
  417. data/app/views/producer_mailer/door_list_notification.text.erb +6 -0
  418. data/app/views/producer_mailer/mailchimp_kit_initial_sync_notification.html.haml +2 -2
  419. data/app/views/regular_donation_kits/edit.html.haml +29 -1
  420. data/app/views/relationships/index.html.haml +1 -1
  421. data/app/views/sales/_boxoffice.html.haml +15 -5
  422. data/app/views/sales/_doorlist.html.haml +2 -2
  423. data/app/views/sales/new.html.haml +2 -2
  424. data/app/views/searches/_company.html.haml +7 -0
  425. data/app/views/searches/_form.html.haml +75 -8
  426. data/app/views/searches/_household.html.haml +2 -2
  427. data/app/views/searches/_individual.html.haml +7 -0
  428. data/app/views/searches/show.html.haml +59 -23
  429. data/app/views/segments/index.html.haml +1 -1
  430. data/app/views/segments/show.html.haml +36 -1
  431. data/app/views/shared/_baseball_card.html.haml +40 -0
  432. data/app/views/shared/_tags.html.haml +10 -8
  433. data/app/views/shows/_empty_ticket_table.html.haml +12 -0
  434. data/app/views/shows/_sections_table.html.haml +78 -69
  435. data/app/views/shows/_ticket_table.html.haml +10 -7
  436. data/app/views/shows/index.html.haml +95 -85
  437. data/app/views/statements/_shows.html.haml +1 -1
  438. data/app/views/store/checkouts/_event_information.html.haml +89 -0
  439. data/app/views/store/checkouts/_shopping_cart_display.haml +8 -0
  440. data/app/views/store/checkouts/_thanks.html.haml +40 -0
  441. data/app/views/store/checkouts/create.html.haml +1 -0
  442. data/app/views/store/checkouts/shopping_cart_display/_discounts.haml +8 -0
  443. data/app/views/store/checkouts/shopping_cart_display/_donations.haml +15 -0
  444. data/app/views/store/checkouts/{_membership_info.html.haml → shopping_cart_display/_membership_info.html.haml} +0 -0
  445. data/app/views/store/checkouts/shopping_cart_display/_memberships.haml +8 -0
  446. data/app/views/store/checkouts/shopping_cart_display/_passes.haml +9 -0
  447. data/app/views/store/checkouts/shopping_cart_display/_tickets.haml +10 -0
  448. data/app/views/store/checkouts/shopping_cart_display/_total.haml +10 -0
  449. data/app/views/store/donations/index.html.haml +4 -0
  450. data/app/views/store/events/_contact_info.html.haml +8 -0
  451. data/app/views/store/events/_venue.html.haml +14 -7
  452. data/app/views/store/events/calendar.html.haml +2 -9
  453. data/app/views/store/events/show.html.haml +2 -9
  454. data/app/views/store/events/single_show.html.haml +3 -3
  455. data/app/views/store/memberships/index.html.haml +3 -3
  456. data/app/views/store/orders/blank.html.erb +34 -0
  457. data/app/views/store/orders/show.html.haml +15 -6
  458. data/app/views/store/passes/index.html.haml +3 -3
  459. data/app/views/store/shared/_small_donate_form.html.haml +21 -18
  460. data/app/views/tags/_edit_modal.html.haml +16 -0
  461. data/app/views/tags/index.html.haml +23 -0
  462. data/app/views/unacknowledged_gifts/_heard_action.html.haml +10 -0
  463. data/app/views/unacknowledged_gifts/index.html.haml +76 -0
  464. data/app/views/user_memberships/_list.html.haml +147 -24
  465. data/config/initializers/delayed_job_config.rb +10 -1
  466. data/config/routes.rb +85 -9
  467. data/db/migrate/20140422193345_add_email_copy_to_pass_type.rb +1 -1
  468. data/db/migrate/20140616045851_add_birthdate_to_search.rb +7 -0
  469. data/db/migrate/20140623131025_create_campaigns.rb +27 -0
  470. data/db/migrate/20140627120214_create_appeals.rb +28 -0
  471. data/db/migrate/20140630151406_add_deleted_at_to_campaigns.rb +9 -0
  472. data/db/migrate/20140701124100_add_segment_id_and_notes_to_appeals.rb +12 -0
  473. data/db/migrate/20140702115815_add_campaign_id_and_appeal_id_to_donations.rb +14 -0
  474. data/db/migrate/20140702121928_add_materials_to_appeals.rb +11 -0
  475. data/db/migrate/20140702150521_add_deleted_at_to_appeals.rb +9 -0
  476. data/db/migrate/20140709151816_add_campaign_id_and_appeal_id_to_orders.rb +14 -0
  477. data/db/migrate/20140723133850_remove_campaign_id_and_apeal_id_from_orders.rb +11 -0
  478. data/db/migrate/20140723134923_add_commitment_date_to_donations.rb +6 -0
  479. data/db/migrate/20140730141515_add_match_eligible_to_donations.rb +15 -0
  480. data/db/migrate/20140818141140_create_soft_credits.rb +12 -0
  481. data/db/migrate/20140903140113_add_pledge_to_donations.rb +9 -0
  482. data/db/migrate/20140904183953_add_destroyed_at_to_relationships.rb +7 -0
  483. data/db/migrate/20140905080503_create_scheduled_pledge_payments.rb +16 -0
  484. data/db/migrate/20140909150251_add_org_to_suggested_household.rb +7 -0
  485. data/db/migrate/20141007114614_add_order_id_to_donations.rb +15 -0
  486. data/db/migrate/20141021134013_add_lifetime_pledges_to_people.rb +5 -0
  487. data/db/migrate/20141027191307_default_overwrite_member_addresses_to_false.rb +5 -0
  488. data/db/migrate/20141031193839_update_relations.rb +9 -0
  489. data/db/migrate/20141126183258_add_mailchimp_status_fields_to_people.rb +6 -0
  490. data/db/migrate/20141126184811_add_processing_at_to_ticket.rb +5 -0
  491. data/db/migrate/20141127135743_add_import_to_campaigns.rb +6 -0
  492. data/db/migrate/20141128143944_add_fiscal_year_to_donations.rb +5 -0
  493. data/db/migrate/20141128151407_remove_fiscal_year_from_scheduled_pledge_payments.rb +3 -0
  494. data/db/migrate/20141204164400_add_more_indexes_to_actions.rb +7 -0
  495. data/db/migrate/20141204171612_add_indexes_to_charts.rb +12 -0
  496. data/db/migrate/20141204172658_add_hide_on_recent_activity_to_actions.rb +5 -0
  497. data/db/migrate/20141204173004_add_indexes_to_ept.rb +9 -0
  498. data/db/migrate/20141204211933_remove_membership_counts_from_members.rb +13 -0
  499. data/db/migrate/20141205202030_create_subscribed_lists.rb +19 -0
  500. data/db/migrate/20141208225228_add_mailchimp_lists_to_searches.rb +5 -0
  501. data/db/migrate/20141210162329_add_bounced_to_subscribed_lists.rb +5 -0
  502. data/db/migrate/20141211165307_add_external_reference_to_actions.rb +5 -0
  503. data/db/migrate/20141212172119_add_not_mailchimp_lists_to_searches.rb +5 -0
  504. data/db/migrate/20141229204605_normalize_states_in_addresses.rb +11 -0
  505. data/db/migrate/20150113091344_add_fiscal_to_organizations.rb +11 -0
  506. data/db/migrate/20150113091434_create_budget_restrictions.rb +14 -0
  507. data/db/migrate/20150113091503_add_budget_restriction_to_campaigns.rb +14 -0
  508. data/db/migrate/20150129091354_add_campaign_to_searches.rb +13 -0
  509. data/db/migrate/20150203174456_create_list_groupings.rb +12 -0
  510. data/db/migrate/20150203222451_add_groupings_to_searches.rb +6 -0
  511. data/db/migrate/20150219153827_add_roles_to_user_memberships.rb +13 -0
  512. data/db/migrate/20150223145657_add_daily_sales_to_user_membership.rb +13 -0
  513. data/db/migrate/20150318171857_create_advanced_searches.rb +16 -0
  514. data/db/migrate/20150319185940_add_org_id_index_to_orders.rb +5 -0
  515. data/db/migrate/20150325085516_add_received_amount_to_donations.rb +16 -0
  516. data/db/migrate/20150325173053_add_pass_index_to_tickets.rb +5 -0
  517. data/db/migrate/20150331084817_add_creator_id_to_orders.rb +11 -0
  518. data/db/migrate/20150406162656_index_tickets_on_action_id.rb +5 -0
  519. data/db/migrate/20150408123349_add_donor_instructions_to_orders.rb +9 -0
  520. data/db/migrate/20150408125600_add_index_to_imports.rb +5 -0
  521. data/db/migrate/20150414193419_add_index_to_phone.rb +5 -0
  522. data/db/migrate/20150414193646_add_index_to_taggings.rb +5 -0
  523. data/db/migrate/20150415113013_add_nickname_to_people.rb +11 -0
  524. data/db/migrate/20150420143707_add_indexes_to_donations.rb +7 -0
  525. data/db/migrate/20150421092302_add_kind_to_addresses.rb +11 -0
  526. data/db/migrate/20150513154513_add_name_to_advanced_search.rb +5 -0
  527. data/db/migrate/20150518131946_add_receive_donation_notification_to_user_memberships.rb +9 -0
  528. data/db/migrate/20150520182350_remove_name_from_advanced_search.rb +9 -0
  529. data/db/migrate/20150520202900_create_advanced_search_segments.rb +9 -0
  530. data/db/migrate/20150520210658_add_advanced_search_id_to_advanced_search_segment.rb +5 -0
  531. data/db/migrate/20150521183507_add_organization_id_to_advanced_search_segment.rb +5 -0
  532. data/db/migrate/20150602204548_add_index_on_deleted_at.rb +5 -0
  533. data/db/migrate/20150711143014_add_receive_door_list_to_user_memberships.rb +10 -0
  534. data/db/migrate/20150716000301_create_job_monitors_table.rb +12 -0
  535. data/db/migrate/20150807005404_re_refresh_show_stats.rb +8 -0
  536. data/db/migrate/20150810133417_convert_business_to_work.rb +5 -0
  537. data/db/migrate/20150812135205_update_notes_nulls.rb +9 -0
  538. data/db/migrate/20150813202745_index_household_id.rb +6 -0
  539. data/db/migrate/20150814075653_add_maiden_name_to_people.rb +9 -0
  540. data/db/migrate/20150911203530_normalize_countries_in_addresses.rb +11 -0
  541. data/db/migrate/20150922155253_add_advanced_search_segment_id_to_membership_types.rb +5 -0
  542. data/db/migrate/20150922155308_add_advanced_search_segment_id_to_pass_types.rb +5 -0
  543. data/db/migrate/20151006180702_add_deleted_at_to_ticket_types.rb +5 -0
  544. data/db/migrate/20151009175206_add_receipt_details_to_ticket_types.rb +5 -0
  545. data/db/migrate/20151112174723_index_list_groupings.rb +5 -0
  546. data/lib/artfully_ose/common_abilities.rb +80 -57
  547. data/lib/artfully_ose/version.rb +1 -1
  548. data/lib/tasks/artfully_ose.rake +46 -4
  549. data/spec/factories/action_factories.rb +2 -0
  550. data/spec/factories/address_factories.rb +2 -2
  551. data/spec/factories/advanced_search_factories.rb +45 -0
  552. data/spec/factories/advanced_search_segment_factories.rb +6 -0
  553. data/spec/factories/advanced_segment_factories.rb +6 -0
  554. data/spec/factories/campaign_factories.rb +20 -0
  555. data/spec/factories/cart_factories.rb +24 -0
  556. data/spec/factories/chart_factories.rb +4 -0
  557. data/spec/factories/event_factories.rb +6 -0
  558. data/spec/factories/item_factories.rb +23 -1
  559. data/spec/factories/kit_factories.rb +27 -3
  560. data/spec/factories/member_factories.rb +8 -1
  561. data/spec/factories/membership_factories.rb +12 -0
  562. data/spec/factories/membership_type_factories.rb +18 -0
  563. data/spec/factories/note_factories.rb +7 -0
  564. data/spec/factories/order_factories.rb +7 -0
  565. data/spec/factories/pass_factories.rb +6 -0
  566. data/spec/factories/payments_factories.rb +1 -1
  567. data/spec/factories/person_factories.rb +46 -4
  568. data/spec/factories/scheduled_pledge_payment_factories.rb +10 -0
  569. data/spec/factories/show_factories.rb +8 -3
  570. data/spec/factories/soft_credit_factories.rb +8 -0
  571. data/spec/factories/ticket_factories.rb +22 -9
  572. data/spec/factories/user_factories.rb +16 -8
  573. metadata +342 -59
  574. data/app/controllers/comps_controller.rb +0 -47
  575. data/app/models/household_suggester.rb +0 -58
  576. data/app/views/searches/_person.html.haml +0 -10
  577. data/app/views/store/checkouts/thanks.html.haml +0 -134
@@ -2,7 +2,6 @@ class OrderMailerJob < Struct.new(:order)
2
2
  def perform
3
3
  OrderMailer.confirmation_for(order).deliver
4
4
  rescue Exception => e
5
- Exceptional.context(:order_id => order.id)
6
- Exceptional.handle(e, "Could not send order confirmation for order")
5
+ Airbrake.notify(e, :parameters => {:order_id => order.id})
7
6
  end
8
7
  end
@@ -33,6 +33,7 @@ class OrderProcessor < Struct.new(:order, :options)
33
33
 
34
34
  generate_qr_codes_and_pdf
35
35
  send_confirmation
36
+ send_donation_notification
36
37
  end
37
38
 
38
39
  def generate_qr_codes_and_pdf
@@ -42,17 +43,27 @@ class OrderProcessor < Struct.new(:order, :options)
42
43
  generate_pdf if order.organization.can? :access, :scannable_tickets
43
44
  end
44
45
  rescue Exception => e
45
- Exceptional.context(:order_id => order.id)
46
- Exceptional.handle(e, "Could not generate PDF for order")
46
+ Airbrake.notify(e, :parameters => {:error_message => "Could not generate PDF for order", :order_id => order.id})
47
47
  end
48
48
  end
49
49
 
50
50
  def send_confirmation
51
51
  begin
52
52
  OrderMailer.confirmation_for(order).deliver unless skip_email
53
+ rescue Exception => e
54
+ Airbrake.notify(e, :parameters => {:error_message => "Could not send confirmation for order", :order_id => order.id})
55
+ end
56
+ end
57
+
58
+ def send_donation_notification
59
+ begin
60
+ organization = order.organization
61
+ if !skip_email && organization.has_kit?(:regular_donation) && order.donations.any?
62
+ recipients = organization.donation_notification_recipients.collect(&:email)
63
+ recipients.each {|to| ProducerMailer.donation_notification(order, to).deliver}
64
+ end
53
65
  rescue Exception => e
54
- Exceptional.context(:order_id => order.id)
55
- Exceptional.handle(e, "Could not send order confirmation for order")
66
+ Airbrake.notify(e, :parameters => {:error_message => "Could not send donation notification for order", :order_id => order.id})
56
67
  end
57
68
  end
58
69
 
@@ -69,7 +80,7 @@ class OrderProcessor < Struct.new(:order, :options)
69
80
  self.order.memberships.each do |membership_item|
70
81
  Member.for(membership_item.product, self.order.person)
71
82
  end
72
- self.order.create_generic_action("memberships")
83
+ self.order.create_generic_action("memberships") unless skip_actions
73
84
  end
74
85
  end
75
86
 
@@ -79,7 +90,7 @@ class OrderProcessor < Struct.new(:order, :options)
79
90
  pass_item.product.person = self.order.person
80
91
  pass_item.product.save
81
92
  end
82
- self.order.create_generic_action("passes")
93
+ self.order.create_generic_action("passes") unless skip_actions
83
94
  PassMailer.pass_info_for(self.order.person, self.order.organization.email,self.order.passes.collect(&:product)).deliver
84
95
  end
85
96
  end
@@ -4,6 +4,7 @@ class PersonLifetimeValueJob
4
4
  p.skip_commit = true
5
5
  p.calculate_lifetime_value
6
6
  p.calculate_lifetime_donations
7
+ p.calculate_lifetime_pledges
7
8
  end
8
9
  Sunspot.delay.commit
9
10
  end
@@ -0,0 +1,33 @@
1
+ class SearchExportJob < Struct.new(:organization_id, :search_id)
2
+ include Monitorable
3
+
4
+ def perform
5
+ organization = Organization.find(organization_id)
6
+ search = Search.find(search_id)
7
+ segment = Segment.where(:search_id => search_id).first
8
+
9
+ Rails.logger.debug(segment.inspect)
10
+
11
+ filename = ""
12
+ if segment.present?
13
+ filename = "#{segment.name}-#{DateTime.now.strftime("%m-%d-%y")}.csv"
14
+ else
15
+ filename = "#{search.id}-#{DateTime.now.strftime("%m-%d-%y")}.csv"
16
+ end
17
+
18
+ people = Person.where(:id => search.people.collect(&:id))
19
+ .includes(:phones, :addresses, :tags)
20
+ .order('lower(people.last_name)')
21
+ csv_string = people.all.to_comma
22
+ s3 = S3Coordinator.new
23
+
24
+ s3.create_file(filename, csv_string)
25
+
26
+ if self.job_monitor.present?
27
+ self.job_monitor.file_url = s3.download_link(filename, 10*60)
28
+ self.job_monitor.finished = true
29
+ self.job_monitor.finished_at = DateTime.now
30
+ self.job_monitor.save
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ class SuggestHouseholdsByAddressJob < Struct.new(:address_id)
2
+ def perform
3
+ return unless address.present?
4
+ return unless address.person.present?
5
+ return unless organization.present?
6
+
7
+ matches = organization.people.joins(:addresses).
8
+ where(:addresses => {:address1 => address.address1, :address2 => address.address2}).
9
+ where('addresses.zip like ?', address.zip).
10
+ where('people.household_id is null')
11
+
12
+ return unless matches.count > 1
13
+
14
+ existing = organization.suggested_households.with_people(matches)
15
+ unless existing.present? && existing.ignored
16
+ organization.suggested_households.find_or_create_with_people(matches)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def address
23
+ @address ||= Address.find(address_id)
24
+ end
25
+
26
+ def organization
27
+ @organization ||= address.person.organization
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ class SuggestHouseholdsBySpouseJob < Struct.new(:relationship_id)
2
+ def perform
3
+ return unless relationship.present?
4
+ return if relationship.person.household.present? || relationship.other.household.present?
5
+
6
+ existing = organization.suggested_households.with_people([relationship.person, relationship.other])
7
+ unless existing.present? && existing.ignored
8
+ organization.suggested_households.find_or_create_with_people([relationship.person, relationship.other])
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def relationship
15
+ @relationship ||= Relationship.find(relationship_id)
16
+ end
17
+
18
+ def organization
19
+ @organization ||= relationship.person.organization
20
+ end
21
+ end
22
+
@@ -0,0 +1,14 @@
1
+ class JobMonitor < ActiveRecord::Base
2
+ belongs_to :organization
3
+
4
+ def self.enqueue(organization, job, job_options={})
5
+ job_monitor = self.new
6
+ job_monitor.organization = organization
7
+ job_monitor.return_url = job_options.delete(:return_url)
8
+ job_monitor.save
9
+ job.job_monitor = job_monitor
10
+
11
+ Delayed::Job.enqueue(job, job_options)
12
+ job_monitor
13
+ end
14
+ end
@@ -5,21 +5,23 @@ class Kit < ActiveRecord::Base
5
5
  belongs_to :organization
6
6
  validates_presence_of :organization
7
7
 
8
- class_attribute :requires_approval, :ability_proc, :configurable, :restricted_to_admins
8
+ class_attribute :requires_approval, :ability_proc, :configurable, :restricted_to_admins, :activate_without_prejudice, :cancel_with_authority
9
9
 
10
10
  def self.visible
11
11
  where(Kit.arel_table[:state].eq("activated").or(Kit.arel_table[:state].eq('pending')))
12
12
  end
13
13
 
14
14
  def self.acts_as_kit(options = {}, &block)
15
- self.requires_approval = options.delete(:with_approval) || false
16
- self.restricted_to_admins = options.delete(:admin_only) || false
15
+ activate_without_prejudice = self.activate_without_prejudice = options.delete(:activate_without_prejudice) || false
16
+ cancel_with_authority = self.cancel_with_authority = options.delete(:cancel_with_authority) || false
17
+ requires_approval = self.requires_approval = options.delete(:with_approval) || false
18
+ restricted_to_admins = self.restricted_to_admins = options.delete(:admin_only) || false
17
19
 
18
20
  state_machine do
19
21
  state :fresh
20
22
  state :pending, :enter => :on_pending
21
23
  state :activated, :enter => :on_activation
22
- state :cancelled
24
+ state :cancelled, :enter => :kit_cancelled
23
25
 
24
26
  event(:activate, :success => :record_activation) { transitions :from => [:fresh, :pending], :to => :activated, :guard => :activatable? }
25
27
  event(:approve, :success => :record_approval) { transitions :from => :pending, :to => :activated, :guard => :approvable? }
@@ -27,12 +29,22 @@ class Kit < ActiveRecord::Base
27
29
  event(:cancel) { transitions :from => [:activated, :pending, :rejected ], :to => :cancelled }
28
30
  event(:reactivate) { transitions :from => :cancelled, :to => :activated, :guard => :activatable? }
29
31
  event(:activate_without_pending) { transitions :from => [:fresh, :pending, :cancelled], :to => :activated }
30
- end
31
32
 
32
- if self.requires_approval
33
- state_machine do
33
+ if requires_approval
34
34
  event(:submit_for_approval) { transitions :from => :fresh, :to => :pending }
35
35
  end
36
+
37
+ if activate_without_prejudice
38
+ event :activate_without_prejudice do
39
+ transitions :from => [:fresh, :activated, :pending, :cancelled], :to => :activated
40
+ end
41
+ end
42
+
43
+ if cancel_with_authority
44
+ event :cancel_with_authority do
45
+ transitions :from => [:fresh, :pending, :activated, :cancelled], :to => :cancelled
46
+ end
47
+ end
36
48
  end
37
49
 
38
50
  class_eval(&block)
@@ -102,6 +114,10 @@ class Kit < ActiveRecord::Base
102
114
  check_requirements
103
115
  end
104
116
 
117
+ def flash_name
118
+ friendly_name
119
+ end
120
+
105
121
  def activatable?
106
122
  return false if organization.nil?
107
123
 
@@ -122,6 +138,10 @@ class Kit < ActiveRecord::Base
122
138
  check_admin_approval
123
139
  end
124
140
 
141
+ def has_active_storefront?
142
+ false
143
+ end
144
+
125
145
  class DuplicateError < StandardError
126
146
  end
127
147
 
@@ -129,6 +149,13 @@ class Kit < ActiveRecord::Base
129
149
  def on_activation; end
130
150
  def on_pending; end
131
151
 
152
+ def dont_approve
153
+ false
154
+ end
155
+
156
+ def kit_cancelled
157
+ end
158
+
132
159
  private
133
160
  def check_requirements
134
161
  check_unlesses(self.class.activation_requirements[:unless]) and check_ifs(self.class.activation_requirements[:if])
@@ -155,4 +182,4 @@ class Kit < ActiveRecord::Base
155
182
  def needs_approval?
156
183
  self.class.requires_approval and fresh?
157
184
  end
158
- end
185
+ end
@@ -0,0 +1,27 @@
1
+ class CampaignsKit < Kit
2
+ include ActionView::Helpers::SanitizeHelper
3
+
4
+ acts_as_kit :with_approval => true, :admin_only => true do
5
+ state_machine :auto_scopes => true do
6
+ state :cancelled, :enter => :kit_cancelled
7
+ event(:submit_for_approval) { transitions :from => :fresh, :to => :pending }
8
+ end
9
+
10
+ when_active do |organization|
11
+ organization.can :access, :campaigns
12
+ end
13
+ end
14
+
15
+ def friendly_name
16
+ 'Campaigns'
17
+ end
18
+
19
+ def flash_name
20
+ 'the Campaigns Kit'
21
+ end
22
+
23
+ def pitch
24
+ 'Set fundraising goals, create appeals, and organize donations.'
25
+ end
26
+
27
+ end
@@ -1,15 +1,11 @@
1
1
  class MailchimpKit < Kit
2
- attr_accessible :api_key
2
+ attr_accessible :api_key, :default_list_id, :fan_subscribe_question
3
3
  QUEUE = "mailchimp"
4
+ REQUIRED_MERGE_VARS = ["EMAIL", "FNAME", "LNAME", "ADDRESS"]
4
5
 
5
- acts_as_kit do
6
+ acts_as_kit :with_approval => true do
6
7
  self.configurable = true
7
8
 
8
- state_machine do
9
- state :cancelled, :enter => :kit_cancelled
10
- event(:submit_for_approval) { transitions :from => :fresh, :to => :pending }
11
- end
12
-
13
9
  when_active do
14
10
  end
15
11
  end
@@ -22,7 +18,8 @@ class MailchimpKit < Kit
22
18
 
23
19
  store :settings, :accessors => [
24
20
  :api_key, :old_api_key, :attached_lists,
25
- :mailchimp_state, :count_from_mailchimp, :count_to_mailchimp, :count_merged_artfully, :count_merged_mailchimp
21
+ :mailchimp_state, :default_list_id, :fan_subscribe_question,
22
+ :count_from_mailchimp, :count_merged_mailchimp, :hide_default_list_alert
26
23
  ]
27
24
 
28
25
  def friendly_name
@@ -45,18 +42,49 @@ class MailchimpKit < Kit
45
42
  def valid_api_key?
46
43
  return unless api_key
47
44
  begin
48
- gibbon.ping == "Everything's Chimpy!"
45
+ gibbon.helper.ping["msg"] == "Everything's Chimpy!"
49
46
  rescue
50
47
  false
51
48
  end
52
49
  end
53
50
 
54
51
  def lists
55
- gibbon.lists({:start => 0, :limit=> 100})["data"].map do |list|
52
+ gibbon.lists.list({ :start => 0, :limit => 100 })["data"].map do |list|
56
53
  [list["name"], list["id"]]
57
54
  end
58
55
  end
59
56
 
57
+ def groups(list_id)
58
+ gibbon.lists.interest_groupings(:id => list_id).map do |grouping|
59
+ grouping["groups"] = grouping["groups"].map do |group|
60
+ group.slice("name")
61
+ end
62
+ grouping.slice("id", "name", "groups")
63
+ end
64
+ rescue Gibbon::MailChimpError
65
+ # The list might not have groups enabled and throw this error
66
+ []
67
+ end
68
+
69
+ def cache_groups_for_attached_lists
70
+ attached_lists.each do |attached_lists|
71
+ attached_lists[:groups] = groups(attached_lists[:list_id])
72
+ end
73
+ save
74
+ end
75
+
76
+ def default_list_id
77
+ if settings[:default_list_id].present?
78
+ settings[:default_list_id]
79
+ elsif attached_lists.one?
80
+ attached_lists.first[:list_id]
81
+ end
82
+ end
83
+
84
+ def fan_subscribe_question
85
+ settings[:fan_subscribe_question] || "Would you like to join our email list?"
86
+ end
87
+
60
88
  def list_attached?(list_id)
61
89
  attached_lists.any? { |list| list[:list_id] == list_id }
62
90
  end
@@ -65,7 +93,8 @@ class MailchimpKit < Kit
65
93
  added_list_names = []
66
94
  added_list_ids = []
67
95
  removed_list_names = []
68
- lists.each do |list|
96
+ mailchimp_lists = lists
97
+ mailchimp_lists.each do |list|
69
98
  list_id = list[1]
70
99
  if !list_attached?(list_id) && new_list_ids.include?(list_id)
71
100
  add_list(list_id)
@@ -76,20 +105,41 @@ class MailchimpKit < Kit
76
105
  removed_list_names << find_list_name(list_id)
77
106
  end
78
107
  end
79
- send_updated_lists_email(added_list_ids, added_list_names, removed_list_names)
108
+
109
+ # Clean out old synced lists
110
+ old_lists = attached_lists.select do |list|
111
+ mailchimp_lists.none? { |mailchimp_list| mailchimp_list[1] == list[:list_id] }
112
+ end
113
+ old_lists.each do |list|
114
+ remove_list(list[:list_id])
115
+ end
116
+
117
+ cache_groups_for_attached_lists
118
+
119
+ start_sync(added_list_names, removed_list_names)
80
120
  end
81
121
 
82
122
  def add_list(list_id)
83
123
  self.attached_lists = attached_lists.reject { |list| list.empty? }
84
124
  attached_lists << {
85
125
  :list_id => list_id,
86
- :list_name => find_list_name(list_id)
126
+ :list_name => find_list_name(list_id),
127
+ :groups => groups(list_id),
87
128
  }
88
129
  save
89
130
  end
90
131
 
91
- def send_updated_lists_email(added_list_ids, added_list_names, removed_list_names)
92
- Delayed::Job.enqueue MailchimpSyncJob.new(self, :type => :initial_sync, :list_ids => added_list_ids, :added_list_names => added_list_names, :removed_list_names => removed_list_names), :queue => QUEUE
132
+ def start_sync(added_list_names, removed_list_names)
133
+ list_ids = attached_lists.map do |list|
134
+ list[:list_id]
135
+ end
136
+ job = MailchimpSyncJob.new(self, {
137
+ :type => :initial_sync,
138
+ :list_ids => list_ids,
139
+ :added_list_names => added_list_names,
140
+ :removed_list_names => removed_list_names,
141
+ })
142
+ Delayed::Job.enqueue job, :queue => QUEUE
93
143
  end
94
144
 
95
145
  def remove_list(list_id)
@@ -98,40 +148,52 @@ class MailchimpKit < Kit
98
148
  end
99
149
 
100
150
  def create_webhooks(list_id)
101
- gibbon.list_webhook_add({
151
+ webhook_url = Rails.application.routes.url_helpers.mailchimp_webhook_url(id, {
152
+ :list_id => list_id,
153
+ :host => MAILCHIMP_WEBHOOK_URL[:host],
154
+ :protocol => MAILCHIMP_WEBHOOK_URL[:protocol] || "http",
155
+ })
156
+
157
+ webhooks_for_list = gibbon.lists.webhooks(:id => list_id)
158
+
159
+ existing_webhook = webhooks_for_list.detect do |webhook|
160
+ webhook["url"] == webhook_url
161
+ end
162
+
163
+ return true if existing_webhook
164
+
165
+ gibbon.lists.webhook_add({
102
166
  :id => list_id,
103
- :url => Rails.application.routes.url_helpers.mailchimp_webhook_url(id, :list_id => list_id, :host => MAILCHIMP_WEBHOOK_URL[:host], :protocol => MAILCHIMP_WEBHOOK_URL[:protocol] || "http")
167
+ :url => webhook_url,
104
168
  })
105
169
  end
106
170
 
107
171
  def destroy_webhooks(list_id)
108
- gibbon = Gibbon.new(old_api_key || api_key)
109
- gibbon.list_webhook_del({
172
+ gibbon = Gibbon::API.new(old_api_key || api_key)
173
+ gibbon.lists.webhook_del({
110
174
  :id => list_id,
111
175
  :url => Rails.application.routes.url_helpers.mailchimp_webhook_url(id, :list_id => list_id, :host => MAILCHIMP_WEBHOOK_URL[:host], :protocol => MAILCHIMP_WEBHOOK_URL[:protocol] || "http")
112
- })
176
+ })["completed"]
113
177
  end
114
178
 
115
179
  def unsubscribe_old_members(list_id)
116
180
  organization.people.each do |person|
117
- unless person.subscribed_lists.delete(list_id).nil?
118
- person.skip_commit = true
119
- person.save
181
+ person.subscribed_lists.where(:list_id => list_id).each do |subscribed_list|
182
+ subscribed_list.sync = false
183
+ subscribed_list.destroy
120
184
  end
121
185
  end
122
186
  Sunspot.delay.commit
123
187
  end
124
188
 
125
189
  def sync_mailchimp_to_artfully_new_members(list_id)
126
- set_count_from_mailchimp(list_id)
127
-
128
- mailchimp_to_artfully_new_members(list_id).each do |member|
190
+ members = mailchimp_to_artfully_new_members(list_id)
191
+ members.each do |member|
129
192
  person = organization.people.create({
130
- :first_name => member["First Name"],
131
- :last_name => member["Last Name"],
132
- :email => member["Email Address"],
193
+ :first_name => member["first name"],
194
+ :last_name => member["last name"],
195
+ :email => member["email address"],
133
196
  :skip_sync_to_mailchimp => true,
134
- :subscribed_lists => [list_id]
135
197
  }) do |person|
136
198
  person.skip_commit = true
137
199
  end
@@ -141,48 +203,73 @@ class MailchimpKit < Kit
141
203
  })
142
204
  note.organization_id = organization_id
143
205
  note.save
206
+
207
+ subscribed_list = person.subscribed_lists.create({
208
+ :list_id => list_id,
209
+ :sync => false,
210
+ :confirmed => true,
211
+ })
212
+
213
+ list = attached_lists.detect { |list| list[:list_id] == list_id }
214
+ create_groups_for_mailchimp_member(list, subscribed_list, member)
144
215
  end
216
+ set_count_from_mailchimp(members.count)
145
217
  end
146
218
 
147
219
  def mailchimp_to_artfully_new_members(list_id)
148
- members_not_in_artfully = []
149
- mailchimp_list_members(list_id).each do |member|
150
- if !organization_people_emails.include?(member["Email Address"])
151
- members_not_in_artfully << member
152
- end
220
+ mailchimp_list_members(list_id).reject do |member|
221
+ organization_people_emails.include?(member["email address"])
153
222
  end
154
- members_not_in_artfully
155
223
  end
156
224
 
157
225
  def sync_mailchimp_to_artfully_update_members(list_id)
158
- set_count_merged_mailchimp(list_id)
159
-
160
- mailchimp_to_artfully_update_members(list_id).each do |member|
161
- person = organization.people.find_by_email(member["Email Address"])
226
+ members = mailchimp_to_artfully_update_members(list_id)
227
+ members.each do |member|
228
+ person = organization.people.find_by_email(member["email address"])
162
229
 
163
230
  next if person.do_not_email?
164
231
 
165
232
  member.each do |attribute, value|
166
233
  attribute = mailchimp_attributes_to_artfully[attribute]
167
- if person.send(attribute).blank?
234
+ if attribute && person.send(attribute).blank?
168
235
  person.send("#{attribute}=", value)
169
236
  end
170
237
  end
171
238
 
172
239
  person.skip_sync_to_mailchimp = true
173
- person.subscribed_lists << list_id
174
240
  person.save
241
+
242
+ subscribed_list = person.subscribed_lists.find_or_initialize_by_list_id(list_id)
243
+ subscribed_list.sync = false
244
+ subscribed_list.confirmed = true
245
+ subscribed_list.bounced = false
246
+ subscribed_list.save
247
+
248
+ list = attached_lists.detect { |list| list[:list_id] == list_id }
249
+ create_groups_for_mailchimp_member(list, subscribed_list, member)
175
250
  end
251
+ set_count_merged_mailchimp(members.count)
176
252
  end
177
253
 
178
- def mailchimp_to_artfully_update_members(list_id)
179
- members_in_mailchimp = []
180
- mailchimp_list_members(list_id).each do |member|
181
- if organization_people_emails.include?(member["Email Address"])
182
- members_in_mailchimp << member
254
+ def create_groups_for_mailchimp_member(list, subscribed_list, member)
255
+ list.fetch(:groups, []).each do |grouping|
256
+ groups = member[grouping["name"].downcase] || ""
257
+ groups = groups.split(",").map(&:strip)
258
+
259
+ groups.each do |group|
260
+ subscribed_list.groupings.create({
261
+ :mailchimp_id => grouping["id"],
262
+ :name => grouping["name"],
263
+ :group => group,
264
+ })
183
265
  end
184
266
  end
185
- members_in_mailchimp
267
+ end
268
+
269
+ def mailchimp_to_artfully_update_members(list_id)
270
+ mailchimp_list_members(list_id).select do |member|
271
+ organization_people_emails.include?(member["email address"])
272
+ end
186
273
  end
187
274
 
188
275
  def sync_merged_loser_to_mailchimp(email)
@@ -192,10 +279,15 @@ class MailchimpKit < Kit
192
279
  def sync_merged_winner_to_mailchimp(person_id, new_lists)
193
280
  person = Person.find(person_id)
194
281
  new_lists.each do |list_id|
195
- gibbon.list_subscribe({
282
+ gibbon.lists.subscribe({
196
283
  :id => list_id,
197
- :email_address => person.email,
198
- :merge_vars => { "FNAME" => person.first_name, "LNAME" => person.last_name },
284
+ :email => {
285
+ :email => person.email,
286
+ },
287
+ :merge_vars => {
288
+ "FNAME" => person.first_name,
289
+ "LNAME" => person.last_name,
290
+ },
199
291
  :double_optin => false
200
292
  })
201
293
  end
@@ -203,8 +295,11 @@ class MailchimpKit < Kit
203
295
 
204
296
  def sync_mailchimp_webhook_new_subscriber(list_id, data)
205
297
  if person = organization.people.find_by_email(data["email"])
206
- person.subscribed_lists << list_id
207
- person.save
298
+ subscribed_list = person.subscribed_lists.where(:list_id => list_id).first
299
+ subscribed_list ||= person.subscribed_lists.create(:list_id => list_id, :sync => false)
300
+ subscribed_list.confirmed = true
301
+ subscribed_list.bounced = false
302
+ subscribed_list.save
208
303
  return sync_mailchimp_webhook_update_person(list_id, data)
209
304
  end
210
305
 
@@ -213,8 +308,12 @@ class MailchimpKit < Kit
213
308
  :last_name => data["merges"]["LNAME"],
214
309
  :email => data["email"],
215
310
  :skip_sync_to_mailchimp => true,
216
- :subscribed_lists => [list_id]
217
311
  })
312
+
313
+ subscribed_list = person.subscribed_lists.create(:list_id => list_id, :sync => false)
314
+ subscribed_list.confirmed = true
315
+ subscribed_list.save
316
+
218
317
  note = person.notes.build({
219
318
  :text => "Imported from MailChimp",
220
319
  :occurred_at => Time.now
@@ -239,15 +338,33 @@ class MailchimpKit < Kit
239
338
  person.send("#{attribute}=", value) if attribute
240
339
  end
241
340
 
242
- person.subscribed_lists.each do |subscribed_list_id|
243
- next if subscribed_list_id == list_id
244
- gibbon.list_update_member({
245
- :id => subscribed_list_id,
246
- :email_address => person.email,
341
+ subscribed_list = person.subscribed_lists.where(:list_id => list_id).first
342
+ if subscribed_list
343
+ data["merges"].fetch("GROUPINGS", {}).each do |_, grouping|
344
+ # Clear out all groupings so we only have an up to date list
345
+ subscribed_list.groupings.where(:name => grouping["name"]).delete_all
346
+ groups = grouping["groups"].split(",").map(&:strip)
347
+ groups.each do |group|
348
+ subscribed_list.groupings.create({
349
+ :mailchimp_id => grouping["id"],
350
+ :name => grouping["name"],
351
+ :group => group,
352
+ })
353
+ end
354
+ end
355
+ end
356
+
357
+ person.subscribed_lists.not_bounced.each do |subscribed_list|
358
+ next if subscribed_list.list_id == list_id
359
+ gibbon.lists.update_member({
360
+ :id => subscribed_list.list_id,
361
+ :email => {
362
+ :email => person.email,
363
+ },
247
364
  :merge_vars => {
248
365
  "FNAME" => person.first_name,
249
- "LNAME" => person.last_name
250
- }
366
+ "LNAME" => person.last_name,
367
+ },
251
368
  })
252
369
  end
253
370
 
@@ -261,7 +378,11 @@ class MailchimpKit < Kit
261
378
  return if person.nil?
262
379
 
263
380
  reason = (data["reason"] == "hard" ? "a hard bounce" : "abuse")
264
- person.subscribed_lists.delete(list_id)
381
+ person.subscribed_lists.where(:list_id => list_id).each do |subscribed_list|
382
+ subscribed_list.bounced = true
383
+ subscribed_list.confirmed = true
384
+ subscribed_list.save
385
+ end
265
386
  person.new_note("MailChimp cleaned #{person.email} from #{list_name(list_id)} because of #{reason}.", Time.now, nil, organization_id)
266
387
  person.save
267
388
  end
@@ -271,11 +392,13 @@ class MailchimpKit < Kit
271
392
  return if person.nil? || person.do_not_email?
272
393
  person.update_attributes(:email => data["new_email"], :skip_sync_to_mailchimp => true)
273
394
 
274
- person.subscribed_lists.each do |subscribed_list_id|
275
- next if subscribed_list_id == list_id
276
- gibbon.list_update_member({
277
- :id => subscribed_list_id,
278
- :email_address => data["old_email"],
395
+ person.subscribed_lists.each do |subscribed_list|
396
+ next if subscribed_list.list_id == list_id
397
+ gibbon.lists.update_member({
398
+ :id => subscribed_list.list_id,
399
+ :email => {
400
+ :email => data["old_email"],
401
+ },
279
402
  :merge_vars => {
280
403
  "EMAIL" => data["new_email"]
281
404
  }
@@ -291,40 +414,201 @@ class MailchimpKit < Kit
291
414
  person = organization.people.find_by_email(data["email"])
292
415
  return unless person
293
416
  person.new_note("Unsubscribed in MailChimp from #{list_name(list_id)}", Time.now, nil, organization_id)
294
- person.subscribed_lists.delete(list_id)
417
+ person.subscribed_lists.where(:list_id => list_id).each do |subscribed_list|
418
+ subscribed_list.sync = false
419
+ subscribed_list.destroy
420
+ end
421
+ person.opted_out_lists << list_id
422
+ person.opted_out_lists.uniq!
295
423
  person.save
296
424
  end
297
425
 
426
+ def list_attached?(list_id)
427
+ attached_lists.detect do |list|
428
+ list[:list_id] == list_id
429
+ end
430
+ end
431
+
298
432
  def sync_mailchimp_webhook_campaign_sent(list_id, data)
299
- occurred_at = Time.now
433
+ return unless list_attached?(list_id)
434
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Starting sync")
435
+ start = Time.now
300
436
 
437
+ occurred_at = Time.now
301
438
  emails = []
302
439
 
440
+ page = 0
303
441
  begin
304
- response = gibbon.campaign_members(:cid => data["id"])
442
+ response = gibbon.reports.sent_to(:cid => data["id"], :opts => {:start => page, :limit => 100})
305
443
  response["data"].each do |user|
306
- emails << user["email"]
444
+ emails << user.fetch("member").fetch("email")
307
445
  end
446
+ page += 1
308
447
  end while response["total"] > emails.count
309
-
310
- organization.people.each do |person|
311
- next if !person.subscribed_lists.include?(list_id)
312
- next if !emails.include?(person.email)
313
- hear_action = HearAction.for_organization(organization)
314
- hear_action.details = %{"#{data["subject"].truncate(25)}" delivered to #{list_name(list_id)} MailChimp list.}
448
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: [#{emails.count}] emails found")
449
+
450
+ system_action = SystemAction.for_organization(organization)
451
+ system_action.details = %{"#{data["subject"]}" delivered to #{emails.count} subscribers of #{list_name(list_id)}.}
452
+ system_action.subtype = "Mailchimp (Sent)"
453
+ system_action.save
454
+
455
+ people_ids = organization.people
456
+ .joins(:subscribed_lists)
457
+ .where(:deleted_at => nil)
458
+ .where(:email => emails)
459
+ .where(:do_not_email => false)
460
+ .where('subscribed_lists.list_id = ?', list_id)
461
+ .pluck(:id)
462
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: [#{people_ids.length}] people found")
463
+
464
+ hear_actions = []
465
+
466
+ people_ids.each_with_index do |person_id, index|
467
+ hear_action = MailchimpHearAction.for_organization(organization)
468
+ hear_action.details = %{"#{data["subject"].truncate(25)}" via #{list_name(list_id)} MailChimp list.}
315
469
  hear_action.occurred_at = occurred_at
316
470
  hear_action.subtype = "Mailchimp (Sent)"
317
- hear_action.person = person
318
- hear_action.save
471
+ hear_action.person_id = person_id
472
+ hear_action.hide_on_recent_activity = true
473
+ hear_action.external_reference = data["id"]
474
+ hear_actions << hear_action
475
+
476
+ if ((index+1) % 100 == 0)
477
+ hear_actions = flush_actions(hear_actions)
478
+ end
479
+ end
480
+
481
+ # Final flush
482
+ hear_actions = flush_actions(hear_actions)
483
+
484
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Finished with people")
485
+ schedule_open_check(data["id"], 1.hour.from_now)
486
+
487
+ finish = Time.now
488
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: took #{finish-start} seconds")
489
+ AdminMailer.mailchimp_sent(organization, data, list_id, emails, people_ids.length, finish - start).deliver
490
+ end
491
+
492
+ def flush_actions(hear_actions)
493
+ if !hear_actions.empty?
494
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Importing [#{hear_actions.length}] actions")
495
+ HearAction.import hear_actions
496
+ Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Flushing actions")
497
+ hear_actions = []
498
+ end
499
+ hear_actions
500
+ end
501
+
502
+ def sync_artfully_open_check(campaign_id)
503
+ members = []
504
+
505
+ page = 0
506
+ begin
507
+ response = gibbon.reports.opened(:cid => campaign_id, :opts => {:start => page})
508
+ response["data"].each do |member|
509
+ members << {
510
+ :email => member.fetch("member").fetch("email"),
511
+ :opened? => member.fetch("opens").to_i > 0,
512
+ }
513
+ end
514
+ page += 1
515
+ end while response["total"].to_i > members.count
516
+
517
+ response = gibbon.campaigns.list(:filters => { :campaign_id => campaign_id })
518
+ campaign = response["data"].first
519
+
520
+ occurred_at = Time.now
521
+
522
+ utc = ActiveSupport::TimeZone.new("UTC")
523
+ send_time = utc.parse(campaign["send_time"])
524
+
525
+ members.each do |member|
526
+ next unless member[:opened?]
527
+
528
+ person = organization.people.find_by_email(member[:email])
529
+ next unless person
530
+
531
+ action = person.actions.where({
532
+ :type => MailchimpHearAction,
533
+ :external_reference => campaign_id,
534
+ }).first
535
+ next unless action
536
+
537
+ next unless action.subtype == "Mailchimp (Sent)"
538
+
539
+ action.details = %{"#{campaign["subject"].truncate(25)}" via the #{list_name(campaign["list_id"])} MailChimp list.}
540
+ action.subtype = "Mailchimp (Opened)"
541
+ action.occurred_at = occurred_at
542
+ action.save
543
+ end
544
+
545
+ difference = Time.now.utc - send_time
546
+ if difference < 1.week
547
+ schedule_open_check(campaign_id, 1.hour.from_now)
548
+ elsif 1.week < difference && difference < 2.weeks
549
+ schedule_open_check(campaign_id, 1.day.from_now)
550
+ end
551
+ end
552
+
553
+ def schedule_open_check(campaign_id, time)
554
+ job = MailchimpSyncJob.new(self, {
555
+ :type => "open_check",
556
+ :campaign_id => campaign_id,
557
+ })
558
+ Delayed::Job.enqueue(job, :queue => QUEUE, :run_at => time)
559
+ end
560
+
561
+ def sync_artfully_grouping_update(person_id)
562
+ person = Person.find_by_id(person_id)
563
+ return unless person
564
+ person.subscribed_lists.each do |subscribed_list|
565
+ list = attached_lists.detect do |list|
566
+ list[:list_id] == subscribed_list.list_id
567
+ end
568
+ next unless list
569
+ groupings = list.fetch(:groups, []).map do |group|
570
+ groups = subscribed_list.groupings.where(:name => group["name"])
571
+ {
572
+ :id => group["id"],
573
+ :groups => groups.map(&:group),
574
+ }
575
+ end
576
+
577
+ gibbon.lists.update_member({
578
+ :id => subscribed_list.list_id,
579
+ :email => {
580
+ :email => person.email,
581
+ },
582
+ :merge_vars => {
583
+ "GROUPINGS" => groupings,
584
+ },
585
+ })
319
586
  end
320
587
  end
321
588
 
322
- def sync_artfully_person_update(person_id, person_changes)
589
+ def sync_artfully_person_subscribe(person_id, new_list_id, single_optin)
590
+ person = organization.people.find_by_id(person_id)
591
+ return unless person
592
+ subscribe_email(person.email, person.first_name, person.last_name, new_list_id, !single_optin)
593
+ end
594
+
595
+ def sync_artfully_person_unsubscribe(person_id, new_list_id)
596
+ person = organization.people.find_by_id(person_id)
597
+ return unless person
598
+ unsubscribe_email(person.email, new_list_id)
599
+ end
600
+
601
+ #
602
+ # The API here is unclear. Sometimes this returns true, sometimes nothing
603
+ # and sometimes the array of subscribed lists
604
+ #
605
+ def sync_artfully_person_update(person_id, person_changes, bounced_list_ids = [])
323
606
  person = organization.people.find_by_id(person_id)
324
607
 
325
608
  return unless person
326
609
 
327
610
  merge_vars = {}
611
+
328
612
  merge_vars["FNAME"] = person_changes["first_name"][1] if person_changes["first_name"]
329
613
  merge_vars["LNAME"] = person_changes["last_name"][1] if person_changes["last_name"]
330
614
  email = person_changes["email"] ? person_changes["email"][0] : person.email
@@ -335,56 +619,188 @@ class MailchimpKit < Kit
335
619
  end
336
620
  end
337
621
 
338
- if person_changes.has_key?("subscribed_lists")
339
- return attached_lists.all? do |list|
340
- old_lists = person_changes["subscribed_lists"][0]
341
- new_lists = person_changes["subscribed_lists"][1]
342
- if !old_lists.include?(list[:list_id]) && new_lists.include?(list[:list_id])
343
- first_name = merge_vars["FNAME"] || person.first_name
344
- last_name = merge_vars["LNAME"] || person.last_name
345
- subscribe_email(email, first_name, last_name, list[:list_id])
346
- elsif old_lists.include?(list[:list_id]) && !new_lists.include?(list[:list_id])
347
- unsubscribe_email(email, list[:list_id])
348
- else
349
- true
350
- end
351
- end
352
- end
353
-
354
622
  return unless sync_person?(person)
355
623
 
356
624
  merge_vars["EMAIL"] = person_changes["email"][1] if person_changes["email"]
357
625
 
358
626
  # go over the person's subscribed lists and update them
359
- person.subscribed_lists.each do |list_id|
360
- next unless attached_lists.any? { |list| list[:list_id] == list_id }
361
- gibbon.list_update_member({
362
- :id => list_id,
363
- :email_address => email,
627
+ person.subscribed_lists.each do |subscribed_list|
628
+ next unless attached_lists.any? { |list| list[:list_id] == subscribed_list.list_id }
629
+
630
+ # Bounced emails are no longer subscribed so we should
631
+ # resubscribe them without sending a notification
632
+ if merge_vars.has_key?("EMAIL") && bounced_list_ids.include?(subscribed_list.id)
633
+ subscribe_email(merge_vars["EMAIL"], person.first_name, person.last_name, subscribed_list.list_id, false)
634
+ next
635
+ end
636
+
637
+ gibbon.lists.update_member({
638
+ :id => subscribed_list.list_id,
639
+ :email => {
640
+ :email => email,
641
+ },
364
642
  :merge_vars => merge_vars
365
643
  })
366
644
  end
367
645
  end
368
646
 
647
+ def sync_artfully_segment(segment_id, list_id)
648
+ segment = organization.segments.find(segment_id)
649
+ return unless segment
650
+ sync_artfully_people_to_mailchimp(segment.people, list_id)
651
+ end
652
+
653
+ def sync_artfully_search(search_id, list_id)
654
+ search = organization.searches.find(search_id)
655
+ return unless search
656
+ sync_artfully_people_to_mailchimp(search.people, list_id)
657
+ end
658
+
659
+ def sync_artfully_advanced_search_segment(advanced_search_segment_id, list_id)
660
+ advanced_search_segment = organization.advanced_search_segments.find(advanced_search_segment_id)
661
+ return unless advanced_search_segment
662
+ sync_artfully_people_to_mailchimp(advanced_search_segment.all_people, list_id)
663
+ end
664
+
665
+ def sync_artfully_advanced_search(advanced_search_id, list_id)
666
+ advanced_search = organization.advanced_searches.find(advanced_search_id)
667
+ return unless advanced_search
668
+ sync_artfully_people_to_mailchimp(advanced_search.all_people, list_id)
669
+ end
670
+
671
+ def sync_artfully_people_to_mailchimp(people, list_id)
672
+ mailchimp_emails = mailchimp_list_members(list_id).map { |member| member["email address"] }
673
+
674
+ # find everyone not in mailchimp list
675
+ subscribe_people = people.select do |person|
676
+ person.email.present? && !mailchimp_emails.include?(person.email)
677
+ end
678
+
679
+ subscribe_people.each do |person|
680
+ subscribe_email(person.email, person.first_name, person.last_name, list_id)
681
+ person.subscribed_lists.create(:list_id => list_id, :sync => false)
682
+ end
683
+ end
684
+
685
+ def sync_artfully_segment_add_group(segment_id, grouping)
686
+ segment = organization.segments.find(segment_id)
687
+ return unless segment
688
+ sync_artfully_people_add_group(segment.people, grouping)
689
+ end
690
+
691
+ def sync_artfully_search_add_group(search_id, grouping)
692
+ search = organization.searches.find(search_id)
693
+ return unless search
694
+ sync_artfully_people_add_group(search.people, grouping)
695
+ end
696
+
697
+ def sync_artfully_advanced_search_segment_add_group(advanced_search_segment_id, grouping)
698
+ advanced_search_segment = organization.advanced_search_segments.find(advanced_search_segment_id)
699
+ return unless advanced_search_segment
700
+ sync_artfully_people_add_group(advanced_search_segment.all_people, grouping)
701
+ end
702
+
703
+ def sync_artfully_advanced_search_add_group(advanced_search_id, grouping)
704
+ advanced_search = organization.advanced_searches.find(advanced_search_id)
705
+ return unless advanced_search
706
+ sync_artfully_people_add_group(advanced_search.all_people, grouping)
707
+ end
708
+
709
+ def sync_artfully_people_add_group(people, grouping)
710
+ grouping_id, group = grouping.split("|")
711
+
712
+ list = attached_lists.detect do |list|
713
+ list.fetch(:groups, []).any? do |grouping|
714
+ grouping["id"] == grouping_id.to_i
715
+ end
716
+ end
717
+ return unless list
718
+
719
+ grouping = list.fetch(:groups, []).detect do |grouping|
720
+ grouping["id"] == grouping_id.to_i
721
+ end
722
+ return unless grouping
723
+
724
+ mailchimp_emails = mailchimp_list_members(list[:list_id]).map { |member| member["email address"] }
725
+
726
+ # find everyone in mailchimp list
727
+ subscribed_people = people.select do |person|
728
+ person.email.present? && mailchimp_emails.include?(person.email)
729
+ end
730
+ subscribed_people.each do |person|
731
+ subscribed_list = person.subscribed_lists.where(:list_id => list[:list_id]).first
732
+ next unless subscribed_list
733
+
734
+ list_grouping = subscribed_list.groupings.create({
735
+ :mailchimp_id => grouping_id,
736
+ :name => grouping["name"],
737
+ :group => group,
738
+ })
739
+
740
+ sync_artfully_grouping_update(person.id) if list_grouping.persisted?
741
+ end
742
+ end
743
+
369
744
  def mailchimp_list_members(list_id)
370
- return @mailchimp_list_members if @mailchimp_list_members
371
745
  response = mailchimp_exporter.list(:id => list_id).to_a
372
746
  headers = JSON.parse(response.shift)
747
+ headers.map!(&:downcase)
373
748
  members = response.map { |line| JSON.parse(line) }
374
749
 
375
- @mailchimp_list_members ||= members.collect do |member|
376
- member_hash = {}
750
+ merge_variables = list_merge_variables(list_id)
751
+
752
+ headers = headers.map do |header|
753
+ if merge_variables.has_key?(header)
754
+ merge_variables[header]
755
+ else
756
+ header
757
+ end
758
+ end
759
+
760
+ grouping_headers = attached_lists.flat_map do |list|
761
+ list.fetch(:groups, []).map { |group| group["name"] }
762
+ end.map(&:downcase)
377
763
 
378
- mailchimp_attributes.inject({}) do |member_hash, attribute|
379
- member_hash[attribute] = member[headers.index(attribute)] unless headers.index(attribute).nil?
764
+ members.collect do |member|
765
+ member_hash = {}
766
+ attributes = mailchimp_attributes + grouping_headers
767
+ attributes.inject({}) do |member_hash, attribute|
768
+ unless headers.index(attribute).nil?
769
+ member_hash[attribute] = member[headers.index(attribute)]
770
+ end
380
771
  member_hash
381
772
  end
382
773
  end
383
774
  end
384
775
 
776
+ def list_merge_variables(list_id)
777
+ response = gibbon.lists.merge_vars(:id => [list_id])
778
+ list = response["data"].first
779
+ variables = list["merge_vars"]
780
+ variables.inject({}) do |hash, variable|
781
+ case variable["tag"]
782
+ when "EMAIL"
783
+ hash[variable["name"].downcase] = "email address"
784
+ when "FNAME"
785
+ hash[variable["name"].downcase] = "first name"
786
+ when "LNAME"
787
+ hash[variable["name"].downcase] = "last name"
788
+ end
789
+ hash
790
+ end
791
+ end
792
+
793
+ def default_list?
794
+ default_list_id.present?
795
+ end
796
+
797
+ def display_default_list_alert?
798
+ api_key.present? && !default_list? && !hide_default_list_alert
799
+ end
800
+
385
801
  private
386
802
  def gibbon
387
- @gibbon ||= Gibbon.new(api_key)
803
+ @gibbon ||= Gibbon::API.new(api_key)
388
804
  end
389
805
 
390
806
  def mailchimp_exporter
@@ -398,14 +814,14 @@ class MailchimpKit < Kit
398
814
  end
399
815
 
400
816
  def mailchimp_attributes
401
- ["Email Address", "First Name", "Last Name"]
817
+ ["email address", "first name", "last name"]
402
818
  end
403
819
 
404
820
  def mailchimp_attributes_to_artfully
405
821
  {
406
- "Email Address" => "email",
407
- "First Name" => "first_name",
408
- "Last Name" => "last_name"
822
+ "email address" => "email",
823
+ "first name" => "first_name",
824
+ "last name" => "last_name"
409
825
  }
410
826
  end
411
827
 
@@ -413,12 +829,12 @@ class MailchimpKit < Kit
413
829
  {
414
830
  "EMAIL" => "email",
415
831
  "FNAME" => "first_name",
416
- "LNAME" => "last_name"
832
+ "LNAME" => "last_name",
417
833
  }
418
834
  end
419
835
 
420
836
  def organization_people_emails
421
- @organization_people_emails ||= organization.people.pluck(:email)
837
+ organization.people.pluck(:email)
422
838
  end
423
839
 
424
840
  def kit_cancelled
@@ -429,13 +845,13 @@ class MailchimpKit < Kit
429
845
  Delayed::Job.enqueue(MailchimpSyncJob.new(self, :type => "kit_cancelled"), :queue => QUEUE)
430
846
  end
431
847
 
432
- def set_count_from_mailchimp(list_id)
433
- self.count_from_mailchimp = mailchimp_to_artfully_new_members(list_id).count
848
+ def set_count_from_mailchimp(count)
849
+ self.count_from_mailchimp = (count_from_mailchimp || 0) + count
434
850
  save
435
851
  end
436
852
 
437
- def set_count_merged_mailchimp(list_id)
438
- self.count_merged_mailchimp = mailchimp_to_artfully_update_members(list_id).count
853
+ def set_count_merged_mailchimp(count)
854
+ self.count_merged_mailchimp = (count_merged_mailchimp || 0) + count
439
855
  save
440
856
  end
441
857
 
@@ -451,21 +867,29 @@ class MailchimpKit < Kit
451
867
  end
452
868
 
453
869
  lists.each do |list_id|
454
- gibbon.list_unsubscribe({
870
+ gibbon.lists.unsubscribe({
455
871
  :id => list_id,
456
- :email_address => email,
872
+ :email => {
873
+ :email => email,
874
+ },
457
875
  :send_goodbye => false,
458
876
  :send_notify => false,
459
- :delete_member => true
877
+ :delete_member => true,
460
878
  })
461
879
  end
462
880
  end
463
881
 
464
- def subscribe_email(email, first_name, last_name, list_id)
465
- response = gibbon.list_subscribe({
882
+ def subscribe_email(email, first_name, last_name, list_id, send_optin = true)
883
+ response = gibbon.lists.subscribe({
466
884
  :id => list_id,
467
- :email_address => email,
468
- :merge_vars => { "FNAME" => first_name, "LNAME" => last_name }
885
+ :email => {
886
+ :email => email,
887
+ },
888
+ :double_optin => send_optin,
889
+ :merge_vars => {
890
+ "FNAME" => first_name,
891
+ "LNAME" => last_name,
892
+ },
469
893
  })
470
894
  end
471
895