diary-ruby 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +2 -0
- data/README.md +1 -1
- data/diary-ruby.gemspec +1 -0
- data/exe/diaryrb +57 -50
- data/lib/diary-ruby.rb +6 -2
- data/lib/diary-ruby/database.rb +22 -0
- data/lib/diary-ruby/database/migrator.rb +90 -0
- data/lib/diary-ruby/database/query.rb +269 -0
- data/lib/diary-ruby/ext/concern.rb +166 -0
- data/lib/diary-ruby/model.rb +98 -0
- data/lib/diary-ruby/models/entry.rb +151 -0
- data/lib/diary-ruby/parser.rb +1 -2
- data/lib/diary-ruby/server/server.rb +8 -28
- data/lib/diary-ruby/server/views/index.erb +1 -1
- data/lib/diary-ruby/version.rb +1 -1
- metadata +22 -3
- data/lib/diary-ruby/entry.rb +0 -85
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f7c7aa9aab6f4119776f1ec8e197487beddc6b0
|
4
|
+
data.tar.gz: d706e3f057f2ba5911e79bf8273057127b21db19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b43c04f2ae1cd94d0e2df2ea58c351a3969b65cf95778f1018bf1d90d586f337cee7cbb8b2be4dd9e39cb435956f10f8b334e79c27881dbc595c9929b9ea88d
|
7
|
+
data.tar.gz: 82806e6364fb1405b7c521769a1cfac4b2cab2a8da4cc495e42f0e7cc4e1f0ad10bb69d6fc7d7702cf042eb5a29aa4881dd4d5620c89817b3d745117560785aa
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
data/diary-ruby.gemspec
CHANGED
data/exe/diaryrb
CHANGED
@@ -7,21 +7,23 @@ require 'thread'
|
|
7
7
|
|
8
8
|
DEFAULT_DIARY = "diaryrb.store"
|
9
9
|
|
10
|
-
prompt_for_password = false
|
10
|
+
# prompt_for_password = false
|
11
11
|
opts = Slop.parse do |o|
|
12
|
-
# o.string '-c', '--configuration', 'config file location'
|
13
12
|
o.string '-d', '--diary', "choose diary storage file (leave blank for default, #{DEFAULT_DIARY})"
|
14
|
-
o.string '-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
13
|
+
# o.string '-c', '--configuration', 'config file location'
|
14
|
+
# o.string '-p', '--passphrase', 'Use given encryption passphrase or prompt if option is used but no passphrase is given.', default: false do |v|
|
15
|
+
# if v.start_with?('-') || v.nil? || v.strip.size == 0
|
16
|
+
# prompt_for_password = true
|
17
|
+
# end
|
18
|
+
# end
|
19
19
|
|
20
20
|
# usage modes
|
21
21
|
o.separator ''
|
22
22
|
o.separator "Actions (can't be used in combination):"
|
23
|
+
o.bool '-x', '--export', 'export all entries immediately to JSON'
|
23
24
|
o.string '-e', '--edit', 'edit a specific post'
|
24
25
|
o.bool '-l', '--list', 'list all posts by date'
|
26
|
+
o.string '-t', '--tag', 'list entries, filtered by tag'
|
25
27
|
o.bool '-s', '--serve', 'start Diary webserver'
|
26
28
|
|
27
29
|
o.separator ''
|
@@ -57,50 +59,50 @@ end
|
|
57
59
|
|
58
60
|
Diary::Configuration.current_diary = _diary
|
59
61
|
|
60
|
-
_passphrase = nil
|
61
|
-
if prompt_for_password
|
62
|
-
require 'io/console'
|
63
|
-
print "Enter passphrase (leave blank for none): "
|
64
|
-
_passphrase = STDIN.noecho {|io| io.gets}.chomp
|
65
|
-
elsif opts[:passphrase] && opts[:passphrase].strip.size > 0
|
66
|
-
_passphrase = opts[:passphrase]
|
67
|
-
elsif ENV['PASSPHRASE']
|
68
|
-
_passphrase = ENV['PASSPHRASE']
|
69
|
-
elsif Diary::Configuration.passphrase
|
70
|
-
_passphrase = Diary::Configuration.passphrase
|
71
|
-
end
|
72
|
-
|
73
62
|
diary_path = Diary::Configuration.path || Diary::Configuration.current_diary
|
74
|
-
|
75
|
-
if _passphrase.nil? || _passphrase.size == 0
|
76
|
-
Diary.debug "LOADING WITH NO PASSPHRASE!"
|
77
|
-
$store = Diary::Store.new(diary_path)
|
78
|
-
else
|
79
|
-
Diary.debug "LOADING WITH PASSPHRASE #{ _passphrase.gsub(/./, '*') }"
|
80
|
-
$store = Diary::SecureStore.new(diary_path, _passphrase)
|
81
|
-
end
|
63
|
+
database = Diary::Database.new(diary_path)
|
82
64
|
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
db[:entries] = db[:entries].compact.uniq.sort
|
87
|
-
db[:tags] ||= []
|
88
|
-
end
|
65
|
+
# make sure we're always up to date
|
66
|
+
migrator = Diary::Migrator.new(database)
|
67
|
+
migrator.migrate!
|
89
68
|
|
90
|
-
|
91
|
-
|
69
|
+
# initialize ORM
|
70
|
+
Diary::Model.connection = database
|
71
|
+
|
72
|
+
if opts.export?
|
92
73
|
puts ''
|
93
74
|
|
94
|
-
if
|
75
|
+
if Diary::Entry.count == 0
|
95
76
|
puts "No entries"
|
96
77
|
exit
|
97
78
|
else
|
98
|
-
|
99
|
-
|
79
|
+
require 'json'
|
80
|
+
|
81
|
+
output = []
|
82
|
+
|
83
|
+
Diary::Entry.order('created_at DESC').each do |entry|
|
84
|
+
output << entry.to_hash
|
85
|
+
end
|
86
|
+
|
87
|
+
puts JSON.pretty_generate(output)
|
88
|
+
end
|
89
|
+
elsif opts.list?
|
90
|
+
puts ''
|
91
|
+
if Diary::Entry.count == 0
|
92
|
+
puts "No entries"
|
93
|
+
exit
|
94
|
+
else
|
95
|
+
Diary::Entry.order('created_at DESC').each do |entry|
|
96
|
+
puts entry.summary
|
100
97
|
end
|
101
98
|
end
|
99
|
+
elsif opts[:tag] && (tag_id = Diary::Model.select_value('select rowid from tags where name = ?', opts[:tag]))
|
100
|
+
entry_ids = Diary::Model.select_values('select entry_id from taggings where tag_id = ?', tag_id)
|
101
|
+
Diary::Entry.where(date_key: entry_ids).each do |entry|
|
102
|
+
puts entry.summary
|
103
|
+
end
|
102
104
|
elsif opts.serve?
|
103
|
-
Diary::Server.store = $store
|
105
|
+
# Diary::Server.store = $store
|
104
106
|
t = Thread.new do
|
105
107
|
Diary::Server.run!
|
106
108
|
end
|
@@ -114,7 +116,7 @@ else
|
|
114
116
|
|
115
117
|
def parse_and_store(file)
|
116
118
|
diary_entry = Diary::Parser.parse_file(file)
|
117
|
-
|
119
|
+
diary_entry.save!
|
118
120
|
end
|
119
121
|
|
120
122
|
# create a tempfile to store entry in progress in EDITOR
|
@@ -127,18 +129,18 @@ else
|
|
127
129
|
time: Time.now.strftime("%T"),
|
128
130
|
tags: "",
|
129
131
|
title: "",
|
130
|
-
|
132
|
+
body: "text goes here"
|
131
133
|
}
|
132
134
|
|
133
|
-
# if --edit option is used with a valid entry, load it
|
134
|
-
if opts[:edit] && (
|
135
|
-
entry = Diary::Entry.
|
136
|
-
entry_source = entry.to_hash
|
137
|
-
|
135
|
+
# # if --edit option is used with a valid entry, load it
|
136
|
+
if opts[:edit] && Diary::Entry.exists?(date_key: opts[:edit])
|
137
|
+
entry = Diary::Entry.find(date_key: opts[:edit])
|
138
|
+
entry_source = entry.to_hash if entry
|
139
|
+
# FIXME: set tags
|
138
140
|
end
|
139
141
|
|
140
142
|
# prepare entry and launch editor
|
141
|
-
tmpl = Diary::Entry.generate(entry_source
|
143
|
+
tmpl = Diary::Entry.generate(entry_source)
|
142
144
|
file.write(tmpl)
|
143
145
|
|
144
146
|
ed = "vim -f"
|
@@ -149,12 +151,17 @@ else
|
|
149
151
|
end
|
150
152
|
|
151
153
|
pid = fork do
|
152
|
-
|
154
|
+
# split the editor into a separate process
|
155
|
+
command = if /%s/ =~ ed
|
156
|
+
ed % [file.path]
|
157
|
+
else
|
158
|
+
"#{ ed } #{ file.path }"
|
159
|
+
end
|
160
|
+
exec(command)
|
153
161
|
end
|
154
162
|
|
155
163
|
# wait for child to finish, exit when the editor exits
|
156
164
|
exit_signal = Queue.new
|
157
|
-
|
158
165
|
trap("CLD") do
|
159
166
|
Diary.log "CHILD PID #{pid} TERMINATED"
|
160
167
|
exit_signal.push(true)
|
data/lib/diary-ruby.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require "diary-ruby/version"
|
2
|
-
require "diary-ruby/store"
|
3
|
-
require "diary-ruby/
|
2
|
+
# require "diary-ruby/store"
|
3
|
+
require "diary-ruby/database"
|
4
|
+
require "diary-ruby/database/migrator"
|
5
|
+
require "diary-ruby/database/query"
|
6
|
+
require "diary-ruby/model"
|
7
|
+
require "diary-ruby/models/entry"
|
4
8
|
require "diary-ruby/parser"
|
5
9
|
require "diary-ruby/configuration"
|
6
10
|
require "diary-ruby/server/server"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
require 'diary-ruby/database/query'
|
3
|
+
|
4
|
+
module Diary
|
5
|
+
class Database
|
6
|
+
attr_reader :database
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
@database = SQLite3::Database.new(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(*query)
|
13
|
+
if block_given?
|
14
|
+
@database.execute(*query) do |row|
|
15
|
+
yield row
|
16
|
+
end
|
17
|
+
else
|
18
|
+
@database.execute(*query)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
MIGRATIONS = {}
|
2
|
+
|
3
|
+
INITIALIZE = %[
|
4
|
+
CREATE TABLE IF NOT EXISTS `versions` (
|
5
|
+
`version` TEXT NOT NULL,
|
6
|
+
`migrated_at` TEXT DEFAULT NULL
|
7
|
+
);
|
8
|
+
]
|
9
|
+
|
10
|
+
## MIGRATION FORMAT:
|
11
|
+
# MIGRATIONS[number] = array of sql statements
|
12
|
+
|
13
|
+
# 001 - create initial tables
|
14
|
+
MIGRATIONS['001'] = [%[
|
15
|
+
CREATE TABLE IF NOT EXISTS `entries` (
|
16
|
+
`date_key` TEXT NOT NULL PRIMARY KEY,
|
17
|
+
`day` TEXT NOT NULL,
|
18
|
+
`time` TEXT NOT NULL,
|
19
|
+
`title` TEXT,
|
20
|
+
`link` TEXT,
|
21
|
+
`body` TEXT,
|
22
|
+
`created_at` TEXT NOT NULL,
|
23
|
+
`updated_at` TEXT DEFAULT NULL
|
24
|
+
);
|
25
|
+
], %[
|
26
|
+
CREATE INDEX IF NOT EXISTS `index_entries_on_key` on `entries` (`date_key`);
|
27
|
+
], %[
|
28
|
+
CREATE TABLE IF NOT EXISTS `tags` (
|
29
|
+
`name` TEXT DEFAULT NULL
|
30
|
+
);
|
31
|
+
], %[
|
32
|
+
CREATE TABLE IF NOT EXISTS `taggings` (
|
33
|
+
`tag_id` INTEGER NOT NULL,
|
34
|
+
`entry_id` TEXT NOT NULL
|
35
|
+
);
|
36
|
+
], %[
|
37
|
+
CREATE INDEX IF NOT EXISTS `index_taggings_on_tag_id` on `taggings` (`tag_id`);
|
38
|
+
], %[
|
39
|
+
CREATE INDEX IF NOT EXISTS `index_taggings_on_entry_id` on `taggings` (`entry_id`);
|
40
|
+
]]
|
41
|
+
|
42
|
+
MIGRATION_VERSIONS = MIGRATIONS.keys.sort
|
43
|
+
|
44
|
+
module Diary
|
45
|
+
class Migrator
|
46
|
+
attr_reader :db
|
47
|
+
|
48
|
+
def initialize(db)
|
49
|
+
@db = db.database
|
50
|
+
end
|
51
|
+
|
52
|
+
def migrate!
|
53
|
+
exists = false
|
54
|
+
db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='versions';" ) do |row|
|
55
|
+
if row
|
56
|
+
exists = true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if !exists
|
61
|
+
db.execute(INITIALIZE)
|
62
|
+
end
|
63
|
+
|
64
|
+
MIGRATION_VERSIONS.each do |version|
|
65
|
+
exists = false
|
66
|
+
on_date = nil
|
67
|
+
db.execute( "select rowid, migrated_at from versions WHERE version = '#{version}'" ) do |row|
|
68
|
+
if row
|
69
|
+
exists = true
|
70
|
+
on_date = row[1]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
if !exists
|
75
|
+
Diary.debug("UPDATING DATABASE TO VERSION #{ version }")
|
76
|
+
if MIGRATIONS[version].is_a?(Array)
|
77
|
+
MIGRATIONS[version].each do |statement|
|
78
|
+
db.execute(statement)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
db.execute(MIGRATIONS[version])
|
82
|
+
end
|
83
|
+
db.execute("INSERT INTO versions VALUES ('#{version}', strftime('%Y-%m-%dT%H:%M:%S+0000'));")
|
84
|
+
else
|
85
|
+
Diary.debug("AT #{ version } SINCE #{ on_date }")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
|
2
|
+
module Diary
|
3
|
+
module Query
|
4
|
+
class Select
|
5
|
+
def initialize(table, context=nil)
|
6
|
+
@table_name = table
|
7
|
+
@context = context
|
8
|
+
@additions = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def context
|
12
|
+
@context
|
13
|
+
end
|
14
|
+
|
15
|
+
## evaluation conditions, called when Select is given a context
|
16
|
+
# FIXME: it's gross that Query::Select knows about Model
|
17
|
+
|
18
|
+
def execute_in_context(sql)
|
19
|
+
Diary.debug("[Query::Select execute_in_context] connection.execute(#{ sql.inspect })")
|
20
|
+
if Array === sql
|
21
|
+
context.connection.execute(*sql)
|
22
|
+
else
|
23
|
+
context.connection.execute(sql)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def first
|
28
|
+
return self unless context
|
29
|
+
result = execute_in_context(self.limit(1).to_sql)
|
30
|
+
context.materialize(result)[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
def all
|
34
|
+
return self unless context
|
35
|
+
result = execute_in_context(self.to_sql)
|
36
|
+
context.materialize(result)
|
37
|
+
end
|
38
|
+
|
39
|
+
def each(&block)
|
40
|
+
return self unless context
|
41
|
+
result = execute_in_context(self.to_sql)
|
42
|
+
context.materialize(result).each(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def map(&block)
|
46
|
+
return self unless context
|
47
|
+
result = execute_in_context(self.to_sql)
|
48
|
+
context.materialize(result).map(&block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def size
|
52
|
+
return self unless context
|
53
|
+
result = execute_in_context(self.to_sql)
|
54
|
+
result.size
|
55
|
+
end
|
56
|
+
alias :count :size
|
57
|
+
|
58
|
+
##
|
59
|
+
|
60
|
+
def select(column_query)
|
61
|
+
@column_query = column_query
|
62
|
+
end
|
63
|
+
|
64
|
+
def exists?(*conditions)
|
65
|
+
if conditions.size > 0
|
66
|
+
@additions = []
|
67
|
+
@additions << Where.new(*conditions)
|
68
|
+
@additions << Limit.new(1)
|
69
|
+
end
|
70
|
+
c = self.count
|
71
|
+
c && c > 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def where(*conditions)
|
75
|
+
# multiple wheres are OR'd
|
76
|
+
@additions << Where.new(*conditions)
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def order(*conditions)
|
81
|
+
@additions << Order.new(*conditions)
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def limit(*conditions)
|
86
|
+
@additions << Limit.new(*conditions)
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def group_by(*conditions)
|
91
|
+
@additions << GroupBy.new(*conditions)
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_sql
|
96
|
+
# combine @additions in order: WHERE () GROUP BY () ORDER () LIMIT ()
|
97
|
+
|
98
|
+
sql_string = []
|
99
|
+
bind_vars = []
|
100
|
+
|
101
|
+
wheres = @additions.select {|a| Where === a}
|
102
|
+
group_bys = @additions.select {|a| GroupBy === a}
|
103
|
+
orders = @additions.select {|a| Order === a}
|
104
|
+
limits = @additions.select {|a| Limit === a}
|
105
|
+
|
106
|
+
if wheres.size > 0
|
107
|
+
sql_string << "WHERE"
|
108
|
+
|
109
|
+
where_params = []
|
110
|
+
|
111
|
+
wheres = wheres.each do |w|
|
112
|
+
if w.has_bound_vars?
|
113
|
+
bind_vars << w.prepared_statement.bind_vars
|
114
|
+
end
|
115
|
+
|
116
|
+
where_params << w.prepared_statement.sql_string
|
117
|
+
end
|
118
|
+
|
119
|
+
sql_string << where_params.map {|wp|
|
120
|
+
"(#{ wp })"
|
121
|
+
}.join(' OR ')
|
122
|
+
end
|
123
|
+
|
124
|
+
if group_bys.size > 0
|
125
|
+
sql_string << "GROUP BY #{group_bys.map {|gb| gb.prepared_statement.sql_string}.join(', ')}"
|
126
|
+
end
|
127
|
+
|
128
|
+
if orders.size > 0
|
129
|
+
sql_string << "ORDER BY #{orders.map {|ord| ord.prepared_statement.sql_string}.join(', ')}"
|
130
|
+
end
|
131
|
+
|
132
|
+
if limits.size > 0
|
133
|
+
# only 1 allowed, last takes precedence
|
134
|
+
limit = limits.last
|
135
|
+
sql_string << "LIMIT #{limit.prepared_statement.sql_string}"
|
136
|
+
end
|
137
|
+
|
138
|
+
query = [
|
139
|
+
"SELECT #{ @column_query || '*' }",
|
140
|
+
"FROM `#{ @table_name }`",
|
141
|
+
sql_string
|
142
|
+
].join(' ')
|
143
|
+
|
144
|
+
# once to_sql is called, the Query is reset
|
145
|
+
@additions = []
|
146
|
+
|
147
|
+
# return sqlite compatible SQL
|
148
|
+
returning = if bind_vars.size > 0
|
149
|
+
[query, bind_vars.flatten]
|
150
|
+
else
|
151
|
+
query
|
152
|
+
end
|
153
|
+
returning
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class SQLBoundParams
|
158
|
+
def initialize(left, right)
|
159
|
+
@for_sql_query = [left, right]
|
160
|
+
end
|
161
|
+
|
162
|
+
def sql_string
|
163
|
+
@for_sql_query[0]
|
164
|
+
end
|
165
|
+
|
166
|
+
def bind_vars
|
167
|
+
@for_sql_query[1]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class SQLString
|
172
|
+
def initialize(value)
|
173
|
+
@for_sql_query = value
|
174
|
+
end
|
175
|
+
|
176
|
+
def sql_string
|
177
|
+
@for_sql_query
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class Node
|
182
|
+
def string_or_symbol?(value)
|
183
|
+
String === value || Symbol === value
|
184
|
+
end
|
185
|
+
|
186
|
+
def prepared_statement
|
187
|
+
@sql_result
|
188
|
+
end
|
189
|
+
|
190
|
+
def has_bound_vars?
|
191
|
+
SQLBoundParams === prepared_statement
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class Where < Node
|
196
|
+
# convert conditions to AND'd list
|
197
|
+
# returns either string or (string, bind_params) 2-tuple
|
198
|
+
def initialize(*conditions)
|
199
|
+
@sql_result = if Hash === conditions[0]
|
200
|
+
attrs = conditions[0]
|
201
|
+
|
202
|
+
keys = attrs.keys
|
203
|
+
vals = keys.map {|k| attrs[k]}
|
204
|
+
|
205
|
+
and_string = keys.map do |k|
|
206
|
+
if attrs[k].is_a?(Array)
|
207
|
+
bind_hold = attrs[k].map {|_| '?'}.join(',')
|
208
|
+
"`#{k}` in (#{bind_hold})"
|
209
|
+
else
|
210
|
+
"`#{k}` = ?"
|
211
|
+
end
|
212
|
+
end.join(' AND ')
|
213
|
+
|
214
|
+
# (string, bind)
|
215
|
+
SQLBoundParams.new(and_string, vals.flatten)
|
216
|
+
elsif conditions.size > 1 && String === conditions[0]
|
217
|
+
# assume (string, bind) given
|
218
|
+
SQLBoundParams.new(conditions[0], conditions[1..-1])
|
219
|
+
elsif conditions.size == 1 && String === conditions[0]
|
220
|
+
SQLString.new(conditions[0])
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class Order < Node
|
226
|
+
def initialize(*conditions)
|
227
|
+
sql_string = if conditions.size == 1
|
228
|
+
if string_or_symbol?(conditions[0])
|
229
|
+
conditions[0]
|
230
|
+
elsif Array === conditions[0]
|
231
|
+
conditions.join(', ')
|
232
|
+
else
|
233
|
+
conditions[0].to_s
|
234
|
+
end
|
235
|
+
elsif conditions.size > 1
|
236
|
+
conditions.join(', ')
|
237
|
+
end
|
238
|
+
|
239
|
+
@sql_result = SQLString.new(sql_string)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
class Limit < Node
|
244
|
+
def initialize(*conditions)
|
245
|
+
sql_string = if conditions.size == 1
|
246
|
+
conditions[0]
|
247
|
+
elsif conditions.size > 1
|
248
|
+
conditions.join(', ')
|
249
|
+
end
|
250
|
+
@sql_result = SQLString.new(sql_string)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class GroupBy < Node
|
255
|
+
def initialize(*conditions)
|
256
|
+
sql_string = if conditions.size == 1
|
257
|
+
conditions[0]
|
258
|
+
elsif conditions.size > 1
|
259
|
+
conditions.join(', ')
|
260
|
+
end
|
261
|
+
@sql_result = SQLString.new(sql_string)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# class Table
|
266
|
+
# extend Select
|
267
|
+
# end
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# taken from: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
|
2
|
+
#
|
3
|
+
# Copyright (c) 2005-2016 David Heinemeier Hansson
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
# a copy of this software and associated documentation files (the
|
7
|
+
# "Software"), to deal in the Software without restriction, including
|
8
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
# the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
#
|
24
|
+
module ActiveSupport
|
25
|
+
# A typical module looks like this:
|
26
|
+
#
|
27
|
+
# module M
|
28
|
+
# def self.included(base)
|
29
|
+
# base.extend ClassMethods
|
30
|
+
# base.class_eval do
|
31
|
+
# scope :disabled, -> { where(disabled: true) }
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# module ClassMethods
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
41
|
+
# written as:
|
42
|
+
#
|
43
|
+
# require 'active_support/concern'
|
44
|
+
#
|
45
|
+
# module M
|
46
|
+
# extend ActiveSupport::Concern
|
47
|
+
#
|
48
|
+
# included do
|
49
|
+
# scope :disabled, -> { where(disabled: true) }
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# class_methods do
|
53
|
+
# ...
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
58
|
+
# and a +Bar+ module which depends on the former, we would typically write the
|
59
|
+
# following:
|
60
|
+
#
|
61
|
+
# module Foo
|
62
|
+
# def self.included(base)
|
63
|
+
# base.class_eval do
|
64
|
+
# def self.method_injected_by_foo
|
65
|
+
# ...
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# module Bar
|
72
|
+
# def self.included(base)
|
73
|
+
# base.method_injected_by_foo
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# class Host
|
78
|
+
# include Foo # We need to include this dependency for Bar
|
79
|
+
# include Bar # Bar is the module that Host really needs
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
83
|
+
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
84
|
+
#
|
85
|
+
# module Bar
|
86
|
+
# include Foo
|
87
|
+
# def self.included(base)
|
88
|
+
# base.method_injected_by_foo
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# class Host
|
93
|
+
# include Bar
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
97
|
+
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
98
|
+
# module dependencies are properly resolved:
|
99
|
+
#
|
100
|
+
# require 'active_support/concern'
|
101
|
+
#
|
102
|
+
# module Foo
|
103
|
+
# extend ActiveSupport::Concern
|
104
|
+
# included do
|
105
|
+
# def self.method_injected_by_foo
|
106
|
+
# ...
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# module Bar
|
112
|
+
# extend ActiveSupport::Concern
|
113
|
+
# include Foo
|
114
|
+
#
|
115
|
+
# included do
|
116
|
+
# self.method_injected_by_foo
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# class Host
|
121
|
+
# include Bar # It works, now Bar takes care of its dependencies
|
122
|
+
# end
|
123
|
+
module Concern
|
124
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
125
|
+
def initialize
|
126
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.extended(base) #:nodoc:
|
131
|
+
base.instance_variable_set(:@_dependencies, [])
|
132
|
+
end
|
133
|
+
|
134
|
+
def append_features(base)
|
135
|
+
if base.instance_variable_defined?(:@_dependencies)
|
136
|
+
base.instance_variable_get(:@_dependencies) << self
|
137
|
+
return false
|
138
|
+
else
|
139
|
+
return false if base < self
|
140
|
+
@_dependencies.each { |dep| base.send(:include, dep) }
|
141
|
+
super
|
142
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
143
|
+
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def included(base = nil, &block)
|
148
|
+
if base.nil?
|
149
|
+
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
150
|
+
|
151
|
+
@_included_block = block
|
152
|
+
else
|
153
|
+
super
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def class_methods(&class_methods_module_definition)
|
158
|
+
mod = const_defined?(:ClassMethods, false) ?
|
159
|
+
const_get(:ClassMethods) :
|
160
|
+
const_set(:ClassMethods, Module.new)
|
161
|
+
|
162
|
+
mod.module_eval(&class_methods_module_definition)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'diary-ruby/ext/concern'
|
2
|
+
|
3
|
+
module Diary
|
4
|
+
module ModelQuery
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Instance Methods
|
8
|
+
def timestamp_sql
|
9
|
+
"strftime('%Y-%m-%dT%H:%M:%S+0000')"
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def columns
|
14
|
+
@columns ||= connection.execute("PRAGMA table_info(#{table_name})")
|
15
|
+
end
|
16
|
+
|
17
|
+
def column_names
|
18
|
+
@column_names ||= columns.map {|col_info| col_info[1]}
|
19
|
+
end
|
20
|
+
|
21
|
+
def results_to_hashes(array_of_rows)
|
22
|
+
array_of_rows.map do |row|
|
23
|
+
Hash[ column_names.zip(row) ]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def materialize(array_of_rows)
|
28
|
+
results_to_hashes(array_of_rows).map do |record_hash|
|
29
|
+
if respond_to?(:from_hash)
|
30
|
+
from_hash(record_hash)
|
31
|
+
else
|
32
|
+
record_hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find(attrs)
|
38
|
+
where(attrs).first
|
39
|
+
end
|
40
|
+
|
41
|
+
def new_select_relation
|
42
|
+
Diary::Query::Select.new(table_name, self)
|
43
|
+
end
|
44
|
+
|
45
|
+
%w(where order group_by limit exists?).each do |q_type|
|
46
|
+
define_method(q_type.to_sym) do |*conditions|
|
47
|
+
new_select_relation.send(q_type.to_sym, *conditions)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
%w(all first count).each do |q_type|
|
52
|
+
define_method(q_type.to_sym) do
|
53
|
+
new_select_relation.send(q_type.to_sym)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
%w(each map).each do |q_type|
|
58
|
+
define_method(q_type.to_sym) do |&block|
|
59
|
+
new_select_relation.send(q_type.to_sym, &block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Model
|
67
|
+
include ModelQuery
|
68
|
+
|
69
|
+
class << self
|
70
|
+
def connection=(db)
|
71
|
+
@@connection = db
|
72
|
+
end
|
73
|
+
|
74
|
+
def connection
|
75
|
+
@@connection
|
76
|
+
end
|
77
|
+
|
78
|
+
# one-off queries
|
79
|
+
def execute(sql, *binds)
|
80
|
+
Diary.debug("[Model execute] #{ sql } #{ binds.inspect }")
|
81
|
+
connection.execute(sql, binds)
|
82
|
+
end
|
83
|
+
|
84
|
+
# one-off queries
|
85
|
+
def select_rows(sql, *binds)
|
86
|
+
execute(sql, *binds)
|
87
|
+
end
|
88
|
+
|
89
|
+
def select_values(sql, *binds)
|
90
|
+
select_rows(sql, *binds).map {|row| row[0]}
|
91
|
+
end
|
92
|
+
|
93
|
+
def select_value(sql, *binds)
|
94
|
+
select_values(sql, *binds).first
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
TEMPLATE = "# last entry posted at %{last_update_at}
|
2
|
+
|
3
|
+
DAY %{day}
|
4
|
+
TIME %{time}
|
5
|
+
TAGS %{tags}
|
6
|
+
TITLE %{title}
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
%{body}
|
11
|
+
"
|
12
|
+
|
13
|
+
require 'rdiscount'
|
14
|
+
require 'diary-ruby/model'
|
15
|
+
|
16
|
+
module Diary
|
17
|
+
class Entry < Model
|
18
|
+
attr_accessor :day, :time, :tags, :body, :link, :title, :date_key
|
19
|
+
|
20
|
+
def self.table_name
|
21
|
+
'entries'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_hash(record)
|
25
|
+
entry = self.new(
|
26
|
+
day: record['day'],
|
27
|
+
time: record['time'],
|
28
|
+
body: record['body'],
|
29
|
+
title: record['title'],
|
30
|
+
date_key: record['date_key'],
|
31
|
+
)
|
32
|
+
|
33
|
+
# taggings!
|
34
|
+
begin
|
35
|
+
tag_ids = select_values('select tag_id from taggings where entry_id = ?', [entry.identifier])
|
36
|
+
bind_hold = tag_ids.map {|_| '?'}.join(',')
|
37
|
+
entry.tags = select_values("select name from tags where rowid in (#{ bind_hold })", *tag_ids)
|
38
|
+
rescue => ex
|
39
|
+
Diary.debug "FAILED TO LOAD TAGS. #{ ex.message }"
|
40
|
+
end
|
41
|
+
|
42
|
+
entry
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.keygen(day, time)
|
46
|
+
"%s-%s" % [day, time]
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.generate(options={})
|
50
|
+
options[:last_update_at] = connection.execute("select max(updated_at) from #{table_name}")[0] || ''
|
51
|
+
|
52
|
+
# convert Arrays to dumb CSV
|
53
|
+
options.each do |(k, v)|
|
54
|
+
if v.is_a?(Array)
|
55
|
+
options[k] = v.join(', ')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
TEMPLATE % options
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(options={})
|
63
|
+
@day = options[:day]
|
64
|
+
@time = options[:time]
|
65
|
+
@tags = options[:tags] || []
|
66
|
+
@body = options[:body]
|
67
|
+
@title = options[:title]
|
68
|
+
|
69
|
+
if options[:date_key].nil?
|
70
|
+
@date_key = identifier
|
71
|
+
else
|
72
|
+
@date_key = options[:date_key]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def identifier
|
77
|
+
self.class.keygen(day, time)
|
78
|
+
end
|
79
|
+
|
80
|
+
def formatted_body
|
81
|
+
RDiscount.new(body).to_html
|
82
|
+
end
|
83
|
+
|
84
|
+
def truncated_body
|
85
|
+
_truncated = body
|
86
|
+
if _truncated.size > 40
|
87
|
+
_truncated = "#{ _truncated[0..40] }..."
|
88
|
+
end
|
89
|
+
_truncated
|
90
|
+
end
|
91
|
+
|
92
|
+
def summary
|
93
|
+
"%-24s%-46s%s" % [date_key, truncated_body, tags.join(', ')]
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_hash
|
97
|
+
{
|
98
|
+
day: day,
|
99
|
+
time: time,
|
100
|
+
tags: tags,
|
101
|
+
body: body,
|
102
|
+
title: title,
|
103
|
+
date_key: date_key,
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def save!
|
108
|
+
if self.class.find(date_key: date_key)
|
109
|
+
# update record
|
110
|
+
sql = "UPDATE entries SET day=?, time=?, body=?, link=?, title=?, updated_at=#{timestamp_sql} WHERE date_key=?"
|
111
|
+
self.class.execute(sql, day, time, body, link, title, date_key)
|
112
|
+
else
|
113
|
+
# insert
|
114
|
+
sql = %[INSERT INTO entries (day, time, body, link, title, date_key, created_at, updated_at)
|
115
|
+
VALUES (?, ?, ?, ?, ?, ?, #{timestamp_sql}, #{timestamp_sql})]
|
116
|
+
self.class.execute(sql, day, time, body, link, title, date_key)
|
117
|
+
end
|
118
|
+
|
119
|
+
begin
|
120
|
+
update_tags!
|
121
|
+
rescue => ex
|
122
|
+
Diary.debug "FAILED TO UPDATE TAGS #{ tags.inspect }. #{ ex.message }"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def update_tags!
|
127
|
+
# clean out existing
|
128
|
+
Diary.debug "CLEANING `taggings`"
|
129
|
+
self.class.execute('delete from taggings where entry_id = ?', [identifier])
|
130
|
+
|
131
|
+
# add back
|
132
|
+
tags.each do |tag|
|
133
|
+
# is tag in db?
|
134
|
+
tag_id = self.class.select_value('select rowid from tags where name = ?', tag)
|
135
|
+
|
136
|
+
if tag_id.nil?
|
137
|
+
Diary.debug "CREATING TAG #{ tag.inspect }"
|
138
|
+
|
139
|
+
# exists
|
140
|
+
Diary.debug self.class.select_rows('PRAGMA table_info(tags)').inspect
|
141
|
+
self.class.execute("insert into tags (name) values (?)", [tag])
|
142
|
+
tag_id = self.class.select_value('select last_insert_rowid()')
|
143
|
+
end
|
144
|
+
|
145
|
+
Diary.debug "CREATING tagging"
|
146
|
+
self.class.execute('insert into taggings (tag_id, entry_id) values (?, ?)', [tag_id, identifier])
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
data/lib/diary-ruby/parser.rb
CHANGED
@@ -46,11 +46,10 @@ module Diary
|
|
46
46
|
Diary.debug "BODY #{ body.join(" ") }"
|
47
47
|
|
48
48
|
return Entry.new(
|
49
|
-
nil,
|
50
49
|
day: metadata['day'],
|
51
50
|
time: metadata['time'],
|
52
51
|
tags: metadata['tags'],
|
53
|
-
|
52
|
+
body: body.join("\n").strip,
|
54
53
|
title: metadata['title'],
|
55
54
|
key: key,
|
56
55
|
)
|
@@ -22,27 +22,10 @@ module Diary
|
|
22
22
|
end
|
23
23
|
|
24
24
|
get '/' do
|
25
|
-
|
26
|
-
|
27
|
-
if keys.nil?
|
28
|
-
store.write do |db|
|
29
|
-
db[:entries] = []
|
30
|
-
end
|
31
|
-
|
32
|
-
keys = []
|
25
|
+
@entries = Entry.order('created_at DESC').map do |entry_hash|
|
26
|
+
Entry.from_hash(entry_hash)
|
33
27
|
end
|
34
28
|
|
35
|
-
@entries = keys.uniq.map {|entry_key|
|
36
|
-
entry = store.read(entry_key)
|
37
|
-
|
38
|
-
if entry
|
39
|
-
logger.debug "LOAD #{ entry }"
|
40
|
-
Entry.from_store(entry)
|
41
|
-
else
|
42
|
-
nil
|
43
|
-
end
|
44
|
-
}.compact
|
45
|
-
|
46
29
|
logger.info "returning keys: #{ keys }"
|
47
30
|
logger.info "returning entries: #{ @entries }"
|
48
31
|
|
@@ -53,16 +36,13 @@ module Diary
|
|
53
36
|
content_type :json
|
54
37
|
|
55
38
|
key = params[:key]
|
56
|
-
entry_hash =
|
57
|
-
entry = Entry.from_store(entry_hash)
|
58
|
-
|
59
|
-
content = RDiscount.new entry.text
|
60
|
-
entry_hash[:formatted] = content.to_html
|
39
|
+
entry_hash = Entry.find(date_key: key)
|
61
40
|
|
62
|
-
if
|
41
|
+
if entry_hash
|
42
|
+
entry = Entry.from_store(entry_hash)
|
43
|
+
content = RDiscount.new entry['body']
|
44
|
+
entry_hash[:formatted] = content.to_html
|
63
45
|
entry_hash.to_json
|
64
|
-
else
|
65
|
-
{}
|
66
46
|
end
|
67
47
|
end
|
68
48
|
|
@@ -81,7 +61,7 @@ module Diary
|
|
81
61
|
day: params[:day],
|
82
62
|
time: params[:time],
|
83
63
|
tags: tags,
|
84
|
-
|
64
|
+
body: params[:body],
|
85
65
|
)
|
86
66
|
|
87
67
|
store.write_entry(entry)
|
data/lib/diary-ruby/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: diary-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Bachman
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '2.4'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sqlite3
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.3'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.3'
|
125
139
|
description: 'A command line diary: diaryrb'
|
126
140
|
email:
|
127
141
|
- adam.bachman@gmail.com
|
@@ -144,9 +158,14 @@ files:
|
|
144
158
|
- exe/diaryrb
|
145
159
|
- lib/diary-ruby.rb
|
146
160
|
- lib/diary-ruby/configuration.rb
|
147
|
-
- lib/diary-ruby/
|
161
|
+
- lib/diary-ruby/database.rb
|
162
|
+
- lib/diary-ruby/database/migrator.rb
|
163
|
+
- lib/diary-ruby/database/query.rb
|
164
|
+
- lib/diary-ruby/ext/concern.rb
|
148
165
|
- lib/diary-ruby/ext/encryptor.rb
|
149
166
|
- lib/diary-ruby/ext/secure_pstore.rb
|
167
|
+
- lib/diary-ruby/model.rb
|
168
|
+
- lib/diary-ruby/models/entry.rb
|
150
169
|
- lib/diary-ruby/parser.rb
|
151
170
|
- lib/diary-ruby/server/public/script.js
|
152
171
|
- lib/diary-ruby/server/public/style.css
|
data/lib/diary-ruby/entry.rb
DELETED
@@ -1,85 +0,0 @@
|
|
1
|
-
TEMPLATE = "# last entry posted at %{last_update_at}
|
2
|
-
|
3
|
-
DAY %{day}
|
4
|
-
TIME %{time}
|
5
|
-
TAGS %{tags}
|
6
|
-
TITLE %{title}
|
7
|
-
|
8
|
-
---
|
9
|
-
|
10
|
-
%{text}
|
11
|
-
"
|
12
|
-
|
13
|
-
require 'rdiscount'
|
14
|
-
|
15
|
-
module Diary
|
16
|
-
class Entry
|
17
|
-
attr_accessor :version, :day, :time, :tags, :text, :title, :key
|
18
|
-
|
19
|
-
CURRENT_VERSION = 1
|
20
|
-
|
21
|
-
def self.from_store(record)
|
22
|
-
if record[:version] == 1
|
23
|
-
self.new(
|
24
|
-
record[:version],
|
25
|
-
day: record[:day],
|
26
|
-
time: record[:time],
|
27
|
-
tags: record[:tags],
|
28
|
-
text: record[:text],
|
29
|
-
title: record[:title],
|
30
|
-
key: record[:key],
|
31
|
-
)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.keygen(day, time)
|
36
|
-
"%s-%s" % [day, time]
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.generate(options={}, store)
|
40
|
-
options[:last_update_at] = store.read(:last_update_at)
|
41
|
-
|
42
|
-
# convert Arrays to dumb CSV
|
43
|
-
options.each do |(k, v)|
|
44
|
-
if v.is_a?(Array)
|
45
|
-
options[k] = v.join(', ')
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
TEMPLATE % options
|
50
|
-
end
|
51
|
-
|
52
|
-
def initialize(version, options={})
|
53
|
-
@version = version || CURRENT_VERSION
|
54
|
-
|
55
|
-
@day = options[:day]
|
56
|
-
@time = options[:time]
|
57
|
-
@tags = options[:tags] || []
|
58
|
-
@text = options[:text]
|
59
|
-
@title = options[:title]
|
60
|
-
|
61
|
-
if options[:key].nil?
|
62
|
-
@key = Entry.keygen(day, time)
|
63
|
-
else
|
64
|
-
@key = options[:key]
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def formatted_text
|
69
|
-
RDiscount.new(text).to_html
|
70
|
-
end
|
71
|
-
|
72
|
-
def to_hash
|
73
|
-
{
|
74
|
-
version: version,
|
75
|
-
day: day,
|
76
|
-
time: time,
|
77
|
-
tags: tags,
|
78
|
-
text: text,
|
79
|
-
title: '',
|
80
|
-
key: key,
|
81
|
-
}
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|