Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 330 lines (227 sloc) 13.883 kB
6449698 @binarylogic Released verversion 0.9.7
authored
1 = Searchgasm
aa5b90a @binarylogic First commit
authored
2
9968056 @binarylogic Updated readme
authored
3 Searchgasm is orgasmic. Maybe not orgasmic, but you will get aroused. So go grab a towel and let's dive in.
1f7251c @binarylogic Added association_collection
authored
4
6449698 @binarylogic Released verversion 0.9.7
authored
5 <b>Searchgasm's inspiration comes right from ActiveRecord. ActiveRecord lets you create objects that represent a record in the database, so why can't you create objects that represent searching the database? Now you can!</b>
c703fbe @binarylogic Updated readme
authored
6
6449698 @binarylogic Released verversion 0.9.7
authored
7 == Under the hood
1271b7a @binarylogic Added gemspec
authored
8
6449698 @binarylogic Released verversion 0.9.7
authored
9 I'm a big fan of understanding what I'm using, so here's a quick explanation: The design behind this plugin is pretty simple. The search object "sanitizes" down into the options passed into ActiveRecord::Base.find(). It serves as a transparent filter between you and ActiveRecord::Base.find(). This filter provides "enhancements" that get translated into options that ActiveRecord::Base.find() can understand. It doesn't dig into the ActiveRecord internals, it only uses what is publicly available. It jumps in and helps out <em>only</em> when needed, otherwise it sits back and lets ActiveRecord do all of the work. Between that and the extensive tests, this is a solid and fast plugin.
cf23760 @binarylogic Added tests, new features, and improved readme
authored
10
6449698 @binarylogic Released verversion 0.9.7
authored
11 == Quicklinks
c4a3956 @binarylogic Updated helpers, cleaned up code, started documentation
authored
12
a1e2d41 @binarylogic Fixed some probs in readme
authored
13 * <b>Documentation:</b> http://searchgasm.rubyforge.org
97a9dbe @binarylogic Updated readme with tutoiral link
authored
14 * <b>Easy pagination, ordering, and searching tutorial:</b> http://www.binarylogic.com/2008/9/7/tutorial-pagination-ordering-and-searching-with-searchgasm
15 * <b>The tutorial above, live:</b> http://searchgasm_example.binarylogic.com
6449698 @binarylogic Released verversion 0.9.7
authored
16
17 == Install and use
18
19 sudo gem install searchgasm
20
3b7f5ae @binarylogic Updated installation guide in readme
authored
21 For rails
6449698 @binarylogic Released verversion 0.9.7
authored
22
3b7f5ae @binarylogic Updated installation guide in readme
authored
23 $ cd vendor/plugins
24 $ sudo gem unpack searchgasm
6449698 @binarylogic Released verversion 0.9.7
authored
25
26 Or as a plugin
27
28 script/plugin install git://github.com/binarylogic/searchgasm.git
29
30 Now try out some of the examples below:
31
32 <b>For all examples, let's assume the following relationships: User => Orders => Line Items</b>
33
34 == The beauty of searchgasm
35
36 Using Searchgasm in rails is the best part, because rails has all kinds of nifty methods to make dealing with ActiveRecord objects quick and easy, especially with forms. So let's take advantage of them! That's the idea behind this plugin. Searchgasm is searching, ordering, and pagination all rolled into one simple plugin. Take all of that pagination and searching cruft out of your models and let Searchgasm handle it. Check it out:
24a7d88 @binarylogic Updated readme
authored
37
38 # app/controllers/users_controller.rb
39 def index
d6f04d3 @binarylogic Big update, added helpers, more features, improved performance
authored
40 @search = User.new_search(params[:search])
24a7d88 @binarylogic Updated readme
authored
41 @users, @users_count = @search.all, @search.count
42 end
43
6449698 @binarylogic Released verversion 0.9.7
authored
44 Now your view. Notice you can use your search object <b>just like</b> an ActiveRecord object.
f66721b @binarylogic Github needs to fix the escaping bug for readme
authored
45
ecd52d5 @binarylogic Updated readme
authored
46 # app/views/users/index.html.haml
47 - form_for @search do |f|
48 - f.fields_for @search.conditions do |users|
49 = users.text_field :first_name_contains
50 = users.calendar_date_select :created_after # nice rails plugin for replacing date_select
51 - users.fields_for users.object.orders do |orders|
52 = orders.select :total_gt, (1..100)
53 = f.submit "Search"
f66721b @binarylogic Github needs to fix the escaping bug for readme
authored
54
ecd52d5 @binarylogic Updated readme
authored
55 %table
56 %tr
57 %th= order_by :first_name
58 %th= order_by :last_name
59 %th= order_by :email
60 - @users.each do |user|
61 %tr
62 %td= user.first_name
63 %td= user.last_name
64 %td= user.email
f66721b @binarylogic Github needs to fix the escaping bug for readme
authored
65
ecd52d5 @binarylogic Updated readme
authored
66 Per page:
67 = per_page
68 Page:
69 = page
24a7d88 @binarylogic Updated readme
authored
70
6449698 @binarylogic Released verversion 0.9.7
authored
71 Nice and simple. 2 lines in your controller, no "cruft" in your models, pagination as simple as calling the "page" and "per_page" helpers, ordering as simple as calling "order_by", and all searching conditions in 1 spot: your form. For documentation on helpers see Searchgasm::Helpers::FormHelper and Searchgasm::Helpers::SearchHelper.
7db3b5b @binarylogic Updated readme
authored
72
a1e2d41 @binarylogic Fixed some probs in readme
authored
73 <b>See this example live: http://searchgasm_example.binarylogic.com</b>
cf23760 @binarylogic Added tests, new features, and improved readme
authored
74
6449698 @binarylogic Released verversion 0.9.7
authored
75 == Simple Searching Example
7db3b5b @binarylogic Updated readme
authored
76
967e690 @binarylogic Cleaned up readme
authored
77 User.all(
78 :conditions => {
760ba27 @binarylogic Added new aliases for date and time columns
authored
79 :first_name_contains => "Ben", # first_name like '%Ben%'
80 :email_ends_with => "binarylogic.com" # email like '%binarylogic.com'
967e690 @binarylogic Cleaned up readme
authored
81 },
760ba27 @binarylogic Added new aliases for date and time columns
authored
82 :per_page => 20 # limit 20
c4a3956 @binarylogic Updated helpers, cleaned up code, started documentation
authored
83 :page => 3 # offset 40, which starts us on page 3
967e690 @binarylogic Cleaned up readme
authored
84 )
1271b7a @binarylogic Added gemspec
authored
85
6449698 @binarylogic Released verversion 0.9.7
authored
86 Instead of using the "all" method you could use any search method: first, find(:all), find(:first), count, sum, average, etc, just like ActiveRecord
4065f0d @binarylogic Updated readme
authored
87
6449698 @binarylogic Released verversion 0.9.7
authored
88 == Exhaustive Example w/ Object Based Searching (great for form_for or fields_for)
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
89
760ba27 @binarylogic Added new aliases for date and time columns
authored
90 # Instantiate
dcf6ebf @binarylogic Updated readme
authored
91 @search = User.new_search(
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
92 :conditions => {
93 :first_name_contains => "Ben",
94 :age_gt => 18,
95 :orders => {:total_lt => 100}
7db3b5b @binarylogic Updated readme
authored
96 },
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
97 :per_page => 20,
98 :page => 2,
99 :order_by => {:orders => :total},
100 :order_as => "DESC"
9968056 @binarylogic Updated readme
authored
101 )
760ba27 @binarylogic Added new aliases for date and time columns
authored
102
6449698 @binarylogic Released verversion 0.9.7
authored
103 # Set local conditions
dcf6ebf @binarylogic Updated readme
authored
104 @search.conditions.email_ends_with = "binarylogic.com"
6449698 @binarylogic Released verversion 0.9.7
authored
105
106 # Set conditions on relationships
107 @search.conditions.oders.line_items.created_after = Time.now # can traverse through all relationships
760ba27 @binarylogic Added new aliases for date and time columns
authored
108
109 # Set options
6449698 @binarylogic Released verversion 0.9.7
authored
110 @search.per_page = 50 # overrides the 20 set above
111 @search.order_by = [:first_name, {:user_group => :name}] # order by first name and then by the user group's name it belongs to
112 @search.order_as = "ASC"
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
113
760ba27 @binarylogic Added new aliases for date and time columns
authored
114 # Set ANY of the ActiveRecord options
dcf6ebf @binarylogic Updated readme
authored
115 @search.group = "last_name"
116 @search.readonly = true
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
117 # ... see ActiveRecord documentation
118
119 # Return results just like ActiveRecord
dcf6ebf @binarylogic Updated readme
authored
120 @search.all
121 @search.first
122
c703fbe @binarylogic Updated readme
authored
123 Take the @search object and pass it right into form\_for or fields\_for (see above).
cf23760 @binarylogic Added tests, new features, and improved readme
authored
124
6449698 @binarylogic Released verversion 0.9.7
authored
125 == Calculations
cf23760 @binarylogic Added tests, new features, and improved readme
authored
126
127 Using the object from above:
128
aa89fc3 @binarylogic Added searching for conditions objects
authored
129 @search.average('id')
130 @search.count
131 @search.maximum('id')
132 @search.minimum('id')
133 @search.sum('id')
6449698 @binarylogic Released verversion 0.9.7
authored
134 @search.calculate(:sum, 'id')
135 # ...any of the above calculations, see ActiveRecord documentation on calculations
1f7251c @binarylogic Added association_collection
authored
136
cf23760 @binarylogic Added tests, new features, and improved readme
authored
137 Or do it from your model:
138
139 User.count(:conditions => {:first_name_contains => "Ben"})
47f2ffe @binarylogic Found pattpattern for search methods
authored
140 User.sum('id', :conditions => {:first_name_contains => "Ben"})
cf23760 @binarylogic Added tests, new features, and improved readme
authored
141 # ... all other calcualtions, etc.
142
6449698 @binarylogic Released verversion 0.9.7
authored
143 == Different ways to search, take your pick
7db3b5b @binarylogic Updated readme
authored
144
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
145 Any of the options used in the above example can be used in these, but for the sake of brevity I am only using a few:
7db3b5b @binarylogic Updated readme
authored
146
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
147 User.all(:conditions => {:age_gt => 18}, :per_page => 20)
7db3b5b @binarylogic Updated readme
authored
148
1271b7a @binarylogic Added gemspec
authored
149 User.first(:conditions => {:age_gt => 18}, :per_page => 20)
7db3b5b @binarylogic Updated readme
authored
150
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
151 User.find(:all, :conditions => {::age_gt => 18}, :per_page => 20)
7db3b5b @binarylogic Updated readme
authored
152
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
153 User.find(:first, :conditions => {::age_gt => 18}, :per_page => 20)
7db3b5b @binarylogic Updated readme
authored
154
760ba27 @binarylogic Added new aliases for date and time columns
authored
155 search = User.new_search(:conditions => {:age_gt => 18}) # build_search is an alias
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
156 search.conditions.first_name_contains = "Ben"
1271b7a @binarylogic Added gemspec
authored
157 search.per_page = 20
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
158 search.all
7db3b5b @binarylogic Updated readme
authored
159
6449698 @binarylogic Released verversion 0.9.7
authored
160 If you want to use Searchgasm directly:
7db3b5b @binarylogic Updated readme
authored
161
6449698 @binarylogic Released verversion 0.9.7
authored
162 search = Searchgasm::Search::Base.new(User, :conditions => {:age_gt => 18})
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
163 search.conditions.first_name_contains = "Ben"
1271b7a @binarylogic Added gemspec
authored
164 search.per_page = 20
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
165 search.all
7db3b5b @binarylogic Updated readme
authored
166
6449698 @binarylogic Released verversion 0.9.7
authored
167 == Search with conditions only
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
168
169 conditions = User.new_conditions(:age_gt => 18)
170 conditions.first_name_contains = "Ben"
171 conditions.all
172 # ... all operations above are available
173
174 Pass a conditions object right into ActiveRecord:
175
6449698 @binarylogic Released verversion 0.9.7
authored
176 User.all(:conditions => conditions)
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
177
6449698 @binarylogic Released verversion 0.9.7
authored
178 Again, if you want to use Searchgasm directly:
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
179
6449698 @binarylogic Released verversion 0.9.7
authored
180 conditions = Searchgasm::Conditions::Base.new(User, :age_gt => 18)
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
181 conditions.first_name_contains = "Ben"
6449698 @binarylogic Released verversion 0.9.7
authored
182 conditions.all
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
183
28cd46c @binarylogic Updated readme
authored
184 Now pass the conditions object right into form\_for or fields\_for (see above for example).
185
6449698 @binarylogic Released verversion 0.9.7
authored
186 == Scoped searching
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
187
86ba608 @binarylogic Updated readme
authored
188 @current_user.orders.find(:all, :conditions => {:total_lte => 500})
189 @current_user.orders.count(:conditions => {:total_lte => 500})
190 @current_user.orders.sum('total', :conditions => {:total_lte => 500})
1271b7a @binarylogic Added gemspec
authored
191
192 search = @current_user.orders.build_search('total', :conditions => {:total_lte => 500})
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
193
6449698 @binarylogic Released verversion 0.9.7
authored
194 == Searching trees
cf23760 @binarylogic Added tests, new features, and improved readme
authored
195
196 For tree data structures you get a few nifty methods. Let's assume Users is a tree data structure.
197
198 # Child of
199 User.all(:conditions => {:child_of => User.roots.first})
200 User.all(:conditions => {:child_of => User.roots.first.id})
201
202 # Sibling of
203 User.all(:conditions => {:sibling_of => User.roots.first})
204 User.all(:conditions => {:sibling_of => User.roots.first.id})
205
f5450d1 @binarylogic Changed structure of conditions
authored
206 # Descendant of (includes all recursive children: children, grand children, great grand children, etc)
207 User.all(:conditions => {:descendant_of => User.roots.first})
208 User.all(:conditions => {:descendant_of => User.roots.first.id})
cf23760 @binarylogic Added tests, new features, and improved readme
authored
209
f5450d1 @binarylogic Changed structure of conditions
authored
210 # Inclusive descendant_of. Same as above but includes the root
211 User.all(:conditions => {:inclusive_descendant_of => User.roots.first})
212 User.all(:conditions => {:inclusive_descendant_of => User.roots.first.id})
cf23760 @binarylogic Added tests, new features, and improved readme
authored
213
214
6449698 @binarylogic Released verversion 0.9.7
authored
215 == Available anywhere (relationships & scopes)
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
216
6449698 @binarylogic Released verversion 0.9.7
authored
217 Not only can you use searchgasm when searching, but you can use it when setting up relationships or scopes. Anywhere you specify conditions in ActiveRecord.
7db3b5b @binarylogic Updated readme
authored
218
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
219 class User < ActiveRecord::Base
220 has_many :expensive_pending_orders, :conditions => {:total_greater_than => 1_000_000, :state => :pending}, :per_page => 20
221 named_scope :sexy, :conditions => {:first_name => "Ben", email_ends_with => "binarylogic.com"}, :per_page => 20
7db3b5b @binarylogic Updated readme
authored
222 end
590f51e @binarylogic Updated readme
authored
223
6449698 @binarylogic Released verversion 0.9.7
authored
224 == Always use protection...against SQL injections
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
225
6449698 @binarylogic Released verversion 0.9.7
authored
226 If there is one thing we all know, it's to always use protection against SQL injections. That's why searchgasm protects you by default. The new\_search and new\_conditions methods are protected by default. This means that various checks are done to ensure it is not possible to perform any type of SQL injection. But this also limits how you can search, meaning you can't write raw SQL. If you want to be daring and search without protection, all that you have to do is add ! to the end of the methods: new\_search! and new\_conditions!.
590f51e @binarylogic Updated readme
authored
227
6449698 @binarylogic Released verversion 0.9.7
authored
228 === Protected from SQL injections
9968056 @binarylogic Updated readme
authored
229
33f192e @binarylogic Ehanced protection
authored
230 search = Account.new_search(params[:search])
231 conditions = Account.new_conditions(params[:conditions])
ae22512 @binarylogic Updated readme
authored
232
6449698 @binarylogic Released verversion 0.9.7
authored
233 === *NOT* protected from SQL injections
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
234
33f192e @binarylogic Ehanced protection
authored
235 accounts = Account.find(params[:search])
7db3b5b @binarylogic Updated readme
authored
236 accounts = Account.all(params[:search])
33f192e @binarylogic Ehanced protection
authored
237 account = Account.first(params[:search])
238 search = Account.new_search!(params[:search])
239 conditions = Account.new_conditions!(params[:conditions])
240
241 Lesson learned: use new\_search and new\_conditions when passing in params as *ANY* of the options.
aa5b90a @binarylogic First commit
authored
242
6449698 @binarylogic Released verversion 0.9.7
authored
243 == Available Conditions
ae22512 @binarylogic Updated readme
authored
244
1271b7a @binarylogic Added gemspec
authored
245 Depending on the type, each column comes preloaded with a bunch of nifty conditions:
ae22512 @binarylogic Updated readme
authored
246
7db3b5b @binarylogic Updated readme
authored
247 all columns
248 => :equals, :does_not_equal
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
249
7db3b5b @binarylogic Updated readme
authored
250 :string, :text
251 => :begins_with, :contains, :keywords, :ends_with
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
252
7db3b5b @binarylogic Updated readme
authored
253 :integer, :float, :decimal,:datetime, :timestamp, :time, :date
254 => :greater_than, :greater_than_or_equal_to, :less_than, :less_than_or_equal_to
ae22512 @binarylogic Updated readme
authored
255
cf23760 @binarylogic Added tests, new features, and improved readme
authored
256 tree data structures (see above "searching trees")
f5450d1 @binarylogic Changed structure of conditions
authored
257 => :child_of, :sibling_of, :descendant_of, :inclusive_descendant_of
cf23760 @binarylogic Added tests, new features, and improved readme
authored
258
7db3b5b @binarylogic Updated readme
authored
259 Some of these conditions come with aliases, so you have your choice how to call the conditions. For example you can use "greater\_than" or "gt":
1f7251c @binarylogic Added association_collection
authored
260
7db3b5b @binarylogic Updated readme
authored
261 :equals; => :is
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
262 :does_not_equal => :is_not, :not
f5450d1 @binarylogic Changed structure of conditions
authored
263 :begins_with => :starts_with, :bw, :start
264 :contains => :like, :has
265 :ends_with => :ew, :ends, :end
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
266 :greater_than => :gt, :after
7db3b5b @binarylogic Updated readme
authored
267 :greater_than_or_equal_to => :at_least, :gte
f5450d1 @binarylogic Changed structure of conditions
authored
268 :keywords => :kwords, :kw
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
269 :less_than => :lt, :before
7db3b5b @binarylogic Updated readme
authored
270 :less_than_or_equal_to => :at_most, :lte
4fb5929 @binarylogic Added new features, cleaned up readme
authored
271
6449698 @binarylogic Released verversion 0.9.7
authored
272 For more information on each condition see Searchgasm::Condition. Each condition has it's own class and the source is pretty simple and self explanatory.
273
274 === Enhanced searching and blacklisted words
bec7f7d @binarylogic Reffeactored and changed everything, much nicer
authored
275
276 You will notice above there is "contains" and "keywords". The difference is that "keywords" is an enhanced search. It acts like a real keyword search. It finds those keywords, in any order, and blacklists meaningless words such as "and", "the", etc. "contains" finds the EXACT string in the column you are searching, spaces and all.
277
6449698 @binarylogic Released verversion 0.9.7
authored
278 === Roll your own conditions
e8274f3 @binarylogic Added example in readme for adding your own conditions
authored
279
280 I didn't include this function because its MySQL specific, and it's probably rarely used, but MySQL supports a "SOUNDS LIKE" function.
281
282 I want to use it, so let's add it:
283
284 # config/initializers/searchgasm.rb
285 # Actual function for MySQL databases only
6449698 @binarylogic Released verversion 0.9.7
authored
286 class SoundsLike < Searchgasm::Condition::Base
e8274f3 @binarylogic Added example in readme for adding your own conditions
authored
287 class << self
288 # I pass you the column, you tell me what you want the method to be called.
289 # If you don't want to add this condition for that column, return nil
290 # It defaults to "#{column.name}_sounds_like". So if thats what you want you don't even need to do this.
291 def name_for_column(column)
292 super
293 end
294
295 # Only do this if you want aliases for your condition
296 def aliases_for_column(column)
297 ["#{column.name}_sounds", "#{column.name}_similar_to"]
298 end
299 end
300
7e23dad @binarylogic Updated readme
authored
301 # You can return an array or a string. NOT a hash, because all of these conditions
302 # need to eventually get merged together. The array or string can be anything you would put in
303 # the :conditions option for ActiveRecord::Base.find()
e8274f3 @binarylogic Added example in readme for adding your own conditions
authored
304 def to_conditions(value)
305 ["#{quoted_table_name}.#{quoted_column_name} SOUNDS LIKE ?", value]
306 end
307 end
308
6449698 @binarylogic Released verversion 0.9.7
authored
309 Searchgasm::Conditions::Base.register_condition(SoundsLike)
e8274f3 @binarylogic Added example in readme for adding your own conditions
authored
310
311 Now test it out:
312
313 search = User.new_search
6449698 @binarylogic Released verversion 0.9.7
authored
314 search.conditions.first_name_sounds_like = "Ben"
e8274f3 @binarylogic Added example in readme for adding your own conditions
authored
315 search.all # will return any user that has a first name that sounds like "Ben"
316
6449698 @binarylogic Released verversion 0.9.7
authored
317 Pretty nifty, huh? You can create any condition ultimately creating any SQL you want. The sky is the limit. For more information see Searchgasm::Condition::Base
318
319 == Reporting problems / bugs
320
321 http://binarylogic.lighthouseapp.com/projects/16601-searchgasm
e8274f3 @binarylogic Added example in readme for adding your own conditions
authored
322
6449698 @binarylogic Released verversion 0.9.7
authored
323 == Credits
4fb5929 @binarylogic Added new features, cleaned up readme
authored
324
1320254 @binarylogic Fixed some probs in readme
authored
325 Author: {Ben Johnson}[http://github.com/binarylogic] of {Binary Logic}[http://www.binarylogic.com]
9b5304b @binarylogic Updated readme
authored
326
1320254 @binarylogic Fixed some probs in readme
authored
327 Credit to {Zack Ham}[http://github.com/zackham] and {Robert Malko}[http://github.com/malkomalko/] for helping with feature suggestions.
aa5b90a @binarylogic First commit
authored
328
329
1320254 @binarylogic Fixed some probs in readme
authored
330 Copyright (c) 2008 {Ben Johnson}[http://github.com/binarylogic] of {Binary Logic}[http://www.binarylogic.com], released under the MIT license
Something went wrong with that request. Please try again.