artfully_ose 1.2.0.pre.15 → 1.2.0.pre.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +8 -8
  2. data/app/assets/javascripts/application.js +22 -1
  3. data/app/assets/javascripts/bootstrap-wysihtml5.js +511 -0
  4. data/app/assets/javascripts/custom/prices.js +1 -0
  5. data/app/assets/javascripts/wysihtml5-0.3.0.min.js +261 -0
  6. data/app/assets/stylesheets/application.sass +25 -1
  7. data/app/assets/stylesheets/bootstrap-wysihtml5.css +102 -0
  8. data/app/assets/stylesheets/bootstrap.css +1 -0
  9. data/app/assets/stylesheets/sass/store.sass +22 -0
  10. data/app/assets/stylesheets/wysiwyg-color.css +67 -0
  11. data/app/controllers/exchanges_controller.rb +7 -1
  12. data/app/controllers/membership_kits_controller.rb +25 -0
  13. data/app/controllers/mobile/dashboard_controller.rb +17 -0
  14. data/app/controllers/mobile/events_controller.rb +16 -0
  15. data/app/controllers/mobile/orders_controller.rb +60 -0
  16. data/app/controllers/mobile/shows_controller.rb +33 -0
  17. data/app/controllers/mobile/tickets_controller.rb +85 -0
  18. data/app/controllers/mobile/users_controller.rb +32 -0
  19. data/app/controllers/people_controller.rb +8 -6
  20. data/app/controllers/refunds_controller.rb +7 -7
  21. data/app/controllers/searches_controller.rb +3 -4
  22. data/app/controllers/store/checkouts_controller.rb +4 -4
  23. data/app/controllers/store/events_controller.rb +14 -2
  24. data/app/controllers/store/memberships_controller.rb +2 -0
  25. data/app/controllers/store/orders_controller.rb +1 -1
  26. data/app/controllers/tickets_controller.rb +2 -2
  27. data/app/helpers/artfully_ose_helper.rb +21 -0
  28. data/app/mailers/order_mailer.rb +28 -1
  29. data/app/models/address.rb +1 -1
  30. data/app/models/checkout.rb +8 -14
  31. data/app/models/contribution.rb +1 -0
  32. data/app/models/event.rb +17 -6
  33. data/app/models/exchange.rb +7 -5
  34. data/app/models/ext/integrations.rb +8 -3
  35. data/app/models/item.rb +4 -0
  36. data/app/models/job/checkout_processor.rb +41 -0
  37. data/app/models/job/order_mailer_job.rb +8 -0
  38. data/app/models/job/order_processor.rb +50 -9
  39. data/app/models/kit.rb +1 -1
  40. data/app/models/kits/membership_kit.rb +55 -0
  41. data/app/models/member.rb +16 -4
  42. data/app/models/membership_type.rb +7 -0
  43. data/app/models/order_handler.rb +4 -3
  44. data/app/models/orders/imported_order.rb +4 -0
  45. data/app/models/orders/order.rb +16 -5
  46. data/app/models/orders/pdf_generation.rb +75 -0
  47. data/app/models/organization.rb +7 -0
  48. data/app/models/person.rb +15 -0
  49. data/app/models/refund.rb +5 -2
  50. data/app/models/section.rb +16 -2
  51. data/app/models/show.rb +2 -8
  52. data/app/models/show_stats_view.rb +10 -0
  53. data/app/models/ticket.rb +38 -15
  54. data/app/models/ticket/qr_code.rb +21 -0
  55. data/app/models/ticket/template.rb +1 -0
  56. data/app/models/ticket/transfers.rb +5 -1
  57. data/app/models/ticket_summary.rb +5 -2
  58. data/app/models/ticket_type.rb +42 -7
  59. data/app/models/user.rb +3 -0
  60. data/app/models/user_membership.rb +1 -0
  61. data/app/views/actions/get/_show.html.haml +1 -12
  62. data/app/views/contributions/_form.html.haml +1 -1
  63. data/app/views/events/_ticket_type_fields.html.haml +21 -0
  64. data/app/views/events/image.html.haml +1 -1
  65. data/app/views/exchanges/new.html.haml +51 -28
  66. data/app/views/layouts/_google_analytics.html.haml +5 -2
  67. data/app/views/layouts/members.html.haml +1 -1
  68. data/app/views/layouts/storefront.html.haml +1 -1
  69. data/app/views/members/invitations/edit.html.erb +14 -0
  70. data/app/views/members/invitations/new.html.erb +14 -0
  71. data/app/views/members/mailer/invitation_instructions.html.haml +11 -0
  72. data/app/views/membership_kits/edit.html.haml +68 -0
  73. data/app/views/membership_types/index.html.haml +7 -3
  74. data/app/views/order_mailer/confirmation_for_exchange.html.haml +34 -0
  75. data/app/views/order_mailer/confirmation_for_exchange.text.haml +26 -0
  76. data/app/views/order_mailer/confirmation_for_refund.html.haml +29 -0
  77. data/app/views/order_mailer/confirmation_for_refund.text.haml +22 -0
  78. data/app/views/organizations/_form.html.haml +1 -1
  79. data/app/views/people/_header.html.haml +4 -4
  80. data/app/views/people/index.html.haml +18 -6
  81. data/app/views/refunds/new.html.haml +41 -27
  82. data/app/views/store/checkouts/_membership_info.html.haml +11 -0
  83. data/app/views/store/checkouts/thanks.html.haml +2 -10
  84. data/app/views/store/events/_calendar.html.haml +1 -4
  85. data/app/views/store/events/calendar.html.haml +49 -0
  86. data/app/views/store/events/show.html.haml +3 -8
  87. data/app/views/store/events/single_show.html.haml +2 -2
  88. data/app/views/store/memberships/index.html.haml +3 -11
  89. data/app/views/store/shows/_show.html.haml +6 -6
  90. data/config/initializers/paperclip.rb +14 -0
  91. data/config/routes.rb +35 -0
  92. data/db/migrate/20130722153731_add_autentication_token_to_users.rb +5 -0
  93. data/db/migrate/20130723212712_add_qr_code_to_tickets.rb +9 -0
  94. data/db/migrate/20130820011240_add_uuid_to_tickets.rb +13 -0
  95. data/db/migrate/20130820025134_make_ticket_uuid_not_null.rb +11 -0
  96. data/db/migrate/20130823191625_add_pdf_to_orders.rb +5 -0
  97. data/db/migrate/20131127162818_add_buyer_id_index_to_tickets.rb +5 -0
  98. data/db/migrate/20131127164000_member_tickets.rb +9 -0
  99. data/lib/artfully_ose.rb +3 -1
  100. data/lib/artfully_ose/engine.rb +5 -4
  101. data/lib/artfully_ose/version.rb +1 -1
  102. metadata +51 -2
