271 lines
9.0 KiB
Ruby
Executable File
271 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
|
|
# 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
|
|
ticket_types.each do |ticket_type|
|
|
new_ticket_type = ticket_type.dup
|
|
new_ticket_type.event = new_event
|
|
new_ticket_type.save!
|
|
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
|