lewt 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +22 -0
  3. data/README.md +238 -0
  4. data/bin/lewt +10 -0
  5. data/lib/config/customers.yml +33 -0
  6. data/lib/config/enterprise.yml +54 -0
  7. data/lib/config/settings.yml +10 -0
  8. data/lib/config/templates/invoice.html.liquid +63 -0
  9. data/lib/config/templates/invoice.text.liquid +40 -0
  10. data/lib/config/templates/meta.html.liquid +0 -0
  11. data/lib/config/templates/meta.text.liquid +1 -0
  12. data/lib/config/templates/metastat.html.liquid +2 -0
  13. data/lib/config/templates/metastat.text.liquid +8 -0
  14. data/lib/config/templates/report.html.liquid +29 -0
  15. data/lib/config/templates/report.text.liquid +15 -0
  16. data/lib/config/templates/style.css +461 -0
  17. data/lib/extension.rb +158 -0
  18. data/lib/extensions/calendar-timekeeping/apple_extractor.rb +63 -0
  19. data/lib/extensions/calendar-timekeeping/calendar-timekeeping.rb +65 -0
  20. data/lib/extensions/calendar-timekeeping/extractor.rb +62 -0
  21. data/lib/extensions/calendar-timekeeping/gcal_extractor.rb +61 -0
  22. data/lib/extensions/calendar-timekeeping/ical_extractor.rb +52 -0
  23. data/lib/extensions/liquid-renderer.rb +106 -0
  24. data/lib/extensions/metastat/metamath.rb +108 -0
  25. data/lib/extensions/metastat/metastat.rb +161 -0
  26. data/lib/extensions/simple-expenses.rb +112 -0
  27. data/lib/extensions/simple-invoices.rb +93 -0
  28. data/lib/extensions/simple-milestones.rb +102 -0
  29. data/lib/extensions/simple-reports.rb +81 -0
  30. data/lib/extensions/store.rb +81 -0
  31. data/lib/lewt.rb +233 -0
  32. data/lib/lewt_book.rb +29 -0
  33. data/lib/lewt_ledger.rb +149 -0
  34. data/lib/lewtopts.rb +170 -0
  35. data/tests/LEWT Schedule.ics +614 -0
  36. data/tests/expenses.csv +1 -0
  37. data/tests/milestones.csv +1 -0
  38. data/tests/run_tests.rb +14 -0
  39. data/tests/tc_Billing.rb +29 -0
  40. data/tests/tc_CalExt.rb +44 -0
  41. data/tests/tc_Lewt.rb +37 -0
  42. data/tests/tc_LewtExtension.rb +31 -0
  43. data/tests/tc_LewtLedger.rb +38 -0
  44. data/tests/tc_LewtOpts.rb +26 -0
  45. metadata +158 -0
