squint 0.0.2 → 2.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9a5809890b110fc3863311aad72c13db199e7e88
4
- data.tar.gz: '080abf910d75ad22c8fbe88888dfdcc3eb097e85'
2
+ SHA256:
3
+ metadata.gz: f1a001a14acd2fc3e2ca16adbb14999e7c0955f7588f7bf2b24ea6b459020cd7
4
+ data.tar.gz: 68a54fa15e06555a8c0320aadac4a9578fbb134f2abb34a4908d9a4f2c034d98
5
5
  SHA512:
6
- metadata.gz: dae6174d587f03165c68a23c29a587b36a10020e58e8270cbd9140a51b206677df2dc5f78f54184baef9ec1caf1c03df483589d93f32baece688a09397a9aff5
7
- data.tar.gz: 0e14e56fda9c7f06fd164a1005e686622ebfc29b07d4208e0030c3b00f63bda28b79182d473a059cc77df03d763a82c6f23e6b3e0f50c37dac128e23c32373ac
6
+ metadata.gz: f9fcb29b013d12a51aaff4dfed8674a76471ff5eb9e6d92727eeae7ff585c51dc4edb0ef53cb4fdefcf7b6b059391e5bf950c03d659b26d0057c6c32b4567c97
7
+ data.tar.gz: 86cbf3a386e89e2d0d2da91fe585c94dc0d53eeaad4fdfda75ccff26dc14147056500a369fa8380f1fc4e85b941d92a86d61d9b74a79b0353ea551a5b76e4f75
@@ -0,0 +1,108 @@
1
+ {
2
+ "projectName": "squint",
3
+ "projectOwner": "ProctorU",
4
+ "files": [
5
+ "readme.md"
6
+ ],
7
+ "imageSize": 100,
8
+ "commit": true,
9
+ "contributors": [
10
+ {
11
+ "login": "chevinbrown",
12
+ "name": "Kevin Brown",
13
+ "avatar_url": "https://avatars2.githubusercontent.com/u/864581?v=3",
14
+ "profile": "https://github.com/chevinbrown",
15
+ "contributions": [
16
+ "design",
17
+ "review"
18
+ ]
19
+ },
20
+ {
21
+ "login": "king601",
22
+ "name": "Andrew Fomera",
23
+ "avatar_url": "https://avatars2.githubusercontent.com/u/1741179?v=3",
24
+ "profile": "http://andrewfomera.com",
25
+ "contributions": [
26
+ "review",
27
+ "code"
28
+ ]
29
+ },
30
+ {
31
+ "login": "rthbound",
32
+ "name": "Ryan T. Hosford",
33
+ "avatar_url": "https://avatars2.githubusercontent.com/u/708692?v=3",
34
+ "profile": "https://github.com/rthbound",
35
+ "contributions": [
36
+ "code"
37
+ ]
38
+ },
39
+ {
40
+ "login": "Jaehdawg",
41
+ "name": "Matthew Jaeh",
42
+ "avatar_url": "https://avatars2.githubusercontent.com/u/1785682?v=3",
43
+ "profile": "https://github.com/Jaehdawg",
44
+ "contributions": [
45
+ "design",
46
+ "review"
47
+ ]
48
+ },
49
+ {
50
+ "login": "licatajustin",
51
+ "name": "Justin Licata",
52
+ "avatar_url": "https://avatars0.githubusercontent.com/u/3933204?v=3",
53
+ "profile": "https://twitter.com/justinlicata",
54
+ "contributions": [
55
+ "code",
56
+ "design",
57
+ "doc",
58
+ "review"
59
+ ]
60
+ },
61
+ {
62
+ "login": "kmiracle86",
63
+ "name": "Kyle Miracle",
64
+ "avatar_url": "https://avatars3.githubusercontent.com/u/24704300?v=4",
65
+ "profile": "https://github.com/kmiracle86",
66
+ "contributions": [
67
+ "bug",
68
+ "review"
69
+ ]
70
+ },
71
+ {
72
+ "login": "dwilkins",
73
+ "name": "David H. Wilkins",
74
+ "avatar_url": "https://avatars2.githubusercontent.com/u/97011?v=3",
75
+ "profile": "http://conecuh.com",
76
+ "contributions": [
77
+ "question",
78
+ "bug",
79
+ "code",
80
+ "design",
81
+ "doc",
82
+ "example",
83
+ "review",
84
+ "test"
85
+ ]
86
+ },
87
+ {
88
+ "login": "TheJayWright",
89
+ "name": "Jay Wright",
90
+ "avatar_url": "https://avatars3.githubusercontent.com/u/19173815?v=3",
91
+ "profile": "https://github.com/TheJayWright",
92
+ "contributions": [
93
+ "review"
94
+ ]
95
+ },
96
+ {
97
+ "login": "jamescook",
98
+ "name": "James Cook",
99
+ "avatar_url": "https://avatars1.githubusercontent.com/u/4067?s=460&u=cb404cc0f1737c2fc53411e300cc8e158ef29295&v=4",
100
+ "profile": "https://github.com/jamescook",
101
+ "contributions": [
102
+ "code",
103
+ "test",
104
+ "review"
105
+ ]
106
+ }
107
+ ]
108
+ }
@@ -3,50 +3,95 @@ require 'active_support/concern'
3
3
  # Squint json, jsonb, hstore queries