data/app/models/kit.rb CHANGED
@@ -61,7 +61,7 @@ class Kit < ActiveRecord::Base
61
61
  end
62
62
 
63
63
  def self.subklasses
64
- @subklasses ||= [ TicketingKit, RegularDonationKit, SponsoredDonationKit, ResellerKit, MailchimpKit ].freeze
64
+ @subklasses ||= [ TicketingKit, RegularDonationKit, SponsoredDonationKit, ResellerKit, MailchimpKit, MembershipKit ].freeze
65
65
  end
66
66
 
67
67
  def self.pad_with_new_kits(kits = [])
@@ -0,0 +1,55 @@
1
+ class MembershipKit < Kit
2
+ include ActionView::Helpers::SanitizeHelper
3
+
4
+ acts_as_kit :with_approval => true, :admin_only => true do
5
+ approve :unless => :no_bank_account?
6
+
7
+ self.configurable = true
8
+
9
+ state_machine do
10
+ state :cancelled, :enter => :kit_cancelled
11
+ end
12
+
13
+ when_active do |organization|
14
+ organization.can :access, :reselling
15
+ end
16
+ end
17
+
18
+ before_save :sanitize_accessors
19
+
20
+ ACCESSORS = [ :marketing_copy_heading, :marketing_copy_sidebar, :thanks_copy, :invitation_email_text_copy ]
21
+
22
+ ACCESSORS.each do |accessor|
23
+ attr_accessible accessor
24
+ end
25
+
26
+ store :settings, :accessors => ACCESSORS
27
+
28
+ def friendly_name
29
+ "Membership"
30
+ end
31
+
32
+ def no_bank_account?
33
+ errors.add(:requirements, "Your organization needs bank account information first.") if organization.bank_account.nil?
34
+ organization.bank_account.nil?
35
+ end
36
+
37
+ def pitch
38
+ "Sell Memberships!"
39
+ end
40
+
41
+ def configured?
42
+ membership_state == "configured"
43
+ end
44
+
45
+ def configured!
46
+ settings[:membership_state] = "configured"
47
+ save
48
+ end
49
+
50
+ def sanitize_accessors
51
+ ACCESSORS.each do |accessor|
52
+ self.send("#{accessor}=", (sanitize self.send(accessor)))
53
+ end
54
+ end
55
+ end
data/app/models/member.rb CHANGED
@@ -17,9 +17,9 @@ class Member < ActiveRecord::Base
17
17
  PAST = :past
