This implementation provides automatic geocoding and map integration: - **Event Model Enhancements:** - Automatic geocoding callback using OpenStreetMap Nominatim API - 3-tier fallback system: exact coordinates → city-based → country default - Fallback coordinates for major French cities (Paris, Lyon, Marseille, etc.) - Robust error handling that prevents event creation failures - **User-Friendly Event Forms:** - Address-first approach - users just enter addresses - Hidden coordinate fields (auto-generated behind scenes) - Real-time geocoding with 1.5s debounce - "Ma position" button for current location with reverse geocoding - "Prévisualiser" button to show map links - Smart feedback system (loading, success, warnings, errors) - **Enhanced Event Show Page:** - Map provider links (OpenStreetMap, Google Maps, Apple Plans) - Warning badges when approximate coordinates are used - Address-based URLs for better map integration - **Comprehensive JavaScript Controller:** - Debounced auto-geocoding to minimize API calls - Multiple geocoding strategies (manual vs automatic) - Promise-based geolocation with proper error handling - Dynamic map link generation with address + coordinates - **Failure Handling:** - Events never fail to save due to missing coordinates - Fallback to city-based coordinates when exact geocoding fails - User-friendly warnings when approximate locations are used - Maintains existing coordinates on update failures 🤖 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
176 lines
6.3 KiB
Ruby
Executable File
176 lines
6.3 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: :venue_address_changed?
|
|
|
|
# === Instance Methods ===
|
|
|
|
# Check if coordinates were successfully geocoded or are fallback coordinates
|
|
def geocoding_successful?
|
|
return false if latitude.blank? || longitude.blank?
|
|
|
|
# Check if coordinates are exactly the fallback coordinates
|
|
fallback_lat = 46.603354
|
|
fallback_lng = 1.888334
|
|
|
|
!(latitude == fallback_lat && longitude == fallback_lng)
|
|
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
|
|
|
|
# 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: 1000 }
|
|
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) }
|
|
|
|
private
|
|
|
|
# Automatically geocode address to get latitude and longitude
|
|
def geocode_address
|
|
return if venue_address.blank?
|
|
|
|
# If we already have coordinates and this is an update, try to geocode
|
|
# If it fails, keep the existing coordinates
|
|
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")
|
|
|
|
response = Net::HTTP.get_response(uri)
|
|
|
|
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 "Geocoded address '#{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 "Geocoding failed for address '#{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
|