feat: add friendly_id for cities and weather arts

- Integrate 'friendly_id' gem for sluggable functionality in City model
- Create ActiveAdmin resources for managing cities and weather arts
- Implement controller logic for cities
- Add database migrations for cities and weather arts creation
- Seed database with sample data for testing
- Create initial test cases for controllers and models

This commit enhances the application's URL handling by allowing friendly URLs for cities and weather arts. The addition of ActiveAdmin resources facilitates easier management through a web interface.
This commit is contained in:
songtianlun 2025-01-19 12:21:00 +08:00
parent 8517905b68
commit e5743a5e3f
36 changed files with 678 additions and 4 deletions

View File

@ -69,6 +69,7 @@
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="formtastic (v5.0.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="formtastic_i18n (v0.7.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="friendly_id (v5.5.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="has_scope (v0.8.2, mise: 3.3.5) [gem]" level="application" />

View File

@ -43,6 +43,7 @@ gem "thruster", require: false
# gem "image_processing", "~> 1.2"
gem 'devise', '~> 4.9'
gem 'activeadmin', '~> 3.2'
gem 'friendly_id', '~> 5.5'
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem

View File

@ -135,6 +135,8 @@ GEM
formtastic (5.0.0)
actionpack (>= 6.0.0)
formtastic_i18n (0.7.0)
friendly_id (5.5.1)
activerecord (>= 4.0.0)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
@ -428,6 +430,7 @@ DEPENDENCIES
cssbundling-rails
debug
devise (~> 4.9)
friendly_id (~> 5.5)
jbuilder
jsbundling-rails
kamal

54
app/admin/cities.rb Normal file
View File

@ -0,0 +1,54 @@
ActiveAdmin.register City do
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :name, :country, :latitude, :longitude, :active, :priority, :timezone, :region, :last_weather_fetch, :last_image_generation, :slug
#
# or
#
# permit_params do
# permitted = [:name, :country, :latitude, :longitude, :active, :priority, :timezone, :region, :last_weather_fetch, :last_image_generation, :slug]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
index do
selectable_column
id_column
column :name
column :slug
column :latitude
column :longitude
column :active
column :created_at
actions
end
filter :name
filter :active
form do |f|
f.inputs do
f.input :active
f.input :name
f.input :country, as: :String
f.input :latitude
f.input :longitude
f.input :priority
f.input :timezone
f.input :region
f.input :last_weather_fetch
f.input :last_image_generation
end
f.actions
end
end

78
app/admin/weather_arts.rb Normal file
View File

@ -0,0 +1,78 @@
ActiveAdmin.register WeatherArt do
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :city_id, :weather_date, :description, :temperature, :feeling_temp, :humidity, :wind_scale, :wind_speed, :precipitation, :pressure, :visibility, :cloud, :prompt
#
# or
#
# permit_params do
# permitted = [:city_id, :weather_date, :description, :temperature, :feeling_temp, :humidity, :wind_scale, :wind_speed, :precipitation, :pressure, :visibility, :cloud, :prompt]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
permit_params :city_id, :weather_date, :description, :temperature,
:feeling_temp, :humidity, :wind_scale, :wind_speed,
:precipitation, :pressure, :visibility, :cloud,
:prompt, :image
remove_filter :image_attachment, :image_blob
index do
selectable_column
id_column
column :city
column :weather_date
column :description
column :temperature
column :image do |weather_art|
image_tag(weather_art.image, size: '100x100') if weather_art.image.attached?
end
actions
end
show do
attributes_table do
row :city
row :weather_date
row :description
row :temperature
row :feeling_temp
row :humidity
row :wind_scale
row :wind_speed
row :precipitation
row :pressure
row :visibility
row :cloud
row :prompt
row :image do |weather_art|
image_tag(weather_art.image) if weather_art.image.attached?
end
end
end
form do |f|
f.inputs do
f.input :city
f.input :weather_date
f.input :description
f.input :temperature
f.input :feeling_temp
f.input :humidity
f.input :wind_scale
f.input :wind_speed
f.input :precipitation
f.input :pressure
f.input :visibility
f.input :cloud
f.input :prompt
f.input :image, as: :file
end
f.actions
end
end

View File

@ -0,0 +1,8 @@
class CitiesController < ApplicationController
def index
@cities = City.friendly.find(params[:id])
end
def show
end
end

View File

@ -0,0 +1,4 @@
class HomeController < ApplicationController
def index
end
end

View File

@ -0,0 +1,4 @@
class WeatherArtsController < ApplicationController
def show
end
end

View File

@ -0,0 +1,2 @@
module CitiesHelper
end

View File

@ -0,0 +1,2 @@
module HomeHelper
end

View File

@ -0,0 +1,2 @@
module WeatherArtsHelper
end

View File

@ -3,4 +3,9 @@ class AdminUser < ApplicationRecord
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable,
:recoverable, :rememberable, :validatable
def self.ransackable_attributes(auth_object = nil)
# 列出你想要允许搜索的属性
%w[created_at email id updated_at]
end
end

23
app/models/city.rb Normal file
View File

@ -0,0 +1,23 @@
class City < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
has_many :weather_arts, dependent: :destroy
validates :name, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
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", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at"]
end
end