18
18
  NONE = :none
19
19
 
20
- scope CURRENT, where("current_memberships_count > 0")
21
- scope LAPSED, where("lapsed_memberships_count > 0").where("current_memberships_count = 0")
22
- scope PAST, where("past_memberships_count > 0").where("lapsed_memberships_count = 0").where("current_memberships_count = 0")
20
+ scope :current, where("current_memberships_count > 0")
21
+ scope :lapsed, where("lapsed_memberships_count > 0").where("current_memberships_count = 0")
22
+ scope :past, where("past_memberships_count > 0").where("lapsed_memberships_count = 0").where("current_memberships_count = 0")
23
23
 
24
24
  #
25
25
  # devise_invitable needs this otherwise it can't set the :from param in an email
@@ -33,6 +33,18 @@ class Member < ActiveRecord::Base
33
33
  end
34
34
  end
35
35
 
36
+ def current_membership_types
37
+ memberships.current.collect(&:membership_type)
38
+ end
39
+
40
+ def member_tickets_purchased_for(event)
41
+ Ticket.joins(:ticket_type)
42
+ .joins(:show => :event)
43
+ .where(:buyer_id => self.person.id)
44
+ .where('shows.event_id' => event.id)
45
+ .where('ticket_types.member_ticket = 1')
46
+ end
47
+
36
48
  #
37
49
  # This is always run DJ'd
38
50
  #
@@ -47,7 +59,7 @@ class Member < ActiveRecord::Base
47
59
  #
48
60
  # Intentionally did not use a state machine for this for a few reasons
49
61
  # 1) I'm not all that happy with transitions
50
- # 2) Can't find another statre machine that I like and is worth the cost of switching to
62
+ # 2) Can't find another state machine that I like and is worth the cost of switching to
51
63
  # 3) This works just fine. I prefer calculating state on the fly here because we're couning the memberships
52
64
  # on this member anyway
53
65
  # 4) Determining a lapsed or past member is a touch more complicated than it sounds
@@ -14,6 +14,7 @@ class MembershipType < ActiveRecord::Base
14
14
 
15
15
  scope :storefront, where(:on_sale => true).where("sales_start_at < ? or sales_start_at is null", DateTime.now).where("sales_end_at > ? or sales_end_at is null", DateTime.now)
16
16
  scope :on_sale, where(:on_sale => true)
17
+ scope :not_ended, where('ends_at > ?', DateTime.now)
17
18
 
18
19
  comma do
19
20
  name
@@ -22,6 +23,9 @@ class MembershipType < ActiveRecord::Base
22
23
  fee
23
24
  number_of_tickets
24
25
  type
26
+ memberships 'Memberships sold' do |m|
27
+ m.count
28
+ end
25
29
  members { |m| m.count }
26
30
  duration
27
31
  period
@@ -32,6 +36,9 @@ class MembershipType < ActiveRecord::Base
32
36
  sales_end_at
33
37
  end
34
38
 
