sum 0.1.1

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 (282) hide show
  1. data/Capfile +3 -0
  2. data/MIT-LICENSE +18 -0
  3. data/README.markdown +69 -0
  4. data/Rakefile +56 -0
  5. data/config.ru +3 -0
  6. data/config/database.example.yml +10 -0
  7. data/config/deploy.example.rb +36 -0
  8. data/config/externals.yml +18 -0
  9. data/config/mail.example.yml +32 -0
  10. data/config/schedule.rb +20 -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 +15 -0
  14. data/features/read_email.feature +445 -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 +36 -0
  19. data/features/step_definitions/when.rb +46 -0
  20. data/features/support/env.rb +38 -0
  21. data/features/support/helpers.rb +50 -0
  22. data/features/support/rspec.rb +11 -0
  23. data/features/support/webrat.rb +3 -0
  24. data/features/update_user.feature +83 -0
  25. data/gemspec.rb +45 -0
  26. data/lib/sum.rb +30 -0
  27. data/lib/sum/boot.rb +29 -0
  28. data/lib/sum/controller/cron.rb +12 -0
  29. data/lib/sum/controller/front.rb +6 -0
  30. data/lib/sum/controller/new.rb +15 -0
  31. data/lib/sum/helper/application.rb +8 -0
  32. data/lib/sum/helper/cron.rb +12 -0
  33. data/lib/sum/helper/new.rb +27 -0
  34. data/lib/sum/model/incoming_mail.rb +89 -0
  35. data/lib/sum/model/user.rb +298 -0
  36. data/lib/sum/model/user_email.rb +31 -0
  37. data/lib/sum/view/email.haml +70 -0
  38. data/lib/sum/view/field.haml +23 -0
  39. data/lib/sum/view/form.haml +13 -0
  40. data/lib/sum/view/front.haml +29 -0
  41. data/lib/sum/view/layout.haml +26 -0
  42. data/lib/sum/view/new.haml +5 -0
  43. data/lib/sum/view/success.haml +14 -0
  44. data/lib/sum/view/tos.haml +3 -0
  45. data/public/image/1.png +0 -0
  46. data/public/image/2.png +0 -0
  47. data/public/image/3.png +0 -0
  48. data/public/image/bg.png +0 -0
  49. data/public/image/bot.png +0 -0
  50. data/public/image/facebox/b.png +0 -0
  51. data/public/image/facebox/bl.png +0 -0
  52. data/public/image/facebox/br.png +0 -0
  53. data/public/image/facebox/closelabel.gif +0 -0
  54. data/public/image/facebox/loading.gif +0 -0
  55. data/public/image/facebox/tl.png +0 -0
  56. data/public/image/facebox/tr.png +0 -0
  57. data/public/image/favicon.png +0 -0
  58. data/public/image/field_off.png +0 -0
  59. data/public/image/field_on.png +0 -0
  60. data/public/image/ribbon.png +0 -0
  61. data/public/image/screenshot_receive.png +0 -0
  62. data/public/image/screenshot_send.png +0 -0
  63. data/public/image/submit.png +0 -0
  64. data/public/image/top.png +0 -0
  65. data/public/javascript/facebox.js +319 -0
  66. data/public/javascript/jquery-1.3.2.js +19 -0
  67. data/public/javascript/sum.js +13 -0
  68. data/public/style/facebox.css +95 -0
  69. data/public/style/ie.css +35 -0
  70. data/public/style/print.css +30 -0
  71. data/public/style/screen.css +256 -0
  72. data/public/style/sum.css +119 -0
  73. data/public/style/sum.less +159 -0
  74. data/spec/spec.opts +1 -0
  75. data/spec/spec_helper.rb +59 -0
  76. data/spec/sum/model/incoming_mail_spec.rb +106 -0
  77. data/spec/sum/model/user_spec.rb +172 -0
  78. data/sum.gemspec +69 -0
  79. data/vendor/webrat/History.txt +358 -0
  80. data/vendor/webrat/MIT-LICENSE.txt +19 -0
  81. data/vendor/webrat/README.rdoc +85 -0
  82. data/vendor/webrat/Rakefile +200 -0
  83. data/vendor/webrat/install.rb +1 -0
  84. data/vendor/webrat/lib/webrat.rb +31 -0
  85. data/vendor/webrat/lib/webrat/core.rb +14 -0
  86. data/vendor/webrat/lib/webrat/core/configuration.rb +102 -0
  87. data/vendor/webrat/lib/webrat/core/elements/area.rb +31 -0
  88. data/vendor/webrat/lib/webrat/core/elements/element.rb +33 -0
  89. data/vendor/webrat/lib/webrat/core/elements/field.rb +409 -0
  90. data/vendor/webrat/lib/webrat/core/elements/form.rb +103 -0
  91. data/vendor/webrat/lib/webrat/core/elements/label.rb +31 -0
  92. data/vendor/webrat/lib/webrat/core/elements/link.rb +93 -0
  93. data/vendor/webrat/lib/webrat/core/elements/select_option.rb +35 -0
  94. data/vendor/webrat/lib/webrat/core/locators.rb +20 -0
  95. data/vendor/webrat/lib/webrat/core/locators/area_locator.rb +38 -0
  96. data/vendor/webrat/lib/webrat/core/locators/button_locator.rb +54 -0
  97. data/vendor/webrat/lib/webrat/core/locators/field_by_id_locator.rb +37 -0
  98. data/vendor/webrat/lib/webrat/core/locators/field_labeled_locator.rb +56 -0
  99. data/vendor/webrat/lib/webrat/core/locators/field_locator.rb +25 -0
  100. data/vendor/webrat/lib/webrat/core/locators/field_named_locator.rb +41 -0
  101. data/vendor/webrat/lib/webrat/core/locators/form_locator.rb +19 -0
  102. data/vendor/webrat/lib/webrat/core/locators/label_locator.rb +34 -0
  103. data/vendor/webrat/lib/webrat/core/locators/link_locator.rb +74 -0
  104. data/vendor/webrat/lib/webrat/core/locators/locator.rb +20 -0
  105. data/vendor/webrat/lib/webrat/core/locators/select_option_locator.rb +59 -0
  106. data/vendor/webrat/lib/webrat/core/logging.rb +24 -0
  107. data/vendor/webrat/lib/webrat/core/matchers.rb +4 -0
  108. data/vendor/webrat/lib/webrat/core/matchers/have_content.rb +73 -0
  109. data/vendor/webrat/lib/webrat/core/matchers/have_selector.rb +74 -0
  110. data/vendor/webrat/lib/webrat/core/matchers/have_tag.rb +21 -0
  111. data/vendor/webrat/lib/webrat/core/matchers/have_xpath.rb +147 -0
  112. data/vendor/webrat/lib/webrat/core/methods.rb +63 -0
  113. data/vendor/webrat/lib/webrat/core/mime.rb +29 -0
  114. data/vendor/webrat/lib/webrat/core/save_and_open_page.rb +48 -0
  115. data/vendor/webrat/lib/webrat/core/scope.rb +350 -0
  116. data/vendor/webrat/lib/webrat/core/session.rb +299 -0
  117. data/vendor/webrat/lib/webrat/core/xml.rb +115 -0
  118. data/vendor/webrat/lib/webrat/core/xml/hpricot.rb +19 -0
  119. data/vendor/webrat/lib/webrat/core/xml/nokogiri.rb +76 -0
  120. data/vendor/webrat/lib/webrat/core/xml/rexml.rb +24 -0
  121. data/vendor/webrat/lib/webrat/core_extensions/blank.rb +58 -0
  122. data/vendor/webrat/lib/webrat/core_extensions/deprecate.rb +8 -0
  123. data/vendor/webrat/lib/webrat/core_extensions/detect_mapped.rb +12 -0
  124. data/vendor/webrat/lib/webrat/core_extensions/meta_class.rb +6 -0
  125. data/vendor/webrat/lib/webrat/core_extensions/nil_to_param.rb +5 -0
  126. data/vendor/webrat/lib/webrat/core_extensions/tcp_socket.rb +27 -0
  127. data/vendor/webrat/lib/webrat/mechanize.rb +74 -0
  128. data/vendor/webrat/lib/webrat/merb.rb +9 -0
  129. data/vendor/webrat/lib/webrat/merb_multipart_support.rb +27 -0
  130. data/vendor/webrat/lib/webrat/merb_session.rb +82 -0
  131. data/vendor/webrat/lib/webrat/rack.rb +25 -0
  132. data/vendor/webrat/lib/webrat/rails.rb +106 -0
  133. data/vendor/webrat/lib/webrat/rspec-rails.rb +10 -0
  134. data/vendor/webrat/lib/webrat/selenium.rb +81 -0
  135. data/vendor/webrat/lib/webrat/selenium/application_server_factory.rb +40 -0
  136. data/vendor/webrat/lib/webrat/selenium/application_servers.rb +5 -0
  137. data/vendor/webrat/lib/webrat/selenium/application_servers/base.rb +46 -0
  138. data/vendor/webrat/lib/webrat/selenium/application_servers/external.rb +26 -0
  139. data/vendor/webrat/lib/webrat/selenium/application_servers/merb.rb +50 -0
  140. data/vendor/webrat/lib/webrat/selenium/application_servers/rails.rb +44 -0
  141. data/vendor/webrat/lib/webrat/selenium/application_servers/sinatra.rb +37 -0
  142. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/button.js +19 -0
  143. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/label.js +16 -0
  144. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webrat.js +5 -0
  145. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webratlink.js +12 -0
  146. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webratlinkwithin.js +15 -0
  147. data/vendor/webrat/lib/webrat/selenium/location_strategy_javascript/webratselectwithoption.js +5 -0
  148. data/vendor/webrat/lib/webrat/selenium/matchers.rb +4 -0
  149. data/vendor/webrat/lib/webrat/selenium/matchers/have_content.rb +66 -0
  150. data/vendor/webrat/lib/webrat/selenium/matchers/have_selector.rb +49 -0
  151. data/vendor/webrat/lib/webrat/selenium/matchers/have_tag.rb +72 -0
  152. data/vendor/webrat/lib/webrat/selenium/matchers/have_xpath.rb +45 -0
  153. data/vendor/webrat/lib/webrat/selenium/selenium_extensions.js +6 -0
  154. data/vendor/webrat/lib/webrat/selenium/selenium_rc_server.rb +84 -0
  155. data/vendor/webrat/lib/webrat/selenium/selenium_session.rb +248 -0
  156. data/vendor/webrat/lib/webrat/selenium/silence_stream.rb +18 -0
  157. data/vendor/webrat/lib/webrat/sinatra.rb +11 -0
  158. data/vendor/webrat/spec/fakes/test_session.rb +34 -0
  159. data/vendor/webrat/spec/integration/merb/Rakefile +35 -0
  160. data/vendor/webrat/spec/integration/merb/app/controllers/application.rb +2 -0
  161. data/vendor/webrat/spec/integration/merb/app/controllers/exceptions.rb +13 -0
  162. data/vendor/webrat/spec/integration/merb/app/controllers/testing.rb +27 -0
  163. data/vendor/webrat/spec/integration/merb/app/views/exceptions/not_acceptable.html.erb +63 -0
  164. data/vendor/webrat/spec/integration/merb/app/views/exceptions/not_found.html.erb +47 -0
  165. data/vendor/webrat/spec/integration/merb/app/views/layout/application.html.erb +12 -0
  166. data/vendor/webrat/spec/integration/merb/app/views/testing/show_form.html.erb +27 -0
  167. data/vendor/webrat/spec/integration/merb/app/views/testing/upload.html.erb +9 -0
  168. data/vendor/webrat/spec/integration/merb/config/environments/development.rb +15 -0
  169. data/vendor/webrat/spec/integration/merb/config/environments/rake.rb +11 -0
  170. data/vendor/webrat/spec/integration/merb/config/environments/test.rb +14 -0
  171. data/vendor/webrat/spec/integration/merb/config/init.rb +25 -0
  172. data/vendor/webrat/spec/integration/merb/config/rack.rb +11 -0
  173. data/vendor/webrat/spec/integration/merb/config/router.rb +34 -0
  174. data/vendor/webrat/spec/integration/merb/spec/spec.opts +1 -0
  175. data/vendor/webrat/spec/integration/merb/spec/spec_helper.rb +24 -0
  176. data/vendor/webrat/spec/integration/merb/spec/webrat_spec.rb +39 -0
  177. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/app_script.rb +31 -0
  178. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/common.rb +64 -0
  179. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/gem_ext.rb +124 -0
  180. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/main.thor +150 -0
  181. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/ops.rb +93 -0
  182. data/vendor/webrat/spec/integration/merb/tasks/merb.thor/utils.rb +40 -0
  183. data/vendor/webrat/spec/integration/rack/Rakefile +5 -0
  184. data/vendor/webrat/spec/integration/rack/app.rb +73 -0
  185. data/vendor/webrat/spec/integration/rack/test/helper.rb +20 -0
  186. data/vendor/webrat/spec/integration/rack/test/webrat_rack_test.rb +62 -0
  187. data/vendor/webrat/spec/integration/rails/Rakefile +30 -0
  188. data/vendor/webrat/spec/integration/rails/app/controllers/application.rb +15 -0
  189. data/vendor/webrat/spec/integration/rails/app/controllers/buttons_controller.rb +7 -0
  190. data/vendor/webrat/spec/integration/rails/app/controllers/fields_controller.rb +4 -0
  191. data/vendor/webrat/spec/integration/rails/app/controllers/links_controller.rb +7 -0
  192. data/vendor/webrat/spec/integration/rails/app/controllers/webrat_controller.rb +43 -0
  193. data/vendor/webrat/spec/integration/rails/app/helpers/buttons_helper.rb +2 -0
  194. data/vendor/webrat/spec/integration/rails/app/helpers/fields_helper.rb +2 -0
  195. data/vendor/webrat/spec/integration/rails/app/helpers/links_helper.rb +2 -0
  196. data/vendor/webrat/spec/integration/rails/app/views/buttons/show.html.erb +11 -0
  197. data/vendor/webrat/spec/integration/rails/app/views/fields/show.html.erb +9 -0
  198. data/vendor/webrat/spec/integration/rails/app/views/links/show.html.erb +5 -0
  199. data/vendor/webrat/spec/integration/rails/app/views/webrat/before_redirect_form.html.erb +4 -0
  200. data/vendor/webrat/spec/integration/rails/app/views/webrat/buttons.html.erb +11 -0
  201. data/vendor/webrat/spec/integration/rails/app/views/webrat/form.html.erb +28 -0
  202. data/vendor/webrat/spec/integration/rails/config/boot.rb +109 -0
  203. data/vendor/webrat/spec/integration/rails/config/environment.rb +12 -0
  204. data/vendor/webrat/spec/integration/rails/config/environments/development.rb +17 -0
  205. data/vendor/webrat/spec/integration/rails/config/environments/selenium.rb +22 -0
  206. data/vendor/webrat/spec/integration/rails/config/environments/test.rb +22 -0
  207. data/vendor/webrat/spec/integration/rails/config/initializers/inflections.rb +10 -0
  208. data/vendor/webrat/spec/integration/rails/config/initializers/mime_types.rb +5 -0
  209. data/vendor/webrat/spec/integration/rails/config/initializers/new_rails_defaults.rb +17 -0
  210. data/vendor/webrat/spec/integration/rails/config/locales/en.yml +5 -0
  211. data/vendor/webrat/spec/integration/rails/config/routes.rb +18 -0
  212. data/vendor/webrat/spec/integration/rails/public/404.html +30 -0
  213. data/vendor/webrat/spec/integration/rails/public/422.html +30 -0
  214. data/vendor/webrat/spec/integration/rails/public/500.html +33 -0
  215. data/vendor/webrat/spec/integration/rails/script/about +4 -0
  216. data/vendor/webrat/spec/integration/rails/script/console +3 -0
  217. data/vendor/webrat/spec/integration/rails/script/dbconsole +3 -0
  218. data/vendor/webrat/spec/integration/rails/script/destroy +3 -0
  219. data/vendor/webrat/spec/integration/rails/script/generate +3 -0
  220. data/vendor/webrat/spec/integration/rails/script/performance/benchmarker +3 -0
  221. data/vendor/webrat/spec/integration/rails/script/performance/profiler +3 -0
  222. data/vendor/webrat/spec/integration/rails/script/performance/request +3 -0
  223. data/vendor/webrat/spec/integration/rails/script/plugin +3 -0
  224. data/vendor/webrat/spec/integration/rails/script/process/inspector +3 -0
  225. data/vendor/webrat/spec/integration/rails/script/process/reaper +3 -0
  226. data/vendor/webrat/spec/integration/rails/script/process/spawner +3 -0
  227. data/vendor/webrat/spec/integration/rails/script/runner +3 -0
  228. data/vendor/webrat/spec/integration/rails/script/server +3 -0
  229. data/vendor/webrat/spec/integration/rails/test/integration/button_click_test.rb +80 -0
  230. data/vendor/webrat/spec/integration/rails/test/integration/fill_in_test.rb +24 -0
  231. data/vendor/webrat/spec/integration/rails/test/integration/link_click_test.rb +27 -0
  232. data/vendor/webrat/spec/integration/rails/test/integration/webrat_test.rb +97 -0
  233. data/vendor/webrat/spec/integration/rails/test/test_helper.rb +25 -0
  234. data/vendor/webrat/spec/integration/sinatra/Rakefile +5 -0
  235. data/vendor/webrat/spec/integration/sinatra/classic_app.rb +64 -0
  236. data/vendor/webrat/spec/integration/sinatra/modular_app.rb +16 -0
  237. data/vendor/webrat/spec/integration/sinatra/test/classic_app_test.rb +37 -0
  238. data/vendor/webrat/spec/integration/sinatra/test/modular_app_test.rb +18 -0
  239. data/vendor/webrat/spec/integration/sinatra/test/test_helper.rb +16 -0
  240. data/vendor/webrat/spec/private/core/configuration_spec.rb +116 -0
  241. data/vendor/webrat/spec/private/core/field_spec.rb +85 -0
  242. data/vendor/webrat/spec/private/core/link_spec.rb +24 -0
  243. data/vendor/webrat/spec/private/core/logging_spec.rb +10 -0
  244. data/vendor/webrat/spec/private/core/session_spec.rb +195 -0
  245. data/vendor/webrat/spec/private/mechanize/mechanize_session_spec.rb +81 -0
  246. data/vendor/webrat/spec/private/merb/attaches_file_spec.rb +93 -0
  247. data/vendor/webrat/spec/private/merb/merb_session_spec.rb +61 -0
  248. data/vendor/webrat/spec/private/nokogiri_spec.rb +77 -0
  249. data/vendor/webrat/spec/private/rails/attaches_file_spec.rb +81 -0
  250. data/vendor/webrat/spec/private/rails/rails_session_spec.rb +112 -0
  251. data/vendor/webrat/spec/private/selenium/application_servers/rails_spec.rb +26 -0
  252. data/vendor/webrat/spec/public/basic_auth_spec.rb +24 -0
  253. data/vendor/webrat/spec/public/check_spec.rb +191 -0
  254. data/vendor/webrat/spec/public/choose_spec.rb +118 -0
  255. data/vendor/webrat/spec/public/click_area_spec.rb +106 -0
  256. data/vendor/webrat/spec/public/click_button_spec.rb +496 -0
  257. data/vendor/webrat/spec/public/click_link_spec.rb +511 -0
  258. data/vendor/webrat/spec/public/fill_in_spec.rb +209 -0
  259. data/vendor/webrat/spec/public/locators/field_by_xpath_spec.rb +19 -0
  260. data/vendor/webrat/spec/public/locators/field_labeled_spec.rb +172 -0
  261. data/vendor/webrat/spec/public/locators/field_with_id_spec.rb +16 -0
  262. data/vendor/webrat/spec/public/matchers/contain_spec.rb +114 -0
  263. data/vendor/webrat/spec/public/matchers/have_selector_spec.rb +142 -0
  264. data/vendor/webrat/spec/public/matchers/have_tag_spec.rb +39 -0
  265. data/vendor/webrat/spec/public/matchers/have_xpath_spec.rb +136 -0
  266. data/vendor/webrat/spec/public/reload_spec.rb +10 -0
  267. data/vendor/webrat/spec/public/save_and_open_spec.rb +70 -0
  268. data/vendor/webrat/spec/public/select_date_spec.rb +112 -0
  269. data/vendor/webrat/spec/public/select_datetime_spec.rb +137 -0
  270. data/vendor/webrat/spec/public/select_spec.rb +249 -0
  271. data/vendor/webrat/spec/public/select_time_spec.rb +100 -0
  272. data/vendor/webrat/spec/public/selenium/application_server_factory_spec.rb +49 -0
  273. data/vendor/webrat/spec/public/selenium/application_servers/external_spec.rb +12 -0
  274. data/vendor/webrat/spec/public/selenium/selenium_session_spec.rb +37 -0
  275. data/vendor/webrat/spec/public/set_hidden_field_spec.rb +5 -0
  276. data/vendor/webrat/spec/public/submit_form_spec.rb +5 -0
  277. data/vendor/webrat/spec/public/visit_spec.rb +58 -0
  278. data/vendor/webrat/spec/public/within_spec.rb +177 -0
  279. data/vendor/webrat/spec/rcov.opts +1 -0
  280. data/vendor/webrat/spec/spec.opts +2 -0
  281. data/vendor/webrat/spec/spec_helper.rb +50 -0
  282. metadata +474 -0
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.93')
26
+ s.add_dependency('haml', '=2.2.2')
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.8')
32
+ s.add_dependency('shotgun', '=0.3')
33
+ s.add_dependency('sinatra', '=0.9.4')
34
+ s.add_dependency('winton-active_wrapper', '=0.1.9')
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.1"
45
+ end
data/lib/sum.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+
3
+ gems = [
4
+ [ 'haml', '=2.2.2' ],
5
+ [ 'sinatra', '=0.9.4' ],
6
+ [ 'winton-active_wrapper', '=0.1.9' ],
7
+ [ 'winton-fetcher', '=0.1.2' ],
8
+ [ 'winton-secret_key', '=0.1.0' ]
9
+ ]
10
+
11
+ gems.each do |name, version|
12
+ begin
13
+ gem name, version
14
+ rescue Exception
15
+ $:.unshift "#{File.dirname(__FILE__)}/../vendor/#{name}/lib"
16
+ end
17
+ end
18
+
19
+ require 'haml'
20
+ require 'sinatra/base'
21
+ require 'active_wrapper'
22
+ require 'fetcher'
23
+ require 'secret_key'
24
+
25
+ class Application < Sinatra::Base
26
+ end
27
+
28
+ Dir["#{File.dirname(__FILE__)}/sum/**/*.rb"].sort.each do |path|
29
+ require path
30
+ 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,27 @@
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(type, attribute, question)
9
+ partial(
10
+ :field,
11
+ :locals => {
12
+ :attribute => attribute,
13
+ :checkbox => type == :checkbox,
14
+ :question => question
15
+ }
16
+ )
17
+ end
18
+
19
+ def valid?(attribute=nil)
20
+ if attribute
21
+ @user && @user.errors.on(attribute).nil?
22
+ else
23
+ @user && @user.valid?
24
+ end
25
+ end
26
+ end
27
+ 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,298 @@
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
+ attr_accessible :tos
9
+
10
+ after_create :after_create_email
11
+ before_create :before_create_timestamps
12
+
13
+ before_save :before_save_recent_transactions
14
+ before_save :before_save_savings_goal
15
+ before_save :before_save_spending_goal
16
+
17
+ has_many :emails, :class_name => 'UserEmail', :dependent => :destroy
18
+
19
+ serialize :recent_transactions
20
+
21
+ not_saved_from_form = lambda do |r|
22
+ r.bills.nil? && r.income.nil? && r.savings.nil?
23
+ end
24
+
25
+ validates_acceptance_of(
26
+ :tos,
27
+ :allow_nil => false,
28
+ :unless => not_saved_from_form
29
+ )
30
+ validates_format_of(
31
+ :email,
32
+ :with => /\S+@\S+\.\S+/,
33
+ :unless => lambda { |r| r.email.blank? }
34
+ )
35
+ validates_numericality_of(
36
+ :bills,
37
+ :income,
38
+ :savings,
39
+ :unless => not_saved_from_form
40
+ )
41
+ validates_presence_of :email, :unless => lambda { |r| r.email.nil? }
42
+
43
+ # Class methods
44
+
45
+ class <<self
46
+
47
+ def reset_users!
48
+ conditions = [ 'reset_at <= ?', Time.now.utc ]
49
+ users = self.find(:all, :conditions => conditions)
50
+ users.each do |user|
51
+ user.reset!
52
+ end
53
+ end
54
+
55
+ def reset_spent_today!
56
+ conditions = [ 'send_at <= ?', Time.now.utc ]
57
+ users = self.find(:all, :conditions => conditions)
58
+ users.each do |user|
59
+ user.reset_spent_today!
60
+ end
61
+ end
62
+
63
+ def send_emails!(&block)
64
+ conditions = [
65
+ 'send_now = 1 OR send_at <= ?',
66
+ Time.now.utc
67
+ ]
68
+ users = self.find(:all, :conditions => conditions)
69
+ users.each do |user|
70
+ user.emails.each do |email|
71
+ next unless email.active? && email.failures <= 5
72
+ begin
73
+ $mail.deliver(
74
+ :from => 'sum@sumapp.com',
75
+ :to => email.email,
76
+ :subject => "Today's budget",
77
+ :body => yield(user)
78
+ )
79
+ email.sent!
80
+ rescue Exception => e
81
+ email.increment!(:failures)
82
+ end
83
+ end
84
+ user.sent!
85
+ end
86
+ end
87
+ end
88
+
89
+ # Instance methods
90
+
91
+ [ :email, :bills, :income, :savings, :tos ].each do |attribute|
92
+ define_method(attribute) do
93
+ read_attribute attribute
94
+ end
95
+ define_method("#{attribute}=") do |value|
96
+ write_attribute(
97
+ attribute,
98
+ attribute == :email || attribute == :tos ?
99
+ value :
100
+ to_number(value)
101
+ )
102
+ end
103
+ end
104
+
105
+ def add_email!(address, send=false)
106
+ UserEmail.create(:email => address, :user_id => self.id)
107
+ if send
108
+ self.flash = "Successfully added #{address} to your account."
109
+ self.send_now = true
110
+ self.save
111
+ end
112
+ end
113
+
114
+ # Beginning of fiscal month
115
+ def beginning_of_month
116
+ self.reset_at - 1.month
117
+ end
118
+
119
+ # Days in this fiscal month
120
+ def days_in_month
121
+ self.reset_at.to_date - self.beginning_of_month.to_date
122
+ end
123
+
124
+ # Days left in this fiscal month
125
+ def days_left
126
+ to_local(self.reset_at).to_date - to_local(Time.now).to_date - 1
127
+ end
128
+
129
+ # Days left in this fiscal month, including today
130
+ def days_left_including_today
131
+ self.days_left + 1
132
+ end
133
+
134
+ # Days passed in this fiscal month
135
+ def days_passed
136
+ to_local(Time.now).to_date - to_local(self.beginning_of_month).to_date
137
+ end
138
+
139
+ # Days passed in this fiscal month, including today
140
+ def days_passed_including_today
141
+ self.days_passed + 1
142
+ end
143
+
144
+ # Reset spent_today
145
+ def reset_spent_today!
146
+ self.update_attribute :spent_today, 0
147
+ end
148
+
149
+ def reset!
150
+ self.temporary_spending_cut = self.total_left * -1
151
+ if self.temporary_spending_cut < 0
152
+ self.temporary_spending_cut = 0
153
+ end
154
+ self.spent_this_month = 0
155
+ update_reset_at
156
+ self.save
157
+ end
158
+
159
+ def sent!
160
+ self.flash = nil
161
+ self.send_now = false
162
+ update_send_at unless self.send_now_changed?
163
+ self.save
164
+ rescue
165
+ # This fixes a really confusing error when running cucumber features:
166
+ # No response yet. Request a page first. (Rack::Test::Error)
167
+ # Happens on User#save after using $mail.deliver.
168
+ end
169
+
170
+ # How much the user should have spent in this period
171
+ def should_have_spent
172
+ self.spending_per_day * self.days_passed_including_today
173
+ end
174
+
175
+ def spend!(amount)
176
+ amount = amount.to_f if amount.respond_to?(:upcase)
177
+ self.recent_transactions ||= []
178
+ self.recent_transactions.unshift(amount)
179
+ self.spent_this_month += amount
180
+ self.spent_today += amount
181
+ self.send_now = true
182
+ self.save
183
+ end
184
+
185
+ # Spending goal for the month
186
+ def spending_goal
187
+ read_attribute(:spending_goal) - self.temporary_spending_cut
188
+ end
189
+
190
+ # Today's spending goal
191
+ def spending_goal_today
192
+ (self.surplus(:exclude_today) / self.days_left_including_today) - self.spent_today
193
+ end
194
+
195
+ # Today's spending goal with savings included in money left to spend
196
+ def spending_goal_today_savings
197
+ (self.savings_goal + self.surplus(:exclude_today)) / self.days_left_including_today
198
+ end
199
+
200
+ # How much the user is spending per day (ideally)
201
+ def spending_per_day
202
+ self.spending_goal / self.days_in_month
203
+ end
204
+
205
+ # How much the user has spent this month
206
+ def spent_this_month(option=nil)
207
+ case option
208
+ when :exclude_today
209
+ read_attribute(:spent_this_month) - self.spent_today
210
+ else
211
+ read_attribute(:spent_this_month)
212
+ end
213
+ end
214
+
215
+ # Variance from budget based on savings_goal
216
+ def surplus(option=nil)
217
+ two_decimals(self.spending_goal - spent_this_month(option))
218
+ end
219
+
220
+ # Variance from budget based on should_have_spent
221
+ def surplus_for_period(option=nil)
222
+ two_decimals(self.should_have_spent - spent_this_month(option))
223
+ end
224
+
225
+ # Total remaining money for the month
226
+ def total_left(option=nil)
227
+ self.surplus(option) + self.savings_goal
228
+ end
229
+
230
+ private
231
+
232
+ def after_create_email
233
+ self.add_email!(read_attribute(:email))
234
+ end
235
+
236
+ def before_create_timestamps
237
+ self.reset_at = update_send_at
238
+ update_reset_at
239
+ end
240
+
241
+ def before_save_recent_transactions
242
+ if self.recent_transactions
243
+ self.recent_transactions = self.recent_transactions[0..4]
244
+ end
245
+ end
246
+
247
+ def before_save_savings_goal
248
+ if self.savings
249
+ self.savings_goal = self.savings
250
+ end
251
+ end
252
+
253
+ def before_save_spending_goal
254
+ if self.income && self.bills && self.savings
255
+ self.spending_goal = self.income - self.bills - self.savings
256
+ end
257
+ end
258
+
259
+ def local_12am_to_server_time
260
+ time = to_local(Time.now)
261
+ time = DateTime.strptime(
262
+ time.strftime("%m/%d/%Y 12:00 AM %Z"),
263
+ "%m/%d/%Y %I:%M %p %Z"
264
+ )
265
+ to_system(time.to_time)
266
+ end
267
+
268
+ def to_local(time)
269
+ return time.utc unless self.timezone_offset
270
+ time.utc + self.timezone_offset
271
+ end
272
+
273
+ def to_number(string)
274
+ string = string.gsub(/[^\d\.]/, '')
275
+ string.blank? ? string : string.to_f
276
+ end
277
+
278
+ def to_system(time)
279
+ return time.utc unless self.timezone_offset
280
+ time.utc - self.timezone_offset
281
+ end
282
+
283
+ def two_decimals(amount)
284
+ sprintf("%.2f", amount).to_f
285
+ end
286
+
287
+ def update_reset_at
288
+ self.reset_at = self.reset_at + 1.month
289
+ end
290
+
291
+ def update_send_at
292
+ if self.send_at
293
+ self.send_at = self.send_at + 1.day
294
+ else
295
+ self.send_at = local_12am_to_server_time
296
+ end
297
+ end
298
+ end