cloudrider 0.3.22 → 0.3.23

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