16
app/models/weather_art.rb Normal file
View File

@ -0,0 +1,16 @@
class WeatherArt < ApplicationRecord
belongs_to :city
has_one_attached :image
validates :weather_date, presence: true
validates :city_id, presence: true
def self.ransackable_associations(auth_object = nil)
["city", "image_attachment", "image_blob"]
end
def self.ransackable_attributes(auth_object = nil)
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
end
end

View File

@ -0,0 +1,2 @@
<h1>Cities#index</h1>
<p>Find me in app/views/cities/index.html.erb</p>

View File

@ -0,0 +1,2 @@
<h1>Cities#show</h1>
<p>Find me in app/views/cities/show.html.erb</p>

View File

@ -0,0 +1,2 @@
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

View File

@ -0,0 +1,2 @@
<h1>WeatherArts#show</h1>
<p>Find me in app/views/weather_arts/show.html.erb</p>

View File

@ -4,12 +4,12 @@ ActiveAdmin.setup do |config|
# Set the title that is displayed on the main layout
# for each of the active admin pages.
#
config.site_title = "Today Ai Weather"
config.site_title = "Today Ai Weather Admin"
# Set the link url for the title. For example, to take
# users to your main site. Defaults to no link.
#
# config.site_title_link = "/"
config.site_title_link = "/"
# Set an optional image to be displayed for the header
# instead of a string (overrides :site_title)

View File

@ -0,0 +1,107 @@
# FriendlyId Global Configuration
#
# Use this to set up shared configuration options for your entire application.
# Any of the configuration options shown here can also be applied to single
# models by passing arguments to the `friendly_id` class method or defining
# methods in your model.
#
# To learn more, check out the guide:
#
# http://norman.github.io/friendly_id/file.Guide.html
FriendlyId.defaults do |config|
# ## Reserved Words
#
# Some words could conflict with Rails's routes when used as slugs, or are
# undesirable to allow as slugs. Edit this list as needed for your app.
config.use :reserved
config.reserved_words = %w[new edit index session login logout users admin
stylesheets assets javascripts images]
# This adds an option to treat reserved words as conflicts rather than exceptions.
# When there is no good candidate, a UUID will be appended, matching the existing
# conflict behavior.
# config.treat_reserved_as_conflict = true
# ## Friendly Finders
#
# Uncomment this to use friendly finders in all models. By default, if
# you wish to find a record by its friendly id, you must do:
#
# MyModel.friendly.find('foo')
#
# If you uncomment this, you can do:
#
# MyModel.find('foo')
#
# This is significantly more convenient but may not be appropriate for
# all applications, so you must explicitly opt-in to this behavior. You can
# always also configure it on a per-model basis if you prefer.
#
# Something else to consider is that using the :finders addon boosts
# performance because it will avoid Rails-internal code that makes runtime
# calls to `Module.extend`.
#
# config.use :finders
#
# ## Slugs
#
# Most applications will use the :slugged module everywhere. If you wish
# to do so, uncomment the following line.
#
# config.use :slugged
#
# By default, FriendlyId's :slugged addon expects the slug column to be named
# 'slug', but you can change it if you wish.
#
# config.slug_column = 'slug'
#
# By default, slug has no size limit, but you can change it if you wish.
#
# config.slug_limit = 255
#
# When FriendlyId can not generate a unique ID from your base method, it appends
# a UUID, separated by a single dash. You can configure the character used as the
# separator. If you're upgrading from FriendlyId 4, you may wish to replace this
# with two dashes.
#
# config.sequence_separator = '-'
#
# Note that you must use the :slugged addon **prior** to the line which
# configures the sequence separator, or else FriendlyId will raise an undefined
# method error.
#
# ## Tips and Tricks
#
# ### Controlling when slugs are generated
#
# As of FriendlyId 5.0, new slugs are generated only when the slug field is
# nil, but if you're using a column as your base method can change this
# behavior by overriding the `should_generate_new_friendly_id?` method that
# FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave
# more like 4.0.
# Note: Use(include) Slugged module in the config if using the anonymous module.
# If you have `friendly_id :name, use: slugged` in the model, Slugged module
# is included after the anonymous module defined in the initializer, so it
# overrides the `should_generate_new_friendly_id?` method from the anonymous module.
#
# config.use :slugged
# config.use Module.new {
# def should_generate_new_friendly_id?
# slug.blank? || <your_column_name_here>_changed?
# end
# }
#
# FriendlyId uses Rails's `parameterize` method to generate slugs, but for
# languages that don't use the Roman alphabet, that's not usually sufficient.
# Here we use the Babosa library to transliterate Russian Cyrillic slugs to
# ASCII. If you use this, don't forget to add "babosa" to your Gemfile.
#
# config.use Module.new {
# def normalize_friendly_id(text)
# text.to_slug.normalize! :transliterations => [:russian, :latin]
# end
# }
end

