class Event < ActiveRecord::Base include Ext::Integrations::Event include Ext::Resellable::Event include Ext::Uuid include Ticket::Reporting include EventPresenter include OhNoes::Destroy include ActionView::Helpers::TextHelper require 'email_validator' CATEGORIES = ["Dance", "Film & Electronic Media", "Literary Arts", "Music", "Theater", "Visual Arts", "Other"] attr_accessible :name, :producer, :description, :contact_email, :contact_phone, :image, :venue_attributes, :show_special_instructions, :special_instructions_caption, :public, :primary_category, :secondary_categories, :primary_category_other, :secondary_category_other, :members_only, :subtitle, :assigned attr_accessor :reserved_seating store :cached_stats, :accessors => [ :on_sale, :off_sale, :sold, :sales_total ] belongs_to :organization belongs_to :venue belongs_to :import accepts_nested_attributes_for :venue has_many :charts has_many :shows, :order => :datetime has_many :tickets, :through => :shows has_many :discounts has_many :events_pass_types has_many :pass_types, :through => :events_pass_types validate :validate_contact_phone validates :contact_email, :presence => true, :email => true validates :name, :presence => true validates :organization_id, :presence => true validate :subtitle, length: { maximum: 255 } has_attached_file :image, :storage => :s3, :path => ":attachment/:id/:style.:extension", :bucket => Rails.configuration.s3.bucket, :s3_protocol => 'https', :s3_credentials => { :access_key_id => Rails.configuration.s3.access_key_id, :secret_access_key => Rails.configuration.s3.secret_access_key }, :styles => { :thumb => "140x140#" } validates_attachment_size :image, :less_than => 1.megabytes, :unless => Proc.new {|model| model.image } validates_attachment_content_type :image, :content_type => ["image/jpeg", "image/gif", "image/png"] before_create :set_primary_category before_create { self.public = true } after_create :create_default_chart after_create :create_venue_chart serialize :secondary_categories, Array default_scope where(:deleted_at => nil).order("events.created_at DESC") scope :storefront, where(:deleted_at => nil).where(:public => true) scope :published, includes(:shows).where(:shows => { :state => "published" }) scope :public, where(:public => true) scope :not_deleted, where('deleted_at IS NULL') delegate :time_zone, :to => :venue ANY_EVENT_TEXT = "(Any Event)" ANY_EVENT_ID = "-1" searchable do text :name end include Ext::DelayedIndexing # # Run synchronously only with great care. # def refresh_stats self.on_sale = self.glance.available.on_sale self.off_sale = self.glance.available.off_sale self.sold = self.glance.sold.total self.sales_total = self.glance.sales.total self.save(:validate => false) self.cached_stats end def free? is_free? end def applies_to_pass?(pass) EventsPassType.active .where(:organization_id => self.organization_id) .where(:event_id => self.id) .where(:pass_type_id => pass.pass_type_id).count > 0 end def to_s name end def artfully_ticketed true end def single_show? shows.length == 1 end # # The list of events to be displayed on their event index # def self.for_event_storefront(organization, member = nil) event_ids = organization.shows .published .joins(:event) .where('datetime > ?', DateTime.now) .merge(Event.storefront) .reorder('datetime asc') .pluck(:event_id) .uniq events = [] event_ids.each {|id| events << Event.includes(:venue).find(id)} events.reject!{|e| e.members_only} if member.nil? events end # # Find a single event for the single event storefront view # def self.storefront_find(id, member = nil) event_rel = Event.includes(:venue) event_rel = event_rel.where(:members_only => false) if member.nil? event_rel.find(id) end def destroyable? items.blank? end def imported? !self.import_id.nil? end def items Item.where(:show_id => self.shows) end def set_primary_category self.primary_category ||= "Other" end def create_default_chart unless default_chart chart = chart_class.default_chart_for(self) self.charts << chart chart.save end chart end # # In the case where producer is re-using chart from a venue # This will return false if a default_chart has been set # No changes will be made if a default chart has been set # def set_default_chart(chart) return false unless default_chart.nil? self.charts << chart chart.event = self chart.save end def chart_class self.assigned? ? AssignedChart : GeneralAdmissionChart end def default_chart return nil unless self.persisted? self.charts.includes(chart_class.included_models).first end def upcoming_shows_rel shows.includes(:event => :venue).where('shows.datetime > ?', (DateTime.now - 1.hours)) end # # You'll almost always want upcoming_public_shows instead # def upcoming_shows(limit = 5, reload = false) shows_rel = upcoming_shows_rel shows_rel = upcoming_shows_rel.limit(limit) unless limit == :all if reload @upcoming = shows_rel.all else @upcoming ||= shows_rel.all end @upcoming end def played_shows(limit = 5) played = shows.select { |show| show.datetime_local_to_event < (DateTime.now - 1.hours) } return played if limit == :all played.take(limit) end def next_public_show upcoming_public_shows.empty? ? nil : upcoming_public_shows.first end def next_show_include_unpublished upcoming_shows.empty? ? nil : upcoming_shows.first end def upcoming_public_shows upcoming_shows(:all, true).select(&:published?) end # # Used to build a flimsy show for use by the Rails form on shows/new # def build_next_show shows.build(:datetime => Show.next_datetime(shows.last)) show = shows.pop show.chart = default_chart.dup!({:save => false}) show end def as_widget_json(options = {}) as_json(options.merge({:methods => ['venue', 'uuid'], :except => [:members_only, :cached_stats, :assigned]})).merge('performances' => upcoming_public_shows.as_json) end EVENT_CALENDAR_COLORS = ["#3a87ad", "#16a085", "#27ae60", "#8e44ad", "#2c3e50", "#d35400", "#c0392b"] # # This drives the all events calendar for producers # def self.as_full_calendar_json(organization, starts_at=nil, ends_at=nil) shows_rel = organization.shows.includes(:event => :venue) shows_rel = shows_rel.where('datetime > ?', starts_at) unless starts_at.nil? shows_rel = shows_rel.where('datetime < ?', ends_at) unless ends_at.nil? shows=shows_rel.all # # Reject shows which have had their events deleted # shows.reject! {|s| s.event.nil?} event_ids = shows.collect(&:event_id).uniq event_ids.sort! colors = EVENT_CALENDAR_COLORS shows.collect do |show| { :title => show.event.name.truncate(30), :start => show.datetime_local_to_event, :allDay => false, :id => show.id, :state => show.state, :event_id => show.event_id, :backgroundColor => show.published? ? colors[(event_ids.index(show.event_id) % colors.length)] : "#adadad", :borderColor => colors[(event_ids.index(show.event_id) % colors.length)] } end end # # This drivers the per-event show calendar for producers # def as_full_calendar_json shows.includes(:event).collect do |p| { :title => '', :start => p.datetime_local_to_event, :allDay => false, :id => p.id, :state => p.state, :event_id => self.id } end end def as_storefront_calendar_json upcoming_public_shows.collect do |p| { :title => '', :start => p.datetime_local_to_event, :allDay => false, :uuid => p.uuid, :color => p.sold_out? ? "#ADADAD" : "#33AADD" } end end def as_json(options = {}) super(options) end def assign_chart(chart) if already_has_chart(chart) self.errors[:base] << "Chart \"#{chart.name}\" has already been added to this event" return self end if is_free? && chart.has_paid_sections? self.errors[:base] << "Cannot add chart with paid sections to a free event" return self end chart.assign_to(self) self end def <=>(obj) return -1 unless obj.kind_of? Event self.name.downcase <=> obj.name.downcase end private # # This is a pretty basic validation to prevent them from entering an email in the phone # number field (we saw this in usability testing) # We can't disallow numbers because some people use them in their phones (718-555-4TIX) # def validate_contact_phone contains_at_sign = /\@/ if (!contact_phone.nil?) && (contact_phone.match contains_at_sign) errors.add(:contact_phone, "doesn't look like a phone number. Your changes have not been saved.") end end def already_has_chart(chart) !self.charts.select{|c| c.name == chart.name }.empty? end def create_venue_chart self.venue.update_default_chart_from(self.default_chart) end end