@@ -0,0 +1,461 @@
1
+ body {
2
+ font-family: Helvetica, arial, sans-serif;
3
+ font-size: 14px;
4
+ line-height: 1.6;
5
+ padding-top: 10px;
6
+ padding-bottom: 10px;
7
+ background-color: white;
8
+ padding: 30px;
9
+ color: #333;
10
+ }
11
+
12
+ body > *:first-child {
13
+ margin-top: 0 !important;
14
+ }
15
+
16
+ body > *:last-child {
17
+ margin-bottom: 0 !important;
18
+ }
19
+
20
+ a {
21
+ color: #4183C4;
22
+ text-decoration: none;
23
+ }
24
+
25
+ a.absent {
26
+ color: #cc0000;
27
+ }
28
+
29
+ a.anchor {
30
+ display: block;
31
+ padding-left: 30px;
32
+ margin-left: -30px;
33
+ cursor: pointer;
34
+ position: absolute;
35
+ top: 0;
36
+ left: 0;
37
+ bottom: 0;
38
+ }
39
+
40
+ h1, h2, h3, h4, h5, h6 {
41
+ margin: 20px 0 10px;
42
+ padding: 0;
43
+ font-weight: bold;
44
+ -webkit-font-smoothing: antialiased;
45
+ cursor: text;
46
+ position: relative;
47
+ }
48
+
49
+ h2:first-child, h1:first-child, h1:first-child + h2, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
50
+ margin-top: 0;
51
+ padding-top: 0;
52
+ }
53
+
54
+ h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
55
+ text-decoration: none;
56
+ }
57
+
58
+ h1 tt, h1 code {
59
+ font-size: inherit;
60
+ }
61
+
62
+ h2 tt, h2 code {
63
+ font-size: inherit;
64
+ }
65
+
66
+ h3 tt, h3 code {
67
+ font-size: inherit;
68
+ }
69
+
70
+ h4 tt, h4 code {
71
+ font-size: inherit;
72
+ }
73
+
74
+ h5 tt, h5 code {
75
+ font-size: inherit;
76
+ }
77
+
78
+ h6 tt, h6 code {
79
+ font-size: inherit;
80
+ }
81
+
82
+ h1 {
83
+ font-size: 28px;
84
+ color: black;
85
+ }
86
+
87
+ h2 {
88
+ font-size: 24px;
89
+ border-bottom: 1px solid #cccccc;
90
+ color: black;
91
+ }
92
+
93
+ h3 {
94
+ font-size: 18px;
95
+ }
96
+
97
+ h4 {
98
+ font-size: 16px;
99
+ }
100
+
101
+ h5 {
102
+ font-size: 14px;
103
+ }
104
+
105
+ h6 {
106
+ color: #777777;
107
+ font-size: 14px;
108
+ }
109
+
110
+ p, blockquote, ul, ol, dl, li, table, pre {
111
+ margin: 15px 0;
112
+ }
113
+
114
+ hr {
115
+ /* background: transparent url("http://tinyurl.com/bq5kskr") repeat-x 0 0; */
116
+ border: 0 none;
117
+ color: #cccccc;
118
+ height: 4px;
119
+ padding: 0;
120
+ }
121
+
122
+ body > h2:first-child {
123
+ margin-top: 0;
124
+ padding-top: 0;
125
+ }
126
+
127
+ body > h1:first-child {
128
+ margin-top: 0;
129
+ padding-top: 0;
130
+ }
131
+
132
+ body > h1:first-child + h2 {
133
+ margin-top: 0;
134
+ padding-top: 0;
135
+ }
136
+
137
+ body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
138
+ margin-top: 0;
139
+ padding-top: 0;
140
+ }
141
+
142
+ a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
143
+ margin-top: 0;
144
+ padding-top: 0;
145
+ }
146
+
147
+ h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
148
+ margin-top: 0;
149
+ }
150
+
151
+ li p.first {
152
+ display: inline-block;
153
+ }
154
+
155
+ ul, ol {
156
+ padding-left: 30px;
157
+ }
158
+
159
+ ul :first-child, ol :first-child {
160
+ margin-top: 0;
161
+ }
162
+
163
+ ul :last-child, ol :last-child {
164
+ margin-bottom: 0;
165
+ }
166
+
167
+ dl {
168
+ padding: 0;
169
+ }
170
+
171
+ dl dt {
172
+ font-size: 14px;
173
+ font-weight: bold;
174
+ font-style: italic;
175
+ padding: 0;
176
+ margin: 15px 0 5px;
177
+ }
178
+
179
+ dl dt:first-child {
180
+ padding: 0;
181
+ }
182
+
183
+ dl dt > :first-child {
184
+ margin-top: 0;
185
+ }
186
+
187
+ dl dt > :last-child {
188
+ margin-bottom: 0;
189
+ }
190
+
191
+ dl dd {
192
+ margin: 0 0 15px;
193
+ padding: 0 15px;
194
+ }
195
+
196
+ dl dd > :first-child {
197
+ margin-top: 0;
198
+ }
199
+
200
+ dl dd > :last-child {
201
+ margin-bottom: 0;
202
+ }
203
+
204
+ blockquote {
205
+ border-left: 4px solid #dddddd;
206
+ padding: 0 15px;
207
+ color: #777777;
208
+ }
209
+
210
+ blockquote > :first-child {
211
+ margin-top: 0;
212
+ }
213
+
214
+ blockquote > :last-child {
215
+ margin-bottom: 0;
216
+ }
217
+
218
+ table {
219
+ padding: 0;
220
+ }
221
+ table tr {
222
+ border-top: 1px solid #cccccc;
223
+ background-color: white;
224
+ margin: 0;
225
+ padding: 0;
226
+ }
227
+
228
+ table tr:nth-child(2n) {
229
+ background-color: #f8f8f8;
230
+ }
231
+
232
+ table tr th {
233
+ font-weight: bold;
234
+ border: 1px solid #cccccc;
235
+ text-align: left;
236
+ margin: 0;
237
+ padding: 6px 13px;
238
+ }
239
+
240
+ table tr td {
241
+ border: 1px solid #cccccc;
242
+ text-align: left;
243
+ margin: 0;
244
+ padding: 6px 13px;
245
+ }
246
+
247
+ table tr th :first-child, table tr td :first-child {
248
+ margin-top: 0;
249
+ }
250
+
251
+ table tr th :last-child, table tr td :last-child {
252
+ margin-bottom: 0;
253
+ }
254
+
255
+ img {
256
+ max-width: 100%;
257
+ }
258
+
259
+ span.frame {
260
+ display: block;
261
+ overflow: hidden;
262
+ }
263
+
264
+ span.frame > span {
265
+ border: 1px solid #dddddd;
266
+ display: block;
267
+ float: left;
268
+ overflow: hidden;
269
+ margin: 13px 0 0;
270
+ padding: 7px;
271
+ width: auto;
272
+ }
273
+
274
+ span.frame span img {
275
+ display: block;
276
+ float: left;
277
+ }
278
+
279
+ span.frame span span {
280
+ clear: both;
281
+ color: #333333;
282
+ display: block;
283
+ padding: 5px 0 0;
284
+ }
285
+
286
+ span.align-center {
287
+ display: block;
288
+ overflow: hidden;
289
+ clear: both;
290
+ }
291
+
292
+ span.align-center > span {
293
+ display: block;
294
+ overflow: hidden;
295
+ margin: 13px auto 0;
296
+ text-align: center;
297
+ }
298
+
299
+ span.align-center span img {
300
+ margin: 0 auto;
301
+ text-align: center;
302
+ }
303
+
304
+ span.align-right {
305
+ display: block;
306
+ overflow: hidden;
307
+ clear: both;
308
+ }
309
+
310
+ span.align-right > span {
311
+ display: block;
312
+ overflow: hidden;
313
+ margin: 13px 0 0;
314
+ text-align: right;
315
+ }
316
+
317
+ span.align-right span img {
318
+ margin: 0;
319
+ text-align: right;
320
+ }
321
+
322
+ span.float-left {
323
+ display: block;
324
+ margin-right: 13px;
325
+ overflow: hidden;
326
+ float: left;
327
+ }
328
+
329
+ span.float-left span {
330
+ margin: 13px 0 0;
331
+ }
332
+
333
+ span.float-right {
334
+ display: block;
335
+ margin-left: 13px;
336
+ overflow: hidden;
337
+ float: right;
338
+ }
339
+
340
+ span.float-right > span {
341
+ display: block;
342
+ overflow: hidden;
343
+ margin: 13px auto 0;
344
+ text-align: right;
345
+ }
346
+
347
+ code, tt {
348
+ margin: 0 2px;
349
+ padding: 0 5px;
350
+ white-space: nowrap;
351
+ border: 1px solid #eaeaea;
352
+ background-color: #f8f8f8;
353
+ border-radius: 3px;
354
+ }
355
+
356
+ pre code {
357
+ margin: 0;
358
+ padding: 0;
359
+ white-space: pre;
360
+ border: none;
361
+ background: transparent;
362
+ }
363
+
364
+ .highlight pre {
365
+ background-color: #f8f8f8;
366
+ border: 1px solid #cccccc;
367
+ font-size: 13px;
368
+ line-height: 19px;
369
+ overflow: auto;
370
+ padding: 6px 10px;
371
+ border-radius: 3px;
372
+ }
373
+
374
+ pre {
375
+ background-color: #f8f8f8;
376
+ border: 1px solid #cccccc;
377
+ font-size: 13px;
378
+ line-height: 19px;
379
+ overflow: auto;
380
+ padding: 6px 10px;
381
+ border-radius: 3px;
382
+ }
383
+
384
+ pre code, pre tt {
385
+ background-color: transparent;
386
+ border: none;
387
+ }
388
+
389
+
390
+ /* generic helper syles */
391
+ ul.unstyled {
392
+ list-style-type: none;
393
+ margin: 0;
394
+ padding: 0;
395
+ }
396
+
397
+
398
+ /* invoice styles */
399
+ ul.invoice-items {
400
+
401
+ }
402
+
403
+ ul.invoice-items li {
404
+ margin: 10px 0;
405
+ padding: 5px 0;
406
+ /* border-bottom: 1px solid #CCC;*/
407
+ page-break-inside : avoid;
408
+ }
409
+
410
+ ul.invoice-items li table {
411
+ width: 100%;
412
+ font-size: 14px;
413
+ }
414
+
415
+ ul.invoice-items td {
416
+ border : none;
417
+ outline : none;
418
+ }
419
+
420
+ ul.invoice-items .invoice-item-description {
421
+ padding-bottom: 15px;
422
+ border: 1px dashed #ccc;
423
+ }
424
+
425
+ .invoice-items .full-row {
426
+ width: 100%;
427
+ }
428
+
429
+ body, html {
430
+ margin: 0;
431
+ padding: 0;
432
+ font-size: 14px;
433
+ }
434
+
435
+ h1, h2, h3, h4 {
436
+ page-break-after: avoid;
437
+ }
438
+
439
+ h1 {
440
+ font-size: 22px;
441
+ }
442
+
443
+ h2 {
444
+ font-size: 18px;
445
+ }
446
+
447
+ h3 {
448
+ font-size: 16px;
449
+ }
450
+
451
+ h4 {
452
+ font-size: 14px;
453
+ }
454
+
455
+ #logo {
456
+ width : 100px;
457
+ height : auto;
458
+ position : absolute;
459
+ top: 0;
460
+ right: 0;
461
+ }
data/lib/extension.rb ADDED
@@ -0,0 +1,158 @@
1
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
2
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
3
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
4
+
5
+ # =THE LEWT EXTENSION BASE CLASS
6
+ #
7
+ # This class provides some basic structuring for LEWT extensions
8
+ # and additionally adds some meta features to extensions which
9
+ # implement it in order to streamline development of LEWT
10
+ # extensions.
11
+
12
+ require "yaml"
13
+
14
+ # This module acts as a container for the LEWT namespace
15
+ module LEWT
16
+
17
+ class Extension
18
+
19
+ # LEWT Stash is the user configured stash path where all extensions, config file, templates etc are stored.
20
+ attr_reader :lewt_stash, :lewt_settings, :customers, :enterprise, :options, :command_name
21
+
22
+ # @@extensions is a registry shared between all extensions that impliment this class
23
+ # containing there class names for invocation by the core system.
24
+ @@lewt_extensions = Array.new
25
+ @options = nil
26
+
27
+ # This method is inoked by subclasses to initialise themselves within Lewt's extension registry.
28
+ # ext_init [Hash]:: contains the keys <tt>:cmd</tt>, <tt>:options</tt> - which are the command name (String) and options (Hash) for the extension respectively.
29
+ def initialize ( ext_init = { :cmd => "lewt_base_extension" } )
30
+ core_settings = YAML.load_file( File.expand_path( '../config/settings.yml', __FILE__) )
31
+ if File.exists? File.expand_path( '~/.lewt_settings', __FILE__)
32
+ core_settings.merge! YAML.load_file( File.expand_path( '~/.lewt_settings', __FILE__) )
33
+ end
34
+
35
+ @lewt_stash = core_settings['lewt_stash'] || File.expand_path('../', __FILE__) + "/config/"
36
+ load_lewt_settings()
37
+ @command_name = ext_init[:cmd]
38
+ @options = ext_init[:options] || nil
39
+ register_extension
40
+ end
41
+
42
+ # returns all registered extensions as an array of class names
43
+ def lewt_extensions
44
+ @@lewt_extensions
45
+ end
46
+
47
+ # This method mathes customers wth query strings provided by users in the CLI
48
+ # query [String]:: A search string to query against.
49
+ # suppress [String]:: A list of clients to exclude. Defaults to nil ie: none.
50
+ def get_matched_customers( query, suppress = nil )
51
+ requestedClients = Array.new
52
+ if query == nil
53
+ @customers.each do |client|
54
+ client_match = [ client["alias"], client["name"] ].join("|")
55
+ if suppress == nil or client_match.match(suppress.gsub(Lewt::OPTION_DELIMITER_REGEX,"|")) == nil
56
+ requestedClients.push(client)
57
+ end
58
+ end
59
+ else
60
+ @customers.each do |client|
61
+ client_match = [ client["alias"], client["name"] ].join("|")
62
+ if client_match.match( query.gsub(Lewt::OPTION_DELIMITER_REGEX,"|") ) != nil
63
+ if suppress == nil or client_match.match(suppress.gsub(Lewt::OPTION_DELIMITER_REGEX,"|")) == nil
64
+ requestedClients.push(client)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ return requestedClients
70
+ end
71
+
72
+ protected
73
+
74
+ # This method loads the core LEWT settings files
75
+ def load_lewt_settings
76
+ @lewt_settings = load_settings( "settings.yml")
77
+ # Start by loading the local config files
78
+ @customers = load_settings("customers.yml")
79
+ @enterprise = load_settings("enterprise.yml")
80
+ end
81
+
82
+ # Writes a key/value pair to the settings file. This can then be accessed with lewt_settings[key] and is persisted
83
+ # on the user's file system in a YAML settings file.
84
+ # key [String]:: The key to write to the settings file
85
+ # value:: The value to assign to this key.
86
+ def write_settings ( file, key, value )
87
+ settings = load_settings(file)
88
+ settings[key] = value
89
+ File.open( @lewt_stash + file, 'w') {|f| f.write settings.to_yaml } #Store
90
+ load_lewt_settings() # reload settings vars
91
+ return settings
92
+ end
93
+
94
+
95
+
96
+ # Formats the --save-name parameter if specified as a template into a string for usage with a File.write method.
97
+ # options [Hash]:: The options hash passed to this function by the Lewt program.
98
+ # i [Integer]:: The current data array index to match the :targets option against
99
+ def format_save_name(o, i)
100
+ match_client = /\#alias/
101
+ match_date = /\#date/
102
+ t = o[:save_file].dup
103
+ c = t.match match_client
104
+ d = t.match match_date
105
+
106
+ if c != nil
107
+ clients = get_matched_customers(o[:target])
108
+ client_alias = clients[i]["alias"]
109
+ t.gsub! match_client, client_alias
110
+ end
111
+
112
+ if d != nil
113
+ t.gsub! match_date, Date.today.to_s
114
+ end
115
+
116
+ return t
117
+ end
118
+
119
+ # Loads the specified settings file
120
+ # property:: The variable you would like to assign the loaded YAML to
121
+ # file [String]:: The settings file to load from lewt stash.
122
+ def load_settings ( file )
123
+ return YAML.load_file( @lewt_stash + file )
124
+ end
125
+
126
+ # register the given extensions' class name with the system for later invocation
127
+ def register_extension
128
+ # only register subclass of this basclass
129
+ if self.class != LEWT::Extension
130
+ @@lewt_extensions << self.clone
131
+ end
132
+ end
133
+
134
+ # This method is used by extensions to hook into the command line
135
+ # call and set some user customisable at runtime.
136
+ # It will be passed the options object from the ruby OptParse class.
137
+ # def register_options opts end
138
+
139
+ # The extract method can be implemented to extract data from custom sources.
140
+ def extract
141
+ raise Exception "An extraction method is not defined by the #{self.class.name} class"
142
+ end
143
+
144
+ # The process method can be implemented to process extracted data.
145
+ # It is passed the extracted data from the previous stage.
146
+ def process extraced_data
147
+ raise Exception "A processing method is not defined by the #{self.class.name} class"
148
+ end
149
+
150
+ # The render method can be implemented to create views for data.
151
+ # It is passed the processed data from the previous stage.
152
+ def render
153
+ raise Exception "A rendering method is not defined by the #{self.class.name} class"
154
+ end
155
+
156
+ end
157
+
158
+ end
@@ -0,0 +1,63 @@
1
+ require 'icalendar'
2
+
3
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
4
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
5
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
6
+
7
+
8
+ module CalendarExtractors
9
+
10
+ # This class handles extraction from iCal sources.
11
+ #
12
+ # ===Usage:
13
+ # - Add the key <tt>osxcal_path</tt> to your settings file and set its value to the location of your calender directory. This must
14
+ # be the full path.
15
+
16
+ class AppleExtractor < CalExtractor
17
+
18
+ # Initialises the object and calls the parent class' super() method.
19
+ def initialize( dateStart, dateEnd, targets, lewt_settings, suppressTargets )
20
+ @calendarPath = lewt_settings["osxcal_path"]
21
+ @suppressTargets = suppressTargets
22
+ super( dateStart, dateEnd, targets )
23
+ end
24
+
25
+
26
+ # Scans the apple calender storage directory and compiles a mashup of ical data it finds
27
+ def aggregateAppleCalenders
28
+ calenders = Array.new
29
+ Dir.glob("#{@calendarPath}**/*.ics").each do |path|
30
+ calenders += Icalendar.parse( File.open(path) )
31
+ end
32
+ return calenders
33
+ end
34
+
35
+ # Open iCalender file, parses it, then check events with the regular CalExtractor methods.
36
+ # Sets the data property of this object if match data is found.
37
+ def extractCalendarData
38
+ calendars = aggregateAppleCalenders()
39
+ calendars.each do |calendar|
40
+ calendar.events.each do |e|
41
+ target = self.isTargetCustomer?( e.summary )
42
+ dstart = Time.parse( e.dtstart.to_s )
43
+ dend = Time.parse( e.dtend.to_s )
44
+ if self.isTargetDate?(dstart) == true && target != false
45
+ timeDiff = (dend - dstart) /60/60
46
+ row = LEWT::LEWTLedger.new({
47
+ :date_start => e.dtstart,
48
+ :date_end => e.dtend,
49
+ :category => @category,
50
+ :entity => target["name"],
51
+ :description => e.description.to_s,
52
+ :quantity => timeDiff,
53
+ :unit_cost => target["rate"]
54
+ })
55
+ @data.push(row)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end