View File

@ -1,4 +1,20 @@
Rails.application.routes.draw do
root "home#index"
resources :cities, only: [ :index, :show ] do
resources :weather_arts, only: [ :show ]
end
# namespace :admin do
# resources :cities
# resources :weather_arts
# root to: "cities#index"
# end
get "weather_arts/show"
get "cities/index"
get "cities/show"
get "home/index"
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

View File

@ -0,0 +1,18 @@
class CreateCities < ActiveRecord::Migration[8.0]
def change
create_table :cities do |t|
t.string :name
t.string :country
t.float :latitude
t.float :longitude
t.boolean :active
t.integer :priority
t.string :timezone
t.string :region
t.datetime :last_weather_fetch
t.datetime :last_image_generation
t.timestamps
end
end
end

View File

@ -0,0 +1,6 @@
class AddSlugToCities < ActiveRecord::Migration[8.0]
def change
add_column :cities, :slug, :string
add_index :cities, :slug, unique: true
end
end

View File

@ -0,0 +1,21 @@
MIGRATION_CLASS =
if ActiveRecord::VERSION::MAJOR >= 5
ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
else
ActiveRecord::Migration
end
class CreateFriendlyIdSlugs < MIGRATION_CLASS
def change
create_table :friendly_id_slugs do |t|
t.string :slug, null: false
t.integer :sluggable_id, null: false
t.string :sluggable_type, limit: 50
t.string :scope
t.datetime :created_at
end
add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id]
add_index :friendly_id_slugs, [:slug, :sluggable_type], length: {slug: 140, sluggable_type: 50}
add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: {slug: 70, sluggable_type: 50, scope: 70}, unique: true
end
end

View File

@ -0,0 +1,21 @@
class CreateWeatherArts < ActiveRecord::Migration[8.0]
def change
create_table :weather_arts do |t|
t.references :city, null: false, foreign_key: true
t.date :weather_date
t.string :description
t.decimal :temperature
t.decimal :feeling_temp
t.decimal :humidity
t.string :wind_scale
t.decimal :wind_speed
t.decimal :precipitation
t.decimal :pressure
t.decimal :visibility
t.decimal :cloud
t.text :prompt
t.timestamps
end
end
end

View File

@ -0,0 +1,57 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end

