benhoskings-hammock 0.2.4
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.
- data/LICENSE +24 -0
- data/Manifest.txt +42 -0
- data/README.rdoc +105 -0
- data/Rakefile +27 -0
- data/lib/hammock.rb +25 -0
- data/lib/hammock/ajaxinate.rb +152 -0
- data/lib/hammock/callbacks.rb +107 -0
- data/lib/hammock/canned_scopes.rb +121 -0
- data/lib/hammock/constants.rb +7 -0
- data/lib/hammock/controller_attributes.rb +66 -0
- data/lib/hammock/export_scope.rb +74 -0
- data/lib/hammock/hamlink_to.rb +47 -0
- data/lib/hammock/javascript_buffer.rb +63 -0
- data/lib/hammock/logging.rb +98 -0
- data/lib/hammock/model_attributes.rb +38 -0
- data/lib/hammock/model_logging.rb +30 -0
- data/lib/hammock/monkey_patches/action_pack.rb +32 -0
- data/lib/hammock/monkey_patches/active_record.rb +227 -0
- data/lib/hammock/monkey_patches/array.rb +73 -0
- data/lib/hammock/monkey_patches/hash.rb +49 -0
- data/lib/hammock/monkey_patches/logger.rb +28 -0
- data/lib/hammock/monkey_patches/module.rb +27 -0
- data/lib/hammock/monkey_patches/numeric.rb +25 -0
- data/lib/hammock/monkey_patches/object.rb +61 -0
- data/lib/hammock/monkey_patches/route_set.rb +200 -0
- data/lib/hammock/monkey_patches/string.rb +197 -0
- data/lib/hammock/overrides.rb +32 -0
- data/lib/hammock/resource_mapping_hooks.rb +28 -0
- data/lib/hammock/resource_retrieval.rb +115 -0
- data/lib/hammock/restful_actions.rb +170 -0
- data/lib/hammock/restful_rendering.rb +114 -0
- data/lib/hammock/restful_support.rb +167 -0
- data/lib/hammock/route_drawing_hooks.rb +22 -0
- data/lib/hammock/route_for.rb +58 -0
- data/lib/hammock/scope.rb +120 -0
- data/lib/hammock/suggest.rb +36 -0
- data/lib/hammock/utils.rb +42 -0
- data/misc/scaffold.txt +83 -0
- data/misc/template.rb +17 -0
- data/tasks/hammock_tasks.rake +5 -0
- data/test/hammock_test.rb +8 -0
- metadata +129 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ModelAttributes
|
3
|
+
MixInto = ActiveRecord::Base
|
4
|
+
|
5
|
+
def self.included base
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def has_defaults attrs
|
13
|
+
write_inheritable_attribute "default_attributes", (default_attributes || {}).merge(attrs)
|
14
|
+
end
|
15
|
+
def attr_accessible_on_create *attributes
|
16
|
+
write_inheritable_attribute "attr_accessible_on_create", Set.new(attributes.map(&:to_s)) + (accessible_attributes_on_create || [])
|
17
|
+
end
|
18
|
+
def attr_accessible_on_update *attributes
|
19
|
+
write_inheritable_attribute "attr_accessible_on_update", Set.new(attributes.map(&:to_s)) + (accessible_attributes_on_update || [])
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_attributes
|
23
|
+
read_inheritable_attribute("default_attributes") || {}
|
24
|
+
end
|
25
|
+
def accessible_attributes_on_create
|
26
|
+
read_inheritable_attribute "attr_accessible_on_create"
|
27
|
+
end
|
28
|
+
def accessible_attributes_on_update
|
29
|
+
read_inheritable_attribute "attr_accessible_on_update"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ModelLogging
|
3
|
+
MixInto = ActiveRecord::Base
|
4
|
+
|
5
|
+
def self.included base
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
include Hammock::Utils::Methods
|
12
|
+
include Hammock::Logging::Methods
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
include Hammock::Utils::Methods
|
17
|
+
include Hammock::Logging::Methods
|
18
|
+
|
19
|
+
def log_with_model *args
|
20
|
+
opts = args.extract_options!
|
21
|
+
|
22
|
+
message = "#{self.class}<#{self.id}>#{(' | ' + args.shift) if args.first.is_a?(String)}"
|
23
|
+
|
24
|
+
log_without_model *args.unshift(message).push(opts.merge(:skip => (opts[:skip] || 0) + 1))
|
25
|
+
end
|
26
|
+
alias_method_chain :log, :model
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ActionControllerPatches
|
3
|
+
MixInto = ActionController::Rescue
|
4
|
+
|
5
|
+
def self.included base
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
|
9
|
+
# base.class_eval {
|
10
|
+
# alias_method_chain :clean_backtrace, :truncation
|
11
|
+
# }
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def clean_backtrace_with_truncation exception
|
23
|
+
if backtrace = clean_backtrace_without_truncation(exception)
|
24
|
+
backtrace.take_while {|line|
|
25
|
+
line['perform_action_without_filters'].nil?
|
26
|
+
}.push("... and so on")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ActiveRecordPatches
|
3
|
+
MixInto = ActiveRecord::Base
|
4
|
+
|
5
|
+
def self.included base # :nodoc:
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods # TODO maybe include in the metaclass instead of extending the class?
|
8
|
+
|
9
|
+
%w[before_undestroy after_undestroy].each {|callback_name|
|
10
|
+
MixInto.define_callbacks callback_name
|
11
|
+
# base.send :define_method, callback_name, lambda { }
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
def new_with attributes
|
18
|
+
default_attributes.merge(attributes).inject(new) {|record,(k,v)|
|
19
|
+
record.send "#{k}=", v
|
20
|
+
record
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def sorter
|
25
|
+
# TODO updated_at DESC
|
26
|
+
proc {|record| record.id }
|
27
|
+
end
|
28
|
+
|
29
|
+
def resource
|
30
|
+
base_class
|
31
|
+
end
|
32
|
+
|
33
|
+
def resource_sym
|
34
|
+
resource_name.to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource_name
|
38
|
+
# TODO almost certainly a better way to do this
|
39
|
+
base_class.to_s.pluralize.underscore
|
40
|
+
end
|
41
|
+
|
42
|
+
def base_model
|
43
|
+
base_class.to_s.underscore
|
44
|
+
end
|
45
|
+
|
46
|
+
def record?; false end
|
47
|
+
def resource?; true end
|
48
|
+
|
49
|
+
def update_statement set_clause, where_clause
|
50
|
+
statement = "UPDATE #{table_name} SET #{set_clause} WHERE #{send :sanitize_sql_array, where_clause}"
|
51
|
+
connection.update statement
|
52
|
+
end
|
53
|
+
|
54
|
+
def reset_cached_column_info
|
55
|
+
reset_column_information
|
56
|
+
subclasses.each &:reset_cached_column_info
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_or_new_with(find_attributes, create_attributes = {})
|
60
|
+
finder = respond_to?(:find_with_deleted) ? :find_with_deleted : :find
|
61
|
+
|
62
|
+
if record = send(finder, :first, :conditions => find_attributes.discard(:deleted_at))
|
63
|
+
# Found the record, so we can return it, if:
|
64
|
+
# (a) the record can't have a stored deletion state,
|
65
|
+
# (b) it can, but it's not actually deleted,
|
66
|
+
# (c) it is deleted, but we want to find one that's deleted, or
|
67
|
+
# (d) we don't want a deleted record, and undestruction succeeds.
|
68
|
+
if (finder != :find_with_deleted) || !record.deleted? || create_attributes[:deleted_at] || record.undestroy
|
69
|
+
record
|
70
|
+
end
|
71
|
+
else
|
72
|
+
creating_class = if create_attributes[:type].is_a?(ActiveRecord::Base)
|
73
|
+
create_attributes.delete(:type)
|
74
|
+
else
|
75
|
+
self
|
76
|
+
end
|
77
|
+
creating_class.new_with create_attributes.merge(find_attributes)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_or_create_with(find_attributes, create_attributes = {}, adjust_attributes = false)
|
82
|
+
if record = find_or_new_with(find_attributes, create_attributes)
|
83
|
+
log "Create failed. #{record.errors.inspect}", :skip => 1 if record.new_record? && !record.save
|
84
|
+
log "Adjust failed. #{record.errors.inspect}", :skip => 1 if adjust_attributes && !record.adjust(create_attributes)
|
85
|
+
record
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# def find_or_create_with! find_attributes, create_attributes = {}, adjust_attributes = false)
|
90
|
+
# record = find_or_new_with find_attributes, create_attributes, adjust_attributes
|
91
|
+
# record.valid? ? record : raise("Save failed. #{record.errors.inspect}")
|
92
|
+
# end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
module InstanceMethods
|
97
|
+
|
98
|
+
def concise_inspect
|
99
|
+
"#{self.class}<#{self.id || 'new'}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
def resource
|
103
|
+
self.class.resource
|
104
|
+
end
|
105
|
+
|
106
|
+
def resource_sym
|
107
|
+
self.class.resource_sym
|
108
|
+
end
|
109
|
+
|
110
|
+
def resource_name
|
111
|
+
self.class.resource_name
|
112
|
+
end
|
113
|
+
|
114
|
+
def record?; true end
|
115
|
+
def resource?; false end
|
116
|
+
|
117
|
+
def id_str
|
118
|
+
if new_record?
|
119
|
+
"new_#{base_model}"
|
120
|
+
else
|
121
|
+
"#{base_model}_#{id}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def id_or_description
|
126
|
+
new_record? ? new_record_description : id
|
127
|
+
end
|
128
|
+
|
129
|
+
def new_record_description
|
130
|
+
attributes.map {|k,v| "#{k}-#{(v.to_s || '')[0..10]}" }.join("_")
|
131
|
+
end
|
132
|
+
|
133
|
+
def base_model
|
134
|
+
self.class.base_model
|
135
|
+
end
|
136
|
+
|
137
|
+
def new_or_deleted_before_save?
|
138
|
+
@new_or_deleted_before_save
|
139
|
+
end
|
140
|
+
def set_new_or_deleted_before_save
|
141
|
+
@new_or_deleted_before_save = new_record? || send_if_respond_to(:deleted?)
|
142
|
+
end
|
143
|
+
|
144
|
+
def undestroy
|
145
|
+
unless new_record?
|
146
|
+
if frozen?
|
147
|
+
self.class.find_with_deleted(self.id).undestroy # Re-fetch ourselves and undestroy the thawed copy
|
148
|
+
else
|
149
|
+
# We can undestroy
|
150
|
+
return false if callback(:before_undestroy) == false
|
151
|
+
result = self.class.update_all ['deleted_at = ?', (self.deleted_at = nil)], ['id = ?', self.id]
|
152
|
+
callback(:after_undestroy)
|
153
|
+
self if result != false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Updates each given attribute to the current time.
|
159
|
+
#
|
160
|
+
# Assumes that each column can accept a +Time+ instance, i.e. that they're all +datetime+ columns or similar.
|
161
|
+
#
|
162
|
+
# The updates are done with update_attribute, and as such they are done with callbacks but
|
163
|
+
# without validation.
|
164
|
+
def touch *attrs
|
165
|
+
now = Time.now
|
166
|
+
attrs.each {|attribute|
|
167
|
+
update_attribute attribute, now
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# Updates each given attribute to the current time, skipping attributes that are already set.
|
172
|
+
#
|
173
|
+
# Assumes that each column can accept a +Time+ instance, i.e. that they're all +datetime+ columns or similar.
|
174
|
+
#
|
175
|
+
# The updates are done with update_attribute, and as such they are done with callbacks but
|
176
|
+
# without validation.
|
177
|
+
def touch_once *attrs
|
178
|
+
touch *attrs.select {|attribute| attributes[attribute.to_s].nil? }
|
179
|
+
end
|
180
|
+
|
181
|
+
def adjust attrs
|
182
|
+
attrs.each {|k,v| send "#{k}=", v }
|
183
|
+
save false
|
184
|
+
end
|
185
|
+
|
186
|
+
def unsaved_attributes
|
187
|
+
self.changed.inject({}) {|hsh,k|
|
188
|
+
hsh[k] = attributes[k]
|
189
|
+
hsh
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Offset +attribute+ by +offset+ atomically in SQL.
|
194
|
+
def offset! attribute, offset
|
195
|
+
if new_record?
|
196
|
+
log "Can't offset! a new record."
|
197
|
+
else
|
198
|
+
# Update the in-memory model
|
199
|
+
send "#{attribute}=", send(attribute) + offset
|
200
|
+
# Update the DB
|
201
|
+
run_updater_sql 'Offset', "#{connection.quote_column_name(attribute)} = #{connection.quote_column_name(attribute)} + #{quote_value(offset)}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def run_updater_sql logger_prefix, set_clause
|
209
|
+
connection.update(
|
210
|
+
"UPDATE #{self.class.table_name} " +
|
211
|
+
"SET #{set_clause} " +
|
212
|
+
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
|
213
|
+
|
214
|
+
"#{self.class.name} #{logger_prefix}"
|
215
|
+
) != false
|
216
|
+
end
|
217
|
+
|
218
|
+
# TODO Use ambition for association queries.
|
219
|
+
# def self.collection_reader_method reflection, association_proxy_class
|
220
|
+
# define_method(reflection.name) do |*params|
|
221
|
+
# reflection.klass.ambition_context.select { |entity| entity.__send__(reflection.primary_key_name) == quoted_id }
|
222
|
+
# end
|
223
|
+
# end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ArrayPatches
|
3
|
+
MixInto = Array
|
4
|
+
|
5
|
+
def self.included base # :nodoc:
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
# Returns true iff +other+ appears exactly at the start of +self+.
|
16
|
+
def starts_with? *other
|
17
|
+
self[0, other.length] == other
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true iff +other+ appears exactly at the end of +self+.
|
21
|
+
def ends_with? *other
|
22
|
+
self[-other.length, other.length] == other
|
23
|
+
end
|
24
|
+
|
25
|
+
def squash
|
26
|
+
self.dup.squash!
|
27
|
+
end
|
28
|
+
def squash!
|
29
|
+
self.delete_if &:blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
def discard *args
|
33
|
+
self.dup.discard! *args
|
34
|
+
end
|
35
|
+
def discard! *args
|
36
|
+
args.each {|arg| self.delete arg }
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def as_index_for &value_function
|
41
|
+
inject({}) do |accum, elem|
|
42
|
+
accum[elem] = value_function.call(elem)
|
43
|
+
accum
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove_framework_backtrace
|
48
|
+
reverse.drop_while {|step|
|
49
|
+
!step.starts_with?(RAILS_ROOT)
|
50
|
+
}.reverse
|
51
|
+
end
|
52
|
+
|
53
|
+
def hash_by *methods, &block
|
54
|
+
hsh = Hash.new {|h,k| h[k] = [] }
|
55
|
+
this_method = methods.shift
|
56
|
+
|
57
|
+
# First, hash this array into +hsh+.
|
58
|
+
self.each {|i| hsh[i.send(this_method)] << i }
|
59
|
+
|
60
|
+
if methods.empty?
|
61
|
+
# If there are no methods remaining, yield this group to the block if required.
|
62
|
+
hsh.each_pair {|k,v| hsh[k] = yield(hsh[k]) } if block_given?
|
63
|
+
else
|
64
|
+
# Recursively hash remaining methods.
|
65
|
+
hsh.each_pair {|k,v| hsh[k] = v.hash_by(*methods, &block) }
|
66
|
+
end
|
67
|
+
|
68
|
+
hsh
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Hammock
|
2
|
+
module HashPatches
|
3
|
+
MixInto = Hash
|
4
|
+
|
5
|
+
def self.included base # :nodoc:
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
def discard! *keys
|
16
|
+
keys.each {|k| delete k }
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def discard *keys
|
21
|
+
dup.discard! *keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def dragnet *keys
|
25
|
+
dup.dragnet! *keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def dragnet! *keys
|
29
|
+
keys.inject({}) {|acc,key|
|
30
|
+
acc[key] = self.delete(key) if self.has_key?(key)
|
31
|
+
acc
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_param_hash prefix = ''
|
36
|
+
hsh = self.dup
|
37
|
+
# TODO these two blocks can probably be combined
|
38
|
+
hsh.keys.each {|k| hsh.merge!(hsh.delete(k).to_param_hash(k)) if hsh[k].is_a?(Hash) }
|
39
|
+
hsh.keys.each {|k| hsh["#{prefix}[#{k}]"] = hsh.delete(k) } unless prefix.blank?
|
40
|
+
hsh
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_flattened_json
|
44
|
+
to_param_hash.to_json
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|