winton-sum 0.1.0

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 (280) hide show
  1. data/Capfile +3 -0
  2. data/MIT-LICENSE +18 -0
  3. data/README.markdown +55 -0
  4. data/Rakefile +45 -0
  5. data/config/database.example.yml +10 -0
  6. data/config/deploy.example.rb +36 -0
  7. data/config/externals.yml +18 -0
  8. data/config/mail.example.yml +32 -0
  9. data/config/schedule.rb +20 -0
  10. data/config.ru +3 -0
  11. data/db/migrate/001_users.rb +22 -0
  12. data/db/migrate/002_user_emails.rb +15 -0
  13. data/features/front_page.feature +14 -0
  14. data/features/read_email.feature +446 -0
  15. data/features/receive_email.feature +59 -0
  16. data/features/step_definitions/email.rb +52 -0
  17. data/features/step_definitions/given.rb +60 -0
  18. data/features/step_definitions/then.rb +32 -0
  19. data/features/step_definitions/when.rb +41 -0
  20. data/features/support/env.rb +27 -0
  21. data/features/support/helpers.rb +44 -0
  22. data/features/support/rspec.rb +11 -0
  23. data/features/support/webrat.rb +3 -0
  24. data/features/update_user.feature +76 -0
  25. data/gemspec.rb +45 -0
  26. data/lib/sum/boot.rb +29 -0
  27. data/lib/sum/controller/cron.rb +12 -0
  28. data/lib/sum/controller/front.rb +6 -0
  29. data/lib/sum/controller/new.rb +15 -0
  30. data/lib/sum/helper/application.rb +8 -0
  31. data/lib/sum/helper/cron.rb +12 -0
  32. data/lib/sum/helper/new.rb +26 -0
  33. data/lib/sum/model/incoming_mail.rb +89 -0
  34. data/lib/sum/model/user.rb +286 -0
  35. data/lib/sum/model/user_email.rb +31 -0
  36. data/lib/sum/view/email.haml +70 -0
  37. data/lib/sum/view/field.haml +10 -0
  38. data/lib/sum/view/form.haml +12 -0
  39. data/lib/sum/view/front.haml +25 -0
  40. data/lib/sum/view/layout.haml +26 -0
  41. data/lib/sum/view/new.haml +15 -0
  42. data/lib/sum.rb +30 -0
  43. data/public/image/1.png +0 -0
  44. data/public/image/2.png +0 -0
  45. data/public/image/3.png +0 -0
  46. data/public/image/bg.png +0 -0
  47. data/public/image/bot.png +0 -0
  48. data/public/image/facebox/b.png +0 -0
  49. data/public/image/facebox/bl.png +0 -0
  50. data/public/image/facebox/br.png +0 -0
  51. data/public/image/facebox/closelabel.gif +0 -0
  52. data/public/image/facebox/loading.gif +0 -0
  53. data/public/image/facebox/tl.png +0 -0
  54. data/public/image/facebox/tr.png +0 -0
  55. data/public/image/favicon.png +0 -0
  56. data/public/image/field_off.png +0 -0
  57. data/public/image/field_on.png +0 -0
  58. data/public/image/ribbon.png +0 -0
  59. data/public/image/screenshot_receive.png +0 -0
  60. data/public/image/screenshot_send.png +0 -0
  61. data/public/image/submit.png +0 -0
  62. data/public/image/top.png +0 -0
  63. data/public/javascript/facebox.js +319 -0
  64. data/public/javascript/jquery-1.3.2.js +19 -0
  65. data/public/javascript/sum.js +9 -0
  66. data/public/style/facebox.css +95 -0
  67. data/public/style/ie.css +35 -0
  68. data/public/style/print.css +30 -0
  69. data/public/style/screen.css +256 -0
  70. data/public/style/sum.css +22 -0
  71. data/public/style/sum.less +135 -0
  72. data/spec/spec.opts +1 -0
  73. data/spec/spec_helper.rb +58 -0
  74. data/spec/sum/model/incoming_mail_spec.rb +106 -0
  75. data/spec/sum/model/user_spec.rb +171 -0
  76. data/sum.gemspec +69 -0
  77. data/vendor/webrat/History.txt +358 -0
  78. data/vendor/webrat/MIT-LICENSE.txt +19 -0
  79. data/vendor/webrat/README.rdoc +85 -0
  80. data/vendor/webrat/Rakefile +200 -0
  81. data/vendor/webrat/install.rb +1 -0
  82. data/vendor/webrat/lib/webrat/core/configuration.rb +102 -0
  83. data/vendor/webrat/lib/webrat/core/elements/area.rb +31 -0
  84. data/vendor/webrat/lib/webrat/core/elements/element.rb +33 -0
  85. data/vendor/webrat/lib/webrat/core/elements/field.rb +409 -0
  86. data/vendor/webrat/lib/webrat/core/elements/form.rb +103 -0
  87. data/vendor/webrat/lib/webrat/core/elements/label.rb +31 -0
  88. data/vendor/webrat/lib/webrat/core/elements/link.rb +93 -0
  89. data/vendor/webrat/lib/webrat/core/elements/select_option.rb +35 -0
  90. data/vendor/webrat/lib/webrat/core/locators/area_locator.rb +38 -0
  91. data/vendor/webrat/lib/webrat/core/locators/button_locator.rb +54 -0
  92. data/vendor/webrat/lib/webrat/core/locators/field_by_id_locator.rb +37 -0
  93. data/vendor/webrat/lib/webrat/core/locators/field_labeled_locator.rb +56 -0
  94. data/vendor/webrat/lib/webrat/core/locators/field_locator.rb +25 -0
  95. data/vendor/webrat/lib/webrat/core/locators/field_named_locator.rb +41 -0
  96. data/vendor/webrat/lib/webrat/core/locators/form_locator.rb +19 -0
  97. data/vendor/webrat/lib/webrat/core/locators/label_locator.rb +34 -0
  98. data/vendor/webrat/lib/webrat/core/locators/link_locator.rb +74 -0
  99. data/vendor/webrat/lib/webrat/core/locators/locator.rb +20 -0
  100. data/vendor/webrat/lib/webrat/core/locators/select_option_locator.rb +59 -0
  101. data/vendor/webrat/lib/webrat/core/locators.rb +20 -0
  102. data/vendor/webrat/lib/webrat/core/logging.rb +24 -0
  103. data/vendor/webrat/lib/webrat/core/matchers/have_content.rb +73 -0
  104. data/vendor/webrat/lib/webrat/core/matchers/have_selector.rb +74 -0
  105. data/vendor/webrat/lib/webrat/core/matchers/have_tag.rb +21 -0
  106. data/vendor/webrat/lib/webrat/core/matchers/have_xpath.rb +147 -0
  107. data/vendor/webrat/lib/webrat/core/matchers.rb +4 -0
  108. data/vendor/webrat/lib/webrat/core/methods.rb +63 -0
  109. data/vendor/webrat/lib/webrat/core/mime.rb +29 -0
  110. data/vendor/webrat/lib/webrat/core/save_and_open_page.rb +48 -0
  111. data/vendor/webrat/lib/webrat/core/scope.rb +350 -0
  112. data/vendor/webrat/lib/webrat/core/session.rb +299 -0
  113. data/vendor/webrat/lib/webrat/core/xml/hpricot.rb +19 -0
  114. data/vendor/webrat/lib/webrat/core/xml/nokogiri.rb +76 -0
  115. data/vendor/webrat/lib/webrat/core/xml/rexml.rb +24 -0
  116. data/vendor/webrat/lib/webrat/core/xml.rb +115 -0
  117. data/vendor/webrat/lib/webrat/core.rb +14 -0
  118. data/vendor/webrat/lib/webrat/core_extensions/blank.rb +58 -0
  119. data/vendor/webrat/lib/webrat/core_extensions/deprecate.rb +8 -0
  120. data/vendor/webrat/lib/webrat/core_extensions/detect_mapped.rb +12 -0
  121. data/vendor/webrat/lib/webrat/core_extensions/meta_class.rb +6 -0
  122. data/vendor/webrat/lib/webrat/core_extensions/nil_to_param.rb +5 -0
  123. data/vendor/webrat/lib/webrat/core_extensions/tcp_socket.rb +27 -0
  124. data/vendor/webrat/lib/webrat/mechanize.rb +74 -0
  125. data/vendor/webrat/lib/webrat/merb.rb +9 -0
  126. data/vendor/webrat/lib/webrat/merb_multipart_support.rb +27 -0
  127. data/vendor/webrat/lib/webrat/merb_session.rb +82 -0
  128. data/vendor/webrat/lib/webrat/rack.rb +25 -0
  129. data/vendor/webrat/lib/webrat/rails.rb +106 -0
  130. data/vendor/webrat/lib/webrat/rspec-rails.rb +10 -0
  131. data/vendor/webrat/lib/webrat/selenium/application_server_factory.rb +40 -0
  132. data/vendor/webrat/lib/webrat/selenium/application_servers/base.rb +46 -0
  133. data/vendor/webrat/lib/webrat/selenium/application_servers/external.rb +26 -0
  134. data/vendor/webrat/lib/webrat/selenium/application_servers/merb.rb +50 -0
  135. data/vendor/webrat/lib/webrat/selenium/application_servers/rails.rb +44 -0
  136. data/vendor/webrat/lib/webrat/selenium/application_servers/sinatra.rb +37 -0
  137. data/vendor/webrat/lib/webrat/selenium/application_servers.rb +5 -0
  138. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/button.js +19 -0
  139. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/label.js +16 -0
  140. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webrat.js +5 -0
  141. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webratlink.js +12 -0
  142. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webratlinkwithin.js +15 -0
  143. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webratselectwithoption.js +5 -0
  144. data/vendor/webrat/lib/webrat/selenium/matchers/have_content.rb +66 -0
  145. data/vendor/webrat/lib/webrat/selenium/matchers/have_selector.rb +49 -0
  146. data/vendor/webrat/lib/webrat/selenium/matchers/have_tag.rb +72 -0
  147. data/vendor/webrat/lib/webrat/selenium/matchers/have_xpath.rb +45 -0
  148. data/vendor/webrat/lib/webrat/selenium/matchers.rb +4 -0
  149. data/vendor/webrat/lib/webrat/selenium/selenium_extensions.js +6 -0
  150. data/vendor/webrat/lib/webrat/selenium/selenium_rc_server.rb +84 -0
  151. data/vendor/webrat/lib/webrat/selenium/selenium_session.rb +248 -0
  152. data/vendor/webrat/lib/webrat/selenium/silence_stream.rb +18 -0
  153. data/vendor/webrat/lib/webrat/selenium.rb +81 -0
  154. data/vendor/webrat/lib/webrat/sinatra.rb +11 -0
  155. data/vendor/webrat/lib/webrat.rb +31 -0
  156. data/vendor/webrat/spec/fakes/test_session.rb +34 -0
  157. data/vendor/webrat/spec/integration/merb/Rakefile +35 -0
  158. data/vendor/webrat/spec/integration/merb/app/controllers/application.rb +2 -0
  159. data/vendor/webrat/spec/integration/merb/app/controllers/exceptions.rb +13 -0
  160. data/vendor/webrat/spec/integration/merb/app/controllers/testing.rb +27 -0
  161. data/vendor/webrat/spec/integration/merb/app/views/exceptions/not_acceptable.html.erb +63 -0
  162. data/vendor/webrat/spec/integration/merb/app/views/exceptions/not_found.html.erb +47 -0
  163. data/vendor/webrat/spec/integration/merb/app/views/layout/application.html.erb +12 -0
  164. data/vendor/webrat/spec/integration/merb/app/views/testing/show_form.html.erb +27 -0
  165. data/vendor/webrat/spec/integration/merb/app/views/testing/upload.html.erb +9 -0
  166. data/vendor/webrat/spec/integration/merb/config/environments/development.rb +15 -0
  167. data/vendor/webrat/spec/integration/merb/config/environments/rake.rb +11 -0
  168. data/vendor/webrat/spec/integration/merb/config/environments/test.rb +14 -0
  169. data/vendor/webrat/spec/integration/merb/config/init.rb +25 -0
  170. data/vendor/webrat/spec/integration/merb/config/rack.rb +11 -0
  171. data/vendor/webrat/spec/integration/merb/config/router.rb +34 -0
  172. data/vendor/webrat/spec/integration/merb/spec/spec.opts +1 -0
  173. data/vendor/webrat/spec/integration/merb/spec/spec_helper.rb +24 -0
  174. data/vendor/webrat/spec/integration/merb/spec/webrat_spec.rb +39 -0
  175. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/app_script.rb +31 -0
  176. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/common.rb +64 -0
  177. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/gem_ext.rb +124 -0
  178. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/main.thor +150 -0
  179. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/ops.rb +93 -0
  180. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/utils.rb +40 -0
  181. data/vendor/webrat/spec/integration/rack/Rakefile +5 -0
  182. data/vendor/webrat/spec/integration/rack/app.rb +73 -0
  183. data/vendor/webrat/spec/integration/rack/test/helper.rb +20 -0
  184. data/vendor/webrat/spec/integration/rack/test/webrat_rack_test.rb +62 -0
  185. data/vendor/webrat/spec/integration/rails/Rakefile +30 -0
  186. data/vendor/webrat/spec/integration/rails/app/controllers/application.rb +15 -0
  187. data/vendor/webrat/spec/integration/rails/app/controllers/buttons_controller.rb +7 -0
  188. data/vendor/webrat/spec/integration/rails/app/controllers/fields_controller.rb +4 -0
  189. data/vendor/webrat/spec/integration/rails/app/controllers/links_controller.rb +7 -0
  190. data/vendor/webrat/spec/integration/rails/app/controllers/webrat_controller.rb +43 -0
  191. data/vendor/webrat/spec/integration/rails/app/helpers/buttons_helper.rb +2 -0
  192. data/vendor/webrat/spec/integration/rails/app/helpers/fields_helper.rb +2 -0
  193. data/vendor/webrat/spec/integration/rails/app/helpers/links_helper.rb +2 -0
  194. data/vendor/webrat/spec/integration/rails/app/views/buttons/show.html.erb +11 -0
  195. data/vendor/webrat/spec/integration/rails/app/views/fields/show.html.erb +9 -0
  196. data/vendor/webrat/spec/integration/rails/app/views/links/show.html.erb +5 -0
  197. data/vendor/webrat/spec/integration/rails/app/views/webrat/before_redirect_form.html.erb +4 -0
  198. data/vendor/webrat/spec/integration/rails/app/views/webrat/buttons.html.erb +11 -0
  199. data/vendor/webrat/spec/integration/rails/app/views/webrat/form.html.erb +28 -0
  200. data/vendor/webrat/spec/integration/rails/config/boot.rb +109 -0
  201. data/vendor/webrat/spec/integration/rails/config/environment.rb +12 -0
  202. data/vendor/webrat/spec/integration/rails/config/environments/development.rb +17 -0
  203. data/vendor/webrat/spec/integration/rails/config/environments/selenium.rb +22 -0
  204. data/vendor/webrat/spec/integration/rails/config/environments/test.rb +22 -0
  205. data/vendor/webrat/spec/integration/rails/config/initializers/inflections.rb +10 -0
  206. data/vendor/webrat/spec/integration/rails/config/initializers/mime_types.rb +5 -0
  207. data/vendor/webrat/spec/integration/rails/config/initializers/new_rails_defaults.rb +17 -0
  208. data/vendor/webrat/spec/integration/rails/config/locales/en.yml +5 -0
  209. data/vendor/webrat/spec/integration/rails/config/routes.rb +18 -0
  210. data/vendor/webrat/spec/integration/rails/public/404.html +30 -0
  211. data/vendor/webrat/spec/integration/rails/public/422.html +30 -0
  212. data/vendor/webrat/spec/integration/rails/public/500.html +33 -0
  213. data/vendor/webrat/spec/integration/rails/script/about +4 -0
  214. data/vendor/webrat/spec/integration/rails/script/console +3 -0
  215. data/vendor/webrat/spec/integration/rails/script/dbconsole +3 -0
  216. data/vendor/webrat/spec/integration/rails/script/destroy +3 -0
  217. data/vendor/webrat/spec/integration/rails/script/generate +3 -0
  218. data/vendor/webrat/spec/integration/rails/script/performance/benchmarker +3 -0
  219. data/vendor/webrat/spec/integration/rails/script/performance/profiler +3 -0
  220. data/vendor/webrat/spec/integration/rails/script/performance/request +3 -0
  221. data/vendor/webrat/spec/integration/rails/script/plugin +3 -0
  222. data/vendor/webrat/spec/integration/rails/script/process/inspector +3 -0
  223. data/vendor/webrat/spec/integration/rails/script/process/reaper +3 -0
  224. data/vendor/webrat/spec/integration/rails/script/process/spawner +3 -0
  225. data/vendor/webrat/spec/integration/rails/script/runner +3 -0
  226. data/vendor/webrat/spec/integration/rails/script/server +3 -0
  227. data/vendor/webrat/spec/integration/rails/test/integration/button_click_test.rb +80 -0
  228. data/vendor/webrat/spec/integration/rails/test/integration/fill_in_test.rb +24 -0
  229. data/vendor/webrat/spec/integration/rails/test/integration/link_click_test.rb +27 -0
  230. data/vendor/webrat/spec/integration/rails/test/integration/webrat_test.rb +97 -0
  231. data/vendor/webrat/spec/integration/rails/test/test_helper.rb +25 -0
  232. data/vendor/webrat/spec/integration/sinatra/Rakefile +5 -0
  233. data/vendor/webrat/spec/integration/sinatra/classic_app.rb +64 -0
  234. data/vendor/webrat/spec/integration/sinatra/modular_app.rb +16 -0
  235. data/vendor/webrat/spec/integration/sinatra/test/classic_app_test.rb +37 -0
  236. data/vendor/webrat/spec/integration/sinatra/test/modular_app_test.rb +18 -0
  237. data/vendor/webrat/spec/integration/sinatra/test/test_helper.rb +16 -0
  238. data/vendor/webrat/spec/private/core/configuration_spec.rb +116 -0
  239. data/vendor/webrat/spec/private/core/field_spec.rb +85 -0
  240. data/vendor/webrat/spec/private/core/link_spec.rb +24 -0
  241. data/vendor/webrat/spec/private/core/logging_spec.rb +10 -0
  242. data/vendor/webrat/spec/private/core/session_spec.rb +195 -0
  243. data/vendor/webrat/spec/private/mechanize/mechanize_session_spec.rb +81 -0
  244. data/vendor/webrat/spec/private/merb/attaches_file_spec.rb +93 -0
  245. data/vendor/webrat/spec/private/merb/merb_session_spec.rb +61 -0
  246. data/vendor/webrat/spec/private/nokogiri_spec.rb +77 -0
  247. data/vendor/webrat/spec/private/rails/attaches_file_spec.rb +81 -0
  248. data/vendor/webrat/spec/private/rails/rails_session_spec.rb +112 -0
  249. data/vendor/webrat/spec/private/selenium/application_servers/rails_spec.rb +26 -0
  250. data/vendor/webrat/spec/public/basic_auth_spec.rb +24 -0
  251. data/vendor/webrat/spec/public/check_spec.rb +191 -0
  252. data/vendor/webrat/spec/public/choose_spec.rb +118 -0
  253. data/vendor/webrat/spec/public/click_area_spec.rb +106 -0
  254. data/vendor/webrat/spec/public/click_button_spec.rb +496 -0
  255. data/vendor/webrat/spec/public/click_link_spec.rb +511 -0
  256. data/vendor/webrat/spec/public/fill_in_spec.rb +209 -0
  257. data/vendor/webrat/spec/public/locators/field_by_xpath_spec.rb +19 -0
  258. data/vendor/webrat/spec/public/locators/field_labeled_spec.rb +172 -0
  259. data/vendor/webrat/spec/public/locators/field_with_id_spec.rb +16 -0
  260. data/vendor/webrat/spec/public/matchers/contain_spec.rb +114 -0
  261. data/vendor/webrat/spec/public/matchers/have_selector_spec.rb +142 -0
  262. data/vendor/webrat/spec/public/matchers/have_tag_spec.rb +39 -0
  263. data/vendor/webrat/spec/public/matchers/have_xpath_spec.rb +136 -0
  264. data/vendor/webrat/spec/public/reload_spec.rb +10 -0
  265. data/vendor/webrat/spec/public/save_and_open_spec.rb +70 -0
  266. data/vendor/webrat/spec/public/select_date_spec.rb +112 -0
  267. data/vendor/webrat/spec/public/select_datetime_spec.rb +137 -0
  268. data/vendor/webrat/spec/public/select_spec.rb +249 -0
  269. data/vendor/webrat/spec/public/select_time_spec.rb +100 -0
  270. data/vendor/webrat/spec/public/selenium/application_server_factory_spec.rb +49 -0
  271. data/vendor/webrat/spec/public/selenium/application_servers/external_spec.rb +12 -0
  272. data/vendor/webrat/spec/public/selenium/selenium_session_spec.rb +37 -0
  273. data/vendor/webrat/spec/public/set_hidden_field_spec.rb +5 -0
  274. data/vendor/webrat/spec/public/submit_form_spec.rb +5 -0
  275. data/vendor/webrat/spec/public/visit_spec.rb +58 -0
  276. data/vendor/webrat/spec/public/within_spec.rb +177 -0
  277. data/vendor/webrat/spec/rcov.opts +1 -0
  278. data/vendor/webrat/spec/spec.opts +2 -0
  279. data/vendor/webrat/spec/spec_helper.rb +50 -0
  280. metadata +554 -0
