task_master 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: da7cd729be877ce97825e77e71389dfdf6d0df90
4
+ data.tar.gz: ca5690c820c86bf15a20e32a400dd8b4828d1320
5
+ SHA512:
6
+ metadata.gz: 0483e83a826ce9aebd09392310ddf5f10529aef91844efca23cd6119597cadb9d9b8bf7205fbc0a590a27e35eb974cf865f84f24ae6e8a1bde4fd6c975898b3b
7
+ data.tar.gz: 189f1f83001d85fb69e57e345f273d27a4ccd34c47b27903c7304f6ec0fea40a47259efaeaad3276d563acb4739d3c2a2cb637742b913e41a77ece1dbaad2ff0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Sean Devine
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/Rakefile ADDED
@@ -0,0 +1,26 @@
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 = 'TaskMaster'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/test_app/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module TaskMaster
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,32 @@
1
+ module TaskMaster
2
+ class WebhookController < ApplicationController
3
+
4
+ def create
5
+ response = JSON.parse(request.body.read)
6
+
7
+ request_instance = nil
8
+
9
+ if response["key"].nil?
10
+ head :bad_request and return
11
+ end
12
+
13
+ # Fancy Hands uses a single URL for all webhooks.
14
+ # We need to iterate through the request classes to find one
15
+ # that has the key from the response.
16
+ [CustomRequest].each do |request_class|
17
+ break if request_instance = request_class.find_by(key: response["key"])
18
+ end
19
+
20
+ if request_instance
21
+ request_instance.responses << response
22
+ request_instance.save!
23
+ end
24
+
25
+ head :ok
26
+
27
+ rescue JSON::ParserError
28
+ head :bad_request
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module TaskMaster
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ require "fancyhands"
2
+
3
+ module TaskMaster
4
+ class Client
5
+ extend Forwardable
6
+ def_delegators :@client, :request
7
+
8
+ def initialize(key = TaskMaster.key, secret = TaskMaster.secret)
9
+ unless key.present? && secret.present?
10
+ raise ArgumentError, "Credentials are not configured. Set TaskMaster.key and TaskMaster.secret with your Fancy Hands credentials."
11
+ end
12
+ @client = FancyHands::V1::Client.new(key, secret)
13
+ end
14
+
15
+ def ping
16
+ request.get("echo", {}) == {}
17
+ end
18
+
19
+ def create_custom_request(data)
20
+ request.post("request/custom", data)
21
+ end
22
+
23
+ def cancel_custom_request(key)
24
+ response = request.post("request/custom/cancel", { key: key })
25
+ response["status"] == true
26
+ end
27
+
28
+ def trigger_callback(key)
29
+ response = request.post("callback", { key: key })
30
+ response["status"] == "ok"
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,177 @@
1
+ module TaskMaster
2
+ class CustomRequest < ActiveRecord::Base
3
+ STATUSES = {
4
+ 1 => "NEW",
5
+ 5 => "OPEN",
6
+ 7 => "AWAITING_RESPONSE",
7
+ 20 => "CLOSED",
8
+ 21 => "EXPIRED"
9
+ }
10
+
11
+ serialize :custom_fields, JSON
12
+
13
+ serialize :responses, JSON
14
+
15
+ serialize :answers, JSON
16
+
17
+ serialize :messages, JSON
18
+
19
+ serialize :phone_calls, JSON
20
+
21
+ belongs_to :requestor, polymorphic: true
22
+
23
+ after_initialize :_initialize_custom_fields
24
+
25
+ validates :description, presence: true
26
+
27
+ validates :numeric_status, inclusion: { in: STATUSES.keys }, allow_nil: true
28
+
29
+ validate :_custom_fields_is_not_empty
30
+
31
+ validate :_custom_fields_are_all_valid
32
+
33
+ before_save :_set_attributes_from_last_response
34
+
35
+ before_save :_set_closed_without_answers
36
+
37
+ after_commit :post_to_fancyhands, on: [:create]
38
+
39
+ # Override this method if you'd like to move the post to Fancy Hands
40
+ # into the background. Redefine #post_to_fancyhands to queue the job
41
+ # in whatever other way you'd prefer.
42
+ def post_to_fancyhands
43
+ post_to_fancyhands_now
44
+ end
45
+
46
+ def post_to_fancyhands_now
47
+ return if key.present?
48
+
49
+ response = Client.new.create_custom_request(_to_fancy_hands_data)
50
+ self.responses << response
51
+ save
52
+ end
53
+
54
+ def cancel
55
+ return false unless key.present?
56
+
57
+ Client.new.cancel_custom_request(key)
58
+ end
59
+
60
+ def trigger_callback
61
+ return false unless key.present?
62
+
63
+ Client.new.trigger_callback(key)
64
+ end
65
+
66
+ def status
67
+ return unless numeric_status.present?
68
+
69
+ STATUSES[numeric_status]
70
+ end
71
+
72
+ def _set_attributes_from_last_response
73
+ sorted_responses = []
74
+ # add the responses that don't have date_updated present to the beginning
75
+ # sorted responses
76
+
77
+ sorted_responses += responses.select do |response|
78
+ response["date_updated"].blank?
79
+ end
80
+
81
+ sorted_responses += responses.select do |response|
82
+ response["date_updated"].present?
83
+ end.sort_by do |response|
84
+ DateTime.parse(response["date_updated"])
85
+ end
86
+
87
+ self.responses = sorted_responses
88
+
89
+ return unless last_response = responses.last
90
+
91
+ if key.blank?
92
+ raise StandardError, "The last response had a blank key. #{last_response.inspect}" if last_response["key"].blank?
93
+ self.key = last_response["key"]
94
+ end
95
+
96
+ if last_response["numeric_status"].present?
97
+ self.numeric_status = Integer(last_response["numeric_status"])
98
+ end
99
+
100
+ if last_response["date_created"].present? && !fancyhands_created_at.present?
101
+ self.fancyhands_created_at = DateTime.parse(last_response["date_created"])
102
+ end
103
+
104
+ if last_response["date_updated"].present?
105
+ self.fancyhands_updated_at = DateTime.parse(last_response["date_updated"])
106
+ end
107
+
108
+ if self.numeric_status == 20
109
+ self.fancyhands_closed_at = fancyhands_updated_at
110
+ end
111
+
112
+ self.answers ||= {}
113
+
114
+ Array(last_response["custom_fields"]).each do |custom_field|
115
+ self.answers[custom_field["field_name"]] = custom_field["answer"]
116
+ end
117
+
118
+ self.messages = Array(last_response["messages"])
119
+
120
+ self.phone_calls = Array(last_response["phone_calls"])
121
+
122
+ Array(last_response["phone_calls"]).each do |phone_call|
123
+ self.phone_call_seconds += Integer(phone_call["duration"])
124
+ end
125
+
126
+ return true
127
+ end
128
+
129
+ def _set_closed_without_answers
130
+ return unless numeric_status == 20
131
+
132
+ self.closed_without_answers = answers.values.all?(&:blank?)
133
+
134
+ # don't stop the save process
135
+ return true
136
+ end
137
+
138
+ def _initialize_custom_fields
139
+ self.custom_fields ||= []
140
+ end
141
+
142
+ def _custom_fields_is_not_empty
143
+ if custom_fields.empty?
144
+ errors.add :custom_fields, "can't be empty"
145
+ end
146
+ end
147
+
148
+ def _custom_fields_are_all_valid
149
+ messages = {}
150
+ custom_fields.map do |f|
151
+ CustomRequestField.new(f.to_hash)
152
+ end.each_with_index do |field, index|
153
+ next if field.valid?
154
+ messages[index] = field.errors.full_messages
155
+ end
156
+ if messages.any?
157
+ errors.add(
158
+ :custom_fields,
159
+ messages.map do |index, full_messages|
160
+ "custom field at index #{index} has errors #{full_messages.join(", ")}."
161
+ end.join(", ")
162
+ )
163
+ end
164
+ end
165
+
166
+ def _to_fancy_hands_data
167
+ {
168
+ title: title,
169
+ description: description,
170
+ bid: bid.to_f,
171
+ expiration_date: expiration_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
172
+ custom_fields: custom_fields.map(&:to_hash).to_json
173
+ }
174
+ end
175
+
176
+ end
177
+ end
@@ -0,0 +1,63 @@
1
+ module TaskMaster
2
+ class CustomRequestField
3
+ include ActiveModel::Model
4
+
5
+ DEFAULT_ATTRIBUTES = { required: false }
6
+
7
+ VALID_TYPES = %w(text textarea tel number email money date datetime-local bool checkbox radio)
8
+
9
+ TYPES_WITH_OPTIONS = %w(checkbox radio)
10
+
11
+ attr_accessor :type, :label, :description, :field_name, :required, :order, :options
12
+
13
+ def initialize(attributes = {})
14
+ attributes = attributes.reverse_merge(DEFAULT_ATTRIBUTES)
15
+ super(attributes)
16
+ end
17
+
18
+ validates :type, inclusion: { in: VALID_TYPES }
19
+
20
+ [:label, :description, :field_name].each do |attribute|
21
+ validates attribute, presence: true
22
+ end
23
+
24
+ validates :order, numericality: { greater_than: 0 }
25
+
26
+ validates :label, length: { maximum: 30 }
27
+
28
+ validates :description, length: { maximum: 300 }
29
+
30
+ validates :field_name, length: { maximum: 30 }
31
+
32
+ with_options if: -> { TYPES_WITH_OPTIONS.include?(type) } do
33
+ validate do |custom_request_field|
34
+ if Array(options).length < 1
35
+ errors.add :options, "must not be empty"
36
+ end
37
+ end
38
+ end
39
+
40
+ def _initialize_required
41
+ self.required = false if self.required.nil?
42
+ end
43
+
44
+ def order
45
+ @order.to_s
46
+ end
47
+
48
+ def to_hash
49
+ hash = {
50
+ type: type,
51
+ label: label,
52
+ description: description,
53
+ field_name: field_name,
54
+ required: required,
55
+ order: order
56
+ }
57
+ if TYPES_WITH_OPTIONS.include?(type)
58
+ hash[:options] = options
59
+ end
60
+ hash
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>TaskMaster</title>
5
+ <%= stylesheet_link_tag "fancyengine/application", media: "all" %>
6
+ <%= javascript_include_tag "fancyengine/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ TaskMaster::Engine.routes.draw do
2
+ post "webhook", to: "webhook#create"
3
+ end
data/config/spring.rb ADDED
@@ -0,0 +1 @@
1
+ Spring.application_root = "spec/test_app"
@@ -0,0 +1,26 @@
1
+ class CreateTaskMasterCustomRequests < ActiveRecord::Migration
2
+ def change
3
+ create_table :task_master_custom_requests do |t|
4
+ t.string :title
5
+ t.text :description, null: false
6
+ t.text :custom_fields, null: false
7
+ t.decimal :bid, null: false
8
+ t.datetime :expiration_date, null: false
9
+ t.string :key
10
+ t.text :responses, default: "[]"
11
+ t.integer :numeric_status
12
+ t.datetime :fancyhands_created_at
13
+ t.datetime :fancyhands_updated_at
14
+ t.datetime :fancyhands_closed_at
15
+ t.text :answers, default: "{}"
16
+ t.reference :requestor, polymorphic: true
17
+ t.text :messages, :text, default: "[]"
18
+ t.text :phone_calls, :text, default: "[]"
19
+ t.boolean :closed_without_answers, default: false
20
+ t.integer :phone_call_seconds, :integer, default: 0
21
+
22
+ t.timestamps null: false
23
+ end
24
+ add_index :task_master_custom_requests, [:requestor_id, :requestor_type], name: "idx_fcr_ri_rt"
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ require "task_master/engine"
2
+
3
+ module TaskMaster
4
+ mattr_accessor :key
5
+ mattr_accessor :secret
6
+ end
@@ -0,0 +1,10 @@
1
+ module TaskMaster
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace TaskMaster
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ g.fixture_replacement :factory_girl, :dir => 'spec/factories'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module TaskMaster
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :task_master do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: task_master
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Sean Devine
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: task_master-fancyhands-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: spring
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.3.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 3.3.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: factory_girl_rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.5'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dotenv
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: test_after_commit
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: A Rails engine that makes it easy to outsource manual tasks using the
126
+ Fancy Hands API.
127
+ email:
128
+ - barelyknown@icloud.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - MIT-LICENSE
134
+ - Rakefile
135
+ - app/assets/javascripts/task_master/application.js
136
+ - app/assets/stylesheets/task_master/application.css
137
+ - app/controllers/task_master/application_controller.rb
138
+ - app/controllers/task_master/webhook_controller.rb
139
+ - app/helpers/task_master/application_helper.rb
140
+ - app/models/task_master/client.rb
141
+ - app/models/task_master/custom_request.rb
142
+ - app/models/task_master/custom_request_field.rb
143
+ - app/views/layouts/task_master/application.html.erb
144
+ - config/routes.rb
145
+ - config/spring.rb
146
+ - db/migrate/20150731173757_create_task_master_custom_requests.rb
147
+ - lib/task_master.rb
148
+ - lib/task_master/engine.rb
149
+ - lib/task_master/version.rb
150
+ - lib/tasks/task_master_tasks.rake
151
+ homepage: http://www.github.com/togglepro/task_master
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 2.4.5
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: A Rails engine that makes it easy to outsource manual tasks using the Fancy
175
+ Hands API.
176
+ test_files: []