4
4
  module Squint
5
5
  extend ActiveSupport::Concern
6
- include ::ActiveRecord::QueryMethods
6
+ if ActiveRecord::VERSION::STRING < '5'
7
+ include ::ActiveRecord::QueryMethods
8
+ end
7
9
 
8
10
  module WhereMethods
9
- # Args may be passed to build_where like:
11
+ # Args may be passed to build/build_where like:
10
12
  # build_where(jsonb_column: {key1: value1})
11
13
  # build_where(jsonb_column: {key1: value1}, jsonb_column: {key2: value2})
12
14
  # build_where(jsonb_column: {key1: value1}, regular_column: value)
13
15
  # build_where(jsonb_column: {key1: value1}, association: {column: value))
14
- def build_where(*args)
16
+ if ActiveRecord::VERSION::STRING > '5'
17
+ method_name = :build
18
+ elsif ActiveRecord::VERSION::STRING < '5'
19
+ method_name = :build_where
20
+ end
21
+ send :define_method, method_name do |*args|
22
+ # For Rails 5, we end up monkey patching WhereClauseFactory for everyone
23
+ # so need to return super if our methods aren't on the AR class
24
+ # doesn't hurt for 4.2.x either
25
+ return super(*args) unless klass.respond_to?(:squint_hash_field_reln)
15
26
  save_args = []
16
27
  reln = args.inject([]) do |memo, arg|
17
- if !save_args.empty?
18
- save_args << arg
19
- memo += super(save_args)
20
- save_args = []
21
- elsif arg.is_a?(Hash)
28
+ if arg.is_a?(Hash)
22
29
  arg.keys.each do |key|
23
- if arg[key].is_a?(Hash) && HASH_DATA_COLUMNS[key]
24
- memo << hash_field_reln(key => arg[key])
30
+ if arg[key].is_a?(Hash) && klass::HASH_DATA_COLUMNS[key]
31
+ memo << klass.squint_hash_field_reln(key => arg[key])
25
32
  else
26
- memo += super(key => arg[key])
33
+ save_args[0] ||= {}
34
+ save_args[0][key] = arg[key]
27
35
  end
28
36
  end
29
37
  elsif arg.present?
30
- if arg.is_a? String
31
- save_args << arg
32
- else
33
- memo += super(arg)
34
- end
38
+ save_args << arg
35
39
  end
36
40
  memo
37
41
  end
38
- reln += super(save_args) unless save_args.empty?
42
+ if ActiveRecord::VERSION::STRING >= '5.2'
43
+ # In 5.2 the WhereClause private class no longer takes two arguments.
44
+ # Commit where it was removed:
45
+ # https://github.com/rails/rails/commit/213796fb4936dce1da2f0c097a054e1af5c25c2c#diff-c9d167bac00ff2f45c5b5e035e8a80e8
46
+ reln = ActiveRecord::Relation::WhereClause.new(reln)
47
+ elsif ActiveRecord::VERSION::STRING > '5'
48
+ reln = ActiveRecord::Relation::WhereClause.new(reln, [])
49
+ end
50
+ save_args << [] if save_args.size == 1
51
+ reln += super(*save_args) unless save_args.empty?
39
52
  reln