@@ -0,0 +1,27 @@
1
+ $testing = true
2
+ $root = File.expand_path(File.dirname(__FILE__) + "/../../")
3
+ $:.unshift "#{$root}/lib"
4
+
5
+ require 'sum'
6
+
7
+ gem 'bmabey-email_spec', '=0.2.0'
8
+ gem 'rspec', '=1.2.7'
9
+
10
+ require 'email_spec'
11
+ require 'rack/test'
12
+ require 'spec/mocks'
13
+ require 'test/unit'
14
+ require "#{$root}/vendor/webrat/lib/webrat"
15
+
16
+ World do
17
+ def app
18
+ Application
19
+ end
20
+
21
+ include EmailSpec::Helpers
22
+ include EmailSpec::Matchers
23
+ include Rack::Test::Methods
24
+ include Test::Unit::Assertions
25
+ include Webrat::Matchers
26
+ include Webrat::Methods
27
+ end
@@ -0,0 +1,44 @@
1
+ def current_email_address
2
+ "cucumber@sumapp.com"
3
+ end
4
+
5
+ def fill_all_with_valid_data(hash={})
6
+ valid = {
7
+ 'savings' => '0',
8
+ 'income' => '0',
9
+ 'bills' => '0',
10
+ 'email' => current_email_address
11
+ }.merge(hash)
12
+ valid.each do |key, value|
13
+ fill_in "user[#{key}]", :with => value
14
+ end
15
+ end
16
+
17
+ def find_user
18
+ UserEmail.find_by_email(current_email_address).user
19
+ end
20
+
21
+ def generate_email(options={})
22
+ <<EMAIL
23
+ Delivered-To: test@sumapp.com
24
+ Return-Path: <#{options[:from] || current_email_address}>
25
+ From: Winton Welsh <#{options[:from] || current_email_address}>
26
+ To: test@sumapp.com
27
+ Subject: #{options[:subject] || ''}
28
+ Content-Type: multipart/alternative; boundary=000325574d2a5057e1046eae4ba5
29
+
30
+ --000325574d2a5057e1046eae4ba5
31
+ Content-Type: text/plain; charset=ISO-8859-1
32
+ Content-Transfer-Encoding: 7bit
33
+
34
+ #{options[:body] || ''}
35
+
36
+ --000325574d2a5057e1046eae4ba5
37
+ Content-Type: text/html; charset=ISO-8859-1
38
+ Content-Transfer-Encoding: 7bit
39
+
40
+ #{options[:body] || ''}<br>
41
+
42
+ --000325574d2a5057e1046eae4ba5--
43
+ EMAIL
44
+ end
@@ -0,0 +1,11 @@
1
+ Before do
2
+ $rspec_mocks ||= Spec::Mocks::Space.new
3
+ end
4
+
5
+ After do
6
+ begin
7
+ $rspec_mocks.verify_all
8
+ ensure
9
+ $rspec_mocks.reset_all
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ Webrat.configure do |config|
2
+ config.mode = :rack
3
+ end
@@ -0,0 +1,76 @@
1
+ Feature: Update user
2
+
3
+ As a user
4
+ I want to submit my information
5
+ In order to see a success page
6
+
7
+ Scenario: I submit a valid form with decimals
8
+ When I visit the front page
9
+ And I submit a valid form with decimals
10
+ Then I should see a success page
11
+
12
+ Scenario: I submit a valid form with numbers only
13
+ When I visit the front page
14
+ And I submit a valid form with numbers only
15
+ Then I should see a success page
16
+
17
+ Scenario: I submit a valid form with dollar signs
18
+ When I visit the front page
19
+ And I submit a valid form with dollar signs
20
+ Then I should see a success page
21
+
22
+ Scenario: I submit an invalid savings amount
23
+ When I visit the front page
24
+ And submit an invalid savings amount
25
+ Then I should see a form
26
+ And the savings field should have an error
27
+ And the error should be "is not a number"
28
+
29
+ Scenario: I submit an empty savings amount
30
+ When I visit the front page
31
+ And submit an empty savings amount
32
+ Then I should see a form
33
+ And the savings field should have an error
34
+ And the error should be "is not a number"
35
+
36
+ Scenario: I submit an invalid income amount
37
+ When I visit the front page
38
+ And submit an invalid income amount
39
+ Then I should see a form
40
+ And the income field should have an error
41
+ And the error should be "is not a number"
42
+
43
+ Scenario: I submit an empty income amount
44
+ When I visit the front page
45
+ And submit an empty income amount
46
+ Then I should see a form
47
+ And the income field should have an error
48
+ And the error should be "is not a number"
49
+
50
+ Scenario: I submit an invalid bills amount
51
+ When I visit the front page
52
+ And submit an invalid bills amount
53
+ Then I should see a form
54
+ And the bills field should have an error
55
+ And the error should be "is not a number"
56
+
57
+ Scenario: I submit an empty bills amount
58
+ When I visit the front page
59
+ And submit an empty bills amount
60
+ Then I should see a form
61
+ And the bills field should have an error
62
+ And the error should be "is not a number"
63
+
64
+ Scenario: I submit an invalid email
65
+ When I visit the front page
66
+ And submit an invalid email
67
+ Then I should see a form
68
+ And the email field should have an error
69
+ And the error should be "is invalid"
70
+
71
+ Scenario: I submit an empty email
72
+ When I visit the front page
73
+ And submit an empty email
74
+ Then I should see a form
75
+ And the email field should have an error
76
+ And the error should be "can't be blank"
data/gemspec.rb ADDED
@@ -0,0 +1,45 @@
1
+ GEM_NAME = 'sum'
2
+ GEM_FILES = FileList['**/*'] - FileList[
3
+ '*.gem',
4
+ '*.log',
5
+ 'config/database.yml',
6
+ 'config/deploy.rb',
7
+ 'config/mail.yml',
8
+ 'coverage',
9
+ 'coverage/**/*',
10
+ 'log',
11
+ 'log/**/*',
12
+ 'pkg',
13
+ 'pkg/**/*',
14
+ 'tmp',
15
+ 'tmp/**/*'
16
+ ]
17
+ GEM_SPEC = Gem::Specification.new do |s|
18
+ # == CONFIGURE ==
19
+ s.author = "Winton Welsh"
20
+ s.email = "mail@wintoni.us"
21
+ s.homepage = "http://github.com/winton/#{GEM_NAME}"
22
+ s.summary = "A simple budgeting app built on Sinatra"
23
+ # == CONFIGURE ==
24
+ s.add_dependency('bmabey-email_spec', '=0.2.0')
25
+ s.add_dependency('cucumber', '>=0.3.11')
26
+ s.add_dependency('haml', '=2.0.9')
27
+ s.add_dependency('less', '>=0.8.11')
28
+ s.add_dependency('javan-whenever', '=0.3.6')
29
+ s.add_dependency('rack', '>=1.0.0')
30
+ s.add_dependency('rack-test', '>=0.4.0')
31
+ s.add_dependency('rspec', '>=1.2.7')
32
+ s.add_dependency('shotgun', '>=0.3')
33
+ s.add_dependency('sinatra', '=0.9.2')
34
+ s.add_dependency('winton-active_wrapper', '=0.1.5')
35
+ s.add_dependency('winton-externals', '=1.0.2')
36
+ s.add_dependency('winton-fetcher', '=0.1.2')
37
+ s.add_dependency('winton-secret_key', '=0.1.0')
38
+ s.extra_rdoc_files = [ "README.markdown" ]
39
+ s.files = GEM_FILES.to_a
40
+ s.has_rdoc = false
41
+ s.name = GEM_NAME
42
+ s.platform = Gem::Platform::RUBY
43
+ s.require_path = "lib"
44
+ s.version = "0.1.0"
45
+ end
data/lib/sum/boot.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "#{File.dirname(__FILE__)}/model/incoming_mail"
2
+
3
+ Application.class_eval do
4
+
5
+ # Sinatra
6
+ enable :raise_errors
7
+ set :environment, $testing ? :test : environment
8
+ set :root, File.expand_path("#{File.dirname(__FILE__)}/../../")
9
+ set :public, "#{root}/public"
10
+ set :logging, true
11
+ set :static, true
12
+ set :views, "#{root}/lib/sum/view"
13
+
14
+ # Database, logging, and email
15
+ $db, $log, $mail = ActiveWrapper.setup(
16
+ :base => root,
17
+ :env => environment,
18
+ :stdout => environment != :test
19
+ )
20
+ $db.establish_connection
21
+ if $mail.config
22
+ ActionMailer::Base.raise_delivery_errors = true
23
+ imap = { :receiver => IncomingMail, :type => :imap }
24
+ $mail.config[:imap].merge!(imap)
25
+ end
26
+
27
+ # Secret key
28
+ $secret_key = SecretKey.new(root).read
29
+ end
@@ -0,0 +1,12 @@
1
+ Application.class_eval do
2
+
3
+ get "/#{environment == :production ? $secret_key : 'cron'}" do
4
+ IncomingMail.process!
5
+ User.reset_users!
6
+ User.reset_spent_today!
7
+ User.send_emails! do |user|
8
+ haml(:email, :layout => false, :locals => { :u => user })
9
+ end
10
+ true
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ Application.class_eval do
2
+
3
+ get '/' do
4
+ haml :front
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ Application.class_eval do
2
+
3
+ post '/new' do
4
+ if email = UserEmail.find_by_email(params[:user][:email])
5
+ if @user = email.user
6
+ @user.send_now = true
7
+ @user.update_attributes(params[:user])
8
+ end
9
+ else
10
+ @user = User.create(params[:user])
11
+ end
12
+ @title = valid? ? 'Success!' : 'Fix mistakes'
13
+ haml :new
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ Application.class_eval do
2
+ helpers do
3
+
4
+ def partial(name, options={})
5
+ haml name, options.merge(:layout => false)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ Application.class_eval do
2
+ helpers do
3
+
4
+ def days(day)
5
+ day <= 1 ? "day" : "#{day} days"
6
+ end
7
+
8
+ def money(amount)
9
+ "$#{sprintf("%.2f", amount)}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ Application.class_eval do
2
+ helpers do
3
+
4
+ def errors_on(attribute)
5
+ @user && @user.errors.on(attribute)
6
+ end
7
+
8
+ def field(attribute, question)
9
+ partial(
10
+ :field,
11
+ :locals => {
12
+ :attribute => attribute,
13
+ :question => question
14
+ }
15
+ )
16
+ end
17
+
18
+ def valid?(attribute=nil)
19
+ if attribute
20
+ @user && @user.errors.on(attribute).nil?
21
+ else
22
+ @user && @user.valid?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,89 @@
1
+ class IncomingMail < ActionMailer::Base
2
+
3
+ class <<self
4
+ def process!
5
+ if $mail.config
6
+ Fetcher.create($mail.config[:imap]).fetch
7
+ end
8
+ end
9
+ end
10
+
11
+ def receive(mail)
12
+ return unless body = get_body(mail)
13
+ emails = parse_emails(mail.subject, body.body)
14
+ numbers = parse_numbers(mail.subject, body.body)
15
+ start = parse_start(mail.subject, body.body)
16
+ stop = parse_stop(mail.subject, body.body)
17
+
18
+ if mail.from[0] && email = UserEmail.find_by_email(mail.from[0])
19
+ user = email.user
20
+ else
21
+ return
22
+ end
23
+
24
+ if user
25
+ emails.each do |email|
26
+ user.add_email!(email, true)
27
+ end
28
+ numbers.each do |number|
29
+ user.spend!(number)
30
+ end
31
+ email.activate! if start
32
+ email.deactivate! if stop
33
+ end
34
+
35
+ [ emails, numbers, start, stop ]
36
+ end
37
+
38
+ private
39
+
40
+ def get_body(mail)
41
+ if mail.parts.empty?
42
+ mail
43
+ else
44
+ mail.parts.select { |part|
45
+ part['content-type'].content_type == 'text/plain'
46
+ }.first
47
+ end
48
+ end
49
+
50
+ def parse_emails(*args)
51
+ args.collect { |text|
52
+ text = text_upto(text, /\s/)
53
+ text.scan(/\S+@\S+\.\S+/)
54
+ }.flatten
55
+ end
56
+
57
+ def parse_numbers(*args)
58
+ args.collect { |text|
59
+ text = text_upto(text, /[^-+\d\.\s]/)
60
+ numbers = text.scan(/([-+]*)(\d+\.*\d*)/)
61
+ numbers.collect do |number|
62
+ operator, number = number
63
+ (operator == '+' ? -1 : 1) * number.to_f
64
+ end
65
+ }.flatten
66
+ end
67
+
68
+ def parse_start(*args)
69
+ args.collect { |text|
70
+ text.strip[0..4].downcase == 'start'
71
+ }.include?(true)
72
+ end
73
+
74
+ def parse_stop(*args)
75
+ args.collect { |text|
76
+ text.strip[0..3].downcase == 'stop'
77
+ }.include?(true)
78
+ end
79
+
80
+ def text_upto(text, regex)
81
+ first_instance = text.index(regex)
82
+ return '' if first_instance == 0
83
+ if first_instance
84
+ text[0..first_instance-1]
85
+ else
86
+ text
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,286 @@
1
+ class User < ActiveRecord::Base
2
+
3
+ attr_accessible :email
4
+ attr_accessible :bills
5
+ attr_accessible :income
6
+ attr_accessible :savings
7
+ attr_accessible :timezone_offset
8
+
9
+ after_create :after_create_email
10
+ before_create :before_create_timestamps
11
+
12
+ before_save :before_save_recent_transactions
13
+ before_save :before_save_savings_goal
14
+ before_save :before_save_spending_goal
15
+
16
+ has_many :emails, :class_name => 'UserEmail', :dependent => :destroy
17
+
18
+ serialize :recent_transactions
19
+
20
+ validates_format_of(
21
+ :email,
22
+ :with => /\S+@\S+\.\S+/,
23
+ :unless => lambda { |r| r.email.blank? }
24
+ )
25
+ validates_numericality_of(
26
+ :bills,
27
+ :income,
28
+ :savings,
29
+ :unless => lambda { |r| r.bills.nil? && r.income.nil? && r.savings.nil? }
30
+ )
31
+ validates_presence_of :email, :unless => lambda { |r| r.email.nil? }
32
+
33
+ # Class methods
34
+
35
+ class <<self
36
+
37
+ def reset_users!
38
+ conditions = [ 'reset_at <= ?', Time.now.utc ]
39
+ users = self.find(:all, :conditions => conditions)
40
+ users.each do |user|
41
+ user.reset!
42
+ end
43
+ end
44
+
45
+ def reset_spent_today!
46
+ conditions = [ 'send_at <= ?', Time.now.utc ]
47
+ users = self.find(:all, :conditions => conditions)
48
+ users.each do |user|
49
+ user.reset_spent_today!
50
+ end
51
+ end
52
+
53
+ def send_emails!(&block)
54
+ conditions = [
55
+ 'send_now = 1 OR send_at <= ?',
56
+ Time.now.utc
57
+ ]
58
+ users = self.find(:all, :conditions => conditions)
59
+ users.each do |user|
60
+ user.emails.each do |email|
61
+ next unless email.active? && email.failures <= 5
62
+ begin
63
+ $mail.deliver(
64
+ :from => 'sum@sumapp.com',
65
+ :to => email.email,
66
+ :subject => "Today's budget",
67
+ :body => yield(user)
68
+ )
69
+ email.sent!
70
+ rescue Exception => e
71
+ email.increment!(:failures)
72
+ end
73
+ end
74
+ user.sent!
75
+ end
76
+ end
77
+ end
78
+
79
+ # Instance methods
80
+
81
+ [ :email, :bills, :income, :savings ].each do |attribute|
82
+ define_method(attribute) do
83
+ read_attribute attribute
84
+ end
85
+ define_method("#{attribute}=") do |value|
86
+ write_attribute(
87
+ attribute,
88
+ attribute == :email ? value : to_number(value)
89
+ )
90
+ end
91
+ end
92
+
93
+ def add_email!(address, send=false)
94
+ UserEmail.create(:email => address, :user_id => self.id)
95
+ if send
96
+ self.flash = "Successfully added #{address} to your account."
97
+ self.send_now = true
98
+ self.save
99
+ end
100
+ end
101
+
102
+ # Beginning of fiscal month
103
+ def beginning_of_month
104
+ self.reset_at - 1.month
105
+ end
106
+
107
+ # Days in this fiscal month
108
+ def days_in_month
109
+ self.reset_at.to_date - self.beginning_of_month.to_date
110
+ end
111
+
112
+ # Days left in this fiscal month
113
+ def days_left
114
+ to_local(self.reset_at).to_date - to_local(Time.now).to_date - 1
115
+ end
116
+
117
+ # Days left in this fiscal month, including today
118
+ def days_left_including_today
119
+ self.days_left + 1
120
+ end
121
+
122
+ # Days passed in this fiscal month
123
+ def days_passed
124
+ to_local(Time.now).to_date - to_local(self.beginning_of_month).to_date
125
+ end
126
+
127
+ # Days passed in this fiscal month, including today
128
+ def days_passed_including_today
129
+ self.days_passed + 1
130
+ end
131
+
132
+ # Reset spent_today
133
+ def reset_spent_today!
134
+ self.update_attribute :spent_today, 0
135
+ end
136
+
137
+ def reset!
138
+ self.temporary_spending_cut = self.total_left * -1
139
+ if self.temporary_spending_cut < 0
140
+ self.temporary_spending_cut = 0
141
+ end
142
+ self.spent_this_month = 0
143
+ update_reset_at
144
+ self.save
145
+ end
146
+
147
+ def sent!
148
+ self.flash = nil
149
+ self.send_now = false
150
+ update_send_at unless self.send_now_changed?
151
+ self.save
152
+ rescue
153
+ # This fixes a really confusing error when running cucumber features:
154
+ # No response yet. Request a page first. (Rack::Test::Error)
155
+ # Happens on User#save after using $mail.deliver.
156
+ end
157
+
158
+ # How much the user should have spent in this period
159
+ def should_have_spent
160
+ self.spending_per_day * self.days_passed_including_today
161
+ end
162
+
163
+ def spend!(amount)
164
+ amount = amount.to_f if amount.respond_to?(:upcase)
165
+ self.recent_transactions ||= []
166
+ self.recent_transactions.unshift(amount)
167
+ self.spent_this_month += amount
168
+ self.spent_today += amount
169
+ self.send_now = true
170
+ self.save
171
+ end
172
+
173
+ # Spending goal for the month
174
+ def spending_goal
175
+ read_attribute(:spending_goal) - self.temporary_spending_cut
176
+ end
177
+
178
+ # Today's spending goal
179
+ def spending_goal_today
180
+ (self.surplus(:exclude_today) / self.days_left_including_today) - self.spent_today
181
+ end
182
+
183
+ # Today's spending goal with savings included in money left to spend
184
+ def spending_goal_today_savings
185
+ (self.savings_goal + self.surplus(:exclude_today)) / self.days_left_including_today
186
+ end
187
+
188
+ # How much the user is spending per day (ideally)
189
+ def spending_per_day
190
+ self.spending_goal / self.days_in_month
191
+ end
192
+
193
+ # How much the user has spent this month
194
+ def spent_this_month(option=nil)
195
+ case option
196
+ when :exclude_today
197
+ read_attribute(:spent_this_month) - self.spent_today
198
+ else
199
+ read_attribute(:spent_this_month)
200
+ end
201
+ end
202
+
203
+ # Variance from budget based on savings_goal
204
+ def surplus(option=nil)
205
+ two_decimals(self.spending_goal - spent_this_month(option))
206
+ end
207
+
208
+ # Variance from budget based on should_have_spent
209
+ def surplus_for_period(option=nil)
210
+ two_decimals(self.should_have_spent - spent_this_month(option))
211
+ end
212
+
213
+ # Total remaining money for the month
214
+ def total_left(option=nil)
215
+ self.surplus(option) + self.savings_goal
216
+ end
217
+
218
+ private
219
+
220
+ def after_create_email
221
+ self.add_email!(read_attribute(:email))
222
+ end
223
+
224
+ def before_create_timestamps
225
+ self.reset_at = update_send_at
226
+ update_reset_at
227
+ end
228
+
229
+ def before_save_recent_transactions
230
+ if self.recent_transactions
231
+ self.recent_transactions = self.recent_transactions[0..4]
232
+ end
233
+ end
234
+
235
+ def before_save_savings_goal
236
+ if self.savings
237
+ self.savings_goal = self.savings
238
+ end
239
+ end
240
+
241
+ def before_save_spending_goal
242
+ if self.income && self.bills && self.savings
243
+ self.spending_goal = self.income - self.bills - self.savings
244
+ end
245
+ end
246
+
247
+ def local_12am_to_server_time
248
+ time = to_local(Time.now)
249
+ time = DateTime.strptime(
250
+ time.strftime("%m/%d/%Y 12:00 AM %Z"),
251
+ "%m/%d/%Y %I:%M %p %Z"
252
+ )
253
+ to_system(time.to_time)
254
+ end
255
+
256
+ def to_local(time)
257
+ return time.utc unless self.timezone_offset
258
+ time.utc + self.timezone_offset
259
+ end
260
+
261
+ def to_number(string)
262
+ string = string.gsub(/[^\d\.]/, '')
263
+ string.blank? ? string : string.to_f
264
+ end
265
+
266
+ def to_system(time)
267
+ return time.utc unless self.timezone_offset
268
+ time.utc - self.timezone_offset
269
+ end
270
+
271
+ def two_decimals(amount)
272
+ sprintf("%.2f", amount).to_f
273
+ end
274
+
275
+ def update_reset_at
276
+ self.reset_at = self.reset_at + 1.month
277
+ end
278
+
279
+ def update_send_at
280
+ if self.send_at
281
+ self.send_at = self.send_at + 1.day
282
+ else
283
+ self.send_at = local_12am_to_server_time
284
+ end
285
+ end
286
+ end