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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/generica/app/assets/javascripts/components/product-display.js.em +2 -2
  3. data/generica/app/assets/javascripts/components/product-listing.js.em +2 -2
  4. data/generica/app/assets/javascripts/controllers/admin/products/slice_controller.js.em +7 -1
  5. data/generica/app/assets/javascripts/controllers/users/offers/index_controller.js.em +10 -6
  6. data/generica/app/assets/javascripts/controllers/users/products/slice_controller.js.em +7 -1
  7. data/generica/app/assets/javascripts/models/product.js.em +2 -0
  8. data/generica/app/assets/javascripts/routes/users/offers/index_route.js.em +11 -1
  9. data/generica/app/assets/javascripts/templates/admin/products/slice.emblem +16 -0
  10. data/generica/app/assets/javascripts/templates/users/offers/index.emblem +5 -1
  11. data/generica/app/assets/javascripts/templates/users/offers/slice.emblem +10 -3
  12. data/generica/app/assets/javascripts/templates/users/products/slice.emblem +16 -0
  13. data/generica/app/controllers/apiv1/offers/index_controller.rb +32 -9
  14. data/generica/app/controllers/apiv1/products/update_controller.rb +27 -0
  15. data/generica/app/controllers/users/products/update_controller.rb +1 -1
  16. data/generica/app/mailers/apiv1/notifications_mailer/new_offer_context.rb +1 -1
  17. data/generica/app/models/admin/user.rb +8 -0
  18. data/generica/app/models/apiv1/email_request.rb +4 -0
  19. data/generica/app/models/apiv1/offer_message.rb +11 -0
  20. data/generica/app/models/apiv1/picture.rb +4 -0
  21. data/generica/app/models/apiv1/product.rb +26 -1
  22. data/generica/app/models/apiv1/products_machine.rb +9 -2
  23. data/generica/app/varissets/javascripts/templates/components/product-display.emblem.erb +6 -0
  24. data/generica/app/varissets/javascripts/templates/components/product-listing.emblem.erb +3 -0
  25. data/generica/app/varissets/javascripts/templates/products/product/offers/_anonymous-form.emblem.erb +3 -3
  26. data/generica/app/varissets/javascripts/templates/products/product/show.emblem.erb +3 -0
  27. data/generica/config/routes.rb +1 -0
  28. data/generica/config/schedule.rb.yml +26 -0
  29. data/generica/db/migrate/20141212162545_add_finished_at_to_apiv1_products.rb +5 -0
  30. data/generica/lib/generica/email_slave.rb +51 -45
  31. data/generica/lib/tasks/email_slave.rake +6 -3
  32. data/generica/spec/generica/email_slave_spec.rb +55 -0
  33. data/lib/cloudrider/backend_commander.rb +6 -1
  34. data/lib/cloudrider/serverside/schedule_ruby.rb +14 -0
  35. data/lib/cloudrider/version.rb +1 -1
  36. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9fcfb4633e2558a041466937f836ba50ce063168
4
- data.tar.gz: 390f69c4c086a0d04ec6c4e3dfbaacd3060ba014
3
+ metadata.gz: 0d2e8d222b358db32bdff3dd54e567bbe612cd7f
4
+ data.tar.gz: 9a0ffe6c7954a0b6de2126945616cff8eac71b29
5
5
  SHA512:
6
- metadata.gz: 3bdc1d45f1b8cdfa9f0aa304fc9933e85a59c82019fbec8969e319ba10f172f909c959c974cb2312ede046af2440d08254260a3dbbe52dbffff9a30bbedc97cd
7
- data.tar.gz: fea6c1ef0c6d3ceb482a4530fcd8b2a7411f614e0e516ecd547fc2475e0bd3da28a6e4fe96ef30ef1d83ec020b51a2964ac9190c12297763eede22794a49a448
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.pictures.firstObject.picUrl
6
- productImageStyle: -> "background-image: url(#{@get 'product.pictures.firstObject.picUrl' });"
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.pictures.firstObject.picUrl
6
- productImageStyle: -> "background-image: url(#{@get 'product.pictures.firstObject.picUrl' });"
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
- +computed page, per
6
- offers: ->
7
- @store.find "offer", page: @page, per: @per
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 offers.content.meta
10
- metadatum: -> @get("offers.content.meta")
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="no one has made you an offer"
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
- link-to "products.product.show" model.product.id
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: _offers.map(&:to_ember_hash), meta: _meta_hash }
3
+ render json: { offers: _offers_search_process.call, meta: _offers_meta_process.call }
4
4
  end
