From c30b3dbcf7d1ed35e1afee06f69a2949afd49eca Mon Sep 17 00:00:00 2001 From: lujanfernaud Date: Fri, 18 May 2018 08:01:09 +0100 Subject: [PATCH] Create pretty URLs using FriendlyId --- Gemfile | 1 + Gemfile.lock | 3 + app/models/event.rb | 32 ++++-- app/models/group.rb | 24 ++++ app/models/sample_event.rb | 1 + app/models/sample_group.rb | 2 + app/models/user.rb | 14 +++ config/initializers/friendly_id.rb | 107 ++++++++++++++++++ ...20180517163813_create_friendly_id_slugs.rb | 22 ++++ .../20180517170138_add_slug_to_groups.rb | 6 + .../20180517190202_add_slug_to_events.rb | 6 + .../20180517212505_add_slug_to_users.rb | 6 + db/schema.rb | 20 +++- test/controllers/events_controller_test.rb | 5 +- test/controllers/groups_controller_test.rb | 9 +- test/controllers/users_controller_test.rb | 5 +- test/integration/users/users_profile_test.rb | 8 +- 17 files changed, 257 insertions(+), 14 deletions(-) create mode 100644 config/initializers/friendly_id.rb create mode 100644 db/migrate/20180517163813_create_friendly_id_slugs.rb create mode 100644 db/migrate/20180517170138_add_slug_to_groups.rb create mode 100644 db/migrate/20180517190202_add_slug_to_events.rb create mode 100644 db/migrate/20180517212505_add_slug_to_users.rb diff --git a/Gemfile b/Gemfile index 7c3c9749..26c5844f 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,7 @@ gem 'figaro', '~> 1.1', '>= 1.1.1' gem 'gravatar_image_tag', '~> 1.2' gem 'inline_svg', '~> 1.3', '>= 1.3.1' gem 'faker', '~> 1.8', '>= 1.8.7' +gem 'friendly_id', '~> 5.2', '>= 5.2.4' # Used for bulk inserting data into database using ActiveRecord. gem 'activerecord-import', '~> 0.23.0' diff --git a/Gemfile.lock b/Gemfile.lock index c7a1fc67..e1acdbe0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,6 +140,8 @@ GEM figaro (1.1.1) thor (~> 0.14) formatador (0.2.5) + friendly_id (5.2.4) + activerecord (>= 4.0.0) geocoder (1.4.7) globalid (0.4.1) activesupport (>= 4.2.0) @@ -401,6 +403,7 @@ DEPENDENCIES devise (~> 4.4, >= 4.4.3) faker (~> 1.8, >= 1.8.7) figaro (~> 1.1, >= 1.1.1) + friendly_id (~> 5.2, >= 5.2.4) geocoder (~> 1.4, >= 1.4.5) gravatar_image_tag (~> 1.2) guard (= 2.14.0) diff --git a/app/models/event.rb b/app/models/event.rb index 67b833d2..4c385f14 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,4 +1,7 @@ class Event < ApplicationRecord + include FriendlyId + friendly_id :slug_candidates, use: :scoped, scope: :group + include Storext.model store_attributes :updated_fields do @@ -20,7 +23,7 @@ class Event < ApplicationRecord delegate :place_name, :street1, :street2, :city, :state, :post_code, :country, - :full_address, :full_address_changed?, to: :address + :full_address, :full_address_changed?, to: :address, allow_nil: true has_many :attendances, foreign_key: "attended_event_id" has_many :attendees, through: :attendances @@ -62,12 +65,19 @@ def short_description private - def no_past_date - if start_date < Time.zone.now - errors.add(:start_date, "can't be in the past") - elsif end_date < start_date - errors.add(:start_date, "can't be later than end date") - end + def should_generate_new_friendly_id? + title_changed? + end + + def slug_candidates + [ + :title, + [:title, :date] + ] + end + + def date + start_date.strftime("%b %d %Y") end def titleize_title @@ -111,4 +121,12 @@ def store_updated_address def touch_group group.touch end + + def no_past_date + if start_date < Time.zone.now + errors.add(:start_date, "can't be in the past") + elsif end_date < start_date + errors.add(:start_date, "can't be later than end date") + end + end end diff --git a/app/models/group.rb b/app/models/group.rb index 269c97cb..41dd26f7 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,6 +1,9 @@ class Group < ApplicationRecord include PgSearch + include FriendlyId + friendly_id :slug_candidates, use: :slugged + resourcify before_save :titleize_name @@ -65,6 +68,27 @@ def remove_from_organizers(member) private + def should_generate_new_friendly_id? + name_changed? + end + + def slug_candidates + [ + :name, + [:name, :location], + [:name, :location, :owner_name], + [:name, :location, :owner_name, :owner_id] + ] + end + + def owner_name + owner.name + end + + def owner_id + owner.id + end + def titleize_name self.name = name.titleize end diff --git a/app/models/sample_event.rb b/app/models/sample_event.rb index 2b273d28..4f9f3d6b 100644 --- a/app/models/sample_event.rb +++ b/app/models/sample_event.rb @@ -32,6 +32,7 @@ def build_event ) @event.build_address(event_address) + @event.send(:set_slug) # We don't validate because we are not setting the image, # so it's going to use the default one set by EventImageUploader. diff --git a/app/models/sample_group.rb b/app/models/sample_group.rb index 12329d77..8d173fda 100644 --- a/app/models/sample_group.rb +++ b/app/models/sample_group.rb @@ -30,6 +30,8 @@ def create_group sample_group: true ) + @group.send(:set_slug) + # We don't validate because we are not setting the image, # so it's going to use the default one set by GroupImageUploader. @group.save(validate: false) diff --git a/app/models/user.rb b/app/models/user.rb index 4d7cd5ed..b4ec290e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,6 +4,9 @@ class User < ApplicationRecord devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable + include FriendlyId + friendly_id :slug_candidates, use: :slugged + before_save :titleize_name before_update :titleize_location before_update :capitalize_bio @@ -96,6 +99,17 @@ def upcoming_attended_events private + def should_generate_new_friendly_id? + name_changed? + end + + def slug_candidates + [ + :name, + [:name, :id] + ] + end + def titleize_name self.name = name.titleize end diff --git a/config/initializers/friendly_id.rb b/config/initializers/friendly_id.rb new file mode 100644 index 00000000..1ccbb652 --- /dev/null +++ b/config/initializers/friendly_id.rb @@ -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 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 explicity 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? || _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 diff --git a/db/migrate/20180517163813_create_friendly_id_slugs.rb b/db/migrate/20180517163813_create_friendly_id_slugs.rb new file mode 100644 index 00000000..691514af --- /dev/null +++ b/db/migrate/20180517163813_create_friendly_id_slugs.rb @@ -0,0 +1,22 @@ +migration_class = + if ActiveRecord::VERSION::MAJOR >= 5 + ActiveRecord::Migration[4.2] + 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_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 + add_index :friendly_id_slugs, :sluggable_type + end +end diff --git a/db/migrate/20180517170138_add_slug_to_groups.rb b/db/migrate/20180517170138_add_slug_to_groups.rb new file mode 100644 index 00000000..7212eecf --- /dev/null +++ b/db/migrate/20180517170138_add_slug_to_groups.rb @@ -0,0 +1,6 @@ +class AddSlugToGroups < ActiveRecord::Migration[5.1] + def change + add_column :groups, :slug, :string + add_index :groups, :slug + end +end diff --git a/db/migrate/20180517190202_add_slug_to_events.rb b/db/migrate/20180517190202_add_slug_to_events.rb new file mode 100644 index 00000000..ef5208cd --- /dev/null +++ b/db/migrate/20180517190202_add_slug_to_events.rb @@ -0,0 +1,6 @@ +class AddSlugToEvents < ActiveRecord::Migration[5.1] + def change + add_column :events, :slug, :string + add_index :events, :slug + end +end diff --git a/db/migrate/20180517212505_add_slug_to_users.rb b/db/migrate/20180517212505_add_slug_to_users.rb new file mode 100644 index 00000000..ef6ad8a5 --- /dev/null +++ b/db/migrate/20180517212505_add_slug_to_users.rb @@ -0,0 +1,6 @@ +class AddSlugToUsers < ActiveRecord::Migration[5.1] + def change + add_column :users, :slug, :string + add_index :users, :slug + end +end diff --git a/db/schema.rb b/db/schema.rb index 432e7e7f..8dbc2a9a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180517084540) do +ActiveRecord::Schema.define(version: 20180517212505) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -53,8 +53,22 @@ t.bigint "group_id" t.jsonb "updated_fields", default: {}, null: false t.boolean "sample_event", default: false + t.string "slug" t.index ["group_id"], name: "index_events_on_group_id" t.index ["organizer_id"], name: "index_events_on_organizer_id" + t.index ["slug"], name: "index_events_on_slug" + end + + create_table "friendly_id_slugs", id: :serial, 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_id"], name: "index_friendly_id_slugs_on_sluggable_id" + t.index ["sluggable_type"], name: "index_friendly_id_slugs_on_sluggable_type" end create_table "group_memberships", force: :cascade do |t| @@ -77,7 +91,9 @@ t.bigint "user_id" t.string "location" t.boolean "sample_group", default: false + t.string "slug" t.index ["location"], name: "index_groups_on_location" + t.index ["slug"], name: "index_groups_on_slug" t.index ["user_id"], name: "index_groups_on_user_id" end @@ -141,8 +157,10 @@ t.string "unconfirmed_email" t.boolean "sample_user", default: false t.boolean "admin", default: false + t.string "slug" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + t.index ["slug"], name: "index_users_on_slug" end create_table "users_roles", id: false, force: :cascade do |t| diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb index e7337d6a..7586a54f 100644 --- a/test/controllers/events_controller_test.rb +++ b/test/controllers/events_controller_test.rb @@ -64,8 +64,11 @@ class EventsControllerTest < ActionDispatch::IntegrationTest patch group_event_url(@group, @event), params: event_params_updated + group = Group.find(@group.id) + event = Event.find(@event.id) + assert_emails_sent_to attendees_emails - assert_redirected_to group_event_url(@group, @event) + assert_redirected_to group_event_url(group, event) end test "should destroy event" do diff --git a/test/controllers/groups_controller_test.rb b/test/controllers/groups_controller_test.rb index 1c544e08..1621dd87 100644 --- a/test/controllers/groups_controller_test.rb +++ b/test/controllers/groups_controller_test.rb @@ -25,7 +25,9 @@ class GroupsControllerTest < ActionDispatch::IntegrationTest post groups_url, params: { group: group_params } end - assert_redirected_to group_url(Group.last) + group = Group.find(Group.last.id) + + assert_redirected_to group_url(group) end test "should show group" do @@ -44,7 +46,10 @@ class GroupsControllerTest < ActionDispatch::IntegrationTest sign_in(@user) patch group_url(@group), params: { group: group_params } - assert_redirected_to group_url(@group) + + group = Group.find(@group.id) + + assert_redirected_to group_url(group) end test "should destroy group" do diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index 2218d1f1..d524e875 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -24,7 +24,10 @@ class UsersControllerTest < ActionDispatch::IntegrationTest sign_in(@phil) patch user_url(@phil), params: user_params - assert_redirected_to user_url(@phil) + + user = User.find(@phil.id) + + assert_redirected_to user_url(user) end private diff --git a/test/integration/users/users_profile_test.rb b/test/integration/users/users_profile_test.rb index 549de255..8d0fb7d2 100644 --- a/test/integration/users/users_profile_test.rb +++ b/test/integration/users/users_profile_test.rb @@ -75,12 +75,16 @@ def setup private def assert_valid_for(user) - assert current_path == user_path(user) + friendly_user = User.find(user.id) + + assert current_path == user_path(friendly_user) assert page.has_content? "updated" end def assert_invalid_for(user) - assert current_path == user_path(user) + friendly_user = User.find(user.id) + + assert current_path == user_path(friendly_user) assert page.has_content? "error" yield if block_given? end