Compare commits
3 Commits
d914ae5c4a
...
feat/image
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d7349a9b | ||
|
|
4ca8d73c8e | ||
|
|
78b675b41d |
@@ -31,6 +31,7 @@ class Event < ApplicationRecord
|
|||||||
# === Callbacks ===
|
# === Callbacks ===
|
||||||
before_validation :generate_slug, if: :should_generate_slug?
|
before_validation :generate_slug, if: :should_generate_slug?
|
||||||
before_validation :geocode_address, if: :should_geocode_address?
|
before_validation :geocode_address, if: :should_geocode_address?
|
||||||
|
before_validation :handle_image_url, if: :should_handle_image_url?
|
||||||
before_update :handle_image_replacement, if: :image_attached?
|
before_update :handle_image_replacement, if: :image_attached?
|
||||||
|
|
||||||
# Validations for Event attributes
|
# Validations for Event attributes
|
||||||
@@ -151,6 +152,9 @@ class Event < ApplicationRecord
|
|||||||
# For old image field, return the URL directly
|
# For old image field, return the URL directly
|
||||||
return self[:image] if self[:image].present?
|
return self[:image] if self[:image].present?
|
||||||
|
|
||||||
|
# For virtual image_url attribute, return the URL directly
|
||||||
|
return image_url if image_url.present?
|
||||||
|
|
||||||
# For attached images, process variants
|
# For attached images, process variants
|
||||||
return nil unless image.attached?
|
return nil unless image.attached?
|
||||||
|
|
||||||
@@ -167,25 +171,22 @@ class Event < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check if event has any image (uploaded or URL)
|
# Check if event has any image (old field, attached, or URL)
|
||||||
def has_image?
|
def has_image?
|
||||||
image.attached? || image_url.present?
|
self[:image].present? || image.attached? || image_url.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get display image source (uploaded or URL)
|
# Get display image source (uploaded or URL)
|
||||||
def display_image
|
def display_image
|
||||||
if image.attached?
|
if image.attached?
|
||||||
image
|
image
|
||||||
else
|
elsif image_url.present?
|
||||||
image_url
|
image_url
|
||||||
|
else
|
||||||
|
self[:image]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check if event has any image (old field or attached)
|
|
||||||
def has_image?
|
|
||||||
self[:image].present? || image.attached?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check if coordinates were successfully geocoded or are fallback coordinates
|
# Check if coordinates were successfully geocoded or are fallback coordinates
|
||||||
def geocoding_successful?
|
def geocoding_successful?
|
||||||
coordinates_look_valid?
|
coordinates_look_valid?
|
||||||
@@ -278,9 +279,10 @@ class Event < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Validate image URL format
|
# Validate image URL format - relaxed for development
|
||||||
def image_url_format
|
def image_url_format
|
||||||
return unless image_url.present?
|
return unless image_url.present?
|
||||||
|
return if Rails.env.development? # Skip validation in development
|
||||||
|
|
||||||
unless image_url.match?(/\Ahttps?:\/\/.+\.(jpg|jpeg|png|gif|webp)(\?.*)?\z/i)
|
unless image_url.match?(/\Ahttps?:\/\/.+\.(jpg|jpeg|png|gif|webp)(\?.*)?\z/i)
|
||||||
errors.add(:image_url, "doit être une URL valide vers une image (JPG, PNG, GIF, WebP)")
|
errors.add(:image_url, "doit être une URL valide vers une image (JPG, PNG, GIF, WebP)")
|
||||||
@@ -302,6 +304,19 @@ class Event < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Determine if we should handle image_url
|
||||||
|
def should_handle_image_url?
|
||||||
|
image_url.present? && new_record?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle image_url by storing it in the legacy image field
|
||||||
|
def handle_image_url
|
||||||
|
# Store the image_url in the legacy image field for backward compatibility
|
||||||
|
if image_url.present?
|
||||||
|
self[:image] = image_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Determine if we should perform server-side geocoding
|
# Determine if we should perform server-side geocoding
|
||||||
def should_geocode_address?
|
def should_geocode_address?
|
||||||
# Don't geocode if address is blank
|
# Don't geocode if address is blank
|
||||||
|
|||||||
@@ -12,11 +12,7 @@
|
|||||||
<!-- Event Header with Image -->
|
<!-- Event Header with Image -->
|
||||||
<% if @event.has_image? %>
|
<% if @event.has_image? %>
|
||||||
<div class="relative h-96">
|
<div class="relative h-96">
|
||||||
<% if @event.image.attached? %>
|
<%= image_tag @event.event_image_variant(:large), class: "w-full h-full object-cover", alt: @event.name %>
|
||||||
<%= image_tag @event.event_image_variant(:large), class: "w-full h-full object-cover" %>
|
|
||||||
<% else %>
|
|
||||||
<%= image_tag @event.image_url, class: "w-full h-full object-cover", alt: @event.name %>
|
|
||||||
<% end %>
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent"></div>
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
|
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="max-w-4xl mx-auto">
|
||||||
|
|||||||
2
db/schema.rb
generated
2
db/schema.rb
generated
@@ -56,7 +56,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_29_222616) do
|
|||||||
t.boolean "allow_booking_during_event", default: false, null: false
|
t.boolean "allow_booking_during_event", default: false, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "image_url"
|
|
||||||
t.index ["featured"], name: "index_events_on_featured"
|
t.index ["featured"], name: "index_events_on_featured"
|
||||||
t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude"
|
t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude"
|
||||||
t.index ["state"], name: "index_events_on_state"
|
t.index ["state"], name: "index_events_on_state"
|
||||||
@@ -159,7 +158,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_29_222616) do
|
|||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "order_promotion_codes", "orders"
|
add_foreign_key "order_promotion_codes", "orders"
|
||||||
add_foreign_key "order_promotion_codes", "promotion_codes"
|
add_foreign_key "order_promotion_codes", "promotion_codes"
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ belle_epoque_event.update!(start_time: 3.days.from_now, end_time: 3.days.from_no
|
|||||||
|
|
||||||
|
|
||||||
# Create ticket types for "La belle époque" event
|
# Create ticket types for "La belle époque" event
|
||||||
belle_epoque_event = Event.find_by!(slug: "la-belle-epoque-par-sisley-events")
|
belle_epoque_event = Event.find_by!(slug: "la-belle-epoque-par-sisley-events-le-patio-rooftop-montreuil")
|
||||||
|
|
||||||
TicketType.find_or_create_by!(event: belle_epoque_event, name: "Free invitation valid before 7 p.m.") do |tt|
|
TicketType.find_or_create_by!(event: belle_epoque_event, name: "Free invitation valid before 7 p.m.") do |tt|
|
||||||
tt.description = "Free invitation ticket valid before 7 p.m. for La Belle Époque"
|
tt.description = "Free invitation ticket valid before 7 p.m. for La Belle Époque"
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ class OrdersControllerPromotionTest < ActionDispatch::IntegrationTest
|
|||||||
price_cents: 2000
|
price_cents: 2000
|
||||||
)
|
)
|
||||||
|
|
||||||
# Debug the ticket creation
|
|
||||||
puts "Ticket saved: #{ticket.persisted?}"
|
|
||||||
puts "Ticket errors: #{ticket.errors.full_messages}" unless ticket.valid?
|
|
||||||
puts "Order tickets count: #{@order.tickets.count}"
|
|
||||||
|
|
||||||
# Recalculate the order total
|
# Recalculate the order total
|
||||||
@order.calculate_total!
|
@order.calculate_total!
|
||||||
|
|
||||||
|
|||||||
@@ -317,4 +317,157 @@ class EventTest < ActiveSupport::TestCase
|
|||||||
# Check that ticket types were NOT duplicated
|
# Check that ticket types were NOT duplicated
|
||||||
assert_equal 0, duplicated_event.ticket_types.count
|
assert_equal 0, duplicated_event.ticket_types.count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Test slug generation functionality
|
||||||
|
test "should generate slug from name and venue" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
name: "Soirée d'ouverture",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
venue_name: "Test Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0
|
||||||
|
)
|
||||||
|
event.save
|
||||||
|
assert_equal "soiree-d-ouverture-test-venue", event.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should generate slug from name, venue, and city" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
name: "Fête de la Musique",
|
||||||
|
venue_name: "Théâtre Principal",
|
||||||
|
venue_address: "15 Rue de la Paix, 75002 Paris",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0
|
||||||
|
)
|
||||||
|
event.save
|
||||||
|
assert_equal "fete-de-la-musique-theatre-principal-paris", event.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should generate fallback slug when no data available" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0
|
||||||
|
)
|
||||||
|
event.save
|
||||||
|
assert_match /^event-\d+$/, event.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should ensure slug uniqueness" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
|
||||||
|
# Create first event
|
||||||
|
event1 = Event.create!(
|
||||||
|
name: "Test Event",
|
||||||
|
venue_name: "Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create second event with same details
|
||||||
|
event2 = Event.create!(
|
||||||
|
name: "Test Event",
|
||||||
|
venue_name: "Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_not_equal event1.slug, event2.slug
|
||||||
|
assert_match /^test-event-venue-1$/, event2.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should extract city from French postal code" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
name: "Concert",
|
||||||
|
venue_address: "5 Avenue des Champs-Élysées, 75008 Paris",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0
|
||||||
|
)
|
||||||
|
event.save
|
||||||
|
assert event.slug.include?("paris")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test image URL functionality
|
||||||
|
test "should accept valid image URL" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
name: "Event with URL Image",
|
||||||
|
slug: "event-url-image",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
venue_name: "Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0,
|
||||||
|
image_url: "https://example.com/image.jpg"
|
||||||
|
)
|
||||||
|
assert event.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject invalid image URL" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
name: "Event with Invalid URL",
|
||||||
|
slug: "event-invalid-url",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
venue_name: "Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0,
|
||||||
|
image_url: "not-a-valid-url"
|
||||||
|
)
|
||||||
|
assert_not event.valid?
|
||||||
|
assert_includes event.errors[:image_url], "doit être une URL valide vers une image (JPG, PNG, GIF, WebP)"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject URL with non-image extension" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123", password_confirmation: "password123")
|
||||||
|
event = Event.new(
|
||||||
|
name: "Event with Non-image URL",
|
||||||
|
slug: "event-non-image-url",
|
||||||
|
description: "Valid description for the event that is long enough",
|
||||||
|
venue_name: "Venue",
|
||||||
|
venue_address: "123 Test Street",
|
||||||
|
user: user,
|
||||||
|
latitude: 48.0,
|
||||||
|
longitude: 2.0,
|
||||||
|
image_url: "https://example.com/document.pdf"
|
||||||
|
)
|
||||||
|
assert_not event.valid?
|
||||||
|
assert_includes event.errors[:image_url], "doit être une URL valide vers une image (JPG, PNG, GIF, WebP)"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has_image? should return true for URL image" do
|
||||||
|
event = Event.new(image_url: "https://example.com/image.jpg")
|
||||||
|
assert event.has_image?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has_image? should return false without image" do
|
||||||
|
event = Event.new
|
||||||
|
assert_not event.has_image?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "display_image should return image URL when no attached image" do
|
||||||
|
event = Event.new(image_url: "https://example.com/image.jpg")
|
||||||
|
assert_equal "https://example.com/image.jpg", event.display_image
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user