40
53
  end
54
+ end
55
+
56
+ included do |base|
57
+ if ActiveRecord::VERSION::STRING < '5'
58
+ ar_reln_module = base::ActiveRecord_Relation
59
+ ar_association_module = base::ActiveRecord_AssociationRelation
60
+ elsif ActiveRecord::VERSION::STRING > '5.1'
61
+ # ActiveRecord_Relation is now a private_constant in 5.1.x
62
+ ar_reln_module = base.relation_delegate_class(ActiveRecord::Relation)::WhereClauseFactory
63
+ ar_association_module = nil
64
+ elsif ActiveRecord::VERSION::STRING > '5.0'
65
+ ar_reln_module = base::ActiveRecord_Relation::WhereClauseFactory
66
+ ar_association_module = nil
67
+ # ar_association_module = base::ActiveRecord_AssociationRelation
68
+ end
69
+
70
+ # put together a list of columns in this model
71
+ # that are hstore, json, or jsonb and will benefit from
72
+ # searchability
73
+ base::HASH_DATA_COLUMNS = base.columns_hash.keys.map do |col_name|
74
+ if %w[hstore json jsonb].include?(base.columns_hash[col_name].sql_type)
75
+ [col_name.to_sym, base.columns_hash[col_name].sql_type]
76
+ end
77
+ end.compact.to_h
78
+
79
+ ar_reln_module.class_eval do
80
+ prepend WhereMethods
81
+ end
41
82
 
42
- # hash_field_reln
83
+ ar_association_module.try(:class_eval) do
84
+ prepend WhereMethods
85
+ end
86
+
87
+ # squint_hash_field_reln
43
88
  # return an Arel object with the appropriate query
44
89
  # Strings want to be a SQL Literal, other things can be
45
90
  # passed in bare to the eq or in operator
46
- def hash_field_reln(*args)
91
+ def self.squint_hash_field_reln(*args)
47
92
  temp_attr = args[0]
48
93
  contains_nil = false
49
- column_type = HASH_DATA_COLUMNS[args[0].keys.first]
94
+ column_type = self::HASH_DATA_COLUMNS[args[0].keys.first]
50
95
  column_name_segments = []
51
96
  quote_char = '"'.freeze
52
97
  while temp_attr.is_a?(Hash)
@@ -108,109 +153,84 @@ module Squint
108
153
  # specified as a query value
109
154
  if check_attr_missing
110
155
  reln = if column_type == 'hstore'.freeze
111
- hstore_element_missing(column_name_segments, reln)
156
+ squint_hstore_element_missing(column_name_segments, reln)
112
157
  else
113
- jsonb_element_missing(column_name_segments, reln)
158
+ squint_jsonb_element_missing(column_name_segments, reln)
114
159
  end
115
160
  end
116
161
  reln
117
162
  end
118
- end
119
-
120
- included do |base|
121
- ar_reln_module = base::ActiveRecord_Relation
122
- ar_association_module = base::ActiveRecord_AssociationRelation
123
163
 
124
- # put together a list of columns in this model
125
- # that are hstore, json, or jsonb and will benefit from
126
- # searchability
127
- HASH_DATA_COLUMNS = base.columns_hash.keys.map do |col_name|
128
- if %w[hstore json jsonb].include?(base.columns_hash[col_name].sql_type)
129
- [col_name.to_sym, base.columns_hash[col_name].sql_type]
164
+ def self.squint_storext_default?(temp_attr, attribute_sym)
165
+ return false unless respond_to?(:storext_definitions)
166
+ if storext_definitions.keys.include?(attribute_sym) &&
167
+ !(storext_definitions[attribute_sym][:opts] &&
168
+ storext_definitions[attribute_sym][:opts][:default]).nil? &&
169
+ [temp_attr].compact.map(&:to_s).
170
+ flatten.
171
+ include?(storext_definitions[attribute_sym][:opts][:default].to_s)
172
+ true
130
173
  end
131
- end.compact.to_h
132
-
133
- ar_reln_module.class_eval do
134
- prepend WhereMethods
135
174
  end
136
175
 
