friendly_id slug not changing on update - ruby-on-rails

I'm using friendly_id 5.0.0.rc1, and also active_admin.
It would appear everything is working perfectly as expected, except for the fact that updating a record's slug attribute/column in active_admin does not do anything (it keeps it the same)
I find the same behavior just using console:
p = Post.first
p.slug
#=> 'test'
p.slug = 'another-test'
p.save
#=> true
p.slug
#=> 'test
My config:
FriendlyId.defaults do |config|
config.use :reserved
config.reserved_words = %w(admin new edit index session users register)
config.use :finders
config.use :slugged
config.slug_column = 'slug'
config.sequence_separator = '-'
config.use Module.new {
def should_generate_new_friendly_id?
slug.blank? || slug_changed?
end
}
end
My model:
class Post < ActiveRecord::Base
default_scope { order('created_at DESC') }
validates :title, presence: true
validates :body, presence: true
validates :views, presence: true, numericality: { only_integer: true }
extend FriendlyId
friendly_id :title, use: [:slugged, :history]
end
my controller:
class PostsController < ApplicationController
def index
#posts = Post.all.page(params[:page]).per(10)
end
def show
#post = Post.find_by_slug!(params[:id])
if request.path != post_path(#post)
redirect_to #post, :status => :moved_permanently and return
else
#post.increment :views if #post
end
end
end
Thanks!

Usually when using friendly id, you never update the slug manually. Instead:
def should_generate_new_friendly_id?
slug.blank? || title_changed?
end
And then every time you change the title, it will automatically update the slug.

more exactly, you should use self.title_changed?
def should_generate_new_friendly_id?
slug.blank? || self.title_changed?
end

Incase anyone else lands here and just need to change a slug:
p = Post.first
p.slug
#=> 'test'
tmp_title = p.title
p.title = 'another-test'
p.slug = nil
p.save
#=> true
p.title = tmp_title
p.slug
#=> 'another-test'

Related

Rails : Wrong number of arguments (given 1, expected 0)

I get this error on my posts index page :
This the model :
class Post < ApplicationRecord
include Filterable
belongs_to :region
belongs_to :category
belongs_to :topic
validates :title, presence: true, length: { maximum: 500 }
validates :content, presence: true
validates :published_at, presence: true
translates :title, :content, :slug, touch: true, fallbacks_for_empty_translations: true
has_attached_file :image, styles: { thumb: "100x70#", featured: "1560x868#", small: "760x868#", big: ">1600x1600" }
validates_attachment :image, content_type: { content_type: ["image/jpeg", "image/gif", "image/png"] }
validates_attachment_presence :image
scope :published, -> (published) { where(published: (['true', true].include? published)).order(featured: :desc, published_at: :desc) }
scope :published_until_now, -> { where("published_at < ?", Time.now).merge(Post.published(true)) }
scope :topic, -> (topic_id) {
joins(:topic).where('topic_id = ?', topic_id) }
scope :category, -> (post_category) {
joins(:category).where('category_id = ?', post_category) }
scope :match, -> (search_term) {
with_translations(I18n.locale).where('content like ? or title like ?', "%#{search_term}%", "%#{search_term}%") }
self.per_page = 10
after_save :unfeature_older_posts, if: Proc.new { |post| post.featured? }
extend FriendlyId
friendly_id :title, use: :globalize
def unfeature_older_posts
featured_posts = Post.where(featured: true).where.not(id: id).order(published_at: :desc)
if featured_posts.size == 1
featured_posts.last.update(featured: false)
end
end
end
This the controller :
class PostsController < ApplicationController
before_action :get_pages_tree, :get_privacy_policy, only: [:index, :show]
def index
#filters = params.slice(:topic, :category)
#posts = Post.published_until_now
.filter(#filters)
.paginate(:page => params[:page], per_page: 11)
end
def show
#post = Post.friendly.find(params[:id])
end
end
and filter is defined here :
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
results = results.public_send(key, value) if value.present?
end
results
end
end
end
I'm not sure where to go from here. I recently upgraded to Ruby on Rails 5 and Ruby 2.7.0, I don't know if it's related.
Try replacing module ClassMethods with class_methods do.
If it works, then please keep in mind:
filter method comes from Ruby. It's defined in Array. As you can see in the doc, filter method on Array takes no argument. That's the direct cause of the error you see.
In Rails, when methods on Array are called on ActiveRecord object (in your case, Post.published_until_now) and when methods cannot be found on a model, it automatically converts itself into an Array. So, it calls filter method on Array. Generally, you don't want to define methods such as filter which is confusing.

How can I test these RSS parsing service objects?

I have some service objects that use Nokogiri to make AR instances. I created a rake task so that I can update the instances with a cron job. What I want to test is if it's adding items that weren't there before, ie:
Create an Importer with a url of spec/fixtures/feed.xml, feed.xml having 10 items.
Expect Show.count == 1 and Episode.count == 10
Edit spec/fixtures/feed.xml to have 11 items
Invoke rake task
Expect Show.count == 1 and Episode.count == 11
How could I test this in RSpec, or modify my code to be more testable?
# models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_importer
validates :title, presence: true
validates :url, presence: true
validates :feed_format, presence: true
private
def parse_importer
Parser.new(self)
end
end
# models/show.rb
class Show < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
has_many :episodes
attr_accessor :entries
end
# models/episode.rb
class Episode < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
belongs_to :show
end
#lib/tasks/admin.rake
namespace :admin do
desc "Checks all Importer URLs for new items."
task refresh: :environment do
#importers = Importer.all
#importers.each do |importer|
Parser.new(importer)
end
end
end
# services/parser.rb
class Parser
def initialize(importer)
feed = Feed.new(importer)
show = Show.where(rss_link: importer.url).first
if show # add new episodes
new_episodes = Itunes::Channel.refresh(feed.origin)
new_episodes.each do |new_episode|
show.episodes.create feed.episode(new_episode)
end
else # create a show and its episodes
new_show = Show.new(feed.show) if (feed && feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create feed.episode(entry)
end
end
end
end
end
# services/feed.rb
class Feed
require "nokogiri"
require "open-uri"
require "formats/itunes"
attr_reader :params, :origin, :show, :episode
def initialize(params)
#params = params
end
def origin
#origin = Nokogiri::XML(open(params[:url]))
end
def format
#format = params[:feed_format]
end
def show
case format
when "iTunes"
Itunes::Channel.fresh(origin)
end
end
def episode(entry)
#entry = entry
case format
when "iTunes"
Itunes::Item.fresh(#entry)
end
end
end
# services/formats/itunes.rb
class Itunes
class Channel
def initialize(origin)
#origin = origin
end
def title
#origin.xpath("//channel/title").text
end
def description
#origin.xpath("//channel/description").text
end
def summary
#origin.xpath("//channel/*[name()='itunes:summary']").text
end
def subtitle
#origin.xpath("//channel/*[name()='itunes:subtitle']/text()").text
end
def rss_link
#origin.xpath("//channel/*[name()='atom:link']/#href").text
end
def main_link
#origin.xpath("//channel/link/text()").text
end
def docs_link
#origin.xpath("//channel/docs/text()").text
end
def release
#origin.xpath("//channel/pubDate/text()").text
end
def image
#origin.xpath("//channel/image/url/text()").text
end
def language
#origin.xpath("//channel/language/text()").text
end
def keywords
keywords_array(#origin)
end
def categories
category_array(#origin)
end
def explicit
explicit_check(#origin)
end
def entries
entry_array(#origin)
end
def self.fresh(origin)
#show = Itunes::Channel.new origin
return {
description: #show.description,
release: #show.release,
explicit: #show.explicit,
language: #show.language,
title: #show.title,
summary: #show.summary,
subtitle: #show.subtitle,
image: #show.image,
rss_link: #show.rss_link,
main_link: #show.main_link,
docs_link: #show.docs_link,
categories: #show.categories,
keywords: #show.keywords,
entries: #show.entries
}
end
def self.refresh(origin)
#show = Itunes::Channel.new origin
return #show.entries
end
private
def category_array(channel)
arr = []
channel.xpath("//channel/*[name()='itunes:category']/#text").each do |category|
arr.push(category.to_s)
end
return arr
end
def explicit_check(channel)
string = channel.xpath("//channel/*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(channel)
keywords = channel.xpath("//channel/*[name()='itunes:keywords']/text()").text
arr = keywords.split(",")
return arr
end
def entry_array(channel)
arr = []
channel.xpath("//item").each do |item|
arr.push(item)
end
return arr
end
end
class Item
def initialize(origin)
#origin = origin
end
def description
#origin.xpath("*[name()='itunes:subtitle']").text
end
def release
#origin.xpath("pubDate").text
end
def image
#origin.xpath("*[name()='itunes:image']/#href").text
end
def explicit
explicit_check(#origin)
end
def duration
#origin.xpath("*[name()='itunes:duration']").text
end
def title
#origin.xpath("title").text
end
def enclosure_url
#origin.xpath("enclosure/#url").text
end
def enclosure_length
#origin.xpath("enclosure/#length").text
end
def enclosure_type
#origin.xpath("enclosure/#type").text
end
def keywords
keywords_array(#origin.xpath("*[name()='itunes:keywords']").text)
end
def self.fresh(entry)
#episode = Itunes::Item.new entry
return {
description: #episode.description,
release: #episode.release,
image: #episode.image,
explicit: #episode.explicit,
duration: #episode.duration,
title: #episode.title,
enclosure_url: #episode.enclosure_url,
enclosure_length: #episode.enclosure_length,
enclosure_type: #episode.enclosure_type,
keywords: #episode.keywords
}
end
private
def explicit_check(item)
string = item.xpath("*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(item)
keywords = item.split(",")
return keywords
end
end
end
Before anything else, good for you for using service objects! I've been using this approach a great deal and find POROs preferable to fat models in many situations.
It appears the behavior you're interested in testing is contained in Parser.initialize.
First, I'd create a class method for Parser called parse. IMO, Parser.parse(importer) is clearer about what Parser is doing than is Parser.new(importer). So, it might look like:
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
end
end
Then add the create_new_episodes and create_show_and_episodes class methods.
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: #importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
def create_new_episodes(new_episodes)
new_episodes.each do |new_episode|
#show.episodes.create #feed.episode(new_episode)
end
end # create_new_episodes
def create_show_and_episodes
new_show = Show.new(#feed.show) if (#feed && #feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create #feed.episode(entry)
end
end
end # create_show_and_episodes
end
end
Now you have a Parser.create_new_episodes method that you can test independently. So, your test might look something like:
require 'rspec_helper'
describe Parser do
describe '.create_new_episodes' do
context 'when an initial parse has been completed' do
before(:each) do
first_file = Nokogiri::XML(open('spec/fixtures/feed_1.xml'))
#second_file = Nokogiri::XML(open('spec/fixtures/feed_2.xml'))
Parser.create_show_and_episodes first_file
end
it 'changes Episodes.count by 1' do
expect{Parser.create_new_episodes(#second_file)}.to change{Episodes.count}.by(1)
end
it 'changes Show.count by 0' do
expect{Parser.create_new_episodes(#second_file)}.to change{Show.count}.by(0)
end
end
end
end
Naturally, you'll need feed_1.xml and feed_2.xml in the spec\fixtures directory.
Apologies for any typos. And, I didn't run the code. So, might be buggy. Hope it helps.

Rails globalize + friendly id

I have a problem with globalize and friendly_id. The site has 2 languages Ru and En. Gem friendly_id, globalize and friendly_id-globalize configured and work. If I change the language from Russian to English, all is well:
http://127.0.0.1:3000/ru/o-saite -> http://127.0.0.1:3000/en/about-site
But when I change from English to Russian going wrong redirection:
http://127.0.0.1:3000/en/about-site -> http://127.0.0.1:3000/ru/about-site
page model:
class Page < ActiveRecord::Base
validates :title, :content, :slug, presence: true
validates :slug, uniqueness: true
validates :title, length: { minimum: 3, maximum: 255 }
validates :content, length: { minimum: 5 }
# globalize
translates :title, :content, :slug
# FriendlyId
extend FriendlyId
friendly_id :slug_candidates, use: [:slugged, :finders, :globalize]
def slug_candidates
[
:title,
[:title, :id]
]
end
def should_generate_new_friendly_id?
title_changed?
end
def normalize_friendly_id(string)
title.to_s.to_slug.normalize(transliterations: :russian).to_s
end
end
migration:
class TranslatePage < ActiveRecord::Migration
def self.up
Page.create_translation_table!({
title: :string,
content: :text,
slug: :string
}, {
migrate_data: true
})
end
def self.down
Page.drop_translation_table! migrate_data: true
end
end
from application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
config.i18n.default_locale = :ru
config.i18n.fallbacks = true
page controller
class PagesController < ApplicationController
before_action :load_page, only: [:show]
def show
end
private
def load_page
#page = Page.friendly.find(params[:id])
redirect_to action: action_name, id: #page.friendly_id, status: 301 unless #page.friendly_id == params[:id]
end
def page_params
params.require(:page).permit(:title, :content,:slug, :published)
end
end
what could be the problem?
SOLVED?
Problem was in the views. In layouts/aplication.html.slim was:
ul class='change_lang'
li = link_to_unless I18n.locale == :en, "EN", locale: :en
li = link_to_unless I18n.locale == :ru, "RU", locale: :ru
Now in pages/show.slim
- content_for :change_lang do
li
- link = I18n.with_locale(:ru){page_path(#page, locale: 'ru')}
= link_to 'RU', link
li
- link = I18n.with_locale(:en){page_path(#page, locale: 'en')}
= link_to 'EN', link
In layouts/aplication.html.slim
ul class='change_lang'
- if content_for?(:change_lang)
= yield :change_lang
- else
li = link_to_unless I18n.locale == :en, "EN", locale: :en
li = link_to_unless I18n.locale == :ru, "RU", locale: :ru
https://github.com/norman/friendly_id-globalize/issues/7
There are more minimalistic approach. But this method server is hung.
http://www.leanpanda.com/blog/2015/09/12/alternate-sitemap/
Redirect same page to different language with Globalize & Friendly_id
i've a generic answer.
please let me now if it works for you.
if you have a namespace you can set up in :shop, if not just url_for(obj)
in your application_helper.rb
def languages(obj=nil)
content_for(:switch_locale) do
I18n.available_locales.each do |locale|
I18n.with_locale(locale) do
concat(
if obj
content_tag(:li, (link_to locale, url_for([:shop, obj]) ))
else
content_tag(:li, (link_to locale, url_for(locale: locale.to_s) ))
end
)
end
end
end
end
in your views
- languages #category
or simply
- languages
in application.html.(erb/haml.slim) or wherever you want to render the translations.
=yield(:switch_locale)

Before Validation loop through self attributes for modification

I have created a simple before_validation:
before_validation :strip_tabs
def strip_tabs
end
In my class I want to loop through all my attributes and remove tabs from each value. Most posts I found on SO are people who want to set 1 attribute. But I want to edit all my values.
Question:
How can I loop all self attributes of a model and edit them.
Friend suggested this, but content_column_names does not exist:
self.content_column_names.each {|n| self[n] = self[n].squish}
UPDATE 1: More code:
class PersonalInfo
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
extend ActiveModel::Translation
extend ActiveModel::Callbacks
include Sappable
require 'ext/string'
attr_accessor \
:first_name, :middle_name, :last_name,:birthdate,:sex,
:telephone,:street,:house_number,:city,:postal_code,:country,
:e_mail, :nationality, :salutation, :com_lang
validates :e_mail, :email => {:strict_mode => true}
validate :validate_telephone_number
validate :age_is_min_17?
before_validation :strip_tabs
def strip_tabs
binding.remote_pry
end
def age_is_min_17?
birthdate_visible = PersonalField.not_hidden.find_by_name "BIRTHDATE"
if birthdate_visible && birthdate && birthdate > (Date.current - 17.years)
#errors.add(:birthdate, I18n.t("apply.errors.birthdate"))
end
end
def validate_telephone_number
telephone_visible = PersonalField.not_hidden.find_by_name "TELEPHONE"
telephone_current = telephone.dup
if telephone_visible && telephone_current && !telephone_current.empty?
if telephone_current[0] == '+' || telephone_current[0] == '0'
telephone_current[0] = ''
#errors.add(:telephone, I18n.t("apply.errors.telephone")) if !telephone_current.is_number?
else
#errors.add(:telephone, I18n.t("apply.errors.telephone"))
end
end
end
def initialize(hash)
simple_attributes = [:first_name, :middle_name, :last_name,:birthdate,:sex,
:telephone,:street,:house_number,:city,:postal_code,:country,
:e_mail, :nationality, :salutation, :com_lang]
simple_attributes.each do |attr|
set_attr_from_json(attr, hash)
end
set_attr_from_json(:birthdate, hash) {|date| Date.parse(date) rescue nil}
end
end
Update 2: Rails Version:
I'm using Rails '3.2.17'
You can do as following:
before_validation :strip_tabs
def strip_tabs
self.attributes.map do |column, value|
self[column] = value.squish.presence
end
end
But I think that .squish will not work on created_at, updated_at, id, ... Because they are not String!
def strip_tabs
self.attributes.map do |column, value|
self[column] = value.kind_of?(String) ? value.squish.presence : value
end
end
Since your class is not a Rails model (ActiveRecord::Base), you can do as following:
def strip_tabs
self.instance_variables.map do |attr|
value = self.instance_variable_get(attr)
value = value.squish if value.kind_of?(String)
self.instance_variable_set(attr, value)
end
end
This should work
def strip_tabs
self.attributes.each do |attr_name, attr_value|
modified_value = ... # calculate your modified value here
self.write_attribute attr_name, modified_value
end
end
Because it's not an ActiveRecord model you won't have attributes or column_names, but you already have an array of your attribute names in your initialize function. I would suggest making that into a constant so you can access it throughout the model:
class PersonalInfo
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
extend ActiveModel::Translation
extend ActiveModel::Callbacks
include Sappable
require 'ext/string'
SIMPLE_ATTRIBUTES = [:first_name, :middle_name, :last_name,:birthdate,:sex,
:telephone,:street,:house_number,:city,:postal_code,:country,
:e_mail, :nationality, :salutation, :com_lang]
attr_accessor *SIMPLE_ATTRIBUTES
before_validation :strip_tabs
def strip_tabs
SIMPLE_ATTRIBUTES.each{ |attr| self[attr] = self[attr].squish }
end
...
def initialize(hash)
SIMPLE_ATTRIBUTES.each do |attr|
set_attr_from_json(attr, hash)
end
set_attr_from_json(:birthdate, hash) {|date| Date.parse(date) rescue nil}
end
end

attr_accessor variable nil when doing before_validation callback

I am doing a before_validation as follows:
event.rb
attr_accessor :start_date
attr_accessible :start_time #recorded in database as a datetime
before_validation :build_start_time
...
def build_start_time
begin
self.start_time = DateTime.parse(start_date)
rescue
errors.add(:start_date, "invalid date")
return false
end
end
and the controller looks like:
def create
#event = events.build(params[:event])
if #event.save
# some other method calls
redirect_to #event
else
redirect_to :root
end
end
start_date is being set by a <%= f.text_field :start_date %> call in a form view, and when I check the params it is being passed to the 'Create' method of the model controller correctly, but in the build_start_time method it is nil, so self.start_time is not being set. Can you explain why it would be nil and what the solution would be? I also tried referring to it as self.start_date but that didn't make a difference.
Thanks
Have you tried making start_date also accessible?
Either you call attr_accessible with start_date so build() can actually set it, or you can change your controller to:
def create
#event = events.build(params[:event])
#event.start_date = params[:event][:start_date]
if #event.save
# some other method calls
redirect_to #event
else
redirect_to :root
end
end
tente assim.
#app/models/adm/video.rb
class Adm::Video < ActiveRecord::Base
validates :titulo, :url_codigo, presence: true
before_validation(on: [ :create, :update ]) do
self.url_codigo = parse_youtube(url_codigo) #url_codigo = params[:adm_video][:url_codigo]
end
private
# pega só o codigo do link youtube para inserir no banco
def parse_youtube(url)
if !url.blank?
regex = /(?:.be\/|\/watch\?v=|\/(?=p\/))([\w\/\-]+)/
return url.match(regex)[1] # https://www.youtube.com/watch?v=iX_rKHnKJSg = iX_rKHnKJSg
end
end
end
grava no banco de dados sò código do video = iX_rKHnKJSg = https://www.youtube.com/watch?v=iX_rKHnKJSg = iX_rKHnKJSg.
records in the database sò code iX_rKHnKJSg video = # = https://www.youtube.com/watch?v=iX_rKHnKJSg iX_rKHnKJSg

Resources