39
+ def self.in_play
40
+ self.find((MembershipType.not_ended.pluck(:id).uniq + Membership.current.select(:membership_type_id).uniq.pluck(:membership_type_id)))
41
+ end
35
42
 
36
43
  def membershipize
37
44
  self.name.end_with?("Membership") ? self.name : self.name + " Membership"
@@ -2,10 +2,11 @@
2
2
  # Handles orders in progress for which customers have not paid
3
3
  #
4
4
  class OrderHandler
5
- attr_accessor :discount_error, :over_limit, :cart, :error
5
+ attr_accessor :discount_error, :over_limit, :cart, :error, :member
6
6
 
7
- def initialize(cart)
7
+ def initialize(cart, member)
8
8
  self.cart = cart
9
+ self.member = member
9
10
  end
10
11
 
11
12
  def handle_tickets(params)
@@ -15,7 +16,7 @@ class OrderHandler
15
16
 
16
17
  ticket_type = TicketType.find(params[:ticket_type_id])
17
18
  Rails.logger.debug("QUANTITY #{params[:quantity].to_i}")
18
- tickets = ticket_type.available_tickets(params[:quantity].to_i)
19
+ tickets = ticket_type.available_tickets(params[:quantity].to_i, member)
19
20
  ids = tickets.collect(&:id)
20
21
  Rails.logger.debug("TICKET IDS: #{ids}")
21
22
  Ticket.lock(tickets, ticket_type, self.cart)
@@ -1,6 +1,10 @@
1
1
  class ImportedOrder < ::Order
2
2
  include Unrefundable
3
3
 
4
+ def skip_confirmation_email?
5
+ true
6
+ end
7
+
4
8
  def location
5
9
  "Artful.ly"
6
10
  end
@@ -25,7 +25,7 @@ class Order < ActiveRecord::Base
25
25
  has_many :items, :dependent => :destroy
26
26
  has_many :actions, :foreign_key => "subject_id", :dependent => :destroy
27
27
 
28
- attr_accessor :skip_actions
28
+ attr_accessor :skip_actions, :skip_email
29
29
 
30
30
  set_watch_for :created_at, :local_to => :organization
31
31
  set_watch_for :created_at, :local_to => :self, :as => :admins
@@ -49,6 +49,8 @@ class Order < ActiveRecord::Base
49
49
  scope :csv_not_imported, where("import_id IS NULL")
50
50
  scope :artfully, where("transaction_id IS NOT NULL")
51
51
 
52
+ has_attached_file :pdf
53
+
52
54
  searchable do
53
55
  text :details, :id, :type, :location, :transaction_id, :payment_method, :special_instructions
54
56
 
@@ -115,6 +117,10 @@ class Order < ActiveRecord::Base
115
117
  def discount_codes
116
118
  tickets.collect(&:discount).uniq.compact
117
119
  end
120
+
121
+ def imported?
122
+ !self.import_id.nil?
123
+ end
118
124
 
119
125
  def total_discount
120
126
  tickets.collect(&:total_discount).sum
@@ -169,6 +175,15 @@ class Order < ActiveRecord::Base
169
175
  def destroyable?
170
176
  ( (type.eql? "ApplicationOrder") || (type.eql? "ImportedOrder") ) && !is_fafs? && !artfully? && has_single_donation?
171
177
  end
178
+
179
+ def skip_confirmation_email?
180
+ @skip_email || anonymous_purchase? || imported? || self.person.email.blank?
181
+ end
182
+
183
+ def process
184
+ @skip_actions ||= false
185
+ OrderProcessor.process(self, {:skip_actions => @skip_actions, :skip_email => self.skip_confirmation_email?})
186
+ end
172
187
 
173
188
  def assignable?
174
189
  anonymous_purchase? && parent.nil?
@@ -355,10 +370,6 @@ class Order < ActiveRecord::Base
355
370
  self.person.calculate_lifetime_donations
356
371
  end
357
372
 
358
- def process
359
- OrderProcessor.process(self, @skip_actions)
360
- end
361
-
362
373
  def purchase_action_class
363
374
  GetAction