5
5
  private
6
- def _meta_hash
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 _offers
14
- @offers ||= current_user.offers.page(_page).per(_per)
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? _product
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
@@ -10,7 +10,7 @@ class Apiv1::NotificationsMailer::NewOfferContext
10
10
  @offer = offer_message
11
11
  end
12
12
  def to
13
- product.try(:user).try(:email)
13
+ product.try(:user).try(:default_email) || "mlresources.inc@gmail.com"
14
14
  end
15
15
  def from
16
16
  "secretary@plasticscrapmarket.org"
@@ -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,
@@ -94,6 +94,10 @@ class Apiv1::EmailRequest < ActiveRecord::Base
94
94
  update status: :delivered
95
95
  end
96
96
 
97
+ def mark_as_failed!
98
+ update status: :failed
99
+ end
100
+
97
101
  private
98
102
  def _mailer_class
99
103
  Object.const_get _mailer_class_name
@@ -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
@@ -36,4 +36,8 @@ class Apiv1::Picture < ActiveRecord::Base
36
36
  attributes.merge pic_url: pic.url,
37
37
  thumb_url: pic.url(:thumb)
38
38
  end
39
+
40
+ def thumbnail
41
+ pic.url :thumb
42
+ end
39
43
  end
@@ -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
@@ -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
  .dateline= product.createdAt
13
16
  p.smaller-details
14
17
  if product.price
@@ -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 anonymous offer"
10
- .the-form class=showOfferForm::hidden
11
- == partial "products/product/offers/offer-form"
11
+ tr-span en="or make a quick offer"
@@ -10,6 +10,9 @@
10
10
  tr-span en=product.material
11
11
  if product.sku
12
12
  span.predash= product.sku
13
+ if product.isFinished
14
+ span.parantheses
15
+ tr-span en="finished"
13
16
 
14
17
  .row
15
18
  .small-12.medium-4.large-3.columns
@@ -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
@@ -0,0 +1,5 @@
1
+ class AddFinishedAtToApiv1Products < ActiveRecord::Migration
2
+ def change
3
+ add_column :apiv1_products, :finished_at, :datetime
4
+ end
5
+ end
@@ -1,75 +1,81 @@
1
1
  module Generica; end
2
2
  class Generica::EmailSlave
3
3
  def dispatch_emails!
4
- return puts "no emails" if _emails.blank?
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 _postal_pipeline
11
- _group_by_user_emails >> _crunch_into_digest >> _send_off!
7
+ def _dispatch_process
8
+ _all_unfulfilled_requests >> _group_by_target_email >= _crunch_down_emails >= (_dispatch_email! % _manage_failure!)
12
9
  end
13
- def _group_by_user_emails
14
- -> (emails) { Sorter.group_by_user_emails emails }
10
+ def _all_unfulfilled_requests
11
+ Arrows.lift Apiv1::EmailRequest.awaiting_delivery
15
12
  end
16
- def _crunch_into_digest
17
- -> (email_hash) { _hash_map_crunch email_hash }
13
+ def _group_by_target_email
14
+ Arrows.lift -> (requests) { Sorter.group_by_to requests }
18
15
  end
19
- def _send_off!
20
- -> (email_hash) { _hash_map_dispatch email_hash }
16
+ def _crunch_down_emails
17
+ Arrows.lift -> (requests) { Cruncher.crunch requests }
21
18
  end
22
- def _requests
23
- @requests ||= _request_process.call Apiv1::EmailRequest
19
+ def _dispatch_email!
20
+ Arrows.lift -> (mail) { Dispatcher.dispatch! mail }
24
21
  end
25
- def _request_process
26
- _consider_workload >> _consider_email_address
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 group_by_user_emails(emails)
52
- emails.reduce({}) do |hash, email|
53
- hash[email.to.first] ||= []
54
- hash[email.to.first].push email
55
- hash
56
- end
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(emails)
64
- return emails.first if emails.count < 2
65
- Apiv1::AggregateMailer.summary emails
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
@@ -1,3 +1,3 @@
1
1
  module Cloudrider
2
- VERSION = "0.3.22"
2
+ VERSION = "0.3.23"
3
3
  end
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.22
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 00:00:00.000000000 Z
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