137
- ar_association_module.class_eval do
138
- prepend WhereMethods
176
+ def self.squint_hstore_element_exists(element, attribute_hash_column, value)
177
+ Arel::Nodes::Equality.new(
178
+ Arel::Nodes::NamedFunction.new(
179
+ "exist",
180
+ [arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
181
+ Arel::Nodes::SqlLiteral.new(element)]
182
+ ), value
183
+ )
139
184
  end
140
185
 
141
- def self.jsonb_element_missing(column_name_segments, reln)
186
+ def self.squint_hstore_element_missing(column_name_segments, reln)
142
187
  element = column_name_segments.pop
143
188
  attribute_hash_column = column_name_segments.join('->'.freeze)
144
189
  # Query generated is equals default or attribute present is null or equals false
145
- # * Is null happens when the the whole column is null
190
+ # * Is null happens the the column is null
146
191
  # * equals false is when the column has jsonb data, but the key doesn't exist
147
192
  # ("posts"."storext_attributes"->>'is_awesome' = 'false' OR
148
- # (("posts"."storext_attributes" ? 'is_awesome') IS NULL OR
149
- # ("posts"."storext_attributes" ? 'is_awesome') = FALSE)
193
+ # (exists("posts"."storext_attributes", 'is_awesome') IS NULL OR
194
+ # exists("posts"."storext_attributes", 'is_awesome') = FALSE)
150
195
  # )
151
196
  Arel::Nodes::Grouping.new(
152
197
  reln.or(
153
198
  Arel::Nodes::Grouping.new(
154
- Arel::Nodes::Equality.new(
155
- Arel::Nodes::Grouping.new(
156
- Arel::Nodes::InfixOperation.new(
157
- Arel::Nodes::SqlLiteral.new('?'),
158
- arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
159
- Arel::Nodes::SqlLiteral.new(element)
160
- )
161
- ), nil
162
- ).or(
163
- Arel::Nodes::Equality.new(
164
- Arel::Nodes::Grouping.new(
165
- Arel::Nodes::InfixOperation.new(
166
- Arel::Nodes::SqlLiteral.new('?'),
167
- arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
168
- Arel::Nodes::SqlLiteral.new(element)
169
- )
170
- ), Arel::Nodes::False.new
171
- )
172
- )
199
+ squint_hstore_element_exists(element, attribute_hash_column, Arel::Nodes::False.new)
200
+ ).or(
201
+ squint_hstore_element_exists(element, attribute_hash_column, nil)
173
202
  )
174
203
  )
175
204
  )
176
205
  end
177
206
 
178
- def self.squint_storext_default?(temp_attr, attribute_sym)
179
- return false unless respond_to?(:storext_definitions)
180
- if storext_definitions.keys.include?(attribute_sym) &&
181
- !storext_definitions[attribute_sym].dig(:opts, :default).nil? &&
182
- [temp_attr].compact.map(&:to_s).
183
- flatten.
184
- include?(storext_definitions[attribute_sym][:opts][:default].to_s)
185
- true
186
- end
207
+ def self.squint_jsonb_element_equality(element, attribute_hash_column, value)
208
+ Arel::Nodes::Equality.new(
209
+ Arel::Nodes::Grouping.new(
210
+ Arel::Nodes::InfixOperation.new(
211
+ Arel::Nodes::SqlLiteral.new('?'),
212
+ arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
213
+ Arel::Nodes::SqlLiteral.new(element)
214
+ )
215
+ ), value
216
+ )
187
217
  end
188
218
 
189
- def self.hstore_element_missing(column_name_segments, reln)
219
+ def self.squint_jsonb_element_missing(column_name_segments, reln)
190
220
  element = column_name_segments.pop
191
221
  attribute_hash_column = column_name_segments.join('->'.freeze)
192
222
  # Query generated is equals default or attribute present is null or equals false
193
- # * Is null happens the the column is null
223
+ # * Is null happens when the the whole column is null
194
224
  # * equals false is when the column has jsonb data, but the key doesn't exist
195
225
  # ("posts"."storext_attributes"->>'is_awesome' = 'false' OR