364
375
  end
@@ -0,0 +1,75 @@
1
+ require 'json'
2
+ require 'digest/md5'
3
+
4
+ module Orders
5
+ class PdfGeneration
6
+ PDF_POST_URL = "http://quick-pdf.geminisbs.net/pdfs.json"
7
+ PDF_GET_URL = "http://quick-pdf.geminisbs.net/pdfs/:id.json?api_key=:key"
8
+ PDF_DOWNLOAD_URL = "http://quick-pdf.geminisbs.net/pdfs/:id/download?api_key=:key"
9
+
10
+ ERB_TEMPLATE = <<-ERB
11
+ <h1>Your Tickets</h1>
12
+ <% order.tickets.map(&:product).each do |ticket| %>
13
+ <img src="<%= ticket.qr_code_url %>" />
14
+ <% end %>
15
+ ERB
16
+
17
+ def initialize(order, pdf_api_key = PDF_API_KEY, http_party = HTTParty, sleeper = Kernel)
18
+ @order = order
19
+ @pdf_api_key = pdf_api_key
20
+ @http_party = http_party
21
+ @sleeper = sleeper
22
+ end
23
+
24
+ def generate
25
+ response = http_party.post(PDF_POST_URL, pdf_attributes)
26
+
27
+ json = JSON.parse(response.body)
28
+ while !json["complete"]
29
+ response = http_party.get(status_url(json))
30
+ json = JSON.parse(response.body)
31
+
32
+ sleeper.sleep 1 unless json["complete"]
33
+ end
34
+
35
+ download_url(json)
36
+ end
37
+
38
+ def content
39
+ ERB.new(ERB_TEMPLATE).result(binding)
40
+ end
41
+
42
+ private
43
+ attr_reader :order, :pdf_api_key, :http_party, :sleeper
44
+
45
+ def pdf_attributes
46
+ {
47
+ :body => {
48
+ :api_key => pdf_api_key,
49
+ :pdf => {
50
+ :html_doc => content,
51
+ :filename => filename,
52
+ :pdf_options => {
53
+ :margin_top => "0.5in",
54
+ :margin_bottom => "0.5in",
55
+ :margin_left => "0.5in",
56
+ :margin_right => "0.5in"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ end
62
+
63
+ def filename
64
+ "#{Digest::MD5.hexdigest order.id.to_s}.pdf"
65
+ end
66
+
67
+ def status_url(json)
68
+ PDF_GET_URL.gsub(":id", json["id"].to_s).gsub(":key", pdf_api_key)
69
+ end
70
+
71
+ def download_url(json)
72
+ PDF_DOWNLOAD_URL.gsub(":id", json["id"].to_s).gsub(":key", pdf_api_key)
73
+ end
74
+ end
75
+ end
@@ -83,6 +83,9 @@ class Organization < ActiveRecord::Base
83
83
  UserMembership.promote(user, self)
84
84
  end
85
85
 
86
+ #
87
+ # In service of the user_memberships.owner migration.
88
+ #
86
89
  def previous_owner
87
90
  users.order('user_memberships.id asc').first
88
91
  end
@@ -99,6 +102,10 @@ class Organization < ActiveRecord::Base
99
102
  name
100
103
  end
101
104
 
105
+ def membership_kit
106
+ self.kits.where(:type => "MembershipKit").first
107
+ end
108
+
102
109
  delegate :can?, :cannot?, :to => :ability
103
110
  def ability
104
111
  OrganizationAbility.new(self)
data/app/models/person.rb CHANGED
@@ -330,6 +330,21 @@ class Person < ActiveRecord::Base
330
330
  person
331
331
  end
332
332
 
333
+ def update_name(first_name, last_name)
334
+ return if (self.first_name == first_name) && (self.last_name == last_name)
335
+
336
+ ActiveRecord::Base.transaction do
337
+ str = "Name changed from checkout."
338
+ unless (self.first_name.blank? && self.last_name.blank?)
339
+ str += " Old name was #{self.first_name} #{self.last_name}."
340
+ end
341
+ new_note(str, Time.now, nil, self.organization.id)
342
+ self.first_name = first_name.try(:capitalize)
343
+ self.last_name = last_name.try(:capitalize)
344
+ self.save
345
+ end
346
+ end
347
+
333
348
  # Needs a serious refactor
334
349
  def update_address(new_address, time_zone, user = nil, updated_by = nil)
335
350
  unless new_address.nil?
data/app/models/refund.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  class Refund
2
- attr_accessor :order, :refund_order, :items, :message
2
+ attr_accessor :order, :refund_order, :items, :message, :send_email_confirmation
3
3
 
4
4
  BRAINTREE_UNSETTLED_MESSAGE = "Cannot refund a transaction unless it is settled. (91506)"
5
- FRIENDLY_UNSETTLED_MESSAGE = "The processor cannot refund that transaction yet. Please try again in a few hours."
5
+ FRIENDLY_UNSETTLED_MESSAGE = "Unfortunately we cannot refund credit card transactions until the day after they were processed. Please re-issue the refund tomorrow."
6
6
 
7
7
  def initialize(order, items)
8
8
  self.order = order
@@ -11,6 +11,8 @@ class Refund
11
11
 
12
12
  def submit(options = {})
13
13
  return_items_to_inventory = options[:and_return] || false
14
+ @send_email_confirmation = options[:send_email_confirmation] || false
15
+
14
16
 
15
17
  ActiveRecord::Base.transaction do
16
18
  items.each do |i|
@@ -75,6 +77,7 @@ class Refund
75
77
  @refund_order.parent = order
76
78
  @refund_order.for_organization order.organization
77
79
  @refund_order.items = items.collect(&:to_refund)
80
+ @refund_order.skip_email = !send_email_confirmation
78
81
  @refund_order.save!
79
82
  @refund_order
80
83
  end
@@ -53,8 +53,22 @@ class Section < ActiveRecord::Base
53
53
  end
54
54
  end
55
55
 
56
- def ticket_types_for(member)
57
- member.nil? ? ticket_types.storefront : ticket_types.members
56
+ def ticket_types_for(member = nil)
57
+ types = []
58
+
59
+ # Add storefront types regardless of anything
60
+ types << ticket_types.storefront
61
+ unless member.nil?
62
+
63
+ #Add "unrestricted member" ticket_types that aren't tired to a particular membership type
64
+ types << ticket_types.members.select{|ticket_type| !ticket_type.member_ticket?} unless member.nil?
65
+
66
+ #Now add member ticket_types that apply to this particular member
67
+ membership_type_ids = member.nil? ? [] : member.current_membership_types.collect(&:id)
68
+ types << ticket_types.select{|ticket_type| ticket_type.members && membership_type_ids.include?(ticket_type.membership_type_id) }
69
+ end
70
+
71
+ types.flatten.uniq
58
72
  end
59
73
 
60
74
  def dup!
data/app/models/show.rb CHANGED
@@ -146,7 +146,7 @@ class Show < ActiveRecord::Base
146
146
  "show_time" => show_time,
147
147
  "datetime" => datetime_local_to_event,
148
148
  "destroyable" => destroyable?,
149
- "chart" => chart_for("storefront", options[:organization_id]).as_json(options)
149
+ "chart" => chart_for("storefront", options[:member], options[:organization_id]).as_json(options)
150
150
  }
151
151
  end
152
152
 
@@ -156,7 +156,7 @@ class Show < ActiveRecord::Base
156
156
  def as_widget_json(options = {})
157
157
  as_json.merge(:event => event.as_json,
158
158
  :venue => event.venue.as_json,
159
- :chart => chart_for("storefront", options[:organization_id]).as_json(options))
159
+ :chart => chart_for("storefront", options[:member], options[:organization_id]).as_json(options))
160
160
  end
161
161
 
162
162
  def bulk_on_sale(ids)
@@ -224,12 +224,6 @@ class Show < ActiveRecord::Base
224
224
  tickets.select(&:compable?)
225
225
  end
226
226
 
227
- def as_widget_json(options = {})
228
- as_json.merge(:event => event.as_json,
229
- :venue => event.venue.as_json,
230
- :chart => chart_for("storefront", options[:organization_id]).as_json(options))
231
- end
232
-
233
227
  def sections_for(member)
234
228
  member.nil? ? self.chart.sections.storefront : self.chart.sections.members
235
229
  end
@@ -0,0 +1,10 @@
1
+ class ShowStatsView < ActiveRecord::Base
2
+ self.table_name = 'show_stats_view'
3
+ self.primary_key = 'id'
4
+
5
+ belongs_to :show
6
+
7
+ set_watch_for :datetime, :local_to => :self, :as => :event
8
+
9
+ default_scope order("datetime DESC")
10
+ end
data/app/models/ticket.rb CHANGED
@@ -6,8 +6,11 @@
6
6
  include Ticket::Pricing
7
7
  include Ticket::Transfers
8
8
  include Ticket::SaleTransitions
9
+ include Ext::Uuid
9
10
  extend ActionView::Helpers::TextHelper
10
-
11
+
12
+ class TicketAlreadyValidated < StandardError; end
13
+
11
14
  attr_accessible :section_id, :section, :venue, :cart_price
12
15
 
13
16
  belongs_to :buyer, :class_name => "Person"
@@ -16,7 +19,7 @@
16
19
  belongs_to :section
17
20
  belongs_to :cart
18
21
  belongs_to :discount
19
- belongs_to :action, :foreign_key => "validated_action_id",:class_name => "GoAction"
22
+ belongs_to :action, :foreign_key => "validated_action_id", :class_name => "GoAction"
20
23
 
21
24
  #
22
25
  # This refs the ticket_type that the ticket was sold under, NOT an array of ticket types available
@@ -25,6 +28,10 @@
25
28
 
26
29
  has_many :items, :as => :product
27
30
 
31
+ has_attached_file :qr_code, TICKET_QR_STORAGE
32
+
33
+ delegate :url, :to => :qr_code, :prefix => true
34
+
28
35
  delegate :event, :to => :show
29
36
  def self.sold_after(datetime)
30
37
  sold.where("sold_at > ?", datetime)
@@ -210,27 +217,43 @@
210
217
  })
