cloudrider 0.3.22 → 0.3.23
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 +4 -4
- data/generica/app/assets/javascripts/components/product-display.js.em +2 -2
- data/generica/app/assets/javascripts/components/product-listing.js.em +2 -2
- data/generica/app/assets/javascripts/controllers/admin/products/slice_controller.js.em +7 -1
- data/generica/app/assets/javascripts/controllers/users/offers/index_controller.js.em +10 -6
- data/generica/app/assets/javascripts/controllers/users/products/slice_controller.js.em +7 -1
- data/generica/app/assets/javascripts/models/product.js.em +2 -0
- data/generica/app/assets/javascripts/routes/users/offers/index_route.js.em +11 -1
- data/generica/app/assets/javascripts/templates/admin/products/slice.emblem +16 -0
- data/generica/app/assets/javascripts/templates/users/offers/index.emblem +5 -1
- data/generica/app/assets/javascripts/templates/users/offers/slice.emblem +10 -3
- data/generica/app/assets/javascripts/templates/users/products/slice.emblem +16 -0
- data/generica/app/controllers/apiv1/offers/index_controller.rb +32 -9
- data/generica/app/controllers/apiv1/products/update_controller.rb +27 -0
- data/generica/app/controllers/users/products/update_controller.rb +1 -1
- data/generica/app/mailers/apiv1/notifications_mailer/new_offer_context.rb +1 -1
- data/generica/app/models/admin/user.rb +8 -0
- data/generica/app/models/apiv1/email_request.rb +4 -0
- data/generica/app/models/apiv1/offer_message.rb +11 -0
- data/generica/app/models/apiv1/picture.rb +4 -0
- data/generica/app/models/apiv1/product.rb +26 -1
- data/generica/app/models/apiv1/products_machine.rb +9 -2
- data/generica/app/varissets/javascripts/templates/components/product-display.emblem.erb +6 -0
- data/generica/app/varissets/javascripts/templates/components/product-listing.emblem.erb +3 -0
- data/generica/app/varissets/javascripts/templates/products/product/offers/_anonymous-form.emblem.erb +3 -3
- data/generica/app/varissets/javascripts/templates/products/product/show.emblem.erb +3 -0
- data/generica/config/routes.rb +1 -0
- data/generica/config/schedule.rb.yml +26 -0
- data/generica/db/migrate/20141212162545_add_finished_at_to_apiv1_products.rb +5 -0
- data/generica/lib/generica/email_slave.rb +51 -45
- data/generica/lib/tasks/email_slave.rake +6 -3
- data/generica/spec/generica/email_slave_spec.rb +55 -0
- data/lib/cloudrider/backend_commander.rb +6 -1
- data/lib/cloudrider/serverside/schedule_ruby.rb +14 -0
- data/lib/cloudrider/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d2e8d222b358db32bdff3dd54e567bbe612cd7f
|
4
|
+
data.tar.gz: 9a0ffe6c7954a0b6de2126945616cff8eac71b29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 797256f15b86160f8388dd0293bb2f0f04a658c5ed17357174132f24ebe0c40456d69886e409b1c66d5bc1eea2fd6c0171665df5419fda2e7b11dfe48e8e809e
|
7
|
+
data.tar.gz: 471366a69c0816ba7032e8681c15ff6357f54b7afd4dbc91a6f8785b22d2b3f0724e450522fd3d578bfe181b73c9ddd82014795993102529b4158339c1524cc2
|
@@ -2,5 +2,5 @@ class Apiv1.ProductDisplayComponent extends Ember.Component
|
|
2
2
|
attributeBindings: ["class"]
|
3
3
|
classNames: ['product-display']
|
4
4
|
|
5
|
-
+computed product.
|
6
|
-
productImageStyle: -> "background-image: url(#{@get 'product.
|
5
|
+
+computed product.thumbnail
|
6
|
+
productImageStyle: -> "background-image: url(#{@get 'product.thumbnail' });"
|
@@ -2,5 +2,5 @@ class Apiv1.ProductListingComponent extends Ember.Component
|
|
2
2
|
attributeBindings: ["class"]
|
3
3
|
classNames: ['product-listing']
|
4
4
|
|
5
|
-
+computed product.
|
6
|
-
productImageStyle: -> "background-image: url(#{@get 'product.
|
5
|
+
+computed product.thumbnail
|
6
|
+
productImageStyle: -> "background-image: url(#{@get 'product.thumbnail' });"
|
@@ -1,4 +1,10 @@
|
|
1
1
|
class Apiv1.AdminProductsSliceController extends Ember.ObjectController
|
2
|
+
successfulSave: (contact) ->
|
3
|
+
Apiv1.Flash.register "success", "successful", 5000
|
4
|
+
failedSave: (reason) ->
|
5
|
+
Apiv1.Flash.register "warning", "unsuccessful: #{reason.status}", 5000
|
2
6
|
actions:
|
7
|
+
requestToggleFinish: ->
|
8
|
+
@model.save().then(_.bind @successfulSave, @).catch(_.bind @failedSave, @)
|
3
9
|
requestDelete: ->
|
4
|
-
@model.destroyRecord()
|
10
|
+
@model.destroyRecord().then(_.bind @successfulSave, @).catch(_.bind @failedSave, @)
|
@@ -1,10 +1,14 @@
|
|
1
1
|
class Apiv1.UsersOffersIndexController extends Ember.ObjectController
|
2
|
-
queryParams: ["page", "per"]
|
2
|
+
queryParams: ["page", "per", "f"]
|
3
3
|
page: 1
|
4
4
|
per: 15
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
f: null
|
6
|
+
choices: [
|
7
|
+
{ id: "2me", display: "offers made to me" },
|
8
|
+
{ id: "4me", display: "offers I've made" }
|
9
|
+
]
|
10
|
+
+computed model
|
11
|
+
offers: -> @model
|
8
12
|
|
9
|
-
+computed
|
10
|
-
metadatum: -> @get("
|
13
|
+
+computed model.content.meta
|
14
|
+
metadatum: -> @get("model.content.meta")
|
@@ -1,4 +1,10 @@
|
|
1
1
|
class Apiv1.UsersProductsSliceController extends Ember.ObjectController
|
2
|
+
successfulSave: (contact) ->
|
3
|
+
Apiv1.Flash.register "success", "successful", 5000
|
4
|
+
failedSave: (reason) ->
|
5
|
+
Apiv1.Flash.register "warning", "unsuccessful: #{reason.status}", 5000
|
2
6
|
actions:
|
7
|
+
requestToggleFinish: ->
|
8
|
+
@model.save().then(_.bind @successfulSave, @).catch(_.bind @failedSave, @)
|
3
9
|
requestDelete: ->
|
4
|
-
@model.destroyRecord()
|
10
|
+
@model.destroyRecord().then(_.bind @successfulSave, @).catch(_.bind @failedSave, @)
|
@@ -9,6 +9,8 @@ class Apiv1.Product extends DS.Model
|
|
9
9
|
showcaseOrder: DS.attr "number"
|
10
10
|
createdAt: DS.attr "date"
|
11
11
|
updatedAt: DS.attr "date"
|
12
|
+
isFinished: DS.attr "boolean"
|
13
|
+
thumbnail: DS.attr "string"
|
12
14
|
attachments: DS.hasMany "attachment", async: true
|
13
15
|
pictures: DS.hasMany "picture", async: true
|
14
16
|
taxons: DS.hasMany "taxon", async: true
|
@@ -1 +1,11 @@
|
|
1
|
-
class Apiv1.UsersOffersIndexRoute extends Ember.Route
|
1
|
+
class Apiv1.UsersOffersIndexRoute extends Ember.Route
|
2
|
+
queryParams:
|
3
|
+
page:
|
4
|
+
refreshModel: true
|
5
|
+
per:
|
6
|
+
refreshModel: true
|
7
|
+
f:
|
8
|
+
refreshModel: true
|
9
|
+
|
10
|
+
model: (params) ->
|
11
|
+
@store.find "offer", params
|
@@ -7,6 +7,22 @@
|
|
7
7
|
if model.hasShowOrder
|
8
8
|
span #
|
9
9
|
span= model.showcaseOrder
|
10
|
+
if model.isFinished
|
11
|
+
span.parantheses
|
12
|
+
tr-span en="finished"
|
13
|
+
.edit
|
14
|
+
if model.isPending
|
15
|
+
i.fa.fa-cog.fa-spin
|
16
|
+
span.prespace
|
17
|
+
tr-span en="loading"
|
18
|
+
else
|
19
|
+
.pointer click="requestToggleFinish"
|
20
|
+
if model.isFinished
|
21
|
+
span.prespace
|
22
|
+
tr-span en="unfinish"
|
23
|
+
else
|
24
|
+
span.prespace
|
25
|
+
tr-span en="mark finish"
|
10
26
|
.edit
|
11
27
|
link-to "admin.product.edit" model.id
|
12
28
|
i.fa.fa-edit
|
@@ -1,3 +1,7 @@
|
|
1
|
+
.row
|
2
|
+
.small-6.medium-4.large-2.columns
|
3
|
+
Ember.Select content=choices optionValuePath="content.id" optionLabelPath="content.display" value=f
|
4
|
+
|
1
5
|
.row.admin-messages-index
|
2
6
|
.small-12.columns
|
3
7
|
if offers.isPending
|
@@ -12,7 +16,7 @@
|
|
12
16
|
.admin-message-slice
|
13
17
|
i.fa.fa-times
|
14
18
|
span.prespace
|
15
|
-
tr-span en="
|
19
|
+
tr-span en="you have no offers"
|
16
20
|
|
17
21
|
.row
|
18
22
|
.small-4.medium-3.large-2.columns
|
@@ -1,5 +1,5 @@
|
|
1
1
|
.admin-message-slice
|
2
|
-
.actions
|
2
|
+
link-to "products.product.show" model.product.id class="actions"
|
3
3
|
.view
|
4
4
|
i.fa.fa-user
|
5
5
|
span.prespace= model.fromCompany
|
@@ -11,8 +11,7 @@
|
|
11
11
|
.media-subject
|
12
12
|
span.bold= model.priceTerms
|
13
13
|
span.spacebar.prespace -
|
14
|
-
|
15
|
-
span= model.product.material
|
14
|
+
span= model.product.material
|
16
15
|
|
17
16
|
.media-text= model.message
|
18
17
|
.media-extras
|
@@ -29,3 +28,11 @@
|
|
29
28
|
dt
|
30
29
|
tr-span en="company location"
|
31
30
|
dd= model.companyAddress
|
31
|
+
if model.message
|
32
|
+
dt
|
33
|
+
tr-span en="message"
|
34
|
+
dd= model.message
|
35
|
+
if model.status
|
36
|
+
dt
|
37
|
+
tr-span en="status"
|
38
|
+
dd= model.status
|
@@ -7,6 +7,22 @@
|
|
7
7
|
if model.hasShowOrder
|
8
8
|
span #
|
9
9
|
span= model.showcaseOrder
|
10
|
+
if model.isFinished
|
11
|
+
span.parantheses
|
12
|
+
tr-span en="finished"
|
13
|
+
.edit
|
14
|
+
if model.isPending
|
15
|
+
i.fa.fa-cog.fa-spin
|
16
|
+
span.prespace
|
17
|
+
tr-span en="loading"
|
18
|
+
else
|
19
|
+
.pointer click="requestToggleFinish"
|
20
|
+
if model.isFinished
|
21
|
+
span.prespace
|
22
|
+
tr-span en="unfinish"
|
23
|
+
else
|
24
|
+
span.prespace
|
25
|
+
tr-span en="mark finish"
|
10
26
|
.edit
|
11
27
|
link-to "users.product.edit" model.id
|
12
28
|
i.fa.fa-edit
|
@@ -1,17 +1,37 @@
|
|
1
1
|
class Apiv1::Offers::IndexController < Apiv1::UsersController
|
2
2
|
def index
|
3
|
-
render json: { offers:
|
3
|
+
render json: { offers: _offers_search_process.call, meta: _offers_meta_process.call }
|
4
4
|
end
|
5
5
|
private
|
6
|
-
def
|
7
|
-
{
|
8
|
-
page: _page,
|
9
|
-
per: _per,
|
10
|
-
count: current_user.offers.count
|
11
|
-
}
|
6
|
+
def _meta_hashify
|
7
|
+
Arrows.lift -> (count) { { page: _page, per: _per, count: count } }
|
12
8
|
end
|
13
|
-
def
|
14
|
-
|
9
|
+
def _offers_meta_process
|
10
|
+
_raw_search_process >> _ask_count >> _meta_hashify
|
11
|
+
end
|
12
|
+
def _ask_count
|
13
|
+
Arrows.lift -> (offers) { offers.count }
|
14
|
+
end
|
15
|
+
def _offers_search_process
|
16
|
+
_raw_search_process >> _apply_pagination >= _ember_hashify
|
17
|
+
end
|
18
|
+
def _ember_hashify
|
19
|
+
Arrows.lift -> (offer) { offer.to_ember_hash }
|
20
|
+
end
|
21
|
+
def _raw_search_process
|
22
|
+
Arrows.lift(current_user.offers) >> _either_from_or_to_me >> (_offers_from_me ^ _offers_to_me)
|
23
|
+
end
|
24
|
+
def _offers_from_me
|
25
|
+
Arrows.lift -> (offers) { offers.made_to current_user }
|
26
|
+
end
|
27
|
+
def _offers_to_me
|
28
|
+
Arrows.lift -> (offers) { offers.made_by current_user }
|
29
|
+
end
|
30
|
+
def _apply_pagination
|
31
|
+
Arrows.lift -> (offers) { offers.page(_page).per(_per) }
|
32
|
+
end
|
33
|
+
def _either_from_or_to_me
|
34
|
+
Arrows.lift -> (offers) { _to_me? ? Arrows.good(offers) : Arrows.evil(offers) }
|
15
35
|
end
|
16
36
|
def _page
|
17
37
|
params[:page] || 1
|
@@ -19,4 +39,7 @@ class Apiv1::Offers::IndexController < Apiv1::UsersController
|
|
19
39
|
def _per
|
20
40
|
params[:per] || 15
|
21
41
|
end
|
42
|
+
def _to_me?
|
43
|
+
params[:f] == "2me"
|
44
|
+
end
|
22
45
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Apiv1::Products::UpdateController < Users::Products::UpdateController
|
2
|
+
before_filter :_ensure_product_ownership
|
3
|
+
def update
|
4
|
+
render json: { product: _toggle_finish_process.call }
|
5
|
+
end
|
6
|
+
private
|
7
|
+
def _toggle_finish_process
|
8
|
+
Arrows.lift(_product) >> _decide_finish_or_unfinish >> (_finish_listing! ^ _unfinish_listing!) >> _ember_hashify
|
9
|
+
end
|
10
|
+
def _ember_hashify
|
11
|
+
Arrows.lift -> (product) { product.to_ember_hash }
|
12
|
+
end
|
13
|
+
def _unfinish_listing!
|
14
|
+
Arrows.lift -> (product) { product.tap(&:mark_unfinish!) }
|
15
|
+
end
|
16
|
+
def _finish_listing!
|
17
|
+
Arrows.lift -> (product) { product.tap(&:mark_finish!) }
|
18
|
+
end
|
19
|
+
def _decide_finish_or_unfinish
|
20
|
+
Arrows.lift -> (product) { product.unfinished? ? Arrows.good(product) : Arrows.evil(product) }
|
21
|
+
end
|
22
|
+
def _ensure_product_ownership
|
23
|
+
unless current_user.owns? _product
|
24
|
+
render json: { message: "This isn't your listing" }, status: 401
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -10,7 +10,7 @@ class Users::Products::UpdateController < Apiv1::UsersController
|
|
10
10
|
end
|
11
11
|
private
|
12
12
|
def _ensure_product_ownership
|
13
|
-
unless current_user.owns?
|
13
|
+
unless current_user.admin? && current_user.owns?(_product)
|
14
14
|
render json: { message: "This isn't your listing" }, status: 401
|
15
15
|
end
|
16
16
|
end
|
@@ -54,6 +54,14 @@ class Admin::User < ActiveRecord::Base
|
|
54
54
|
through: :products,
|
55
55
|
class_name: 'Apiv1::OfferMessage'
|
56
56
|
|
57
|
+
def default_email
|
58
|
+
primary_contact.try(:email) || email
|
59
|
+
end
|
60
|
+
|
61
|
+
def all_known_emails
|
62
|
+
contacts.map(&:email).push(email).select(&:present?)
|
63
|
+
end
|
64
|
+
|
57
65
|
def to_ember_hash
|
58
66
|
{
|
59
67
|
id: id,
|
@@ -22,10 +22,21 @@ class Apiv1::OfferMessage < ActiveRecord::Base
|
|
22
22
|
belongs_to :product,
|
23
23
|
class_name: 'Apiv1::Product'
|
24
24
|
|
25
|
+
has_one :product_owner,
|
26
|
+
through: :product,
|
27
|
+
source: :user,
|
28
|
+
class_name: 'Admin::User'
|
29
|
+
|
25
30
|
validates :price_terms,
|
26
31
|
:product,
|
27
32
|
presence: true
|
28
33
|
|
34
|
+
scope :made_to,
|
35
|
+
-> (user) { joins(:product_owner).where "#{Admin::User.table_name}.id" => user.id }
|
36
|
+
|
37
|
+
scope :made_by,
|
38
|
+
-> (user) { where "#{self.table_name}.sender_email" => user.all_known_emails }
|
39
|
+
|
29
40
|
def to_ember_hash
|
30
41
|
attributes.merge price_terms: price_terms,
|
31
42
|
notes: notes
|
@@ -15,6 +15,7 @@
|
|
15
15
|
# updated_at :datetime
|
16
16
|
# showcase_order :integer
|
17
17
|
# deleted_at :datetime
|
18
|
+
# finished_at :datetime
|
18
19
|
#
|
19
20
|
|
20
21
|
class Apiv1::Product < ActiveRecord::Base
|
@@ -57,6 +58,12 @@ class Apiv1::Product < ActiveRecord::Base
|
|
57
58
|
|
58
59
|
scope :belonging_to_taxon,
|
59
60
|
-> (taxon) { union_of_taxon_ids taxon.id }
|
61
|
+
|
62
|
+
scope :has_been_finished,
|
63
|
+
-> { where("#{self.table_name}.finished_at < ?", DateTime.now) }
|
64
|
+
|
65
|
+
scope :still_unfinished,
|
66
|
+
-> { where("#{self.table_name}.finished_at is null or #{self.table_name}.finished_at > ?", DateTime.now) }
|
60
67
|
|
61
68
|
scope :union_of_taxon_ids,
|
62
69
|
-> (taxon_ids) { joins(:taxon_relationships).where("#{Apiv1::Listings::TaxonRelationship.table_name}.taxon_id" => taxon_ids) }
|
@@ -97,7 +104,25 @@ class Apiv1::Product < ActiveRecord::Base
|
|
97
104
|
taxons: taxons.map(&:id),
|
98
105
|
attachments: attachments.map(&:id),
|
99
106
|
offers: offers.map(&:id),
|
100
|
-
user_id: user.try(:id)
|
107
|
+
user_id: user.try(:id),
|
108
|
+
thumbnail: pictures.first.try(:thumbnail),
|
109
|
+
isFinished: finished?
|
110
|
+
end
|
111
|
+
|
112
|
+
def mark_finish!
|
113
|
+
update finished_at: DateTime.now
|
114
|
+
end
|
115
|
+
|
116
|
+
def mark_unfinish!
|
117
|
+
update finished_at: nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def unfinished?
|
121
|
+
finished_at.blank? || finished_at > DateTime.now
|
122
|
+
end
|
123
|
+
|
124
|
+
def finished?
|
125
|
+
not unfinished?
|
101
126
|
end
|
102
127
|
|
103
128
|
def rough_summary
|
@@ -18,7 +18,7 @@ class Apiv1::ProductsMachine
|
|
18
18
|
@elastic_query.try(:count) || Apiv1::Product.count
|
19
19
|
end
|
20
20
|
def _filter_pipeline
|
21
|
-
_possible_query_search >> _paginate >> _unify_type >> _process_taxons >> _process_ordering
|
21
|
+
_possible_query_search >> _paginate >> _unify_type >> _process_taxons >> _process_ordering >> _process_finishing
|
22
22
|
end
|
23
23
|
def _possible_query_search
|
24
24
|
lambda do |product|
|
@@ -45,6 +45,13 @@ class Apiv1::ProductsMachine
|
|
45
45
|
-> (product) { product.order_by_created_at }
|
46
46
|
end
|
47
47
|
end
|
48
|
+
def _process_finishing
|
49
|
+
if _user.blank?
|
50
|
+
lambda(&:still_unfinished)
|
51
|
+
else
|
52
|
+
-> (product) { product }
|
53
|
+
end
|
54
|
+
end
|
48
55
|
def _process_taxons
|
49
56
|
lambda do |product|
|
50
57
|
if _taxon_ids.present?
|
@@ -58,7 +65,7 @@ class Apiv1::ProductsMachine
|
|
58
65
|
-> (t) { t.respond_to?(:records) ? t.records.load : t }
|
59
66
|
end
|
60
67
|
def _user
|
61
|
-
Admin::User.find_by_id params[:user_id]
|
68
|
+
@user ||= Admin::User.find_by_id params[:user_id]
|
62
69
|
end
|
63
70
|
def _query
|
64
71
|
params[:query]
|
@@ -9,6 +9,9 @@ if product
|
|
9
9
|
tr-span en=product.material
|
10
10
|
if product.sku
|
11
11
|
span.predash= product.sku
|
12
|
+
if product.isFinished
|
13
|
+
span.parantheses
|
14
|
+
tr-span en="finished"
|
12
15
|
if product.others
|
13
16
|
p.product-summary
|
14
17
|
tr-span en=product.others
|
@@ -36,6 +39,9 @@ if product
|
|
36
39
|
tr-span en=product.material
|
37
40
|
if product.sku
|
38
41
|
span.predash= product.sku
|
42
|
+
if product.isFinished
|
43
|
+
span.parantheses
|
44
|
+
tr-span en="finished"
|
39
45
|
|
40
46
|
.product-specs
|
41
47
|
if product.amount
|
data/generica/app/varissets/javascripts/templates/products/product/offers/_anonymous-form.emblem.erb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
.the-form class=showOfferForm::hidden
|
2
|
+
== partial "products/product/offers/offer-form"
|
1
3
|
.somesort-of-request
|
2
4
|
button.button.secondary.capitalize click="displayModal 'login'"
|
3
5
|
tr-span en="login if you have an account"
|
@@ -6,6 +8,4 @@
|
|
6
8
|
tr-span en="or register an account"
|
7
9
|
.somesort-of-request
|
8
10
|
button.button.secondary.capitalize click="toggleOfferForm"
|
9
|
-
tr-span en="or make
|
10
|
-
.the-form class=showOfferForm::hidden
|
11
|
-
== partial "products/product/offers/offer-form"
|
11
|
+
tr-span en="or make a quick offer"
|
data/generica/config/routes.rb
CHANGED
@@ -5,6 +5,7 @@ Rails.application.routes.draw do
|
|
5
5
|
resources :products, only: [:index], controller: 'products/index'
|
6
6
|
resources :products, only: [:show], controller: 'products/show'
|
7
7
|
resources :products, only: [:destroy], controller: 'products/destroy'
|
8
|
+
resources :products, only: [:update], controller: 'products/update'
|
8
9
|
resource :product_metadatum, only: [:show], controller: 'product_metadatum/show'
|
9
10
|
resource :product_metadata, only: [:show], controller: 'product_metadatum/show'
|
10
11
|
resources :pictures, only: [:show], controller: 'pictures/show'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Use this file to easily define all of your cron jobs.
|
2
|
+
#
|
3
|
+
# It's helpful, but not entirely necessary to understand cron before proceeding.
|
4
|
+
# http://en.wikipedia.org/wiki/Cron
|
5
|
+
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# set :output, "/path/to/my/cron_log.log"
|
9
|
+
#
|
10
|
+
# every 2.hours do
|
11
|
+
# command "/usr/bin/some_great_command"
|
12
|
+
# runner "MyModel.some_method"
|
13
|
+
# rake "some:great:rake:task"
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# every 4.days do
|
17
|
+
# runner "AnotherModel.prune_old_records"
|
18
|
+
# end
|
19
|
+
|
20
|
+
# Learn more: http://github.com/javan/whenever
|
21
|
+
set :job_template, "bash -l -c -i ':job'"
|
22
|
+
set :output, '/home/ia/workspace/<%= project_name %>/log/cron_log.log'
|
23
|
+
|
24
|
+
every 2.hours do
|
25
|
+
rake "email_slave:work_the_queue"
|
26
|
+
end
|
@@ -1,75 +1,81 @@
|
|
1
1
|
module Generica; end
|
2
2
|
class Generica::EmailSlave
|
3
3
|
def dispatch_emails!
|
4
|
-
|
5
|
-
_requests.update_all(status: :attempt_dispatch)
|
6
|
-
_postal_pipeline.call _emails
|
7
|
-
_requests.update_all(status: :delivered)
|
4
|
+
_dispatch_process.call
|
8
5
|
end
|
9
6
|
private
|
10
|
-
def
|
11
|
-
|
7
|
+
def _dispatch_process
|
8
|
+
_all_unfulfilled_requests >> _group_by_target_email >= _crunch_down_emails >= (_dispatch_email! % _manage_failure!)
|
12
9
|
end
|
13
|
-
def
|
14
|
-
|
10
|
+
def _all_unfulfilled_requests
|
11
|
+
Arrows.lift Apiv1::EmailRequest.awaiting_delivery
|
15
12
|
end
|
16
|
-
def
|
17
|
-
-> (
|
13
|
+
def _group_by_target_email
|
14
|
+
Arrows.lift -> (requests) { Sorter.group_by_to requests }
|
18
15
|
end
|
19
|
-
def
|
20
|
-
-> (
|
16
|
+
def _crunch_down_emails
|
17
|
+
Arrows.lift -> (requests) { Cruncher.crunch requests }
|
21
18
|
end
|
22
|
-
def
|
23
|
-
|
19
|
+
def _dispatch_email!
|
20
|
+
Arrows.lift -> (mail) { Dispatcher.dispatch! mail }
|
24
21
|
end
|
25
|
-
def
|
26
|
-
|
27
|
-
end
|
28
|
-
def _consider_email_address
|
29
|
-
-> (scope) { @user.present? ? scope.meant_for_email(@email) : scope }
|
30
|
-
end
|
31
|
-
def _consider_workload
|
32
|
-
-> (klass) { klass.belonging_to_the_user_with_oldest_undelivered }
|
33
|
-
end
|
34
|
-
def _emails
|
35
|
-
@emails ||= _requests.map(&:mailify)
|
36
|
-
end
|
37
|
-
def _hash_map_dispatch(hash)
|
38
|
-
_right_map(hash) { |email| Dispatcher.dispatch email }
|
39
|
-
end
|
40
|
-
def _hash_map_crunch(hash)
|
41
|
-
_right_map(hash) { |emails| Cruncher.crunch emails }
|
42
|
-
end
|
43
|
-
def _right_map(hash, &block)
|
44
|
-
thing = hash.map { |key_and_value| [key_and_value.first, yield(key_and_value.last)] }
|
45
|
-
Hash[thing]
|
22
|
+
def _manage_failure!
|
23
|
+
Arrows.lift -> (requests) { GarbageMan.cleanup! requests }
|
46
24
|
end
|
47
25
|
end
|
48
26
|
|
49
27
|
class Generica::EmailSlave::Sorter
|
50
28
|
class << self
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
29
|
+
def group_by_to(requests)
|
30
|
+
requests.group_by { |request| request.to }.map(&:last)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Generica::EmailSlave::GarbageMan
|
36
|
+
class << self
|
37
|
+
def cleanup!(requests)
|
38
|
+
return if requests.blank?
|
39
|
+
requests.map(&:mark_as_failed!)
|
57
40
|
end
|
58
41
|
end
|
59
42
|
end
|
60
43
|
|
61
44
|
class Generica::EmailSlave::Cruncher
|
62
45
|
class << self
|
63
|
-
def crunch(
|
64
|
-
|
65
|
-
|
46
|
+
def crunch(requests)
|
47
|
+
mails_or_fails = requests.map { |request| _convert_request request }
|
48
|
+
_crunch_process.call mails_or_fails
|
49
|
+
end
|
50
|
+
private
|
51
|
+
def _crunch_process
|
52
|
+
_separate_good_and_evil >> (_payload % _payload) >> (_crunch_good_emails % Arrows::ID)
|
53
|
+
end
|
54
|
+
def _crunch_good_emails
|
55
|
+
Arrows.lift -> (emails) { emails.count < 2 ? emails.first : Apiv1::AggregateMailer.summary(emails) }
|
56
|
+
end
|
57
|
+
def _payload
|
58
|
+
Arrows.lift -> (eithers) { eithers.map &:payload }
|
59
|
+
end
|
60
|
+
def _separate_good_and_evil
|
61
|
+
Arrows.lift -> (mails_and_fails) { [mails_and_fails.select(&:good?), mails_and_fails.reject(&:good?)] }
|
62
|
+
end
|
63
|
+
def _convert_request(request)
|
64
|
+
begin
|
65
|
+
mail = request.mailify
|
66
|
+
request.mark_as_delivered!
|
67
|
+
Arrows.good mail
|
68
|
+
rescue
|
69
|
+
Arrows.evil request
|
70
|
+
end
|
66
71
|
end
|
67
72
|
end
|
68
73
|
end
|
69
74
|
|
70
75
|
class Generica::EmailSlave::Dispatcher
|
71
76
|
class << self
|
72
|
-
def dispatch(email)
|
77
|
+
def dispatch!(email)
|
78
|
+
return if email.blank?
|
73
79
|
gmail.compose(email).deliver!
|
74
80
|
end
|
75
81
|
def gmail(&block)
|
@@ -1,11 +1,14 @@
|
|
1
|
+
require "generica/email_slave"
|
1
2
|
namespace :email_slave do
|
2
3
|
desc "checks the email job queue for emails that require sending, batches them together, and sends them off"
|
3
4
|
task work_the_queue: :environment do
|
4
|
-
require "generica/email_slave"
|
5
5
|
puts "begin dispatching emails..."
|
6
|
-
Generica::EmailSlave.new.dispatch_emails
|
6
|
+
puts Generica::EmailSlave.new.dispatch_emails!.inspect
|
7
7
|
puts "finished"
|
8
|
-
sleep 1
|
9
8
|
end
|
10
9
|
|
10
|
+
desc "just blandly outputs the emails to be sent"
|
11
|
+
task display_the_queue: :environment do
|
12
|
+
|
13
|
+
end
|
11
14
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
require "generica/email_slave"
|
3
|
+
|
4
|
+
RSpec.describe Generica::EmailSlave do
|
5
|
+
let(:user) { Admin::UserFactory.mock }
|
6
|
+
let(:owner) { Admin::UserFactory.mock }
|
7
|
+
let(:product) { Apiv1::ProductFactory.new.create }
|
8
|
+
before do
|
9
|
+
product.update user: owner
|
10
|
+
@offer_params = {
|
11
|
+
product_id: product.id,
|
12
|
+
price_terms: "Are you feeling lucky, punk?",
|
13
|
+
notes: "Nothing to say here"
|
14
|
+
}
|
15
|
+
end
|
16
|
+
let(:offer) { Users::Products::OfferFactory.new(user, @offer_params).tap(&:satisfy_specifications?).tap(&:save!).offer }
|
17
|
+
let(:postboy) { described_class.new offer }
|
18
|
+
context '#dispatch_emails!' do
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec.describe Generica::EmailSlave::Cruncher do
|
24
|
+
before do
|
25
|
+
expect(Apiv1::AggregateMailer).to receive(:summary).and_return("first mail")
|
26
|
+
@f = lambda do
|
27
|
+
req = instance_double "Apiv1::EmailRequest"
|
28
|
+
allow(req).to receive(:mailify) { throw :uhoh_fail }
|
29
|
+
allow(req).to receive(:mark_as_delivered!).and_return("mark_as_delivered!")
|
30
|
+
req
|
31
|
+
end
|
32
|
+
@r = lambda do
|
33
|
+
req = instance_double "Apiv1::EmailRequest"
|
34
|
+
allow(req).to receive(:mailify).and_return("mailified")
|
35
|
+
allow(req).to receive(:mark_as_delivered!).and_return("mark_as_delivered!")
|
36
|
+
req
|
37
|
+
end
|
38
|
+
end
|
39
|
+
context ':crunch' do
|
40
|
+
let(:requests) { [@r.call, @r.call, @r.call, @r.call] }
|
41
|
+
let(:expected) { ["first mail", []] }
|
42
|
+
subject { described_class.crunch requests }
|
43
|
+
specify { should eq expected }
|
44
|
+
end
|
45
|
+
context ':crunch2' do
|
46
|
+
before do
|
47
|
+
@f1 = @f.call
|
48
|
+
@f2 = @f.call
|
49
|
+
end
|
50
|
+
let(:requests) { [@r.call, @r.call, @r.call, @r.call, @f1, @f2] }
|
51
|
+
let(:expected) { ["first mail", [@f1, @f2]] }
|
52
|
+
subject { described_class.crunch requests }
|
53
|
+
specify { should eq expected }
|
54
|
+
end
|
55
|
+
end
|
@@ -29,8 +29,11 @@ class Cloudrider::BackendCommander
|
|
29
29
|
def _squash
|
30
30
|
[Cloudrider::Serverside::SquashRuby]
|
31
31
|
end
|
32
|
+
def _schedule
|
33
|
+
[Cloudrider::Serverside::ScheduleRuby]
|
34
|
+
end
|
32
35
|
def _configs
|
33
|
-
_nginx + _unicorn + _database + _squash
|
36
|
+
_nginx + _unicorn + _database + _squash + _schedule
|
34
37
|
end
|
35
38
|
def _related_backends
|
36
39
|
case _backend_name
|
@@ -44,6 +47,8 @@ class Cloudrider::BackendCommander
|
|
44
47
|
_database
|
45
48
|
when "squash"
|
46
49
|
_squash
|
50
|
+
when "schedule"
|
51
|
+
_schedule
|
47
52
|
else
|
48
53
|
raise UnknownOrUnimplmenetedVarisset, _backend_name
|
49
54
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Cloudrider::Serverside::ScheduleRuby < Cloudrider::Serverside::Base
|
2
|
+
class Context
|
3
|
+
attr_accessor :project_name
|
4
|
+
end
|
5
|
+
private
|
6
|
+
def _file_name
|
7
|
+
"schedule.rb"
|
8
|
+
end
|
9
|
+
def _context
|
10
|
+
Context.new.tap do |c|
|
11
|
+
c.project_name = @protosite.project_name
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/cloudrider/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloudrider
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.23
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Chen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-12-
|
11
|
+
date: 2014-12-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -429,6 +429,7 @@ files:
|
|
429
429
|
- generica/app/controllers/apiv1/products/destroy_controller.rb
|
430
430
|
- generica/app/controllers/apiv1/products/index_controller.rb
|
431
431
|
- generica/app/controllers/apiv1/products/show_controller.rb
|
432
|
+
- generica/app/controllers/apiv1/products/update_controller.rb
|
432
433
|
- generica/app/controllers/apiv1/taxons/destroy_controller.rb
|
433
434
|
- generica/app/controllers/apiv1/taxons/index_controller.rb
|
434
435
|
- generica/app/controllers/apiv1/taxons/show_controller.rb
|
@@ -564,6 +565,7 @@ files:
|
|
564
565
|
- generica/config/nginx.conf
|
565
566
|
- generica/config/nginx.conf.erb
|
566
567
|
- generica/config/routes.rb
|
568
|
+
- generica/config/schedule.rb.yml
|
567
569
|
- generica/config/secrets.yml
|
568
570
|
- generica/config/unicorn.conf.rb
|
569
571
|
- generica/config/unicorn.conf.rb.erb
|
@@ -591,6 +593,7 @@ files:
|
|
591
593
|
- generica/db/migrate/20141127230304_create_apiv1_email_requests.rb
|
592
594
|
- generica/db/migrate/20141127230455_create_apiv1_email_objects.rb
|
593
595
|
- generica/db/migrate/20141209185352_create_apiv1_user_contacts.rb
|
596
|
+
- generica/db/migrate/20141212162545_add_finished_at_to_apiv1_products.rb
|
594
597
|
- generica/db/schema.rb
|
595
598
|
- generica/db/seeds.rb
|
596
599
|
- generica/db/seeds/dimensions.rb
|
@@ -649,6 +652,7 @@ files:
|
|
649
652
|
- generica/spec/factories/apiv1/product_factory.rb
|
650
653
|
- generica/spec/factories/apiv1/taxon_factory.rb
|
651
654
|
- generica/spec/factories/base_factory.rb
|
655
|
+
- generica/spec/generica/email_slave_spec.rb
|
652
656
|
- generica/spec/generica/image_preloader_spec.rb
|
653
657
|
- generica/spec/mailers/apiv1/aggregate_mailer_spec.rb
|
654
658
|
- generica/spec/models/admin/pictures_factory_spec.rb
|
@@ -724,6 +728,7 @@ files:
|
|
724
728
|
- lib/cloudrider/serverside/base.rb
|
725
729
|
- lib/cloudrider/serverside/database_yaml.rb
|
726
730
|
- lib/cloudrider/serverside/nginx_conf.rb
|
731
|
+
- lib/cloudrider/serverside/schedule_ruby.rb
|
727
732
|
- lib/cloudrider/serverside/squash_ruby.rb
|
728
733
|
- lib/cloudrider/serverside/unicorn_conf_ruby.rb
|
729
734
|
- lib/cloudrider/version.rb
|