ledger_web 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/LICENSE +7 -0
- data/README.md +140 -0
- data/Rakefile +11 -0
- data/bin/ledger_web +14 -0
- data/ledger_web.gemspec +26 -0
- data/lib/ledger_web/app.rb +69 -0
- data/lib/ledger_web/config.rb +87 -0
- data/lib/ledger_web/db/migrate/20111226180900_initial_schema.rb +38 -0
- data/lib/ledger_web/db/migrate/20111231132900_add_views.rb +37 -0
- data/lib/ledger_web/db.rb +54 -0
- data/lib/ledger_web/helpers.rb +62 -0
- data/lib/ledger_web/public/bootstrap-dropdown.js +55 -0
- data/lib/ledger_web/public/bootstrap.min.css +356 -0
- data/lib/ledger_web/public/codemirror/keymap/emacs.js +29 -0
- data/lib/ledger_web/public/codemirror/keymap/vim.js +76 -0
- data/lib/ledger_web/public/codemirror/lib/codemirror.css +104 -0
- data/lib/ledger_web/public/codemirror/lib/codemirror.js +2761 -0
- data/lib/ledger_web/public/codemirror/lib/util/dialog.css +23 -0
- data/lib/ledger_web/public/codemirror/lib/util/dialog.js +63 -0
- data/lib/ledger_web/public/codemirror/lib/util/foldcode.js +66 -0
- data/lib/ledger_web/public/codemirror/lib/util/formatting.js +291 -0
- data/lib/ledger_web/public/codemirror/lib/util/javascript-hint.js +83 -0
- data/lib/ledger_web/public/codemirror/lib/util/overlay.js +51 -0
- data/lib/ledger_web/public/codemirror/lib/util/runmode.js +27 -0
- data/lib/ledger_web/public/codemirror/lib/util/search.js +114 -0
- data/lib/ledger_web/public/codemirror/lib/util/searchcursor.js +117 -0
- data/lib/ledger_web/public/codemirror/lib/util/simple-hint.css +16 -0
- data/lib/ledger_web/public/codemirror/lib/util/simple-hint.js +66 -0
- data/lib/ledger_web/public/codemirror/mode/clike/clike.js +249 -0
- data/lib/ledger_web/public/codemirror/mode/clike/index.html +101 -0
- data/lib/ledger_web/public/codemirror/mode/clojure/clojure.js +207 -0
- data/lib/ledger_web/public/codemirror/mode/clojure/index.html +66 -0
- data/lib/ledger_web/public/codemirror/mode/coffeescript/LICENSE +22 -0
- data/lib/ledger_web/public/codemirror/mode/coffeescript/coffeescript.js +325 -0
- data/lib/ledger_web/public/codemirror/mode/coffeescript/index.html +721 -0
- data/lib/ledger_web/public/codemirror/mode/css/css.js +124 -0
- data/lib/ledger_web/public/codemirror/mode/css/index.html +55 -0
- data/lib/ledger_web/public/codemirror/mode/diff/diff.css +3 -0
- data/lib/ledger_web/public/codemirror/mode/diff/diff.js +13 -0
- data/lib/ledger_web/public/codemirror/mode/diff/index.html +99 -0
- data/lib/ledger_web/public/codemirror/mode/gfm/gfm.js +108 -0
- data/lib/ledger_web/public/codemirror/mode/gfm/index.html +47 -0
- data/lib/ledger_web/public/codemirror/mode/groovy/groovy.js +210 -0
- data/lib/ledger_web/public/codemirror/mode/groovy/index.html +71 -0
- data/lib/ledger_web/public/codemirror/mode/haskell/haskell.js +242 -0
- data/lib/ledger_web/public/codemirror/mode/haskell/index.html +60 -0
- data/lib/ledger_web/public/codemirror/mode/htmlembedded/htmlembedded.js +68 -0
- data/lib/ledger_web/public/codemirror/mode/htmlembedded/index.html +49 -0
- data/lib/ledger_web/public/codemirror/mode/htmlmixed/htmlmixed.js +83 -0
- data/lib/ledger_web/public/codemirror/mode/htmlmixed/index.html +51 -0
- data/lib/ledger_web/public/codemirror/mode/javascript/index.html +77 -0
- data/lib/ledger_web/public/codemirror/mode/javascript/javascript.js +360 -0
- data/lib/ledger_web/public/codemirror/mode/jinja2/index.html +37 -0
- data/lib/ledger_web/public/codemirror/mode/jinja2/jinja2.js +42 -0
- data/lib/ledger_web/public/codemirror/mode/lua/index.html +72 -0
- data/lib/ledger_web/public/codemirror/mode/lua/lua.js +140 -0
- data/lib/ledger_web/public/codemirror/mode/markdown/index.html +339 -0
- data/lib/ledger_web/public/codemirror/mode/markdown/markdown.js +242 -0
- data/lib/ledger_web/public/codemirror/mode/ntriples/index.html +32 -0
- data/lib/ledger_web/public/codemirror/mode/ntriples/ntriples.js +172 -0
- data/lib/ledger_web/public/codemirror/mode/pascal/LICENSE +7 -0
- data/lib/ledger_web/public/codemirror/mode/pascal/index.html +48 -0
- data/lib/ledger_web/public/codemirror/mode/pascal/pascal.js +138 -0
- data/lib/ledger_web/public/codemirror/mode/perl/LICENSE +19 -0
- data/lib/ledger_web/public/codemirror/mode/perl/index.html +62 -0
- data/lib/ledger_web/public/codemirror/mode/perl/perl.js +816 -0
- data/lib/ledger_web/public/codemirror/mode/php/index.html +48 -0
- data/lib/ledger_web/public/codemirror/mode/php/php.js +120 -0
- data/lib/ledger_web/public/codemirror/mode/plsql/index.html +62 -0
- data/lib/ledger_web/public/codemirror/mode/plsql/plsql.js +217 -0
- data/lib/ledger_web/public/codemirror/mode/python/LICENSE.txt +21 -0
- data/lib/ledger_web/public/codemirror/mode/python/index.html +122 -0
- data/lib/ledger_web/public/codemirror/mode/python/python.js +333 -0
- data/lib/ledger_web/public/codemirror/mode/r/LICENSE +24 -0
- data/lib/ledger_web/public/codemirror/mode/r/index.html +73 -0
- data/lib/ledger_web/public/codemirror/mode/r/r.js +141 -0
- data/lib/ledger_web/public/codemirror/mode/rpm/changes/changes.js +19 -0
- data/lib/ledger_web/public/codemirror/mode/rpm/changes/index.html +53 -0
- data/lib/ledger_web/public/codemirror/mode/rpm/spec/index.html +99 -0
- data/lib/ledger_web/public/codemirror/mode/rpm/spec/spec.css +5 -0
- data/lib/ledger_web/public/codemirror/mode/rpm/spec/spec.js +66 -0
- data/lib/ledger_web/public/codemirror/mode/rst/index.html +525 -0
- data/lib/ledger_web/public/codemirror/mode/rst/rst.js +326 -0
- data/lib/ledger_web/public/codemirror/mode/ruby/LICENSE +24 -0
- data/lib/ledger_web/public/codemirror/mode/ruby/index.html +171 -0
- data/lib/ledger_web/public/codemirror/mode/ruby/ruby.js +195 -0
- data/lib/ledger_web/public/codemirror/mode/rust/index.html +48 -0
- data/lib/ledger_web/public/codemirror/mode/rust/rust.js +411 -0
- data/lib/ledger_web/public/codemirror/mode/scheme/index.html +64 -0
- data/lib/ledger_web/public/codemirror/mode/scheme/scheme.js +202 -0
- data/lib/ledger_web/public/codemirror/mode/smalltalk/index.html +55 -0
- data/lib/ledger_web/public/codemirror/mode/smalltalk/smalltalk.js +139 -0
- data/lib/ledger_web/public/codemirror/mode/sparql/index.html +40 -0
- data/lib/ledger_web/public/codemirror/mode/sparql/sparql.js +143 -0
- data/lib/ledger_web/public/codemirror/mode/stex/index.html +95 -0
- data/lib/ledger_web/public/codemirror/mode/stex/stex.js +167 -0
- data/lib/ledger_web/public/codemirror/mode/tiddlywiki/index.html +183 -0
- data/lib/ledger_web/public/codemirror/mode/tiddlywiki/tiddlywiki.css +21 -0
- data/lib/ledger_web/public/codemirror/mode/tiddlywiki/tiddlywiki.js +374 -0
- data/lib/ledger_web/public/codemirror/mode/velocity/index.html +103 -0
- data/lib/ledger_web/public/codemirror/mode/velocity/velocity.js +146 -0
- data/lib/ledger_web/public/codemirror/mode/xml/index.html +44 -0
- data/lib/ledger_web/public/codemirror/mode/xml/xml.js +252 -0
- data/lib/ledger_web/public/codemirror/mode/xmlpure/index.html +59 -0
- data/lib/ledger_web/public/codemirror/mode/xmlpure/xmlpure.js +485 -0
- data/lib/ledger_web/public/codemirror/mode/yaml/index.html +67 -0
- data/lib/ledger_web/public/codemirror/mode/yaml/yaml.js +95 -0
- data/lib/ledger_web/public/codemirror/theme/cobalt.css +18 -0
- data/lib/ledger_web/public/codemirror/theme/eclipse.css +25 -0
- data/lib/ledger_web/public/codemirror/theme/elegant.css +10 -0
- data/lib/ledger_web/public/codemirror/theme/monokai.css +28 -0
- data/lib/ledger_web/public/codemirror/theme/neat.css +9 -0
- data/lib/ledger_web/public/codemirror/theme/night.css +21 -0
- data/lib/ledger_web/public/codemirror/theme/rubyblue.css +21 -0
- data/lib/ledger_web/public/jquery-1.7.1.min.js +4 -0
- data/lib/ledger_web/public/jquery.tablesorter.min.js +4 -0
- data/lib/ledger_web/public/ledger.css +14 -0
- data/lib/ledger_web/report.rb +187 -0
- data/lib/ledger_web/reports/savings_rate.erb +49 -0
- data/lib/ledger_web/version.rb +3 -0
- data/lib/ledger_web/views/error.erb +5 -0
- data/lib/ledger_web/views/help.erb +6 -0
- data/lib/ledger_web/views/layout.erb +44 -0
- data/lib/ledger_web/views/table.erb +31 -0
- data/lib/ledger_web/watcher.rb +37 -0
- data/lib/ledger_web.rb +20 -0
- metadata +229 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2011 Peter Keen <pete@bugsplat.info>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
Ledger Web
|
2
|
+
----------
|
3
|
+
|
4
|
+
Ledger Web is a web-based, postgresql-backed reporting system for the [Ledger](http://www.ledger-cli.org) command-line accounting system.
|
5
|
+
It is intended to be completely flexible, allowing you to write whatever reports you want. Note that Ledger Web requires **PostgreSQL version 9.0 or greater**.
|
6
|
+
|
7
|
+
To install:
|
8
|
+
|
9
|
+
$ gem install ledger_web
|
10
|
+
$ createdb ledger
|
11
|
+
|
12
|
+
To run:
|
13
|
+
|
14
|
+
$ ledger_web
|
15
|
+
|
16
|
+
From there, open up http://localhost:9090 in your browser and poke around. You'll see a few example reports.
|
17
|
+
|
18
|
+
#### Configuration
|
19
|
+
|
20
|
+
Configuring Ledger Web is pretty simple. Create a file at `~/.ledger_web/config.rb` that looks something like this:
|
21
|
+
|
22
|
+
|
23
|
+
LedgerWeb::Config.new do |config|
|
24
|
+
config.set :database_url, "postgres://localhost/ledger"
|
25
|
+
end
|
26
|
+
|
27
|
+
`:database_url` should point at your database instance. It doesn't have to be local, but the configured user needs to be able to alter the schema. There are a bunch more settings that you can set:
|
28
|
+
|
29
|
+
* `:index_report` is the report that Ledger Web will redirect your browser to when you open it up the first time. Defaults to `:help`
|
30
|
+
* `:port` is the port that Ledger Web will run on. Defaults to `9090`
|
31
|
+
* `:ledger_file` is the file that Ledger Web will read. Defaults to the `LEDGER_FILE` environment variable
|
32
|
+
* `:ledger_bin_path` is the path to the ledger binary. Defaults to finding it in the `PATH`
|
33
|
+
|
34
|
+
#### Writing Reports
|
35
|
+
|
36
|
+
Reports are just HTML ERB files that live in `~/.ledger_web/reports`. Ledger Web provides a few useful helpers that let you easily define SQL queries. Here's an example report:
|
37
|
+
|
38
|
+
<% @query = query do %>
|
39
|
+
select
|
40
|
+
xtn_month,
|
41
|
+
account,
|
42
|
+
sum(amount)
|
43
|
+
from
|
44
|
+
ledger
|
45
|
+
where
|
46
|
+
(account ~ 'Income'
|
47
|
+
or account ~ 'Expenses')
|
48
|
+
and xtn_date between :from and :to
|
49
|
+
group by
|
50
|
+
xtn_month,
|
51
|
+
account
|
52
|
+
<% end %>
|
53
|
+
<%= table @query %>
|
54
|
+
|
55
|
+
The `query` helper takes a block of SQL and returns a `LedgerWeb::Report` instance. It can take a few options:
|
56
|
+
|
57
|
+
* `:pivot` is the name of a column to pivot the report on.
|
58
|
+
* `:pivot_sort_order` says how to order the resulting pivoted columns. Can be `asc` or `desc`. Defaults to `asc`.
|
59
|
+
|
60
|
+
Ledger Web uses [Twitter Bootstrap](http://twitter.github.com/bootstrap) for formatting, so you can use whatever you want to format your reports from there.
|
61
|
+
|
62
|
+
The `table` helper takes a query produced by the `query` helper and some options and builds an HTML table. Also, it can take a `:links` option which will linkify values in the table. Here's an example:
|
63
|
+
|
64
|
+
:links => {"Account" => "/reports/register?account=:1"}
|
65
|
+
|
66
|
+
This says that every value in the `Account` column will be surrounded with an `<a>` tag pointing at `/reports/register?account=:1`, where `:1` will be replaced by the value in column 1 of that particular row. You can also use `:title` in a link template. It will get replaced with the title of the column that is currently getting linked. In this case, `:title` would get replaced with `Account`.
|
67
|
+
|
68
|
+
#### Customizing
|
69
|
+
|
70
|
+
Any ruby files in `~/.ledger_web/lib` will be loaded at startup so you can define your own classes, modules, whatever you want. In addition, you can put [Sequel migrations](http://sequel.rubyforge.org/rdoc/files/doc/migration_rdoc.html) in `~/.ledger_web/migrate` and they'll get applied as necessary at startup.
|
71
|
+
|
72
|
+
#### Hooks
|
73
|
+
|
74
|
+
Ledger Web provides several different hooks that get run during the data load process.
|
75
|
+
|
76
|
+
* `:before_insert_row` gets the Sequel database and the current row immediatley before insertion. Row is to be modified in place.
|
77
|
+
* `:after_insert_row` gets the Sequel database and the current row. Row modifications don't matter.
|
78
|
+
* `:before_load` gets the Sequel database
|
79
|
+
* `:after_load` gets the Sequel database
|
80
|
+
|
81
|
+
To define a hook, put something like this in your config file:
|
82
|
+
|
83
|
+
config.add_hook :before_insert_row do |db, row|
|
84
|
+
# modify the row in place
|
85
|
+
end
|
86
|
+
|
87
|
+
#### Schema
|
88
|
+
|
89
|
+
The base table is named `ledger`. Here's the DDL:
|
90
|
+
|
91
|
+
create table ledger (
|
92
|
+
xtn_id integer, -- line number of the first line of the transaction
|
93
|
+
xtn_date date, -- date of the transaction
|
94
|
+
xtn_month date, -- month pre-extracted from the date
|
95
|
+
xtn_year date, -- year pre-extracted from the date
|
96
|
+
checknum text, -- check number (code)
|
97
|
+
note text, -- payee
|
98
|
+
account text, -- account name
|
99
|
+
commodity text, -- commodity
|
100
|
+
amount number, -- amount
|
101
|
+
tags text, -- any tags attached to the transaction
|
102
|
+
virtual boolean, -- if the transaction is virutal or not
|
103
|
+
cleared boolean -- if the transaction is cleared or not
|
104
|
+
)
|
105
|
+
|
106
|
+
In addition, there's a few predefined views:
|
107
|
+
|
108
|
+
create view accounts_months as
|
109
|
+
with
|
110
|
+
_a as (select account from ledger group by account),
|
111
|
+
_m as (select xtn_month from ledger group by xtn_month)
|
112
|
+
select
|
113
|
+
account,
|
114
|
+
xtn_month
|
115
|
+
from
|
116
|
+
_a cross join _m
|
117
|
+
;
|
118
|
+
|
119
|
+
create view accounts_days as
|
120
|
+
with
|
121
|
+
_a as (select account from ledger group by account),
|
122
|
+
_d as (select xtn_date from ledger group by xtn_date)
|
123
|
+
select
|
124
|
+
account,
|
125
|
+
xtn_date
|
126
|
+
from
|
127
|
+
_a cross join _d
|
128
|
+
;
|
129
|
+
|
130
|
+
create view accounts_years as
|
131
|
+
with
|
132
|
+
_a as (select account from ledger group by account),
|
133
|
+
_y as (select xtn_year from ledger group by xtn_year)
|
134
|
+
select
|
135
|
+
account,
|
136
|
+
xtn_year
|
137
|
+
from
|
138
|
+
_a cross join _y
|
139
|
+
;
|
140
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
require "ledger_web/version"
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
task :build do
|
6
|
+
system "gem build ledger_web.gemspec"
|
7
|
+
end
|
8
|
+
|
9
|
+
task :release => :build do
|
10
|
+
system "gem push ledger_web-#{LedgerWeb::VERSION}.gem"
|
11
|
+
end
|
data/bin/ledger_web
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'ledger_web'
|
7
|
+
rescue LoadError => e
|
8
|
+
path = File.expand_path '../../lib', __FILE__
|
9
|
+
$:.unshift(path) if File.directory?(path) && !$:.include?(path)
|
10
|
+
require 'ledger_web'
|
11
|
+
end
|
12
|
+
|
13
|
+
LedgerWeb::Watcher.run!
|
14
|
+
LedgerWeb::Application.run!(:port => CONFIG.get(:port))
|
data/ledger_web.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require 'ledger_web/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ledger_web"
|
7
|
+
s.version = LedgerWeb::VERSION
|
8
|
+
s.date = "2011-12-31"
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Pete Keen"]
|
11
|
+
s.email = ["pete@bugsplat.info"]
|
12
|
+
s.homepage = "https://github.com/peterkeen/ledger-web"
|
13
|
+
s.summary = %q{A web-based, sql-backed front-end for the Ledger command-line accounting system}
|
14
|
+
s.description = %q{Allows arbitrary reporting on your ledger using easy-to-write SQL queries}
|
15
|
+
|
16
|
+
s.add_dependency("pg")
|
17
|
+
s.add_dependency("sequel")
|
18
|
+
s.add_dependency("directory_watcher")
|
19
|
+
s.add_dependency("sinatra")
|
20
|
+
s.add_dependency("sinatra-session")
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra/session'
|
4
|
+
|
5
|
+
module LedgerWeb
|
6
|
+
class Application < Sinatra::Base
|
7
|
+
register Sinatra::Session
|
8
|
+
set :session_secret, CONFIG.get(:session_secret)
|
9
|
+
set :session_expire, CONFIG.get(:session_expire)
|
10
|
+
set :views, CONFIG.get(:report_directories) + [File.join(File.dirname(__FILE__), 'views')]
|
11
|
+
set :reload_templates, true
|
12
|
+
|
13
|
+
helpers LedgerWeb::Helpers
|
14
|
+
|
15
|
+
def find_template(views, name, engine, &block)
|
16
|
+
Array(views).each { |v| super(v, name, engine, &block) }
|
17
|
+
end
|
18
|
+
|
19
|
+
before do
|
20
|
+
if not session?
|
21
|
+
session_start!
|
22
|
+
today = Date.today
|
23
|
+
session[:from] = Date.new(today.year - 1, today.month, today.day)
|
24
|
+
session[:to] = today
|
25
|
+
end
|
26
|
+
Report.session = session
|
27
|
+
Report.params = params
|
28
|
+
|
29
|
+
@reports = find_all_reports
|
30
|
+
end
|
31
|
+
|
32
|
+
post '/update-date-range' do
|
33
|
+
|
34
|
+
if params[:reset]
|
35
|
+
today = Date.today
|
36
|
+
session[:from] = Date.new(today.year - 1, today.month, today.day).strftime('%Y/%m/%d')
|
37
|
+
session[:to] = today.strftime('%Y/%m/%d')
|
38
|
+
else
|
39
|
+
session[:from] = Date.strptime(params[:from], '%Y/%m/%d').strftime('%Y/%m/%d')
|
40
|
+
session[:to] = Date.strptime(params[:to], '%Y/%m/%d').strftime('%Y/%m/%d')
|
41
|
+
end
|
42
|
+
|
43
|
+
redirect back
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/reports/:name' do
|
47
|
+
begin
|
48
|
+
erb params[:name].to_sym
|
49
|
+
rescue Exception => e
|
50
|
+
@error = e
|
51
|
+
erb :error
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
get '/' do
|
56
|
+
index_report = CONFIG.get :index_report
|
57
|
+
if index_report
|
58
|
+
redirect "/reports/#{index_report.to_s}"
|
59
|
+
else
|
60
|
+
redirect '/help'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
get '/help' do
|
65
|
+
erb :help
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module LedgerWeb
|
2
|
+
|
3
|
+
class Config
|
4
|
+
attr_reader :vars, :hooks
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@vars = {}
|
8
|
+
@hooks = {}
|
9
|
+
|
10
|
+
if block_given?
|
11
|
+
yield self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def set(key, value)
|
16
|
+
@vars[key] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(key)
|
20
|
+
@vars[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_hook(phase, &block)
|
24
|
+
_add_hook(phase, block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def _add_hook(phase, hook)
|
28
|
+
@hooks[phase] ||= []
|
29
|
+
@hooks[phase] << hook
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_hooks(phase, data)
|
33
|
+
if @hooks.has_key? phase
|
34
|
+
@hooks[phase].each do |hook|
|
35
|
+
hook.call(data)
|
36
|
+
end
|
37
|
+
return data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def override_with(config)
|
42
|
+
config.vars.each do |key, value|
|
43
|
+
set key, value
|
44
|
+
end
|
45
|
+
|
46
|
+
config.hooks.each do |phase, hooks|
|
47
|
+
hooks.each do |hook|
|
48
|
+
_add_hook phase, hook
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from_file(filename)
|
54
|
+
File.open(filename) do |file|
|
55
|
+
return eval(file.read, nil, filename)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
CONFIG = LedgerWeb::Config.new do |config|
|
62
|
+
config.set :database_url, "postgres://localhost/ledger"
|
63
|
+
config.set :port, "9090"
|
64
|
+
config.set :ledger_file, ENV['LEDGER_FILE']
|
65
|
+
config.set :report_directories, ["#{File.dirname(__FILE__)}/reports"]
|
66
|
+
config.set :session_secret, 'SomethingSecretThisWayPassed'
|
67
|
+
config.set :session_expire, 60*60
|
68
|
+
config.set :watch_interval, 5
|
69
|
+
config.set :watch_stable_count, 3
|
70
|
+
config.set :ledger_bin_path, "ledger"
|
71
|
+
|
72
|
+
config.set :ledger_format, "%(quoted(xact.beg_line)),%(quoted(date)),%(quoted(payee)),%(quoted(account)),%(quoted(commodity)),%(quoted(quantity(scrub(display_amount)))),%(quoted(cleared)),%(quoted(virtual)),%(quoted(join(note | xact.note)))\n"
|
73
|
+
|
74
|
+
ledger_web_dir = "#{ENV['HOME']}/.ledger_web"
|
75
|
+
|
76
|
+
if File.directory? ledger_web_dir
|
77
|
+
if File.directory? "#{ledger_web_dir}/reports"
|
78
|
+
dirs = config.get(:report_directories)
|
79
|
+
dirs.unshift "#{ledger_web_dir}/reports"
|
80
|
+
config.set :report_directories, dirs
|
81
|
+
end
|
82
|
+
|
83
|
+
if File.exists? "#{ledger_web_dir}/config.rb"
|
84
|
+
config.override_with(LedgerWeb::Config.from_file("#{ledger_web_dir}/config.rb"))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
create_table(:ledger, :ignore_index_errors=>true) do
|
4
|
+
Date :xtn_date
|
5
|
+
String :checknum, :text=>true
|
6
|
+
String :note, :text=>true
|
7
|
+
String :account, :text=>true
|
8
|
+
String :commodity, :text=>true
|
9
|
+
BigDecimal :amount
|
10
|
+
String :tags, :text=>true
|
11
|
+
Date :xtn_month
|
12
|
+
Date :xtn_year
|
13
|
+
TrueClass :virtual
|
14
|
+
Integer :xtn_id
|
15
|
+
TrueClass :cleared
|
16
|
+
|
17
|
+
index [:account]
|
18
|
+
index [:commodity]
|
19
|
+
index [:note]
|
20
|
+
index [:tags]
|
21
|
+
index [:virtual]
|
22
|
+
index [:xtn_date]
|
23
|
+
index [:xtn_month]
|
24
|
+
index [:xtn_year]
|
25
|
+
end
|
26
|
+
|
27
|
+
create_table(:schema_info) do
|
28
|
+
String :filename, :text=>true, :null=>false
|
29
|
+
|
30
|
+
primary_key [:filename]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
down do
|
35
|
+
drop_table(:ledger, :schema_info)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_or_replace_view(:accounts_days, <<HERE)
|
4
|
+
with
|
5
|
+
_a as (select account from ledger group by account),
|
6
|
+
_d as (select xtn_date from ledger group by xtn_date)
|
7
|
+
select
|
8
|
+
account,
|
9
|
+
xtn_date
|
10
|
+
from
|
11
|
+
_a cross join _d
|
12
|
+
HERE
|
13
|
+
|
14
|
+
create_or_replace_view(:accounts_months, <<HERE)
|
15
|
+
with
|
16
|
+
_a as (select account from ledger group by account),
|
17
|
+
_m as (select xtn_month from ledger group by xtn_month)
|
18
|
+
select
|
19
|
+
account,
|
20
|
+
xtn_month
|
21
|
+
from
|
22
|
+
_a cross join _m
|
23
|
+
HERE
|
24
|
+
|
25
|
+
create_or_replace_view(:accounts_years, <<HERE)
|
26
|
+
with
|
27
|
+
_a as (select account from ledger group by account),
|
28
|
+
_y as (select xtn_year from ledger group by xtn_year)
|
29
|
+
select
|
30
|
+
account,
|
31
|
+
xtn_year
|
32
|
+
from
|
33
|
+
_a cross join _y
|
34
|
+
HERE
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'sequel/extensions/migration'
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
DB = Sequel.connect(CONFIG.get(:database_url))
|
6
|
+
|
7
|
+
Sequel::Migrator.apply(DB, File.join(File.dirname(__FILE__), "db/migrate"))
|
8
|
+
|
9
|
+
home_migrations = File.join(ENV['HOME'], '.ledger_web', 'migrate')
|
10
|
+
if File.directory?(home_migrations)
|
11
|
+
Sequel::Migrator.run(DB, home_migrations, :table => "user_schema_changes")
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_database
|
15
|
+
ledger_format = CONFIG.get :ledger_format
|
16
|
+
ledger_bin_path = CONFIG.get :ledger_bin_path
|
17
|
+
ledger_file = CONFIG.get :ledger_file
|
18
|
+
|
19
|
+
# dump ledger to tempfile
|
20
|
+
print " dumping ledger to file...."
|
21
|
+
file = Tempfile.new('ledger')
|
22
|
+
system "#{ledger_bin_path} -f #{ledger_file} --format='#{ledger_format}' reg > #{file.path}"
|
23
|
+
puts "done"
|
24
|
+
counter = 0
|
25
|
+
DB.transaction do
|
26
|
+
|
27
|
+
CONFIG.run_hooks(:before_load, DB)
|
28
|
+
|
29
|
+
print " clearing ledger table...."
|
30
|
+
DB["DELETE FROM ledger"].delete
|
31
|
+
puts "done"
|
32
|
+
|
33
|
+
print " loading into database...."
|
34
|
+
CSV.foreach(file.path) do |row|
|
35
|
+
counter += 1
|
36
|
+
row = Hash[*[:xtn_id, :xtn_date, :note, :account, :commodity, :amount, :cleared, :virtual, :tags].zip(row).flatten]
|
37
|
+
|
38
|
+
xtn_date = Date.strptime(row[:xtn_date], '%Y/%m/%d')
|
39
|
+
|
40
|
+
row[:xtn_month] = xtn_date.strftime('%Y/%m/01')
|
41
|
+
row[:xtn_year] = xtn_date.strftime('%Y/01/01')
|
42
|
+
|
43
|
+
row = CONFIG.run_hooks(:before_insert_row, row)
|
44
|
+
DB[:ledger].insert(row)
|
45
|
+
CONFIG.run_hooks(:after_insert_row, row)
|
46
|
+
end
|
47
|
+
|
48
|
+
CONFIG.run_hooks(:after_load, DB)
|
49
|
+
end
|
50
|
+
print " analyzing ledger table"
|
51
|
+
DB.fetch('VACUUM ANALYZE ledger')
|
52
|
+
puts "done"
|
53
|
+
counter
|
54
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module LedgerWeb
|
2
|
+
module Helpers
|
3
|
+
def partial (template, locals = {})
|
4
|
+
erb(template, :layout => false, :locals => locals)
|
5
|
+
end
|
6
|
+
|
7
|
+
def table(report, options = {})
|
8
|
+
links = options[:links] || {}
|
9
|
+
partial(:table, :report => report, :links => links)
|
10
|
+
end
|
11
|
+
|
12
|
+
def query(options={}, &block)
|
13
|
+
q = erb_with_output_buffer block
|
14
|
+
report = LedgerWeb::Report.from_query(q)
|
15
|
+
if options[:pivot]
|
16
|
+
report = report.pivot(options[:pivot], options[:pivot_sort_order])
|
17
|
+
end
|
18
|
+
return report
|
19
|
+
end
|
20
|
+
|
21
|
+
def erb_with_output_buffer(buf = '', block)
|
22
|
+
@_out_buf, old_buffer = buf, @_out_buf
|
23
|
+
block.call
|
24
|
+
@_out_buf
|
25
|
+
ensure
|
26
|
+
@_out_buf = old_buffer
|
27
|
+
end
|
28
|
+
|
29
|
+
def expect(expected)
|
30
|
+
not_present = []
|
31
|
+
expected.each do |key|
|
32
|
+
if not params.has_key? key
|
33
|
+
not_present << key
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if not_present.length > 0
|
38
|
+
raise "Missing params: #{not_present.join(', ')}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def linkify(links, row, value, display_value)
|
43
|
+
links.each do |key, val|
|
44
|
+
if key.is_a? String
|
45
|
+
key = /^#{key}$/
|
46
|
+
end
|
47
|
+
|
48
|
+
if key.match(value[1].title.to_s)
|
49
|
+
url = String.new(links[key])
|
50
|
+
row.each_with_index do |v,i|
|
51
|
+
url.gsub!(":#{i}", v[0].to_s)
|
52
|
+
end
|
53
|
+
|
54
|
+
url.gsub!(':title', value[1].title.to_s)
|
55
|
+
display_value = "<a href='#{url}'>#{display_value}</a>"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
display_value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
/* ============================================================
|
2
|
+
* bootstrap-dropdown.js v1.4.0
|
3
|
+
* http://twitter.github.com/bootstrap/javascript.html#dropdown
|
4
|
+
* ============================================================
|
5
|
+
* Copyright 2011 Twitter, Inc.
|
6
|
+
*
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
* you may not use this file except in compliance with the License.
|
9
|
+
* You may obtain a copy of the License at
|
10
|
+
*
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
*
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
* See the License for the specific language governing permissions and
|
17
|
+
* limitations under the License.
|
18
|
+
* ============================================================ */
|
19
|
+
|
20
|
+
|
21
|
+
!function( $ ){
|
22
|
+
|
23
|
+
"use strict"
|
24
|
+
|
25
|
+
/* DROPDOWN PLUGIN DEFINITION
|
26
|
+
* ========================== */
|
27
|
+
|
28
|
+
$.fn.dropdown = function ( selector ) {
|
29
|
+
return this.each(function () {
|
30
|
+
$(this).delegate(selector || d, 'click', function (e) {
|
31
|
+
var li = $(this).parent('li')
|
32
|
+
, isActive = li.hasClass('open')
|
33
|
+
|
34
|
+
clearMenus()
|
35
|
+
!isActive && li.toggleClass('open')
|
36
|
+
return false
|
37
|
+
})
|
38
|
+
})
|
39
|
+
}
|
40
|
+
|
41
|
+
/* APPLY TO STANDARD DROPDOWN ELEMENTS
|
42
|
+
* =================================== */
|
43
|
+
|
44
|
+
var d = 'a.menu, .dropdown-toggle'
|
45
|
+
|
46
|
+
function clearMenus() {
|
47
|
+
$(d).parent('li').removeClass('open')
|
48
|
+
}
|
49
|
+
|
50
|
+
$(function () {
|
51
|
+
$('html').bind("click", clearMenus)
|
52
|
+
$('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' )
|
53
|
+
})
|
54
|
+
|
55
|
+
}( window.jQuery || window.ender );
|