Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit of the Rails 3 version of PFC.

  • Loading branch information...
commit 72674d8857ac88a605d41e913df6f4b5e1713d2f 0 parents
@eventualbuddha eventualbuddha authored
Showing with 20,807 additions and 0 deletions.
  1. +13 −0 .gitignore
  2. +83 −0 .irbrc
  3. +33 −0 Gemfile
  4. +211 −0 Gemfile.lock
  5. +19 −0 README.markdown
  6. +10 −0 Rakefile
  7. +71 −0 app/controllers/account_creds_controller.rb
  8. +40 −0 app/controllers/account_merchant_tag_stats_controller.rb
  9. +136 −0 app/controllers/accounts_controller.rb
  10. +75 −0 app/controllers/api/upload_controller.rb
  11. +91 −0 app/controllers/application_controller.rb
  12. +47 −0 app/controllers/attachments_controller.rb
  13. +61 −0 app/controllers/brcm_controller.rb
  14. +3 −0  app/controllers/dashboard_controller.rb
  15. +168 −0 app/controllers/financial_insts_controller.rb
  16. +19 −0 app/controllers/merchants_controller.rb
  17. +21 −0 app/controllers/page_controller.rb
  18. +82 −0 app/controllers/profiles_controller.rb
  19. +25 −0 app/controllers/rational_txactions_controller.rb
  20. +115 −0 app/controllers/sessions_controller.rb
  21. +33 −0 app/controllers/snapshots_controller.rb
  22. +64 −0 app/controllers/ssu_jobs_controller.rb
  23. +70 −0 app/controllers/targets_controller.rb
  24. +6 −0 app/controllers/trends_controller.rb
  25. +228 −0 app/controllers/txactions_controller.rb
  26. +229 −0 app/controllers/uploads_controller.rb
  27. +51 −0 app/controllers/user_preferences_controller.rb
  28. +195 −0 app/controllers/users_controller.rb
  29. +31 −0 app/helpers/accounts_helper.rb
  30. +27 −0 app/helpers/api/upload_helper.rb
  31. +169 −0 app/helpers/application_helper.rb
  32. +16 −0 app/helpers/currency_helper.rb
  33. +17 −0 app/helpers/dashboard_helper.rb
  34. +438 −0 app/helpers/export_helper.rb
  35. +11 −0 app/helpers/financial_insts_helper.rb
  36. +6 −0 app/helpers/profile_helper.rb
  37. +50 −0 app/helpers/uploads_helper.rb
  38. +55 −0 app/models/abstract_txaction.rb
  39. +341 −0 app/models/account.rb
  40. +27 −0 app/models/account/maintenance_concern.rb
  41. +10 −0 app/models/account/sample_concern.rb
  42. +65 −0 app/models/account/status_concern.rb
  43. +13 −0 app/models/account/uploads_concern.rb
  44. +11 −0 app/models/account_balance.rb
  45. +124 −0 app/models/account_cred.rb
  46. +247 −0 app/models/account_merchant_tag_stat.rb
  47. +157 −0 app/models/account_type.rb
  48. +13 −0 app/models/account_upload.rb
  49. +113 −0 app/models/attachment.rb
  50. +12 −0 app/models/client_platform.rb
  51. +28 −0 app/models/country.rb
  52. +312 −0 app/models/currency.rb
  53. +68 −0 app/models/currency_exchange_rate.rb
  54. +429 −0 app/models/data_source/abstract_txaction.rb
  55. +3 −0  app/models/data_source/investment_txaction.rb
  56. +41 −0 app/models/data_source/txaction.rb
  57. +368 −0 app/models/financial_inst.rb
  58. +10 −0 app/models/inbox_attachment.rb
  59. +62 −0 app/models/investment_account.rb
  60. +40 −0 app/models/investment_balance.rb
  61. +4 −0 app/models/investment_other_balance.rb
  62. +17 −0 app/models/investment_position.rb
  63. +35 −0 app/models/investment_security.rb
  64. +86 −0 app/models/investment_txaction.rb
  65. +370 −0 app/models/merchant.rb
  66. +60 −0 app/models/merchant_user.rb
  67. +255 −0 app/models/money.rb
  68. +210 −0 app/models/money/bag.rb
  69. +119 −0 app/models/money/formatter.rb
  70. +40 −0 app/models/ofx/builder/investment_account_builder.rb
  71. +48 −0 app/models/ofx/builder/investment_balance_builder.rb
  72. +47 −0 app/models/ofx/builder/investment_other_balance_builder.rb
  73. +60 −0 app/models/ofx/builder/investment_position_builder.rb
  74. +45 −0 app/models/ofx/builder/investment_security_builder.rb
  75. +72 −0 app/models/ofx/builder/investment_txaction_builder.rb
  76. +19 −0 app/models/ofx/builder/statement_builder.rb
  77. +36 −0 app/models/ofx/element.rb
  78. +130 −0 app/models/ofx/investment_statement.rb
  79. +52 −0 app/models/ofx/parser.rb
  80. +46 −0 app/models/snapshot.rb
  81. +119 −0 app/models/ssu_job.rb
  82. +296 −0 app/models/ssu_jobs/activity.rb
  83. +6 −0 app/models/stock.rb
  84. +377 −0 app/models/tag.rb
  85. +67 −0 app/models/tagging.rb
  86. +105 −0 app/models/tags_spending_summary.rb
  87. +86 −0 app/models/target.rb
  88. +459 −0 app/models/txaction.rb
  89. +45 −0 app/models/txaction/attachments_concern.rb
  90. +27 −0 app/models/txaction/currency_concern.rb
  91. +27 −0 app/models/txaction/dates_concern.rb
  92. +325 −0 app/models/txaction/form.rb
  93. +132 −0 app/models/txaction/merchants_concern.rb
  94. +16 −0 app/models/txaction/month_paginator.rb
  95. +11 −0 app/models/txaction/paginator.rb
  96. +188 −0 app/models/txaction/tags_concern.rb
  97. +130 −0 app/models/txaction/transfers_concern.rb
  98. +4 −0 app/models/txaction_attachment.rb
  99. +124 −0 app/models/txaction_tagging.rb
  100. +16 −0 app/models/txaction_type.rb
  101. +337 −0 app/models/upload.rb
  102. +10 −0 app/models/upload_account.rb
  103. +6 −0 app/models/upload_account_info.rb
  104. +6 −0 app/models/upload_format.rb
  105. +687 −0 app/models/user.rb
  106. +43 −0 app/models/user/account_update_manager.rb
  107. +13 −0 app/models/user/current_user_concern.rb
  108. +45 −0 app/models/user/merchants_concern.rb
  109. +22 −0 app/models/user/messages_concern.rb
  110. +39 −0 app/models/user/roles_concern.rb
  111. +13 −0 app/models/user/time_concern.rb
  112. +14 −0 app/models/user_login.rb
  113. +92 −0 app/models/user_preferences.rb
  114. +27 −0 app/models/user_profile.rb
  115. +26 −0 app/presenters/account_presenter.rb
  116. +11 −0 app/presenters/presenter_base.rb
  117. +17 −0 app/presenters/recurring_txaction_presenter.rb
  118. +60 −0 app/presenters/ssu_job_presenter.rb
  119. +21 −0 app/presenters/target_presenter.rb
  120. +57 −0 app/presenters/txaction_presenter.rb
  121. +21 −0 app/presenters/user_preferences_presenter.rb
  122. +6 −0 app/presenters/user_presenter.rb
  123. +11 −0 app/views/account_merchant_tag_stats/edit.html.erb
  124. +44 −0 app/views/accounts/_export.html.erb
  125. +9 −0 app/views/accounts/_investment_header.html.erb
  126. +36 −0 app/views/accounts/_transactions_chart.html.erb
  127. +165 −0 app/views/accounts/_widget.html.erb
  128. +218 −0 app/views/accounts/account-transactions.html.erb
  129. +7 −0 app/views/accounts/index.html.erb
  130. +18 −0 app/views/api/upload/config.rxml
  131. +5 −0 app/views/api/upload/error/format_change_ofx_to_qif.rxml
  132. +5 −0 app/views/api/upload/error/format_change_qif_to_ofx.rxml
  133. +6 −0 app/views/api/upload/error/import_failed.rxml
  134. +5 −0 app/views/api/upload/error/unsupported_statement_type.rxml
  135. +6 −0 app/views/api/upload/error/unsupported_uploader_version.rxml
  136. +6 −0 app/views/api/upload/error/version.rxml
  137. +1 −0  app/views/api/upload/statement.rxml
  138. +35 −0 app/views/charts/_pie.html.erb
  139. +30 −0 app/views/charts/_sve.html.erb
  140. +3 −0  app/views/charts/_txn.html.erb
  141. +22 −0 app/views/dashboard/index.html.erb
  142. +37 −0 app/views/financial_insts/confirm_merge.html.erb
  143. +4 −0 app/views/financial_insts/destroy.html.erb
  144. +112 −0 app/views/financial_insts/edit.html.erb
  145. +74 −0 app/views/financial_insts/index.html.erb
  146. +1 −0  app/views/financial_insts/index.json.erb
  147. +16 −0 app/views/financial_insts/index.xml.builder
  148. +65 −0 app/views/financial_insts/new.html.erb
  149. +454 −0 app/views/financial_insts/show.html.erb
  150. +20 −0 app/views/layouts/_footer_nav.html.erb
  151. +31 −0 app/views/layouts/_header.html.erb
  152. +9 −0 app/views/layouts/_header_top.html.erb
  153. +74 −0 app/views/layouts/_masthead.html.erb
  154. +13 −0 app/views/layouts/application.html.erb
  155. +27 −0 app/views/layouts/public.html.erb
  156. +36 −0 app/views/layouts/user.html.erb
  157. +151 −0 app/views/page/_help_sidebar.html.erb
  158. +47 −0 app/views/page/_sidebar.html.erb
  159. +24 −0 app/views/page/contribute.html.erb
  160. +42 −0 app/views/page/help-dont-panic.html.erb
  161. +50 −0 app/views/page/help-user-manual-about-uploaders.html.erb
  162. +65 −0 app/views/page/help-user-manual-account-management.html.erb
  163. +41 −0 app/views/page/help-user-manual-automatic-uploader.html.erb
  164. +87 −0 app/views/page/help-user-manual-balances.html.erb
  165. +37 −0 app/views/page/help-user-manual-cash-accounts.html.erb
  166. +33 −0 app/views/page/help-user-manual-data-format.html.erb
  167. +35 −0 app/views/page/help-user-manual-dates.html.erb
  168. +65 −0 app/views/page/help-user-manual-downloading.html.erb
  169. +36 −0 app/views/page/help-user-manual-duplicates.html.erb
  170. +30 −0 app/views/page/help-user-manual-export.html.erb
  171. +53 −0 app/views/page/help-user-manual-groups.html.erb
  172. +35 −0 app/views/page/help-user-manual-loans-investments.html.erb
  173. +46 −0 app/views/page/help-user-manual-manual-upload.html.erb
  174. +44 −0 app/views/page/help-user-manual-passwords.html.erb
  175. +38 −0 app/views/page/help-user-manual-search.html.erb
  176. +131 −0 app/views/page/help-user-manual-specific-fis.html.erb
  177. +47 −0 app/views/page/help-user-manual-tags.html.erb
  178. +54 −0 app/views/page/help-user-manual-targets.html.erb
  179. +27 −0 app/views/page/help-user-manual-transaction-names.html.erb
  180. +32 −0 app/views/page/help-user-manual-transfers.html.erb
  181. +55 −0 app/views/page/help-user-manual-troubleshooting.html.erb
  182. +49 −0 app/views/page/help-user-manual-uploading.html.erb
  183. +58 −0 app/views/page/help-user-manual-user-options.html.erb
  184. +32 −0 app/views/page/help-user-manual-viewing-transactions.html.erb
  185. +46 −0 app/views/page/mobile.html.erb
  186. +137 −0 app/views/page/terms.html.erb
  187. +41 −0 app/views/page/tour.html.erb
  188. +23 −0 app/views/page/welcome.html.erb
  189. +39 −0 app/views/page/what.html.erb
  190. +86 −0 app/views/profiles/edit.html.erb
  191. +35 −0 app/views/profiles/show.html.erb
  192. +24 −0 app/views/profiles/show.xml.builder
  193. +8 −0 app/views/rational_txactions/index.xml.builder
  194. +37 −0 app/views/sessions/new.html.erb
  195. +9 −0 app/views/shared/_notification.html.erb
  196. +19 −0 app/views/snapshots/show.html.erb
  197. +19 −0 app/views/spending_summary/_trends_summary_widget.html.erb
  198. +81 −0 app/views/spending_summary/_widget.html.erb
  199. +5 −0 app/views/ssu_jobs/_create_error.js.erb
  200. +2 −0  app/views/ssu_jobs/create_error_denied.js.erb
  201. +6 −0 app/views/ssu_jobs/create_error_denied.xml.builder
  202. +2 −0  app/views/ssu_jobs/create_error_pending.js.erb
  203. +6 −0 app/views/ssu_jobs/create_error_pending.xml.builder
  204. +1 −0  app/views/ssu_jobs/index.js.erb
  205. +7 −0 app/views/ssu_jobs/index.xml.builder
  206. +1 −0  app/views/ssu_jobs/show.js.erb
  207. +3 −0  app/views/ssu_jobs/show.xml.builder
  208. +98 −0 app/views/tags/_widget.html.erb
  209. +82 −0 app/views/targets/_widget.html.erb
  210. +17 −0 app/views/targets/index.xml.builder
  211. +15 −0 app/views/targets/show.xml.builder
  212. +17 −0 app/views/trends/index.html.erb
  213. +8 −0 app/views/txactions/_merchant_auto_complete.html.erb
  214. +6 −0 app/views/txactions/_transfer_select.html.erb
  215. +14 −0 app/views/uploads/_cash_account_form.html.erb
  216. +5 −0 app/views/uploads/_failed_error.html.erb
  217. +6 −0 app/views/uploads/_retry_error.html.erb
  218. +64 −0 app/views/uploads/error.html.erb
  219. +72 −0 app/views/uploads/index.html.erb
  220. +117 −0 app/views/uploads/manual.html.erb
  221. +71 −0 app/views/uploads/new.html.erb
  222. +106 −0 app/views/uploads/select.html.erb
  223. +151 −0 app/views/uploads/ssu.html.erb
  224. +15 −0 app/views/users/_confirm_delete.html.erb
  225. +71 −0 app/views/users/_form.html.erb
  226. +10 −0 app/views/users/_homepage_userbar.html.erb
  227. +20 −0 app/views/users/_sidebar.html.erb
  228. +15 −0 app/views/users/_userbar.html.erb
  229. +62 −0 app/views/users/become.html.erb
  230. +44 −0 app/views/users/change_password.html.erb
  231. +60 −0 app/views/users/delete_membership.html.erb
  232. +24 −0 app/views/users/download_data.html.erb
  233. +2 −0  app/views/users/edit_filter_tags.rjs
  234. +61 −0 app/views/users/new.html.erb
  235. +36 −0 app/views/users/reset_password.html.erb
  236. +4 −0 config.ru
  237. +4 −0 config/.gitignore
  238. +51 −0 config/application.rb
  239. +17 −0 config/boot.rb
  240. +14 −0 config/database.example.yml
  241. +5 −0 config/environment.rb
  242. +23 −0 config/environments/development.rb
  243. +35 −0 config/environments/production.rb
  244. +29 −0 config/environments/test.rb
  245. +17 −0 config/initializers/00_required_libraries.rb
  246. +3 −0  config/initializers/01_required_gems.rb
  247. +12 −0 config/initializers/02_required_internals.rb
  248. +3 −0  config/initializers/03_vendor_load_paths.rb
  249. +1 −0  config/initializers/04_required_vendors.rb
  250. +9 −0 config/initializers/08_fix_ofx_config.rb
  251. +7 −0 config/initializers/09_api_constants.rb
  252. +3 −0  config/initializers/10_cache_financial_insts.rb
  253. +25 −0 config/initializers/14_ssu_server.rb
  254. +4 −0 config/initializers/19_delayed_job.rb
  255. +20 −0 config/initializers/active_record_retry_once.rb
  256. +7 −0 config/initializers/backtrace_silencers.rb
  257. +55 −0 config/initializers/inflections.rb
  258. +10 −0 config/initializers/mime_types.rb
  259. +36 −0 config/initializers/net_http_patches.rb
  260. +43 −0 config/initializers/rexml-expansion-fix.rb
  261. +3 −0  config/initializers/services_config.rb
  262. +5 −0 config/locales/en.yml
  263. +98 −0 config/routes.rb
  264. +20 −0 config/services.example.yml
  265. +7 −0 config/sphinx.example.yml
  266. +7 −0 config/ssu-service.example.yml
  267. +615 −0 db/schema.rb
  268. +16 −0 db/seeds.rb
  269. +541 −0 db/seeds/countries.yml
  270. BIN  db/seeds/financial_insts.json.gz
  271. BIN  db/seeds/investment_securities.json.gz
  272. +35 −0 db/seeds/txaction_types.yml
  273. +2 −0  doc/README_FOR_APP
  274. +93 −0 lib/acts_as_taggable.rb
  275. +26 −0 lib/api/constants.rb
  276. +3 −0  lib/authentication.rb
  277. +161 −0 lib/authentication/controller_methods.rb
  278. +134 −0 lib/authentication/login_throttle.rb
  279. +86 −0 lib/conditions_constructor.rb
  280. +84 −0 lib/constants.rb
  281. +35 −0 lib/crypto.rb
  282. +43 −0 lib/data_preloading.rb
  283. +7 −0 lib/exporter.rb
  284. +20 −0 lib/exporter/txaction.rb
  285. +58 −0 lib/exporter/txaction/csv.rb
  286. +11 −0 lib/exporter/txaction/xls.rb
  287. +449 −0 lib/exporter/wesabe.rb
  288. +9 −0 lib/generators/js_list_widget/js_list_widget_generator.rb
  289. +1 −0  lib/generators/js_list_widget/templates/model.js
  290. +61 −0 lib/generators/js_model/js_model_generator.rb
  291. +29 −0 lib/generators/js_model/templates/model.js
  292. +13 −0 lib/generators/js_widget/js_widget_generator.rb
  293. +30 −0 lib/generators/js_widget/templates/model.js
  294. +158 −0 lib/image_processing.rb
  295. +22 −0 lib/image_processing/thumbnailer.rb
  296. +85 −0 lib/importer.rb
  297. +431 −0 lib/importer/wesabe.rb
  298. +67 −0 lib/makeofx2.rb
  299. +515 −0 lib/ofx2importer.rb
  300. +38 −0 lib/option_set.rb