196
- # (exists("posts"."storext_attributes", 'is_awesome') IS NULL OR
197
- # exists("posts"."storext_attributes", 'is_awesome') = FALSE)
226
+ # (("posts"."storext_attributes" ? 'is_awesome') IS NULL OR
227
+ # ("posts"."storext_attributes" ? 'is_awesome') = FALSE)
198
228
  # )
199
229
  Arel::Nodes::Grouping.new(
200
230
  reln.or(
201
231
  Arel::Nodes::Grouping.new(
202
- Arel::Nodes::NamedFunction.new(
203
- "exist",
204
- [arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
205
- Arel::Nodes::SqlLiteral.new(element)]
206
- ).eq(Arel::Nodes::False.new)
207
- ).or(
208
- Arel::Nodes::Equality.new(
209
- Arel::Nodes::NamedFunction.new(
210
- "exist",
211
- [arel_table[Arel::Nodes::SqlLiteral.new(attribute_hash_column)],
212
- Arel::Nodes::SqlLiteral.new(element)]
213
- ), nil
232
+ squint_jsonb_element_equality(element, attribute_hash_column, nil).or(
233
+ squint_jsonb_element_equality(element, attribute_hash_column, Arel::Nodes::False.new)
214
234
  )
215
235
  )
216
236
  )
@@ -1,3 +1,3 @@
1
1
  module Squint
2
- VERSION = "0.0.2".freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
@@ -0,0 +1,186 @@
1
+ <p align="center">
2
+ <a href="https://twitter.com/ProctorUEng">
3
+ <img src="https://s3-us-west-2.amazonaws.com/dev-team-resources/squint-wordmark.svg" width=198 height=72>
4
+ </a>
5
+
6
+ <p align="center">
7
+ Search PostgreSQL <code>jsonb</code> and <code>hstore</code> columns.
8
+ </p>
9
+ </p>
10
+
11
+ <br>
12
+
13
+ > Full database searching inside columns containing semi-structured data like `json`,
14
+ `jsonb` and `hstore`. <strong>Compatible with the awesome
15
+ <a href="https://github.com/G5/storext">storext</a> gem</strong>.
16
+
17
+ ## Table of contents
18
+
19
+ - [Status](#status)
20
+ - [Quick start](#quick-start)
21
+ - [Performance](#performance)
22
+ - [Storext attributes](#storext-attributes)
23
+ - [Developing](#developing)
24
+ - [Contributors](#contributors)
25
+ - [Credits](#credits)
26
+
27
+ ## Status
28
+ [![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors)
29
+ [![CircleCI](https://circleci.com/gh/ProctorU/squint.svg?style=svg)](https://circleci.com/gh/ProctorU/squint)
30
+
31
+ ## Quick Start
32
+
33
+ Add to your Gemfile:
34
+
35
+ ```ruby
36
+ gem 'squint'
37
+ ```
38
+
39
+ Include it in your models:
40
+
41
+ ```ruby
42
+ class Post < ActiveRecord::Base
43
+ include Squint
44
+ # ...
45
+ end
46
+ ```
47
+
48
+ Assuming a table with the following structure:
49
+ ```
50
+ Table "public.posts"
51
+ Column | Type | Modifiers
52
+ ---------------------------+-----------------------------+----------------------------------------------------
53
+ id | integer | not null default nextval('posts_id_seq'::regclass)
54
+ title | character varying |
55
+ body | character varying |
56
+ request_info | jsonb |
57
+ properties | hstore |
58
+ storext_jsonb_attributes | jsonb |
59
+ storext_hstore_attributes | jsonb |
60
+ created_at | timestamp without time zone | not null
61
+ updated_at | timestamp without time zone | not null
62
+ Indexes:
63
+ "posts_pkey" PRIMARY KEY, btree (id)
64
+ ```
65
+
66
+ In your code use queries like:
67
+ ```ruby
68
+ Post.where(properties: { referer: 'http://example.com/one' } )
69
+ # SELECT "posts".* FROM "posts" WHERE "posts"."properties"->'referer' = 'http://example.com/one'
70
+
71
+ Post.where(properties: { referer: nil } )
72
+ # SELECT "posts".* FROM "posts" WHERE "posts"."properties"->'referer' IS NULL
73
+
74
+ Post.where(properties: { referer: ['http://example.com/one',nil] } )
75
+ # SELECT "posts".* FROM "posts" WHERE ("posts"."properties"->'referer' = 'http://example.com/one'
76
+ # OR "posts"."properties"->'referer' IS NULL)
77
+
78
+ Post.where(request_info: { referer: ['http://example.com/one',nil] } )
79
+ # SELECT "posts".* FROM "posts" WHERE ("posts"."request_info"->>'referer' = 'http://example.com/one'
80
+ # OR "posts"."request_info"->>'referer' IS NULL)
81
+ ```
82
+
83
+ Squint only operates on json, jsonb and hstore columns. ActiveRecord
84
+ will throw a StatementInvalid exception like always if the column type is unsupported by
85
+ Squint.
86
+
87
+ ```ruby
88
+ Post.where(title: { not_there: "any value will do" } )
89
+ ```
90
+
91
+ ```
92
+ ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "title"
93
+ LINE 1: SELECT COUNT(*) FROM "posts" WHERE "title"."not_there" = 'an...
94
+ ^
95
+ : SELECT COUNT(*) FROM "posts" WHERE "title"."not_there" = 'any value will do'
96
+ ```
97
+
98
+ ## Performance
99
+ To get the most performance out searching jsonb/hstore attributes, add a GIN (preferred) or
100
+ GIST index to those columns. Find out more
101
+ [here](https://www.postgresql.org/docs/9.5/static/textsearch-indexes.html)
102
+
103
+ TL;DR:
104
+
105
+ SQL: 'CREATE INDEX name ON table USING GIN (column);'
106
+
107
+ Rails Migration: `add_index(:table, :column_name, using: 'gin')`
108
+
109
+
110
+ ## Storext attributes
111
+ Assuming the database schema above and a model like so:
112
+ ```ruby
113
+ class Post < ActiveRecord::Base
114
+ include Storext.model
115
+ include Squint
116
+
117
+ store_attribute :storext_jsonb_attributes, :zip_code, String, default: '90210'
118
+ store_attribute :storext_jsonb_attributes, :friend_count, Integer, default: 0
119
+ end
120
+ ```
121
+
122
+ Example using StoreXT with a default value:
123
+ ```ruby
124
+ Post.where(storext_jsonb_attributes: { zip_code: '90210' } )
125
+ # -- jsonb
126
+ # SELECT "posts".* FROM "posts" WHERE ("posts"."storext_jsonb_attributes"->>'zip_code' = '90210' OR
127
+ # (("posts"."storext_jsonb_attributes" ? 'zip_code') IS NULL OR
128
+ # ("posts"."storext_jsonb_attributes" ? 'zip_code') = FALSE))
129
+ # -- hstore
130
+ # SELECT "posts".* FROM "posts" WHERE ("posts"."storext_hstore_attributes"->'zip_code' = '90210' OR
131
+ # ((exist("posts"."storext_hstore_attributes", 'zip_code') = FALSE) OR
132
+ # exist("posts"."storext_hstore_attributes", 'zip_code') IS NULL))
133
+ #
134
+ #
135
+ ```
136
+ If (as in the example above) the default value for the StoreXT attribute is specified, then extra
137
+ checks for missing column ( `("posts"."storext_jsonb_attributes" ? 'zip_code') IS NULL` ) or
138
+ missing key ( `("posts"."storext_jsonb_attributes" ? 'zip_code') = FALSE)` ) are added
139
+
140
+ When non-default storext values are specified, these extra checks won't be added.
141
+
142
+ The Postgres SQL for jsonb and hstore is different. No support for checking for missing `json`
143
+ columns exists, so don't use those with StoreXT + Squint
144
+
145
+ ## Developing
146
+
147
+ 1. Thank you!
148
+ 1. Clone the repository
149
+ 1. `bundle`
150
+ 1. `bundle exec rake --rakefile test/dummy/Rakefile db:setup` # create the db for tests
151
+ 1. `bundle exec rake` # run the tests
152
+ 1. make your changes in a thoughtfully named branch
153
+ 1. ensure good test coverage
154
+ 1. submit a Pull Request
155
+
156
+ ## Contributors
157
+
158
+ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
159
+
160
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
161
+ | [<img src="https://avatars2.githubusercontent.com/u/864581?v=3" width="100px;"/><br /><sub>Kevin Brown</sub>](https://github.com/chevinbrown)<br />[🎨](#design-chevinbrown "Design") [👀](#review-chevinbrown "Reviewed Pull Requests") | [<img src="https://avatars2.githubusercontent.com/u/1741179?v=3" width="100px;"/><br /><sub>Andrew Fomera</sub>](http://andrewfomera.com)<br />[👀](#review-king601 "Reviewed Pull Requests") [💻](https://github.com/ProctorU/squint/commits?author=king601 "Code") | [<img src="https://avatars2.githubusercontent.com/u/708692?v=3" width="100px;"/><br /><sub>Ryan T. Hosford</sub>](https://github.com/rthbound)<br />[💻](https://github.com/ProctorU/squint/commits?author=rthbound "Code") | [<img src="https://avatars2.githubusercontent.com/u/1785682?v=3" width="100px;"/><br /><sub>Matthew Jaeh</sub>](https://github.com/Jaehdawg)<br />[🎨](#design-Jaehdawg "Design") [👀](#review-Jaehdawg "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3933204?v=3" width="100px;"/><br /><sub>Justin Licata</sub>](https://twitter.com/justinlicata)<br />[💻](https://github.com/ProctorU/squint/commits?author=licatajustin "Code") [🎨](#design-licatajustin "Design") [📖](https://github.com/ProctorU/squint/commits?author=licatajustin "Documentation") [👀](#review-licatajustin "Reviewed Pull Requests") | [<img src="https://avatars3.githubusercontent.com/u/24704300?v=4" width="100px;"/><br /><sub>Kyle Miracle</sub>](https://github.com/kmiracle86)<br />[🐛](https://github.com/ProctorU/squint/issues?q=author%3Akmiracle86 "Bug reports") [👀](#review-kmiracle86 "Reviewed Pull Requests") | [<img src="https://avatars2.githubusercontent.com/u/97011?v=3" width="100px;"/><br /><sub>David H. Wilkins</sub>](http://conecuh.com)<br />[💬](#question-dwilkins "Answering Questions") [🐛](https://github.com/ProctorU/squint/issues?q=author%3Adwilkins "Bug reports") [💻](https://github.com/ProctorU/squint/commits?author=dwilkins "Code") [🎨](#design-dwilkins "Design") [📖](https://github.com/ProctorU/squint/commits?author=dwilkins "Documentation") [💡](#example-dwilkins "Examples") [👀](#review-dwilkins "Reviewed Pull Requests") [⚠️](https://github.com/ProctorU/squint/commits?author=dwilkins "Tests") |
162
+ | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
163
+ | [<img src="https://avatars3.githubusercontent.com/u/19173815?v=3" width="100px;"/><br /><sub>Jay Wright</sub>](https://github.com/TheJayWright)<br />[👀](#review-TheJayWright "Reviewed Pull Requests") | [<img src="https://avatars1.githubusercontent.com/u/4067?s=460&u=cb404cc0f1737c2fc53411e300cc8e158ef29295&v=4" width="100px;"/><br /><sub>James Cook</sub>](https://github.com/jamescook)<br />[💻](https://github.com/ProctorU/squint/commits?author=jamescook "Code") [⚠️](https://github.com/ProctorU/squint/commits?author=jamescook "Tests") [👀](#review-jamescook "Reviewed Pull Requests") |
164
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
165
+
166
+ This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
167
+
168
+ ## Credits
169
+
170
+ Squint is maintained and funded by [ProctorU](https://twitter.com/ProctorUEng).
171
+
172
+ <br>
173
+
174
+ <p align="center">
175
+ <a href="https://twitter.com/ProctorUEng">
176
+ <img src="https://s3-us-west-2.amazonaws.com/dev-team-resources/procki-eyes.svg" width=108 height=72>
177
+ </a>
178
+
179
+ <h3 align="center">
180
+ <a href="https://twitter.com/ProctorUEng">ProctorU Engineering & Design</a>
181
+ </h3>
182
+
183
+ <p align="center">
184
+ A simple online proctoring service that allows you to take exams or certification tests at home.
185
+ </p>
186
+ </p>