On the event page, promoter can choose to mark the event as "sold out" using the status field or as "published". Only published event can be marked as sold out if promoter thinks he cannot handle all the people available.
272 lines
9.0 KiB
Ruby
Executable File
272 lines
9.0 KiB
Ruby
Executable File
# Event model representing nightlife events and events
|
|
# Manages event details, location data, and publication state
|
|
require "net/http"
|
|
require "json"
|
|
|
|
class Event < ApplicationRecord
|
|
# Define states for Event lifecycle management
|
|
# draft: Initial state when Event is being created
|
|
# published: Event is visible to public and can be discovered
|
|
# canceled: Event has been canceled by organizer
|
|
# sold_out: Event has reached capacity and tickets are no longer available
|
|
enum :state, {
|
|
draft: 0,
|
|
published: 1,
|
|
canceled: 2,
|
|
sold_out: 3
|
|
}, default: :draft
|
|
|
|
# === Relations ===
|
|
belongs_to :user
|
|
has_many :ticket_types
|
|
has_many :tickets, through: :ticket_types
|
|
has_many :orders
|
|
|
|
# === Callbacks ===
|
|
before_validation :geocode_address, if: :should_geocode_address?
|
|
|
|
# Validations for Event attributes
|
|
# Basic information
|
|
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
|
validates :slug, presence: true, length: { minimum: 3, maximum: 100 }
|
|
validates :description, presence: true, length: { minimum: 10, maximum: 2000 }
|
|
validates :state, presence: true, inclusion: { in: states.keys }
|
|
validates :image, length: { maximum: 500 } # URL or path to image
|
|
|
|
# Venue information
|
|
validates :venue_name, presence: true, length: { maximum: 100 }
|
|
validates :venue_address, presence: true, length: { maximum: 200 }
|
|
|
|
# Geographic coordinates for map display
|
|
validates :latitude, presence: true, numericality: {
|
|
greater_than_or_equal_to: -90,
|
|
less_than_or_equal_to: 90
|
|
}
|
|
validates :longitude, presence: true, numericality: {
|
|
greater_than_or_equal_to: -180,
|
|
less_than_or_equal_to: 180
|
|
}
|
|
|
|
# Scopes for querying events with common filters
|
|
scope :featured, -> { where(featured: true) } # Get featured events for homepage
|
|
scope :published, -> { where(state: :published) } # Get publicly visible events
|
|
scope :search_by_name, ->(query) { where("name ILIKE ?", "%#{query}%") } # Search by name (case-insensitive)
|
|
|
|
# Scope for published events ordered by start time
|
|
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
|
|
|
# === Instance Methods ===
|
|
|
|
# Check if coordinates were successfully geocoded or are fallback coordinates
|
|
def geocoding_successful?
|
|
coordinates_look_valid?
|
|
end
|
|
|
|
# Get a user-friendly status message about geocoding
|
|
def geocoding_status_message
|
|
return nil if geocoding_successful?
|
|
|
|
"Les coordonnées exactes n'ont pas pu être déterminées automatiquement. Une localisation approximative a été utilisée."
|
|
end
|
|
|
|
# Check if ticket booking is currently allowed for this event
|
|
def booking_allowed?
|
|
return false unless published?
|
|
return false if sold_out?
|
|
return false if canceled?
|
|
|
|
# Check if event has started and if booking during event is disabled
|
|
if event_started? && !allow_booking_during_event?
|
|
return false
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
# Check if the event has already started
|
|
def event_started?
|
|
return false if start_time.blank?
|
|
Time.current >= start_time
|
|
end
|
|
|
|
# Check if the event has ended
|
|
def event_ended?
|
|
return false if end_time.blank?
|
|
Time.current >= end_time
|
|
end
|
|
|
|
# Check if booking is allowed during the event
|
|
# This is a simple attribute reader that defaults to false if nil
|
|
def allow_booking_during_event?
|
|
!!allow_booking_during_event
|
|
end
|
|
|
|
# Duplicate an event with all its ticket types
|
|
def duplicate(clone_ticket_types: true)
|
|
# Duplicate the event
|
|
new_event = self.dup
|
|
new_event.name = "Copie de #{name}"
|
|
new_event.slug = "#{slug}-copy-#{Time.current.to_i}"
|
|
new_event.state = :draft
|
|
new_event.created_at = Time.current
|
|
new_event.updated_at = Time.current
|
|
|
|
Event.transaction do
|
|
if new_event.save
|
|
# Duplicate all ticket types if requested
|
|
if clone_ticket_types
|
|
ticket_types.each do |ticket_type|
|
|
new_ticket_type = ticket_type.dup
|
|
new_ticket_type.event = new_event
|
|
new_ticket_type.save!
|
|
end
|
|
end
|
|
new_event
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
rescue
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
# Determine if we should perform server-side geocoding
|
|
def should_geocode_address?
|
|
# Don't geocode if address is blank
|
|
return false if venue_address.blank?
|
|
|
|
# Don't geocode if we already have valid coordinates (likely from frontend)
|
|
return false if coordinates_look_valid?
|
|
|
|
# Only geocode if address changed and we don't have coordinates
|
|
venue_address_changed?
|
|
end
|
|
|
|
# Check if the current coordinates look like they were set by frontend geocoding
|
|
def coordinates_look_valid?
|
|
return false if latitude.blank? || longitude.blank?
|
|
|
|
lat_f = latitude.to_f
|
|
lng_f = longitude.to_f
|
|
|
|
# Basic sanity checks for coordinate ranges
|
|
return false if lat_f < -90 || lat_f > 90
|
|
return false if lng_f < -180 || lng_f > 180
|
|
|
|
# Check if coordinates are not the default fallback coordinates
|
|
fallback_lat = 46.603354
|
|
fallback_lng = 1.888334
|
|
|
|
# Check if coordinates are not exactly 0,0 (common invalid default)
|
|
return false if lat_f == 0.0 && lng_f == 0.0
|
|
|
|
# Coordinates are valid if they're not exactly the fallback coordinates
|
|
!(lat_f == fallback_lat && lng_f == fallback_lng)
|
|
end
|
|
|
|
# Automatically geocode address to get latitude and longitude
|
|
# This only runs when no valid coordinates are provided (fallback for non-JS users)
|
|
def geocode_address
|
|
Rails.logger.info "Running server-side geocoding for '#{venue_address}' (no frontend coordinates provided)"
|
|
|
|
# Store original coordinates in case we need to fall back
|
|
original_lat = latitude
|
|
original_lng = longitude
|
|
|
|
begin
|
|
# Use OpenStreetMap Nominatim API for geocoding
|
|
encoded_address = URI.encode_www_form_component(venue_address.strip)
|
|
uri = URI("https://nominatim.openstreetmap.org/search?q=#{encoded_address}&format=json&limit=1&addressdetails=1")
|
|
|
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
http.use_ssl = true
|
|
|
|
request = Net::HTTP::Get.new(uri)
|
|
request["User-Agent"] = "AperoNight Event Platform/1.0 (https://aperonight.com)"
|
|
request["Accept"] = "application/json"
|
|
|
|
response = http.request(request)
|
|
|
|
if response.code == "200"
|
|
data = JSON.parse(response.body)
|
|
|
|
if data.any?
|
|
result = data.first
|
|
self.latitude = result["lat"].to_f.round(6)
|
|
self.longitude = result["lon"].to_f.round(6)
|
|
Rails.logger.info "Server-side geocoded '#{venue_address}' to coordinates: #{latitude}, #{longitude}"
|
|
return
|
|
end
|
|
end
|
|
|
|
# If we reach here, geocoding failed
|
|
handle_geocoding_failure(original_lat, original_lng)
|
|
|
|
rescue => e
|
|
Rails.logger.error "Server-side geocoding failed for '#{venue_address}': #{e.message}"
|
|
handle_geocoding_failure(original_lat, original_lng)
|
|
end
|
|
end
|
|
|
|
# Handle geocoding failure with fallback strategies
|
|
def handle_geocoding_failure(original_lat, original_lng)
|
|
# Strategy 1: Keep existing coordinates if this is an update
|
|
if original_lat.present? && original_lng.present?
|
|
self.latitude = original_lat
|
|
self.longitude = original_lng
|
|
Rails.logger.warn "Geocoding failed for '#{venue_address}', keeping existing coordinates: #{latitude}, #{longitude}"
|
|
return
|
|
end
|
|
|
|
# Strategy 2: Try to extract country/city and use approximate coordinates
|
|
fallback_coordinates = get_fallback_coordinates_from_address
|
|
if fallback_coordinates
|
|
self.latitude = fallback_coordinates[:lat]
|
|
self.longitude = fallback_coordinates[:lng]
|
|
Rails.logger.warn "Using fallback coordinates for '#{venue_address}': #{latitude}, #{longitude}"
|
|
return
|
|
end
|
|
|
|
# Strategy 3: Use default coordinates (center of France) as last resort
|
|
# This ensures the event can still be created
|
|
self.latitude = 46.603354 # Center of France
|
|
self.longitude = 1.888334
|
|
Rails.logger.warn "Using default coordinates for '#{venue_address}' due to geocoding failure: #{latitude}, #{longitude}"
|
|
end
|
|
|
|
# Extract country/city from address and return approximate coordinates
|
|
def get_fallback_coordinates_from_address
|
|
address_lower = venue_address.downcase
|
|
|
|
# Common French cities with approximate coordinates
|
|
french_cities = {
|
|
"paris" => { lat: 48.8566, lng: 2.3522 },
|
|
"lyon" => { lat: 45.7640, lng: 4.8357 },
|
|
"marseille" => { lat: 43.2965, lng: 5.3698 },
|
|
"toulouse" => { lat: 43.6047, lng: 1.4442 },
|
|
"nice" => { lat: 43.7102, lng: 7.2620 },
|
|
"nantes" => { lat: 47.2184, lng: -1.5536 },
|
|
"montpellier" => { lat: 43.6110, lng: 3.8767 },
|
|
"strasbourg" => { lat: 48.5734, lng: 7.7521 },
|
|
"bordeaux" => { lat: 44.8378, lng: -0.5792 },
|
|
"lille" => { lat: 50.6292, lng: 3.0573 }
|
|
}
|
|
|
|
# Check if any known city is mentioned in the address
|
|
french_cities.each do |city, coords|
|
|
if address_lower.include?(city)
|
|
return coords
|
|
end
|
|
end
|
|
|
|
# Check for common country indicators
|
|
if address_lower.include?("france") || address_lower.include?("french")
|
|
return { lat: 46.603354, lng: 1.888334 } # Center of France
|
|
end
|
|
|
|
nil
|
|
end
|
|
end
|