211
218
  t.show = show
212
219
  t.organization = show.organization
220
+ t.set_uuid
213
221
  t.state = 'on_sale' if on_sale
214
222
  t
215
223
  end
216
224
 
217
- #
218
- # Can't name this validate for obvious reasons
219
- #
220
- def validate_ticket(user = nil)
221
- transaction do
222
- go_action = GoAction.for(self.show, self.buyer, Time.now) do |go_action|
223
- go_action.creator = user
224
- go_action.save
225
+ def generate_qr_code
226
+ file = Tempfile.new(['qr-code', '.png'])
227
+ QRCode.new(self, sold_item.try(:order)).render(file)
228
+ self.qr_code = file
229
+
230
+ # If we don't save the ticket here, paperclip will leak a file handle.
231
+ # Even though we close our Tempfile below, paperclip copies that into
232
+ # another Tempfile, and only closes the handle when the ticket is saved.
233
+ save
234
+
235
+ file.close
236
+ end
237
+
238
+ def validate_ticket!(user = nil)
239
+ if committed? && !validated?
240
+ Ticket.transaction do
241
+ self.action = GoAction.for(self.show, self.buyer, Time.now) do |go_action|
242
+ go_action.creator = user
243
+ end
244
+ self.validated = true
245
+ save
225
246
  end
226
- self.update_attributes({:validated_action_id => go_action.id, :validated => true}, :without_protection => true)
227
247
  end
228
248
  end
229
249
 
230
- def unvalidate_ticket
231
- transaction do
232
- self.action.destroy unless self.action.tickets.count > 1
233
- self.update_attributes({:validated_action_id => nil, :validated => false}, :without_protection => true)
250
+ def unvalidate_ticket!
251
+ if validated?
252
+ Ticket.transaction do
253
+ action.destroy unless self.action.tickets.count > 1
254
+ self.validated = false
255
+ save
256
+ end
234
257
  end
235
258
  end
236
259
  end