81
db/schema.rb generated
View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_01_18_163460) do
ActiveRecord::Schema[8.0].define(version: 2025_01_19_032348) do
create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace"
t.text "body"
@ -25,6 +25,34 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_18_163460) do
t.index ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource"
end
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "admin_users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@ -36,4 +64,55 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_18_163460) do
t.index ["email"], name: "index_admin_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
end
create_table "cities", force: :cascade do |t|
t.string "name"
t.string "country"
t.float "latitude"
t.float "longitude"
t.boolean "active"
t.integer "priority"
t.string "timezone"
t.string "region"
t.datetime "last_weather_fetch"
t.datetime "last_image_generation"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "slug"
t.index ["slug"], name: "index_cities_on_slug", unique: true
end
create_table "friendly_id_slugs", force: :cascade do |t|
t.string "slug", null: false
t.integer "sluggable_id", null: false
t.string "sluggable_type", limit: 50
t.string "scope"
t.datetime "created_at"
t.index ["slug", "sluggable_type", "scope"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type_and_scope", unique: true
t.index ["slug", "sluggable_type"], name: "index_friendly_id_slugs_on_slug_and_sluggable_type"
t.index ["sluggable_type", "sluggable_id"], name: "index_friendly_id_slugs_on_sluggable_type_and_sluggable_id"
end
create_table "weather_arts", force: :cascade do |t|
t.integer "city_id", null: false
t.date "weather_date"
t.string "description"
t.decimal "temperature"
t.decimal "feeling_temp"
t.decimal "humidity"
t.string "wind_scale"
t.decimal "wind_speed"
t.decimal "precipitation"
t.decimal "pressure"
t.decimal "visibility"
t.decimal "cloud"
t.text "prompt"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["city_id"], name: "index_weather_arts_on_city_id"
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 "weather_arts", "cities"
end

View File

@ -7,4 +7,39 @@
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
# MovieGenre.find_or_create_by!(name: genre_name)
# end
AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development?
AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development?
guangzhou = City.create!(
name: 'Guangzhou',
country: 'China',
latitude: 23.1291,
longitude: 113.2644,
active: true,
priority: 50,
timezone: 'Asia/Shanghai',
region: 'Asia',
last_weather_fetch: 10.hours.ago,
last_image_generation: 10.hours.ago
)
guangzhou_weather_art = WeatherArt.create!(
city: guangzhou,
weather_date: Date.today,
description: 'Sunny with some clouds',
temperature: 28.5,
feeling_temp: 30.2,
humidity: 65,
wind_scale: "3级",
wind_speed: 15,
precipitation: 0,
pressure: 1013,
visibility: 10,
cloud: 30,
prompt: "A sunny day in Guangzhou with modern buildings under blue sky and white clouds, digital art style"
)
guangzhou_weather_art.image.attach(
io: File.open("db/seeds/images/sample-guangzhou-weather-art.png"),
filename: "sample-guangzhou-weather-art.png",
content_type: "image/png"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,13 @@
require "test_helper"
class CitiesControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
get cities_index_url
assert_response :success
end
test "should get show" do
get cities_show_url
assert_response :success
end
end

View File

@ -0,0 +1,8 @@
require "test_helper"
class HomeControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
get home_index_url
assert_response :success
end
end

View File

@ -0,0 +1,8 @@
require "test_helper"
class WeatherArtsControllerTest < ActionDispatch::IntegrationTest
test "should get show" do
get weather_arts_show_url
assert_response :success
end
end

29
test/fixtures/cities.yml vendored Normal file
View File

@ -0,0 +1,29 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
#one:
# name: MyString
# country: MyString
# slug: MyString
# latitude: 1.5
# longitude: 1.5
# active: false
# priority: 1
# timezone: MyString
# population: 1
# region: MyString
# last_weather_fetch: 2025-01-19 10:28:26
# last_image_generation: 2025-01-19 10:28:26
#
#two:
# name: MyString
# country: MyString
# slug: MyString
# latitude: 1.5
# longitude: 1.5
# active: false
# priority: 1
# timezone: MyString
# population: 1
# region: MyString
# last_weather_fetch: 2025-01-19 10:28:26
# last_image_generation: 2025-01-19 10:28:26

31
test/fixtures/weather_arts.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
city: one
weather_date: 2025-01-19
description: MyString
temperature: 9.99
feeling_temp: 9.99
humidity: 9.99
wind_scale: MyString
wind_speed: 9.99
precipitation: 9.99
pressure: 9.99
visibility: 9.99
cloud: 9.99
prompt: MyText
two:
city: two
weather_date: 2025-01-19
description: MyString
temperature: 9.99
feeling_temp: 9.99
humidity: 9.99
wind_scale: MyString
wind_speed: 9.99
precipitation: 9.99
pressure: 9.99
visibility: 9.99
cloud: 9.99
prompt: MyText

7
test/models/city_test.rb Normal file
View File

@ -0,0 +1,7 @@
require "test_helper"
class CityTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View File

@ -0,0 +1,7 @@
require "test_helper"
class WeatherArtTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end