today_ai_weather/app/models/city.rb
songtianlun 799f3222a9 feat: add city search functionality
- Implement search_by_name scope in City model
- Add SearchController for handling search input
- Include _search_city partial in cities index view
- Update cities_controller to filter cities based on search query

This commit introduces a new feature that allows users to search for
cities by name using an input field. The search is implemented as a
scope in the City model, and it is integrated into the existing
CitiesController. A dedicated SearchController manages the input
submission with a debouncing mechanism for better performance. The
search field is rendered in the cities index view, enhancing user
interactivity and experience.
2025-02-12 14:00:03 +08:00

190 lines
6.4 KiB
Ruby

class City < ApplicationRecord
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
belongs_to :country, optional: true
belongs_to :state, optional: true
has_many :weather_arts, dependent: :destroy
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
delegate :region, to: :country
validates :name, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
delegate :region, to: :country
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
scope :by_country, ->(country_id) { where(country_id: country_id) }
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
# 在 City 模型中
scope :by_popularity, ->(period = :year, limit = 100) {
# 根据时间周期确定时间范围
time_range =
case period.to_sym
when :today
Time.current.beginning_of_day..Time.current.end_of_day
when :week
Time.current.beginning_of_week..Time.current.end_of_week
when :month
Time.current.beginning_of_month..Time.current.end_of_month
when :year
Time.current.beginning_of_year..Time.current.end_of_year
else
Time.current.beginning_of_year..Time.current.end_of_year
end
# 根据数据库类型构建不同的查询
base_query = if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
joins(<<-SQL.squish)
LEFT JOIN ahoy_events ON
json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'
AND ahoy_events.time BETWEEN '#{time_range.begin}' AND '#{time_range.end}'
SQL
else
joins(<<-SQL.squish)
LEFT JOIN ahoy_events ON
(ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'
AND ahoy_events.time BETWEEN '#{time_range.begin}' AND '#{time_range.end}'
SQL
end
base_query
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
.limit(limit)
}
scope :least_popular_active, ->(limit = 100) {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
active
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC").limit(limit)
else
active
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC").limit(limit)
end
}
scope :most_popular_inactive, ->(limit = 100) {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
where(active: false)
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC").limit(limit)
else
where(active: false)
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC").limit(limit)
end
}
scope :search_by_name, ->(query) {
return all if query.blank?
decoded_query = URI.decode_www_form_component(query).downcase
where(
"LOWER(cities.name) LIKE :query", query: "%#{decoded_query}%"
)
}
def to_s
name
end
def slug_candidates
[
:name,
[ :country, :name ]
]
end
def localized_name
I18n.t("cities.#{name.parameterize.underscore}", default: name)
end
def full_name
"#{name}, #{country}"
end
def should_generate_new_friendly_id?
name_changed? || super
end
def self.ransackable_associations(auth_object = nil)
[ "weather_arts" ]
end
def self.ransackable_attributes(auth_object = nil)
[ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
end
def last_weather_fetch
# latest_weather_art&.created_at
Rails.cache.fetch("city/#{id}/last_weather_fetch", expires_in: 1.hour) do
latest_weather_art&.created_at
end
end
def last_image_generation
# latest_weather_art&.image&.created_at
Rails.cache.fetch("city/#{id}/last_image_generation", expires_in: 1.hour) do
latest_weather_art&.image&.created_at
end
end
def latest_weather_art
weather_arts.order(weather_date: :desc).first
end
def view_count(period = :day)
start_time = case period
when :day
1.day.ago
when :week
7.days.ago
when :month
1.month.ago
when :year
1.year.ago
else
1.year.ago
end
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
# Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
Ahoy::Event
.where("time >= ?", start_time)
.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?",
self.id)
.count
else
# Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
Ahoy::Event
.where("time >= ?", start_time)
.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?",
self.id)
.count
end
end
end