squint 0.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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>