userq 0.0.1
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 +15 -0
- data/MIT-LICENSE +20 -0
- data/README.md +102 -0
- data/Rakefile +32 -0
- data/lib/generators/userq/install_generator.rb +44 -0
- data/lib/generators/userq/templates/migration.rb +12 -0
- data/lib/userq.rb +201 -0
- data/lib/userq/version.rb +3 -0
- data/test/Gemfile.test +5 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/db/schema.rb +16 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +414 -0
- data/test/dummy/log/test.log +1955 -0
- data/test/dummy/public/404.html +58 -0
- data/test/dummy/public/422.html +58 -0
- data/test/dummy/public/500.html +57 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/test_helper.rb +15 -0
- data/test/userq_test.rb +102 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MWVhMzUwNTA1MzU1NjIwOGE0NzA2MDFjYzY3ZGU0ZWU3YWVkODE0MA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZDkxNWIzZTdlOTVmNzlkZWYzYmY3OTYwZjNjNTFhYWEzZGUwMWNmMg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZTBkZjBmODIwMDBkNDQ0NmIwODNhNmI5YTI3NzNmZmQ4Zjc4MTUyYTAwMjAy
|
10
|
+
MWUwNDU3OTY4NTE3YjcyM2ExODIwNjdjNDU4YTEzNjhkYjk4YzBiMjlkMjgy
|
11
|
+
NTgxN2EyM2YyN2Q2MGU5NmMyMDI5OWY4YmJjMjIyZGRhMjIwMjI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZWIzZWNkNTA1MTY4OGM3ZWFhMzRlMTBkYTNjYzE1Y2NjMjdmZDQ3NDQ2ZTRh
|
14
|
+
YzQ1ZmM3NjdkMzFlYzBiMzAyZDExZTEwNzZhYzBjMmI3MzQ1NmNiMTU1Yzgw
|
15
|
+
YzIxN2YxOGMxNmY2YTFkNjdmMTNiY2MwZmUyZTU4ZDFjYzUxNjE=
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 StudentHack
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# UserQ: Simple User Queues for Rails
|
2
|
+
|
3
|
+
UserQ allows you to **very quickly** integrate awesome user-based queues into your Rails application. Many large ticketing websites such as [Ticketmaster](http://www.ticketmaster.com) use queues as a means of user experience.
|
4
|
+
|
5
|
+
[](https://travis-ci.org/studenthack/userq)
|
6
|
+
[](https://gemnasium.com/studenthack/userq)
|
7
|
+

|
8
|
+
|
9
|
+

|
10
|
+
|
11
|
+
### Example code
|
12
|
+
```ruby
|
13
|
+
queue = UserQ::Queue.new(capacity: 50)
|
14
|
+
|
15
|
+
## Can we enter the queue?
|
16
|
+
if queue.enter_into_queue?
|
17
|
+
entry = queue.enter
|
18
|
+
# They have a place in the queue. Sell them a ticket!
|
19
|
+
else
|
20
|
+
# No place in queue. Run this block again after n seconds.
|
21
|
+
end
|
22
|
+
|
23
|
+
# Default expiry: 180 seconds
|
24
|
+
puts entry.expires # => 180
|
25
|
+
sleep(5)
|
26
|
+
puts entry.expires # => 175
|
27
|
+
```
|
28
|
+
|
29
|
+
### How does it work?
|
30
|
+
Tell it how much you capacity you have (i.e. how many available tickets are left) and then UserQ does its beautiful magic. Some of the questions you can ask UserQ:
|
31
|
+
|
32
|
+
* How many "people" are in the queue?
|
33
|
+
* When does the current users spot in the queue expire?
|
34
|
+
* When do you think another spot in the queue will open? (i.e. average wait time)
|
35
|
+
* Can I extend/shorten the current users spot in the queue?
|
36
|
+
|
37
|
+
### Install
|
38
|
+
```
|
39
|
+
Install the gem:
|
40
|
+
$ gem install userq
|
41
|
+
|
42
|
+
Install the database model (automatically migrates):
|
43
|
+
$ rails generate userq:install
|
44
|
+
|
45
|
+
You're all setup!
|
46
|
+
```
|
47
|
+
|
48
|
+
|
49
|
+
### UserQ Documentation
|
50
|
+
See the [full documentation on the Wiki](/studenthack/userq/wiki/Documentation) to see what you can do with UserQ.
|
51
|
+
|
52
|
+
|
53
|
+
### Uninstall UserQ
|
54
|
+
```
|
55
|
+
Revoke the UserQ migration:
|
56
|
+
$ rake db:rollback
|
57
|
+
|
58
|
+
Remove everything UserQ:
|
59
|
+
$ rails destroy userq:install
|
60
|
+
```
|
61
|
+
|
62
|
+
|
63
|
+
### Development
|
64
|
+
Directly clone the repository (or fork it and clone your fork):
|
65
|
+
```
|
66
|
+
$ git clone https://github.com/studenthack/userq.git
|
67
|
+
```
|
68
|
+
|
69
|
+
Do some awesome stuff. To test simply run
|
70
|
+
```
|
71
|
+
$ rake test
|
72
|
+
```
|
73
|
+
|
74
|
+
We love pull requests! Make sure you write a test for your contribution.
|
75
|
+
|
76
|
+
### Roadmap
|
77
|
+
- Lots of more awesome looking tests
|
78
|
+
- Develop the UserQ Wiki
|
79
|
+
- Assign queue places in a chronological order (first into queue = first entry) instead of randomly
|
80
|
+
|
81
|
+
### Release History
|
82
|
+
- 23/12/13: The initial version
|
83
|
+
|
84
|
+
### LICENCE
|
85
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
86
|
+
a copy of this software and associated documentation files (the
|
87
|
+
"Software"), to deal in the Software without restriction, including
|
88
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
89
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
90
|
+
permit persons to whom the Software is furnished to do so, subject to
|
91
|
+
the following conditions:
|
92
|
+
|
93
|
+
The above copyright notice and this permission notice shall be
|
94
|
+
included in all copies or substantial portions of the Software.
|
95
|
+
|
96
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
97
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
98
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
99
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
100
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
101
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
102
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'UserQ'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Userq
|
2
|
+
class InstallGenerator < ::Rails::Generators::Base
|
3
|
+
source_root File.expand_path("../templates", __FILE__)
|
4
|
+
|
5
|
+
# Setup and create the migration
|
6
|
+
def do_migration
|
7
|
+
migration_exists = Dir["db/migrate/*_create_user_queues.rb"].count > 0
|
8
|
+
|
9
|
+
if migration_exists and installing?
|
10
|
+
puts "UserQ is already installed. Maybe a 'rake db:migrate' command might help?"
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
create_migration
|
15
|
+
|
16
|
+
puts "Success! UserQ is installed. You can now use it in your application." if installing?
|
17
|
+
puts "UserQ has already been uninstalled. Remember the 'user_queue' table needs to be manually destroyed." if destroying? and migration_exists == false
|
18
|
+
puts "Success! UserQ is uninstalled. The table 'userq_queue' needs to be destroyed manually to complete removal." if destroying? and migration_exists
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def create_migration
|
24
|
+
migration_id = Time.now.to_i
|
25
|
+
migration_exists = Dir["db/migrate/*_create_user_queues.rb"].count > 0
|
26
|
+
|
27
|
+
copy_file "migration.rb", "db/migrate/#{migration_id}_create_user_queues.rb" if installing?
|
28
|
+
copy_file "migration.rb", Dir["db/migrate/*_create_user_queues.rb"][0].to_s if destroying? and migration_exists
|
29
|
+
|
30
|
+
run "rake db:migrate VERSION=#{migration_id}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Thanks to ynkr: http://stackoverflow.com/a/8829177/408177
|
34
|
+
protected
|
35
|
+
|
36
|
+
def installing?
|
37
|
+
:invoke == behavior
|
38
|
+
end
|
39
|
+
|
40
|
+
def destroying?
|
41
|
+
:revoke == behavior
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/userq.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
module UserQ
|
2
|
+
|
3
|
+
class Queue
|
4
|
+
attr_accessor :queue_constraints
|
5
|
+
|
6
|
+
# pre: constraints is a hash - capacity (int), taken (int), entry_time (int of seconds)
|
7
|
+
# post: instance of UserQ::Queue
|
8
|
+
def initialize constraints = {}
|
9
|
+
self.queue_constraints = { context: 'all', capacity: 2, taken: 0, entry_time: 180, auto_clean: true }.merge(constraints)
|
10
|
+
end
|
11
|
+
|
12
|
+
def constraints input # Update the constraints
|
13
|
+
queue_constraints.merge!(input)
|
14
|
+
end
|
15
|
+
|
16
|
+
def constraint input # Alias of self.constraints
|
17
|
+
constraints(input)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# Beautiful syntax.
|
22
|
+
|
23
|
+
def enter_into_queue? # Check if enough space in queue
|
24
|
+
current_limit = queue_constraints[:capacity].to_i
|
25
|
+
current_usage = queue_constraints[:taken].to_i + UserQ::UserQueue.count_unexpired(queue_constraints[:context])
|
26
|
+
|
27
|
+
# Assess whether enough space left into queue
|
28
|
+
current_usage < current_limit
|
29
|
+
end
|
30
|
+
|
31
|
+
def nearest_entry # In seconds how long the next entry in the queue will expire.
|
32
|
+
UserQ::UserQueue.next_in_seconds(queue_constraints[:context])
|
33
|
+
end
|
34
|
+
|
35
|
+
def avg_wait_time # nearest_time but more human-readable
|
36
|
+
case nearest_entry.floor
|
37
|
+
when 0..30
|
38
|
+
return "Less than 30 seconds"
|
39
|
+
when 31..60
|
40
|
+
return "Less than a minute"
|
41
|
+
else
|
42
|
+
return "approximately #{(nearest_entry.floor / 60).floor} minutes"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def empty?
|
47
|
+
UserQ::UserQueue.count_in_context(queue_constraints[:context]) == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
def empty_queue # Clean entries that aren't ever going to be re-used.
|
51
|
+
UserQ::UserQueue.empty(queue_constraints[:context])
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset
|
56
|
+
UserQ::UserQueue.reset(queue_constraints[:context])
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def enter data = {}
|
61
|
+
return false unless enter_into_queue?
|
62
|
+
|
63
|
+
entry = UserQ::UserQueue.new
|
64
|
+
entry.code = UserQ::Internal.generate_code
|
65
|
+
entry.expires_at = UserQ::Internal.current_time + queue_constraints[:entry_time]
|
66
|
+
entry.data = data.to_json
|
67
|
+
entry.context = queue_constraints[:context]
|
68
|
+
entry.save
|
69
|
+
|
70
|
+
# Automatically clean expired tokens. Woohoo!
|
71
|
+
empty_queue if queue_constraints[:auto_clean]
|
72
|
+
|
73
|
+
Entry.new(entry.code)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
class Entry
|
79
|
+
attr_accessor :code, :entry
|
80
|
+
|
81
|
+
def initialize code
|
82
|
+
self.entry = UserQ::UserQueue.find_by_code(self.code = code)
|
83
|
+
raise "Entry code #{code} not valid" if self.entry.count == 0
|
84
|
+
|
85
|
+
# Woohoo!
|
86
|
+
self.entry = self.entry.first
|
87
|
+
end
|
88
|
+
|
89
|
+
def valid_context? context
|
90
|
+
entry.context == context
|
91
|
+
end
|
92
|
+
|
93
|
+
def expired?
|
94
|
+
entry.expires_at < UserQ::Internal.current_time
|
95
|
+
end
|
96
|
+
|
97
|
+
def alive? # Opposite of expired?
|
98
|
+
entry.expires_at >= UserQ::Internal.current_time
|
99
|
+
end
|
100
|
+
|
101
|
+
def expires
|
102
|
+
expires = (entry.expires_at - UserQ::Internal.current_time).floor
|
103
|
+
expires > -1 ? expires : -1
|
104
|
+
end
|
105
|
+
|
106
|
+
def expire
|
107
|
+
entry.expires_at = UserQ::Internal.current_time - 1
|
108
|
+
entry.save
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def remove
|
113
|
+
entry.destroy
|
114
|
+
entry = nil
|
115
|
+
code = nil
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
def removed?
|
120
|
+
true if entry.nil? or code.nil?
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
def shorten seconds
|
125
|
+
|
126
|
+
entry.expires_at -= seconds if entry.expires_at >= UserQ::Internal.current_time
|
127
|
+
entry.save
|
128
|
+
|
129
|
+
expires
|
130
|
+
end
|
131
|
+
|
132
|
+
def extend seconds
|
133
|
+
|
134
|
+
if entry.expires_at < UserQ::Internal.current_time
|
135
|
+
entry.expires_at = UserQ::Internal.current_time + seconds
|
136
|
+
else
|
137
|
+
entry.expires_at += seconds
|
138
|
+
end
|
139
|
+
|
140
|
+
entry.save
|
141
|
+
expires
|
142
|
+
end
|
143
|
+
|
144
|
+
def alive
|
145
|
+
entry.created_at - UserQ::Internal.current_time
|
146
|
+
end
|
147
|
+
|
148
|
+
def data
|
149
|
+
entry.data || Hash.new
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# UserQ::Queue.constraint(capacity: 100)
|
154
|
+
|
155
|
+
|
156
|
+
class UserQueue < ActiveRecord::Base
|
157
|
+
def self.find_by_code code
|
158
|
+
where(code: code)
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.count_unexpired context
|
162
|
+
where(context: context).where("expires_at > ?", UserQ::Internal.current_time).count
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.count_in_context context
|
166
|
+
where(context: context).count
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.reset context
|
170
|
+
where(context: context).destroy_all
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.next_in_seconds context
|
174
|
+
seconds = (where(context: context).order(expires_at: :asc).first.expires_at - UserQ::Internal.current_time).to_i
|
175
|
+
seconds > 0 ? seconds : 0
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.empty context
|
179
|
+
where(context: context).where("expires_at <= ?", UserQ::Internal.current_time).destroy_all
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Useful internal tools we use to make it beautiful.
|
185
|
+
class Internal
|
186
|
+
def self.symbolize array
|
187
|
+
result = {}
|
188
|
+
array.each { |key, value| result[key.to_sym] = value }
|
189
|
+
result
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.current_time # Time all comes from here. Beautiful to update.
|
193
|
+
Time.now
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.generate_code
|
197
|
+
rand(36**24).to_s(36)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
data/test/Gemfile.test
ADDED
data/test/dummy/Rakefile
ADDED