Sorry, we could not display the entire diff because too many files (1,050) changed.
13 .gitignore
@@ -0,0 +1,13 @@
+db/development_structure.sql
+db/production_structure.sql
+config/initializers/99_local_env.rb
+log
+tmp
+doc/app
+.DS_Store
+public/images/wesabeans_montage.jpg
+public/images/group_avatars
+public/images/user_photos
+config/initializers/99_local_env.rb
+db/development_structure.sql
+.bundle
83 .irbrc
@@ -0,0 +1,83 @@
+# Some niceness for using the pfc console
+
+# Find users by typing their username
+def username(name)
+ User.find_by_username(name.to_s)
+end
+
+# Authenticate by username and password
+def auth(username, password)
+ User.authenticate(username, password)
+end
+
+# Filter transactions
+def f(string)
+ TxactionFilter.filter(string.to_s)
+end
+
+# Find account
+def a(id)
+ Account.find(id.to_i)
+end
+
+# Re-filter txactions in account
+def refilter(account_or_id)
+ if account_or_id.is_a?(Account)
+ account = account_or_id
+ else
+ account = Account.find(account_or_id.to_i)
+ end
+ account.txactions.each do |tx|
+ tx.generate_filtered_and_cleaned_names!
+ tx.save
+ end
+ puts "refiltered #{account.txactions.size} txactions"
+ return account.txactions.size
+end
+
+### http://gist.github.com/72234.git
+# mysql-style output for an array of Ruby objects
+#
+# Usage:
+# report(records) # displays report with all fields
+# report(records, :field1, :field2, ...) # displays report with given fields
+#
+# Example:
+# >> report(records, :id, :amount, :created_at)
+# +------+-----------+--------------------------------+
+# | id | amount | created_at |
+# +------+-----------+--------------------------------+
+# | 8301 | $12.40 | Sat Feb 28 09:20:47 -0800 2009 |
+# | 6060 | $39.62 | Sun Feb 15 14:45:38 -0800 2009 |
+# | 6061 | $167.52 | Sun Feb 15 14:45:38 -0800 2009 |
+# | 6067 | $12.00 | Sun Feb 15 14:45:40 -0800 2009 |
+# | 6059 | $1,000.00 | Sun Feb 15 14:45:38 -0800 2009 |
+# +------+-----------+--------------------------------+
+# 5 rows in set
+#
+def report(items, *fields)
+ # find max length for each field; start with the field names themselves
+ fields = items.first.attribute_names unless fields.any?
+ max_len = Hash[*fields.map {|f| [f, f.to_s.length]}.flatten]
+ items.each do |item|
+ fields.each do |field|
+ len = item.send(field).to_s.length
+ max_len[field] = len if len > max_len[field]
+ end
+ end
+
+ border = '+-' + fields.map {|f| '-' * max_len[f] }.join('-+-') + '-+'
+ title_row = '| ' + fields.map {|f| sprintf("%-#{max_len[f]}s", f.to_s) }.join(' | ') + ' |'
+
+ puts border
+ puts title_row
+ puts border
+
+ items.each do |item|
+ row = '| ' + fields.map {|f| sprintf("%-#{max_len[f]}s", item.send(f)) }.join(' | ') + ' |'
+ puts row
+ end
+
+ puts border
+ puts "#{items.length} rows in set\n"
+end
33 Gemfile
@@ -0,0 +1,33 @@
+source :gemcutter
+
+# Wesabe gems
+# gem 'keyword_search', '1.3.99' # in vendor/gems
+gem 'chronic', '0.2.3'
+
+# Regular gems
+gem 'rails', '3.0.0.beta4'
+gem 'oniguruma' # sudo port install oniguruma5; sudo gem install indirect-oniguruma -s http://gems.github.com -- --with-onig-dir=/opt/local
+gem 'fastercsv'
+gem 'systemu'
+gem 'riddle'
+gem 'memcache-client', :require => 'memcache'
+gem 'mime-types', :require => 'mime/types'
+gem 'rest-client', :require => 'rest_client'
+gem 'rubyzip', :require => 'zip/zip'
+gem 'libxml-ruby', '=1.1.3', :require => 'xml/libxml'
+gem 'daemons'
+gem 'rdoc'
+gem 'delayed_job', '2.1.0.pre'
+
+group :development do
+ gem 'mysql' # in production: apt-get install libmysql-ruby
+ gem 'libcharguess' # in production: apt-get install libcharguess-ruby1.8
+end
+
+group :test do
+ gem 'webmock'
+ gem 'rspec-rails', '>= 2.0.0.beta.8'
+ gem 'ruby-debug'
+ gem 'machinist'
+ gem 'faker'
+end
211 Gemfile.lock
@@ -0,0 +1,211 @@
+---
+dependencies:
+ faker:
+ group:
+ - :test
+ version: ">= 0"
+ ruby-debug:
+ group:
+ - :test
+ version: ">= 0"
+ fastercsv:
+ group:
+ - :default
+ version: ">= 0"
+ libxml-ruby:
+ group:
+ - :default
+ version: = 1.1.3
+ require:
+ - xml/libxml
+ rails:
+ group:
+ - :default
+ version: = 3.0.0.beta4
+ mysql:
+ group:
+ - :development
+ version: ">= 0"
+ memcache-client:
+ group:
+ - :default
+ version: ">= 0"
+ require:
+ - memcache
+ machinist:
+ group:
+ - :test
+ version: ">= 0"
+ mime-types:
+ group:
+ - :default
+ version: ">= 0"
+ require:
+ - mime/types
+ delayed_job:
+ group:
+ - :default
+ version: = 2.1.0.pre
+ riddle:
+ group:
+ - :default
+ version: ">= 0"
+ rspec-rails:
+ group:
+ - :test
+ version: ">= 2.0.0.beta.8"
+ rest-client:
+ group:
+ - :default
+ version: ">= 0"
+ require:
+ - rest_client
+ oniguruma:
+ group:
+ - :default
+ version: ">= 0"
+ libcharguess:
+ group:
+ - :development
+ version: ">= 0"
+ systemu:
+ group:
+ - :default
+ version: ">= 0"
+ chronic:
+ group:
+ - :default
+ version: = 0.3.0
+ daemons:
+ group:
+ - :default
+ version: ">= 0"
+ rubyzip:
+ group:
+ - :default
+ version: ">= 0"
+ require:
+ - zip/zip
+ webmock:
+ group:
+ - :test
+ version: ">= 0"
+ rdoc:
+ group:
+ - :default
+ version: ">= 0"
+specs:
+- rake:
+ version: 0.8.7
+- abstract:
+ version: 1.0.0
+- activesupport:
+ version: 3.0.0.beta4
+- builder:
+ version: 2.1.2
+- i18n:
+ version: 0.4.1
+- activemodel:
+ version: 3.0.0.beta4
+- erubis:
+ version: 2.6.6
+- rack:
+ version: 1.1.0
+- rack-mount:
+ version: 0.6.6
+- rack-test:
+ version: 0.5.4
+- tzinfo:
+ version: 0.3.22
+- actionpack:
+ version: 3.0.0.beta4
+- mime-types:
+ version: "1.16"
+- polyglot:
+ version: 0.3.1
+- treetop:
+ version: 1.4.8
+- mail:
+ version: 2.2.5
+- actionmailer:
+ version: 3.0.0.beta4
+- arel:
+ version: 0.4.0
+- activerecord:
+ version: 3.0.0.beta4
+- activeresource:
+ version: 3.0.0.beta4
+- addressable:
+ version: 2.1.2
+- bundler:
+ version: 0.9.26
+- chronic:
+ version: 0.3.0
+- columnize:
+ version: 0.3.1
+- crack:
+ version: 0.1.7
+- daemons:
+ version: 1.1.0
+- delayed_job:
+ version: 2.1.0.pre
+- diff-lcs:
+ version: 1.1.2
+- faker:
+ version: 0.3.1
+- fastercsv:
+ version: 1.5.3
+- libcharguess:
+ version: "1.0"
+- libxml-ruby:
+ version: 1.1.3
+- linecache:
+ version: "0.43"
+- machinist:
+ version: 1.0.6
+- memcache-client:
+ version: 1.8.3
+- mysql:
+ version: 2.8.1
+- nokogiri:
+ version: 1.4.2
+- oniguruma:
+ version: 1.1.0
+- thor:
+ version: 0.13.7
+- railties:
+ version: 3.0.0.beta4
+- rails:
+ version: 3.0.0.beta4
+- rdoc:
+ version: 2.5.8
+- rest-client:
+ version: 1.5.1
+- riddle:
+ version: 1.0.10
+- rspec-core:
+ version: 2.0.0.beta.14
+- rspec-expectations:
+ version: 2.0.0.beta.14
+- rspec-mocks:
+ version: 2.0.0.beta.14
+- rspec:
+ version: 2.0.0.beta.14
+- webrat:
+ version: 0.7.1
+- rspec-rails:
+ version: 2.0.0.beta.14.2
+- ruby-debug-base:
+ version: 0.10.3
+- ruby-debug:
+ version: 0.10.3
+- rubyzip:
+ version: 0.9.4
+- systemu:
+ version: 1.2.0
+- webmock:
+ version: 1.3.0
+hash: 89b51d479b38eb0ac1cb9f23f6f501390ab875c9
+sources:
+- Rubygems:
+ uri: http://gemcutter.org
19 README.markdown
@@ -0,0 +1,19 @@
+PFC
+===
+
+PFC originally stood for Personal Finance Community and is the main part of the Wesabe website. Wesabe is a personal finance website, formerly available at wesabe.com, which has since been released as open source. The web site is now available to run on your own Mac, Linux, or Windows (with VMWare) computer. The site's main features are:
+
+1. Account Aggregation: seeing all your checking, savings, credit accounts, etc in one place
+2. Tags & Spending Targets: edit your transactions and add tags so you can set spending limits on certain categories
+3. Spending Trends: get analysis on your spending over time
+
+PFC is one of the projects required for running the Wesabe website. For more information on setting it up on your own computer, see http://github.com/wesabe/wesabe.
+
+Issues
+------
+
+* There is a fair amount of unused code that was never purged, so don't be too confused if you find something and you have no idea why it's there. It probably doesn't need to be. This is particularly true in parts of the accounts and transactions systems, much of which were moved to the BRCM (Java) backend. Delete!
+* Currency exchange rates are not provided in this initial release. Wesabe used a commercial exchange rate provider (xe.com) for this data, and we can't use that service here or redistribute their data. If anyone would like to add exchange rate support for a particular provider, here's how you would add an exchange rate to the database:
+ # exchange rates are stored in units of USD. If a rate is not present in the database for a given date, the
+ # nearest existing rate will be used
+ CurrencyExchangeRate.create(:currency => "EUR", :rate => 0.7873, :date => Date.parse("2010-07-13"))
10 Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require File.expand_path('../config/application', __FILE__)
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+Rails::Application.load_tasks
71 app/controllers/account_creds_controller.rb
@@ -0,0 +1,71 @@
+class AccountCredsController < ApplicationController
+ before_filter :check_authentication
+ before_filter :require_account_cred, :only => [:destroy, :show]
+ before_filter :check_ssu_enabled
+
+ def index
+ @account_creds = current_user.account_creds
+
+ respond_to do |format|
+ format.json { render :json => @account_creds }
+ end
+ end
+
+ def create
+ @fi = FinancialInst.find_by_wesabe_id(params[:fid])
+ return redirect_to(user_home_path) unless @fi
+ @account_cred = AccountCred.create(
+ :cred_guid => params[:credguid],
+ :cred_key => params[:credkey],
+ :user => current_user,
+ :financial_inst => @fi)
+ @job = SsuJob.start(current_user, @account_cred) if @account_cred.valid?
+ if @job && @job.valid?
+ logger.info "SSU START job guid #{@job.job_guid}"
+ elsif @job
+ logger.error "SSU ERROR #{@account_cred.error_sentence} #{@job.error_sentence}"
+ else
+ logger.error "SSU ERROR cred is #{'in' unless @account_cred.valid?}valid and job was not created"
+ end
+ render :nothing => true
+ end
+
+ def destroy
+ if @account_cred
+ @account_cred.destroy
+ if params[:back] && request.env["HTTP_REFERER"]
+ redirect_to :back
+ elsif params[:intended_uri] && params[:intended_uri] =~ %r{^/}
+ redirect_to params[:intended_uri]
+ else
+ redirect_to ssu_new_upload_path(:fi => @account_cred.financial_inst, :second_try => params[:second_try])
+ end
+ else
+ redirect_to user_home_path
+ end
+ end
+
+ def start
+ @account_creds = current_user.account_creds.find_by_financial_inst_id(params[:id])
+ @account_creds.each{ |ac| SsuJob.start(current_user, ac) }
+ redirect_to user_home_path
+ end
+
+ def show
+ respond_to do |format|
+ format.json { render :json => @account_cred }
+ end
+ end
+
+protected
+
+ def check_ssu_enabled
+ render :text => "The automatic uploader is disabled.", :status => 503 unless ssu_enabled?
+ end
+
+ def require_account_cred
+ @account_cred = AccountCred.find_by_cred_guid(params[:id])
+ redirect_to home_path unless @account_cred && @account_cred.destroyable_by?(current_user)
+ end
+
+end
40 app/controllers/account_merchant_tag_stats_controller.rb
@@ -0,0 +1,40 @@
+class AccountMerchantTagStatsController < ApplicationController
+ before_filter :check_authentication
+ before_filter :get_merchant_and_sign
+ layout nil
+
+ def edit
+ @autotags = AccountMerchantTagStat.autotags_string_for(current_user, @merchant, @sign)
+ end
+
+ def update
+ new_tags = TagParser.parse(params["autotags"])
+ if new_tags.empty?
+ MerchantUser.disable_autotags(current_user, @merchant, @sign)
+ remove_tags = []
+ tags = ""
+ else
+ old_tags = TagParser.parse(params["old_tags"])
+ remove_tags = old_tags - new_tags
+
+ AccountMerchantTagStat.force_on(current_user, @merchant, @sign, new_tags)
+ AccountMerchantTagStat.force_off(current_user, @merchant, @sign, remove_tags)
+ MerchantUser.enable_autotags(current_user, @merchant, @sign)
+ tags = AccountMerchantTagStat.autotags_string_for(current_user, @merchant, @sign)
+
+ if params["update_all"]
+ Txaction.add_tags_for_merchant(current_user, @merchant, @sign, new_tags)
+ Txaction.remove_tags_for_merchant(current_user, @merchant, @sign, remove_tags)
+ end
+ end
+
+ render :json => {"added" => new_tags, "removed" => remove_tags, "tags" => tags}
+ end
+
+private
+
+ def get_merchant_and_sign
+ @merchant = Merchant.find(params[:id])
+ @sign = params[:sign].to_i > 0 ? 1 : -1
+ end
+end
136 app/controllers/accounts_controller.rb
@@ -0,0 +1,136 @@
+class AccountsController < ApplicationController
+ before_filter :check_authentication
+
+ # GET /accounts
+ def index
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ # GET /accounts/1
+ def show
+ if account.nil?
+ redirect_to accounts_url
+ else
+ respond_to do |format|
+ format.html do
+ redirect_to accounts_url(:anchor => account_path(account))
+ end
+ end
+ end
+ end
+
+ # PUT /accounts/1
+ def update
+ if account.nil?
+ return render :nothing => true, :status => :not_found
+ end
+
+ account.name = params[:name] unless params[:name].blank?
+ if params[:currency]
+ account.currency = Currency.known?(params[:currency]) ? params[:currency] : 'USD'
+ end
+
+ # Update status
+ case params[:status]
+ when "active"
+ account.status = Constants::Status::ACTIVE
+ when "archived"
+ account.status = Constants::Status::ARCHIVED
+ end
+
+ # update balance and account type
+ if params[:enable_balance] && account.manual_account?
+ account.account_type_id = (params[:enable_balance] =~ /^(true|1|t)$/) ?
+ AccountType::MANUAL : AccountType::CASH
+ end
+
+ if account.has_balance? && params[:current_balance]
+ account.balance = params[:current_balance]
+ end
+
+ account.save!
+
+ respond_to do |format|
+ format.json { render :json => present(account) }
+ end
+ end
+
+ # DELETE /accounts/1
+ def destroy
+ if account.nil?
+ return render :text => "Could not delete your account.", :status => :forbidden
+ end
+
+ # require correct password to delete an account
+ if not current_user.valid_password?(params[:password])
+ return render :text => "Incorrect password", :status => :forbidden
+ end
+
+ @account.safe_delete
+ @account.send_later(:destroy)
+ render :nothing => true
+ end
+
+ # GET /accounts/1/financial_institution_site
+ def financial_institution_site
+ if account && account.financial_inst && account.financial_inst.url.present?
+ redirect_to account.financial_inst.url
+ else
+ render :nothing => true, :status => :not_found
+ end
+ end
+
+ # POST /accounts
+ def create
+ # default to USD if the currency is missing or unknown
+ params[:currency] = 'USD' if !Currency.known?(params[:currency])
+ @account = Account.create(:name => params[:name],
+ :user => current_user,
+ :account_type_id => params[:balance] ? AccountType::MANUAL : AccountType::CASH,
+ :currency => params[:currency])
+ @account.balance = params[:balance] if params[:balance]
+
+ respond_to do |format|
+ format.html { redirect_to account_url(@account) }
+ format.json do
+ if @account.errors.any?
+ render(:json => {:errors => @account.errors.full_messages.to_json}, :status => :bad_request)
+ else
+ render(:json => {:id => @account.id_for_user, :guid => @account.guid }, :status => :ok)
+ end
+ end
+ end
+ end
+
+ # POST /accounts/trigger_updates
+ def trigger_updates
+ User::AccountUpdateManager.login!(current_user, self, :force => true)
+ render :nothing => true
+ end
+
+ # POST /accounts/enable
+ def enable
+ params[:accounts] && params[:accounts].each do |id, input|
+ account = current_user.account_by_id_for_user(id)
+ account.name = input[:name]
+ if input[:enabled]
+ account.status = Constants::Status::ACTIVE
+ account.save
+ else
+ account.destroy # set status to DISABLED and delete txactions
+ end
+ end
+ redirect_to(params[:add_another] ? new_upload_path : user_home_path)
+ end
+
+private
+
+ def account
+ # if the id looks like a guid, do the look up by that
+ @account ||= (params[:id] =~ /^[0-9a-z]{64}$/) ?
+ current_user.accounts.find_by_guid(params[:id]) :
+ current_user.accounts.find_by_id_for_user(params[:id])
+ end
+end
75 app/controllers/api/upload_controller.rb
@@ -0,0 +1,75 @@
+class Api::UploadController < ApplicationController
+ include Api::UploadHelper
+
+ layout nil
+
+ before_filter :check_basic_auth_credentials
+
+ # exception we throw if we get a request that's too large
+ class RequestSizeExceeded < Exception; end
+ class UnsupportedUploader < Exception; end
+
+ # return config file with user accounts
+ def config
+ # check api version
+ api_version = get_api_version(request.user_agent)
+ return render(:action => 'error/version') unless compatible_version?(api_version)
+
+ # KLUDGE: throw an exception if the version is 1.09/Mac because Jay released debug code
+ if request.user_agent =~ /1\.0\.9.*?darwin/i
+ return render(:action => 'error/unsupported_uploader_version')
+ end
+
+ # update last api login
+ @user.last_api_login = Time.now
+ @user.save
+
+ @accounts = @user.active_accounts
+ end
+
+ # method to upload a set of transaction data
+ def statement
+ if request.post?
+ begin
+ Importer.import_request(@user, request, @account_cred_id)
+ rescue RequestSizeExceeded
+ return render_text('', '413 Request Entity Too Large')
+ rescue Importer::UnsupportedStatementType
+ return render(:action => 'error/unsupported_statement_type')
+ rescue Exception => e
+ logger.error([e.message, *e.backtrace].join("\n"))
+ render(:action => 'error/import_failed')
+ end
+ end
+ end
+
+private
+
+ def check_basic_auth_credentials
+ if params[:job_guid] && params[:user_id]
+ job = SsuJob.find_by_job_guid(params[:job_guid])
+ if job && !job.expired? && (@user = User.find_by_id(params[:user_id]))
+ @account_cred_id = job.account_cred_id
+ set_current_user(@user)
+ else
+ render :text => "Invalid job: Access denied.", :status => 401
+ end
+ else
+ @user = authenticate_or_request_with_http_basic "Wesabe Upload API" do |username, password|
+ if user = User.authenticate(username, password)
+ set_current_user(user)
+ end
+ user
+ end
+ end
+ rescue Authentication::LoginThrottle::UserThrottleError
+ render(
+ :text => 'Too many failed login attempts. This account is temporarily disabled. Please try again later.',
+ :status => :forbidden,
+ :layout => false
+ )
+ return false
+ end
+
+end
+
91 app/controllers/application_controller.rb
@@ -0,0 +1,91 @@
+require 'base64'
+
+# Filters added to this controller will be run for all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+class ApplicationController < ActionController::Base
+ protect_from_forgery
+ before_filter :clear_current_user_global
+
+ include Stylesheeter
+ include Authentication::ControllerMethods
+
+ helper :all
+
+ before_filter :unmunge_excel_accept_headers,
+ :decode_tag_name,
+ :reset_session_timeout,
+ :handle_x_frame_header
+ after_filter :add_xss_blocker_to_js
+
+ def ssu_enabled?
+ !File.exist?("/var/wesabe/ssu-down")
+ end
+ helper_method :ssu_enabled?
+ hide_action :ssu_enabled?
+
+protected
+
+ def clear_current_user_global
+ User.current = nil
+ end
+
+ # remove the X-Frame-Options header if that feature is disabled
+ def handle_x_frame_header
+ response.headers["X-Frame-Options"] = "Sameorigin"
+ end
+
+ def decode_tag_names
+ # turn strings like "foo/bar-slash-baz" into arrays like ["foo", "bar/baz"]
+ # this works around an apache bug that unescapes %2F when proxying, even when explicitly told not to
+ params[:tags] = params[:tags].split("/").map{|n| Tag.decode_name!(n) } if params[:tags]
+ end
+
+ def decode_tag_name
+ Tag.decode_name!(params[:tag]) if params[:tag]
+ end
+
+ # remove the shitty Accept-munging that Excel+IE does on older machines
+ def unmunge_excel_accept_headers
+ request.headers["HTTP_ACCEPT"].gsub!(/application\/vnd.ms-excel(,)*/, "") if request.headers["HTTP_ACCEPT"]
+ end
+
+ include ActionView::Helpers::CaptureHelper
+ include ApplicationHelper
+
+ def user_home_path
+ dashboard_path
+ end
+
+ #--------------------------------------------------------------------------
+ # Authorization and Authentication
+
+
+ # used in before filters where only admins are allowed to view the page
+ def check_for_role(role)
+ role_method = role.to_s + '?'
+ return true if current_user && current_user.respond_to?(role_method) && current_user.send(role_method)
+
+ flash[:notice] = Error::UNAUTHORIZED
+ request.env["HTTP_REFERER"] ? (redirect_to :back) : (redirect_to root_url)
+ return false
+ end
+
+ # used in before filters where only admins are allowed to view the page
+ def check_for_admin
+ check_for_role(:admin)
+ end
+
+ def only_allow_html
+ render :nothing => true, :status => 406 if params[:format] && params[:format] != "html"
+ end
+
+ # prevents <script> tags on other web sites from accessing
+ # potentially sensitive information.
+ # see http://dev.rubyonrails.org/changeset/6556
+ def add_xss_blocker_to_js
+ case response.content_type
+ when %r{^text/javascript}, %r{^application/json}
+ response.body = "/*-secure- #{response.body} */"
+ end
+ end
+end
47 app/controllers/attachments_controller.rb
@@ -0,0 +1,47 @@
+class AttachmentsController < ApplicationController
+ before_filter :check_authentication
+
+ # GET /attachments/1
+ def show
+ if attachment = current_user.attachments.find_by_guid(params[:id])
+ send_data(attachment.read, :filename => attachment.filename, :type => attachment.content_type)
+ end
+ end
+
+ # POST /attachments.xml
+ def create
+ @attachment = Attachment.generate(current_user, params)
+ InboxAttachment.create!(:user => current_user, :attachment => @attachment) unless @attachment.new_record?
+
+ respond_to do |format|
+ if @attachment && @attachment.valid?
+ # flash[:notice] = 'Attachment was successfully created.'
+ # format.html { redirect_to(@attachment) }
+ format.xml { render :xml => @attachment, :status => :created, :location => @attachment }
+ else
+ # format.html { render :action => "new" }
+ format.xml do
+ if @attachment
+ errors = @attachment.errors
+ else
+ errors = ActiveRecord::Errors.new
+ errors.add_to_base("Could not create attachment")
+ end
+ render :xml => errors, :status => :unprocessable_entity
+ end
+ end
+ end
+ rescue Attachment::MaxSizeExceeded => e
+ logger.error(e.message)
+ render :text => e.message, :status => :request_entity_too_large
+ end
+
+ # DELETE /attachments/:id
+ def destroy
+ if attachment = current_user.attachments.find_by_guid(params[:id])
+ attachment.destroy
+ end
+
+ render :nothing => true, :status => :ok
+ end
+end
61 app/controllers/brcm_controller.rb
@@ -0,0 +1,61 @@
+# REVIEW: Make this not ugly, or replace it with something which isn't a hideous hack.
+require 'exporter/txaction/csv'
+require 'exporter/txaction/xls'
+
+# A simple pass-through controller for brcm-accounts-api.
+class BrcmController < ApplicationController
+ before_filter :check_authentication
+
+ def passthrough
+ res = brcm.get(request.fullpath.sub(%r{^/data}, "")) do |req|
+ req.user = current_user
+ req.headers["Accept-Language"] = request.headers["HTTP_ACCEPT_LANGUAGE"]
+ req.headers["Accept"] = request.headers["HTTP_ACCEPT"]
+ end
+
+ assert_valid_response(res)
+
+ render(:text => res.body, :content_type => res.content_type, :status => res.code)
+ end
+
+ def transactions
+ res = brcm.get(request.fullpath.sub(%r{^/data}, "")) do |req|
+ req.user = current_user
+ req.headers["Accept-Language"] = request.headers["HTTP_ACCEPT_LANGUAGE"]
+ req.headers["Accept"] = (params[:format] == 'xml') ? 'application/xml' : 'application/json'
+ end
+
+ assert_valid_response(res)
+
+ # extract tag name if that's one of the params
+ tag = params[:tag].gsub(%r{^/tags/},'') if params[:tag] # param comes to us as "/tags/<tag>"
+
+ respond_to do |format|
+ format.json { render :text => res.body, :content_type => res.content_type, :status => res.code }
+ format.xml { render :text => res.body, :content_type => res.content_type, :status => res.code }
+ format.csv {
+ exporter = Exporter::Txaction::Csv.new(current_user, res.body, :tag => tag)
+ exporter.render(self, "wesabe-transactions.csv")
+ }
+ format.xls {
+ exporter = Exporter::Txaction::Xls.new(current_user, res.body, :tag => tag)
+ exporter.render(self, "wesabe-transactions.xls")
+ }
+ format.any { render :text => res.body, :content_type => res.content_type, :status => res.code }
+ end
+ end
+
+ private
+
+ def brcm
+ Service.get(:brcm)
+ end
+
+ def assert_valid_response(res)
+ if res.nil?
+ raise "BRCM sucked at life: request to #{request.fullpath} received no response"
+ elsif res.code >= 400
+ raise "BRCM sucked at life: request to #{request.fullpath} received #{res.code} response:\n\n#{res.headers}\n\n#{res.body}"
+ end
+ end
+end
3  app/controllers/dashboard_controller.rb
@@ -0,0 +1,3 @@
+class DashboardController < ApplicationController
+ before_filter :check_authentication
+end
168 app/controllers/financial_insts_controller.rb
@@ -0,0 +1,168 @@
+class FinancialInstsController < ApplicationController
+ before_filter :check_authentication
+ before_filter :check_for_admin, :only => [:edit, :update, :destroy, :unapproved, :confirm_merge, :merge]
+
+ # GET /financial-institutions
+ # GET /financial-institutions.xml
+ # GET /financial_insts/list?format=xml
+ def index
+ @financial_insts = FinancialInst.for_user(current_user).order('name ASC')
+ respond_to do |format|
+ format.html { check_for_admin } # index.html.erb ## admin-only. stupid stupid stupid
+ format.xml { render :layout => false } # index.xml.builder
+ format.json { render :layout => false } # index.json.erb
+ end
+ end
+
+ # GET /financial-institutions/unapproved
+ def unapproved
+ @financial_insts = FinancialInst.paginated_find_all_unapproved(params[:page], :per_page => 1000)
+ end
+
+ # GET /financial-institutions/new
+ def new
+ get_countries
+ @financial_inst = FinancialInst.new(:name => params[:name], :country_id => current_user.country_id)
+ end
+
+ # POST /financial-institutions
+ def create
+ # only allow users to set name, homepage_url, and country
+ fi_params = (params[:financial_inst] || {}).slice(:name, :homepage_url, :country_id)
+ @financial_inst = FinancialInst.new(fi_params.update(:creating_user_id => current_user.id, :approved => false))
+ if @financial_inst.save
+ # continue on with the upload process
+ flash[:fi_confirmation] = "Your financial institution has been added."
+ redirect_to manual_uploads_path(:fi => @financial_inst)
+ else
+ get_countries
+ render :action => "new"
+ end
+ end
+
+ # GET /financial-institutions/1/edit
+ # GET /financial-institutions/us-000238/edit
+ def edit
+ @financial_inst = FinancialInst.find_for_user(params[:id], current_user) || raise(ActiveRecord::RecordNotFound)
+ end
+
+ # PUT /financial-institutions/1
+ # PUT /financial-institutions/us-000238
+ def update
+ @financial_inst = FinancialInst.find_for_user(params[:id], current_user) || raise(ActiveRecord::RecordNotFound)
+ if @financial_inst.update_attributes(params[:financial_inst])
+ redirect_to(financial_inst_url(@financial_inst))
+ else
+ render :action => "edit"
+ end
+ end
+
+ # GET /financial-institutions/1
+ # GET /financial-institutions/us-000238
+ # GET /financial-institutions/1.xml
+ # GET /financial_insts/1?format=xml
+ def show
+ @financial_inst = FinancialInst.find_for_user(params[:id], @user || current_user) || raise(ActiveRecord::RecordNotFound)
+
+ respond_to do |format|
+ format.html {
+ raise(ActiveRecord::RecordNotFound) unless check_for_admin
+ generate_fi_stats; render :action => 'show' # show.html.erb
+ }
+ format.xml { render :xml => @financial_inst }
+ end
+ end
+
+
+ def generate_fi_stats
+ @recent_jobs = @financial_inst.
+ all_ssu_jobs.
+ latest(10).
+ map(&:presenter)
+ @recent_signups = @financial_inst.
+ all_ssu_jobs.
+ signups.
+ limit(10).
+ order('created_at DESC').
+ map(&:presenter)
+
+ ## FIXME: This is really inefficient, need to add a real by_fi filter
+ activity_30d = SsuJobs::Activity.new(30.days.ago, Time.now)
+ activity_30d.wesabe_id = @financial_inst.wesabe_id
+ activity_30d.summarize!()
+ @fi_stats_30d = activity_30d.get_stats_for_wesabe_id(@financial_inst.wesabe_id)
+ return unless @fi_stats_30d
+
+ @fi_okay = []
+ @fi_fail = []
+ @fi_auth = []
+ @fi_pend = []
+ @fi_all = []
+
+ ## FIXME: Sorting shouldn't happen here!
+ @fi_stats_30d.slot_stats.sort_by {|_| _.slot }.each do |slot_stat|
+ @fi_okay << [slot_stat.slot*1000, slot_stat.sum_okay]
+ @fi_fail << [slot_stat.slot*1000, slot_stat.sum_fail]
+ @fi_auth << [slot_stat.slot*1000, slot_stat.sum_auth]
+ @fi_pend << [slot_stat.slot*1000, slot_stat.sum_pending]
+ @fi_all << [slot_stat.slot*1000, slot_stat.sum_jobs]
+ end
+
+ activity_07d = SsuJobs::Activity.new(
+ Chronic.parse('7 days ago'),
+ Chronic.parse('today')
+ )
+ activity_07d.wesabe_id = @financial_inst.wesabe_id
+ activity_07d.summarize!
+ @fi_stats_07d = activity_07d.get_stats_for_wesabe_id(@financial_inst.wesabe_id)
+ end
+ hide_action :generate_fi_stats
+
+ # DELETE /financial-institutions/1
+ # DELETE /financial-institutions/us-000238
+ def destroy
+ @financial_inst = FinancialInst.find_for_user(params[:id], current_user) || raise(ActiveRecord::RecordNotFound)
+ if @financial_inst.destroy
+ redirect_to unapproved_financial_insts_url
+ else
+ render :action => "destroy"
+ end
+ end
+
+ # GET /financial-institutions/:id/confirm_merge
+ def confirm_merge
+ @financial_inst = FinancialInst.find_by_wesabe_id(params[:id])
+ @target_fi = FinancialInst.find(params[:target_fi_id])
+ redirect_to financial_insts_url unless @financial_inst && @target_fi
+ end
+
+ # POST /financial-institutions/:id/merge
+ def merge
+ @financial_inst = FinancialInst.find_by_wesabe_id(params[:id])
+ @target_fi = FinancialInst.find(params[:target_fi_id])
+ @financial_inst.mapped_to_id = @target_fi.id
+ FinancialInst.merge(@financial_inst.id, @financial_inst.mapped_to_id)
+ redirect_to(financial_inst_url(@target_fi))
+ end
+
+private
+
+ # Retrieves a list of countries for the select box on #new and #create.
+ def get_countries
+ @countries = Country.ids_and_names
+ end
+
+ def check_authentication
+ if params[:job_guid]
+ job = SsuJob.find_by_job_guid(params[:job_guid])
+ if job && !job.expired? && (@user = User.find_by_id(params[:user_id]))
+ return true
+ else
+ render :nothing => true, :status => 401
+ end
+ else
+ super
+ end
+ end
+
+end
19 app/controllers/merchants_controller.rb
@@ -0,0 +1,19 @@
+class MerchantsController < ApplicationController
+ before_filter :check_authentication, :except => [:show]
+
+ def user_index
+ render :json => current_user.merchants.
+ sort_by {|m| [-m.count.to_i, m.name]}.
+ map(&:name)
+ end
+
+ def public_index
+ render :json => Merchant.all_publicly_visible_names - current_user.merchants.map(&:name)
+ end
+
+ private
+
+ def user_index_cache_path
+ "merchants/user_index/#{current_user.id}"
+ end
+end
21 app/controllers/page_controller.rb
@@ -0,0 +1,21 @@
+class PageController < ApplicationController
+ before_filter :check_authentication, :only => [:knownissues]
+ before_filter :only_allow_html
+
+ def about
+ redirect_to :action => "what"
+ end
+
+ def help
+ redirect_to help_url('user-manual/uploading')
+ end
+
+ # REVIEW: See comment in app/views/page/_help_sidebar.html.erb for an explanation.
+ def help_dont_panic
+ redirect_to help_url('user-manual/troubleshooting')
+ end
+
+ def founders
+ redirect_to :action => "execs", :status => :moved_permanently
+ end
+end
82 app/controllers/profiles_controller.rb
@@ -0,0 +1,82 @@
+class ProfilesController < ApplicationController
+ before_filter :check_authentication
+
+ def show
+ @user = current_user
+ @profile = @user.profile
+ end
+
+ def edit
+ @user = current_user
+
+ @user.profile = UserProfile.new unless @user.profile
+ @profile = @user.profile
+
+ @email_change = EmailChangeForm.new(params[:email_change] || {:email => @user.email, :password => ''})
+ end
+
+ def update
+ @user = current_user
+
+ @user.profile = UserProfile.new unless @user.profile
+ @profile = @user.profile
+
+ if params[:email_change]
+ @email_change = EmailChangeForm.new(params[:email_change])
+
+ if @email_change.email != @user.email
+ # make sure email address isn't already taken
+ if User.find(:first, :conditions => ["email = ? and id != ?", @email_change.email, @user.id])
+ @email_change.errors.add(:email, "That email address is already taken.")
+ return render(:action => "edit")
+ end
+ # require password to change email
+ if !@user.valid_password?(@email_change.password)
+ @email_change.errors.add(:password, 'The password is incorrect.')
+ return render(:action => "edit")
+ end
+
+ params[:user][:email] = @email_change.email
+ else
+ # don't allow email to be set via params. could use attr_protected in User, but I'm worried I might break something
+ params[:user].delete(:email)
+ end
+ @email_change.password = "" # make sure we don't keep the password around
+ end
+
+ image_field = params[:photo] && params[:photo]['photo']
+ if image_field.respond_to?('original_filename') && !image_field.original_filename.blank?
+ @user.image_file = image_field
+ end
+
+ if @user.update_attributes(params[:user]) && @profile.update_attributes(params[:profile])
+ set_current_user(@user) # update current user
+ notify_success "Your profile has been updated."
+ redirect_to edit_profile_url
+ return
+ elsif not @user.valid?
+ notify_error(@user.errors.full_messages.first)
+ elsif not @profile.valid?
+ notify_error(@profile.errors.full_messages.first)
+ end
+
+ render :action => "edit"
+ end
+
+private
+
+ def notify_success(title, message=nil)
+ title, message = "Profile Updated", title if message.nil?
+ flash.now[:notification] = {:type => 'success', :title => title, :message => message}
+ end
+
+ def notify_error(title, message=nil)
+ title, message = "Error Updating Profile", title if message.nil?
+ flash.now[:notification] = {:type => 'error', :title => title, :message => message}
+ end
+
+ # form for changing email address in user/edit/profile
+ class EmailChangeForm < ActiveForm
+ attr_accessor :password, :email
+ end
+end
25 app/controllers/rational_txactions_controller.rb
@@ -0,0 +1,25 @@
+class RationalTxactionsController < ApplicationController
+ before_filter :check_authentication
+ before_filter :set_expiration
+
+ def index
+ @txactions = DataSource::Txaction.new(current_user) do |ds|
+ ds.rationalize = true
+ ds.filter_transfers = true
+ ds.filtered_tags = current_user.filter_tags
+ ds.start_date = (params[:start_date] || 1.month.ago)
+ ds.end_date = (params[:end_date] || Date.today)
+ ds.amount = params[:type] if params[:type]
+ end.txactions
+
+ respond_to do |format|
+ format.xml
+ end
+ end
+
+protected
+
+ def set_expiration
+ expires_in 1.minute
+ end
+end
115 app/controllers/sessions_controller.rb
@@ -0,0 +1,115 @@
+# REVIEW: Move this to resource routing.
+class SessionsController < ApplicationController
+ layout "public"
+ skip_before_filter :reset_session_timeout
+
+ def new
+ # if user is already logged in, just redirect to index
+ redirect_to(intended_uri || root_url) if current_user
+ end
+
+ # REVIEW: Refactor this into a series of private methods instead of a 40-line monster.
+ def create
+ # email is the new username
+ username_or_email = params[:email] || params[:username]
+
+ if current_user
+ return redirect_to(intended_uri || root_url)
+ end
+
+ begin
+ unless user = User.authenticate(username_or_email, params[:password])
+ logger.debug("email/username or password incorrect for: #{username_or_email}")
+ flash.now[:error] = {:title => "Username/Email or Password incorrect",
+ :message => "Re-enter your email and password and try again." }
+ return render(:action => "new")
+ end
+ rescue Authentication::LoginThrottle::UserThrottleError
+ logger.debug("throttled login attempt for: #{username_or_email}")
+ flash.now[:error] = {:title => "Too many failed login attempts",
+ :message => "This account is temporarily disabled. Please try again later."}
+ return render(:action => "new")
+ end
+
+ logger.debug("logging in: " + username_or_email)
+
+ # reset session to avoid session fixation attack
+ reset_session
+
+ # sign the user in
+ set_current_user(user, :update_login_timestamp => true)
+ current_user.after_login(self)
+
+ # redirect to intended action if it exists
+ if intended_uri.present? && intended_uri !~ %r{(/user/(login|logout|timeout|signup))|financial_insts}
+ logger.debug("redirecting to intended uri: #{intended_uri}")
+ return redirect_to(intended_uri)
+ else
+ return redirect_to(root_url)
+ end
+ end
+
+ # Redirect the user's browser to the timeout page if they haven't been active
+ # during the timeout period
+ def show
+ return redirect_to(root_url) if !request.xhr?
+
+ if time_left > 0
+ render :nothing => true
+ else
+ logger.info("*** session timed out for #{session[:session_id]}; time_left: #{time_left}; session[:expires_at]: #{session[:expires_at]}")
+ render :update do |page|
+ page.redirect_to login_url
+ end
+ end
+ end
+
+ # REVIEW: Move this to #update.
+ # Reset the session timeout
+ # called from javascript when users are typing in a form
+ def reset_timeout
+ reset_session_timeout
+ render :nothing => true
+ end
+
+ def destroy
+ logger.info("*** [SessionController#delete] called for #{session[:session_id]}; time_left: #{time_left}; session[:expires_at]: #{session[:expires_at]}; params: #{params.inspect}; session: #{session.inspect}")
+ # REVIEW: Encapsulate all this logic in something.
+ case params[:reason]
+ when :timeout
+ logger.debug("*** [SessionController#delete] session timeout")
+ # only show timeout error if they timed out relatively recently
+ if session[:expires_at] && session[:expires_at] < 12.hours.ago
+ flash[:error] = Error::TIMEOUT
+ end
+ else
+ clear_current_user
+ return redirect_to(login_url(:signed_out => true))
+ end
+
+ clear_current_user(:clear_intended_uri => false)
+ return redirect_to(login_url)
+ end
+
+private
+
+ def intended_uri
+ # If they were referred to this page by another person
+ # Don't use the referer if an intended_uri is already set or if the user just logged out manually (in
+ # which case the referer is the page they logged out from)
+ @intended_uri ||= begin
+ uri = params[:intended_uri] || session[:intended_uri]
+
+ if !params[:signed_out] && !uri && referer = internal_referer
+ # and it's not this controller or a static page
+ if !["page", "sessions"].include?(referer[:controller])
+ uri = url_for(referer)
+ logger.debug "setting intended URI to #{uri}"
+ end
+ end
+
+ uri
+ end
+ end
+
+end
33 app/controllers/snapshots_controller.rb
@@ -0,0 +1,33 @@
+class SnapshotsController < ApplicationController
+ before_filter :check_authentication
+
+ def create
+ Snapshot.async_build_snapshot_for_user(current_user)
+ render :nothing => true, :status => :created
+ end
+
+ def show
+ respond_to do |format|
+ format.html
+ format.zip { download(:zip) }
+ format.wesabeSnapshot { download(:wesabeSnapshot) }
+ end
+ end
+
+ private
+
+ def download(type)
+ if snapshot && snapshot.built?
+ send_file snapshot.archive,
+ :filename => "#{current_user.to_param}.#{type}",
+ :type => type,
+ :disposition => 'attachment'
+ else
+ render :nothing => true, :status => :not_found
+ end
+ end
+
+ def snapshot
+ @snapshot ||= Snapshot.find_by_uid(params[:id])
+ end
+end
64 app/controllers/ssu_jobs_controller.rb
@@ -0,0 +1,64 @@
+class SsuJobsController < ApplicationController
+ before_filter :get_ssu_job, :only => :update
+ before_filter :check_authentication, :except => [:update]
+ before_filter :find_account_cred, :except => [:update]
+
+ def update
+ @job.update_status(params)
+ render :json => @job.presenter.to_internal_json
+ end
+
+ def create
+ if job = SsuJob.start(current_user, @account_cred)
+ @job = job.presenter
+ respond_to do |format|
+ format.html { redirect_to root_url }
+ format.xml { render :action => 'show', :layout => false } # show.xml.builder
+ format.js { render :action => 'show', :layout => false } # show.js.erb
+ end
+ elsif job = @account_cred.last_ssu_job
+ @job = job.presenter
+ action = if job.denied? then 'create_error_denied' else 'create_error_pending' end
+ respond_to do |format|
+ # format.html # 405 Method Not Allowed
+ format.xml { render :action => action, :status => 400 }
+ format.js { render :action => action, :status => 400 }
+ end
+ end
+ end
+
+ def index
+ @jobs = @account_cred.all_ssu_jobs.map {|job| job.presenter}
+ respond_to do |format|
+ format.xml { render :action => 'index' } # index.xml.builder
+ format.js { render :action => 'index' } # index.js.erb
+ end
+ end
+
+ def show
+ if job = @account_cred.all_ssu_jobs.find_by_job_guid(params[:id])
+ @job = job.presenter
+ respond_to do |format|
+ # format.html # 405 Method Not Allowed
+ format.xml { render :action => 'show', :layout => false } # show.xml.builder
+ format.js { render :action => 'show', :layout => false } # show.js.erb
+ end
+ else
+ render :text => "Cannot find job with id=#{params[:id]}", :status => 404
+ end
+ end
+
+protected
+
+ def find_account_cred
+ render :text => "Cannot find credential with id=#{params[:credential_id]}", :status => 404 unless
+ @account_cred = current_user.account_creds.find_by_id(params[:credential_id]) ||
+ current_user.account_creds.find_by_cred_guid(params[:credential_id])
+ end
+
+ def get_ssu_job
+ render :text => "No such job", :status => 404 unless
+ @job = SsuJob.find_by_job_guid(params[:id])
+ end
+
+end
70 app/controllers/targets_controller.rb
@@ -0,0 +1,70 @@
+class TargetsController < ApplicationController
+ before_filter :check_authentication
+ layout nil
+
+ # show all targets for this user
+ def index
+ @targets = current_user.targets
+ # allow a period to be specified with start_date & end_date
+ @period = nil
+ if params[:start_date] && params[:end_date]
+ @period = Time.parse(params[:start_date]).beginning_of_day..Time.parse(params[:end_date]).end_of_day
+ end
+ @targets.each { |target| target.calculate!(current_user, @period) }
+
+ respond_to do |format|
+ format.xml { render :layout => false } # index.xml.builder
+ format.json { render :json => @targets.map {|t| present(t) } }
+ end
+ end
+
+ # show the target for a single tag
+ def show
+ if @target = Target.for_tag(params[:tag], current_user)
+ @target.calculate!(current_user)
+ end
+
+ respond_to do |format|
+ format.xml { render :layout => false } # show.xml.builder
+ end
+ end
+
+ def create
+ tag = Tag.find_or_create_by_name(params[:tag])
+ amount = Currency.normalize(params[:amount])
+
+ if tag && amount
+ @target = Target.for_tag(tag, current_user)
+ @target ||= Target.create(:tag => tag, :tag_name => tag.user_name, :amount_per_month => amount, :user => current_user)
+ end
+
+ if @target
+ render :json => present(@target)
+ else
+ render :json => {}, :status => :bad_request
+ end
+ end
+
+ def update
+ tag = Tag.find_by_name(params[:tag])
+ @target = Target.for_tag(tag, current_user) || raise(ActiveRecord::RecordNotFound)
+
+ if amount = Currency.normalize(params[:amount])
+ @target.amount_per_month = amount
+ @target.save
+ end
+
+ respond_to do |format|
+ format.json { render :json => present(@target) }
+ end
+ end
+
+ def destroy
+ target = Target.for_tag(params[:tag], current_user) || raise(ActiveRecord::RecordNotFound)
+ target.destroy
+
+ respond_to do |format|
+ format.json { render :json => "TIIIIIMMM!!!" }
+ end
+ end
+end
6 app/controllers/trends_controller.rb
@@ -0,0 +1,6 @@
+class TrendsController < ApplicationController
+ before_filter :check_authentication
+
+ def index
+ end
+end
228 app/controllers/txactions_controller.rb
@@ -0,0 +1,228 @@
+class TxactionsController < ApplicationController
+ layout nil
+ before_filter :check_authentication
+
+ def create
+ return unless account
+
+ # only allow txactions to be created in manual accounts
+ if not account.manual_account?
+ flash.now[:error] = %{There was a problem saving your transaction. Please reload the page and try again.}
+ flash.now[:error_for] = 'merchant_name'
+ return _render_response(:create)
+ end
+
+ txaction ||= account.new_txaction
+
+ if params[:merchant_name].blank?
+ flash.now[:error] = 'Please enter a merchant name.'
+ flash.now[:error_for] = 'merchant_name'
+ return _render_response(:create)
+ end
+
+ # get merchant name
+ unless merchant = Merchant.find_by_name(params[:merchant_name])
+ merchant = Merchant.create(:name => params[:merchant_name])
+ expire_action("merchants/user_index/#{current_user.id}")
+ end
+
+ txaction.merchant = merchant
+
+ # set amount and date
+ begin
+ txaction_form = Txaction::Form.update(txaction, params)
+ rescue Txaction::Form::UpdateValidationFailed => e
+ flash.now[:error] = e.message
+ flash.now[:error_for] = e.field
+ return _render_response(:create)
+ end
+
+ # if the merchant on the txaction has changed, expire the user's merchant list cache
+ if txaction.changed.include?("merchant_id")
+ expire_action("merchants_for_user_#{current_user.id}")
+ end
+
+ # handle file attachement
+ _process_file_attachments(txaction)
+ txaction.save!
+ txaction.attach_matching_transfer
+
+ respond_to do |format|
+ # yes, this is right. because of the stupid iframe, if the content type is json, we get a popup window
+ format.html { render :json => txaction.to_json }
+ format.json { render :json => txaction.to_json }
+ end
+ end
+
+ # Saves edits to Txactions.
+ def update
+ return unless txaction
+
+ _process_file_attachments(txaction)
+
+ response_object = txaction
+ response_status = nil
+
+ # set amount and date
+ begin
+ Txaction::Form.update(txaction, params)
+ # if the merchant on the txaction has changed, expire the user's merchant list cache
+ if txaction.changed.include?("merchant_id")
+ expire_action("merchants_for_user_#{current_user.id}")
+ end
+ rescue Txaction::Form::UpdateValidationFailed => e
+ response_object = {:error => {:message => e.message, :field => e.field}}
+ response_status = :bad_request
+ end
+
+ # hack for file uploads, see File Uploads at http://malsup.com/jquery/form/#code-samples
+ if request.xhr?
+ render :json => response_object.to_json, :status => response_status
+ else
+ self.content_type = 'text/html'
+ render :text => "<textarea>#{response_object.to_json}</textarea>", :status => response_status
+ end
+ end
+
+ def destroy
+ return unless txaction
+
+ txaction.safe_delete
+
+ respond_to do |format|
+ format.html { render :nothing => true }
+ format.json { render :json => {} }
+ end
+ end
+
+ def undelete
+ return unless txaction
+
+ txaction.update_attribute(:status, Constants::Status::ACTIVE)
+ render :nothing => true
+ end
+
+ # returns a list of possible check matches
+ def merchant_list_checks
+ @check_merchants = Merchant.find_most_likely_merchants(txaction, :check => true, :limit => 2).map(&:name)
+ render :json => @check_merchants
+ end
+
+ # called when a merchant is selected from the auto-complete dropdown
+ def on_select_merchant
+ return unless txaction
+
+ if @merchant = Merchant.find_edited_by_name(params[:name])
+ @suggested_tags = @merchant.suggested_tags
+ sign = txaction ? txaction.amount.sign : -1
+ @merchant_user = MerchantUser.get_merchant_user(current_user, @merchant, sign)
+ @autotags = AccountMerchantTagStat.autotags_for(current_user, @merchant, sign)
+ end
+
+ respond_to do |format|
+ format.json { _on_select_merchant_json }
+ end
+ end
+
+ def _on_select_merchant_json
+ if @merchant
+ render :json => {
+ 'id' => @merchant.id,
+ 'suggested-tags' => @suggested_tags.map {|display_name| {'display' => display_name}},
+ 'tags' => @autotags.any? ? {
+ # FIXME: once these are from AccountMerchantStats, use @ams.autotags_string
+ 'display' => @autotags.map(&:display_name).join(" "),
+ 'value' => @autotags.map {|tagging|
+ {'name' => {'value' => tagging.name_without_split, 'display' => tagging.display_name}}
+ }
+ } : nil
+ }
+ else
+ render :json => {}
+ end
+ end
+
+ def transfer_selector
+ return unless txaction
+
+ @possibilities = txaction.find_all_matching_transfers
+ render :partial => 'txactions/transfer_select', :locals => {:txaction => txaction}
+ end
+
+ private
+
+ def _process_file_attachments(txaction)
+ # read in any attachments
+ files = []
+ 0.upto(4) do |i|
+ file = params["file_#{i}"]
+ unless file.blank?
+ files << { :data => file.read,
+ :content_type => file.content_type,
+ :filename => file.original_filename }
+ end
+ end
+
+ return if files.empty?
+
+ # save attachments
+ files.each do |f|
+ begin
+ txaction.attach(Attachment.generate(current_user, f))
+ rescue Attachment::MaxSizeExceeded => e
+ logger.error(e.message)
+ # FIXME: do some proper error handling
+ end
+ end
+
+
+ # delete attachments
+ if params[:deleted_attachments]
+ params[:deleted_attachments].split.each do |attachment_id|
+ if attachment = current_user.attachments.find_by_id(attachment_id)
+ txaction.detach(attachment)
+ attachment.destroy
+ end
+ end
+ end
+
+ # see if they specified an inbox attachment
+ unless params['inbox_attachment'].blank?
+ if inbox_attachment = current_user.inbox_attachment(params['inbox_attachment'])
+ txaction.attach(inbox_attachment.attachment)
+ inbox_attachment.destroy
+ end
+ end
+ txaction.save
+ end
+
+ def _render_response(action)
+ respond_to do |format|
+ format.json do
+ if flash[:error]
+ render :json => {:error => flash[:error]}, :status => :bad_request
+ else
+ render :json => {}, :status => :ok
+ end
+ end
+ end
+ end
+
+ def account
+ @account ||= current_user.accounts.find_by_id_for_user(params[:account_id]).tap do |account|
+ if account.nil?
+ render :nothing => true, :status => :forbidden
+ end
+ end
+ end
+
+ def txaction
+ @txaction ||= Txaction.find_by_id(params[:id]).tap do |txaction|
+ # make sure user owns this txaction
+ if txaction.nil? || (not current_user.can_edit_txaction?(txaction))
+ render :nothing => true, :status => :forbidden
+ return nil
+ end
+ end
+ end
+end
229 app/controllers/uploads_controller.rb
@@ -0,0 +1,229 @@
+class UploadsController < ApplicationController
+ before_filter :check_authentication
+ before_filter :only_allow_html, :only => [:new, :manual, :ssu]
+ before_filter :require_ac, :only => [:error, :security]
+
+ # FIXME: we should not be declaring stylesheets in the controller
+ stylesheet 'upload-accounts'
+
+ WEB_UPLOADER_CLIENT_NAME = 'Wesabe-WebUploader'
+ WEB_UPLOADER_CLIENT_VERSION = '1.1'
+
+ def index
+ if params[:account_id]
+ if @account = current_user.account(params[:account_id])
+ @uploads = @account.uploads.reject {|u| u.txaction_count_for_account(@account) == 0}
+ @last_balance = @account.last_balance
+ return render
+ end
+ end
+ redirect_to root_url # if all else fails, just send the user home
+ end
+
+ # show the file associated with this upload
+ def destroy
+ if upload = Upload.find_by_guid(params[:id])
+ if upload.owned_by_user?(current_user)
+ upload.destroy_for_account(current_user.account(params[:account_id]))
+ end
+ end
+ render :nothing => true
+ end
+
+ def new
+ @featured_fis = FinancialInst.find_all_by_featured(true, :order => "name")
+ end
+
+ def choose
+ return unless financial_inst
+
+ if ssu_enabled? && financial_inst.ssu_support?(current_user)
+ # If possible, send users to SSU
+ @path = ssu_new_upload_path(:fi => financial_inst)
+ end
+
+ # Worst case (or by request), manual uploads
+ if params[:upload_type] != "manual" && @path
+ redirect_to @path
+ else
+ redirect_to manual_uploads_path(:fi => financial_inst)
+ end
+ end
+
+ def manual
+ @financial_inst = financial_inst
+ @need_more_info = params[:need_more_info]
+ @show_form = "display:none;" unless flash[:error]
+ end