I'm working on a project that's left me stumped, hoping someone out there might have some interesting input. I see there are several gems available for obfuscation of urls, but they seem to stop at the slug level instead of the controller level - i.e. www.foo.com/mycontroller/8sZ16lp. I'm looking for a method to produce something along the lines of www.foo.com/8asd31Ud, dropping the controller name. I checked the docs of the obsufacate_id gem, but it doesn't appear to go that far.
To give more background - I'm really hoping to have www.foo.com/mycontroller/15/edit = www.foo.com/95Ali32
As this doesn't match the convention of rails RESTful URL's my guess is you'll likely just need to write your own routes and maybe more. Out of curiosity, how would you envision that the system knows what type of object to load with "95Ali32"? This might be better to build with Sinatra or something that gives you more control over the routing and less convention.
Here's one possible approach that uses a table with your slugs that maps to type and object id:
# migration
create_table :slugs do |t|
t.string :object_type, :null => false
t.string :object_id, :null => false
t.string :slug
t.timestamps
end
# models
class Slugs < ActiveRecord::Base
belongs_to :object, :polymorhic => true
end
class AModel < ActiveRecord::Base
has_one :slug, :as => :owner
end
# routes.rb
# note that anything else should go above this because this will catch all other URL's
get '*slug', to: 'slugs#show'
# controller
class SlugsController < ApplicationController
def show
#object = Slug.where(slug: params[:slug])
raise ActiveRecord::NotFound.new unless #object
render #object.kind
end
end
You would then need to build views for each type of object. See this related question
Update
Here's another idea. How obscure do you need the slug? What if each model has a known code and then the ID is encoded some way and appended to the model code? Then you could do something much simpler in the code using routes that are preconfigured:
# You could generate this too, or just hard-code them
prefixes = ['8sZ', '95Ali']
[:a_model, :another_model].each do |model|
match "#{prefixes.pop}:id", :controller => model.to_s.underscore.pluralize, :action => :show, :as => model
end
This would give you routes like these
/8sZ1 #=> AModelsController#show(:id => 1)
/95Ali341 #=> AModelsController#show(:id => 341)
You could take this another level and use friendly_id to generate slugs for the model ID's. Or use UUID's instead of integer ID's (supported by PostgreSQL).
Related
I have been working on an App called CtrlPanel for the company I work for.
This app was originally running on Ruby v2.2.2 and rails v4.2.1. I could not get that environment to work on ANYTHING; I tried both PC and Linux. Since I couldn't get that environment running and since it needed to be updated to the newest version anyway I figured I would just get it working on the latest version.
I had no idea what I was in for, that was a little over a month ago. I am happy to report I now have everything in the program working with one exception. There is a catalog that displays all of the items and it uses a scope in the model with a lambda expression that is rather complicated (at least to me). I have had to update the syntax ALL over this application due to the older version of Rails and now being on the newest version and this is the only one I can't seem to figure out. I am pretty sure again that it is just a Syntax problem from Rails v4.2.1 to Rails v6.1.3.1 but I just can't seem to figure it out and I am sure people who are more experienced than myself will know what it is.
Here is the model in question:
category.rb
# == Schema Information
#
# Table name: categories
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Category < ActiveRecord::Base
attr_accessible :name
# accepts_nested_attributes_for :product_categories
has_many :products, through: :product_categories
has_many :product_categories, dependent: :destroy
default_scope { order("id") }
scope :with_published_products, -> { joins{product_categories.product}.where{products.status.eq "published"}.uniq }
scope :with_matched_search_terms, ->(search_terms) { joins{product_categories.product}.where{products.name.like search_terms}.uniq }
end
The line with the issue is:
scope :with_published_products, -> { joins{product_categories.product}.where{products.status.eq "published"}.uniq }
It is giving the error:
The method .joins() must contain arguments
I had other joins statements that I had to fix (numerous). It was due to syntax for a very BASIC example from joins{arguments} to joins(arguments). I assume that is the same case here. In this case there are many more elements and that lambda expression thrown into the mix which is making it much harder (for me at least) to get it corrected. I have tried every version of changing that line around I can think of and probably many that do not make sense. I have one version where it will get past the line but then when that "with_published_products" is called later in the view it errors out with a bad PG statement and when I traced it back it was due to the very same query. Here is the view involved:
- if current_user
.pull-left{style: "position:absolute; top: 10px; left: 20px;"}
= link_to products_path do
%button.btn{style: "margin-top: 0px; "}
%i.icon-arrow-left.icon-large
%b back to products
.row-fluid
.span12
%h1.bigred DPF Product Catalog
%hr
.row-fluid
.span12
- unless #categories.empty?
.tabbable.tabbable-left
%ul.nav.nav-tabs{style: "text-align:right;"}
%li.active
%a{"data-toggle" => "tab", :href => "#tab-#{#categories.first.id}"} #{#categories.first.name}
- #categories.each do |category|
-unless category == #categories.first
%li
%a{"data-toggle" => "tab", :href => "#tab-#{category.id}"} #{category.name}
.tab-content
- #categories.each_with_index do |category, i|
.tab-pane.fade{id: "tab-#{category.id}", class: "#{ 'in active' if i.zero? }"}
%h1.bigred.professional{style: "text-align:left;"} #{category.name}
%hr
- #params_name.blank? ? #products = category.products.published.includes(:pictures).order("name asc") : #products = category.products.searched(#params_name).published.includes(:pictures).order("name asc")
= render partial: 'product', collection: #products, cache: true
- else
%h2
There were no products matching
%b #{#params_name.gsub(/%/, '')}
Here is the Controller:
class StaticPagesController < ApplicationController
#skip_before_filter :authenticate_user!, :only => [:catalog, :roi]
skip_before_action :authenticate_user!, :only => [:catalog, :roi]
#skip_authorization_check
before_action
def home
end
def help
end
def catalog
if params[:name].present?
#params_name = "%#{params[:name]}%"
#categories = Category.with_published_products.with_matched_search_terms(#params_name)
else
#categories = Category.with_published_products
end
render layout: "catalog"
end
def roi
render layout: "roi"
end
end
Best I can tell the original author is trying to display product_catagoires that have the status of "published". I might be over simplfying the statement but I beleive that is what I understand it to be doing. I know from other joins I had to fix I had to eliminate the .eq and put => in its place which I tried many versions of and it always complained about the syntax. As an example of what I mean this was another section I fixed:
FROM:
cds_item = item.variants.joins{vendor}.where{ vendor.code.eq 'cds' }.first
TO:
cds_item = item.variants.joins(:vendor).where( :vendor_id => 'cds' ).first
I tried to change the line I am having the problem with to a similar format and every combination I could think of I just can't seem to speak to Rails the way it wants.
If there are any other files I need to attach please let me know.
I appreciate any input anyone has to offer.
I am willing to make the scopes less complicated and more verbose if it solves the problem I just am too new to figure this last piece out.
Thank You,
Scott
Thank You #engineersmnky,
I am now getting the following error:
Cannot have a has_many :through association 'Category#products' which goes through 'Category#product_categories' before the through association is defined.
It is referencing this line in the view:
- #params_name.blank? ? #products = category.products.published.includes(:pictures).order("name asc") : #products = category.products.searched(#params_name).published.includes(:pictures).order("name asc")
Sorry if this is not the right way to add more information, I couldn't post the line dealing with the code in a comment.
My guess is that your legacy app used a gem called Squeel which monkeypatched core methods in ActiveRecord like where, joins etc to take a block argument:
Person.joins(:articles => {:comments => :person})
Becomes:
Person.joins{articles.comments.person}
The authors ambition was that Squeel would be incorporated into ActiveRecord but that didn't pass and the fact that the monkeypatches broke with every Rails release burned out the maintainers. Squeel was abandoned after Rails 4.2. But parts of it live on in the baby_squeel gem.
Some of the easier cases are going to be relatively straight forward to unfurl into modern ActiveRecord code. Others like a LIKE query will require some Arel:
class Category < ActiveRecord::Base
# Do not use attr_accessible
# - model level whitelisting was replaced by strong params in Rails 4
# attr_accessible :name
# accepts_nested_attributes_for :product_categories
# You need to declare the relations you are joining through first
has_many :product_categories, dependent: :destroy
has_many :products, through: :product_categories
# default scope is evil!
# default_scope { order("id") }
# use `lambda do` for multi-line lambdas to keep reasonable line lengths
# or just write regular class methods
scope :with_published_products, lambda do
joins(product_categories: :product)
.where( products: { status: "published" } )
.distinct
end
scope :with_matched_search_terms, lambda do |search_terms|
joins(product_categories: :product)
.where(
Product.arel_attribute(:name)
.lower
.matches("%#{search_terms.downcase}%")
.distinct
)
end
end
I'm not sure what #uniq was supposed to do in the Squeel - but I believe it may have added a distinct clause - like ActiveRecord::Relation#uniq which was deprechiated in Rails 5.0.
I'm really confused about virtual attributes in Rails 3.2 and all my research haven't help making things clearer.
# input model
class Input < ActiveRecord::Base
# Attributes --------------------
attr_accessible :title, :parent
attr_accessor :parent
def parent=(id)
wrtite_attribute(:parent, id.to_i)
self.parent = id.to_i
self[:parent] = id.to_i
#parent = id.to_i # seems to be the only one working. Why?
end
end
# inputs controller
class InputsController < ApplicationController
def new
#input = Input.new({
start_date: #current_scope_company.creation_date,
parent: 'bla'
})
#input.parent = 'bla'
#input[;parent] = 'bla'
end
end
# inputs table
create_table "inputs", :force => true do |t|
t.string "title"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
Above, I have compiled pretty much all the alternatives I found on the internet. It is NOT the code I run, just couple versions of the same thing. Though, whatever I try, I get the following warning:
DEPRECATION WARNING: You're trying to create an attribute 'parent'. Writing arbitrary attributes on a model is deprecated. Please just use 'attr_writer' etc.
Sometimes, I even get a stack level too deep. I'd love to understand how attributes work.
1/ attr_accessor is attr_writer plus attr_reader right? Why am I asked to use attr_writer in the warning?
2/ How am I supposed to write attributes from the model (and why)
3/ How am I supposed to write attributes from the controller (and why)
Thanks a lot!
Update
After further test, it looks like the proper way to do it is #parent = id.to_i. I would still love to get explanation why. I'm really confused why self. wouldn't work.
Atttr_accessor should be fine. So in your model, it looks like you're trying to do the same thing in four different ways:
def parent=(id)
write_attribute(:parent, id.to_i)
self.parent = id.to_i
self[:parent] = id.to_i
#parent = id.to_i # <- all these are redundant and scary
end
(Furthermore, in your controller it looks like you're also trying to do the same thing in different ways, unnecessarily.) Also, I'm pretty sure the syntax 'def parent=(id)' is only for real attributes. Looking at this railscast and from my own experience, you just do something like this for a virtual attribute:
def parent
#id.to_i
end
Where is "id" coming from that's supposed to go into 'parent'? Virtual attributes have to somehow related to something that's actually being stored in the db, either through a real attribute or through an association's real attribute. If you explain a bit more of what parent is actually supposed to be calculated from, it'd be easier to help.
I asked a similar question about select options a while ago but I still can't seem to wrap my head around it. I'm rather new to rails but here's what I'm trying to do
I have a Post table & in it, I have a "post_status" column. I
would like to give each post 3 options:
Draft
Pending
Publish
How would I go about creating these 3 options in Rails? (I was advised not to use booleans for this)
Thank you in advance
Elaborating on #Alexander Kobelev answer, I'd put it all in the model:
class Post < ActiveRecord::Base
STATUS_OPTIONS = {
:draft => 'Draft',
:pending => 'Pending',
:published => 'Published'
}
validates_inclusion_of :post_status, :in => STATUS_OPTIONS.keys
end
in your view:
Post Status: <%= select(:post, :post_status, Post::STATUS_OPTIONS.invert) %>
In this particular instance they look like status flags that could be handled a few ways, but you've asked about select options so here's a solution for that method.
Because you don't specify if you need to keep the values already in the table I've detailed a method that allows you to keep them by converting them to IDs (assuming they are currently strings), if this is not relevant then follow only the bold instructions.
Create a PostStatus resource (model, migrate, controller/view if you need the ability to change them).
Define the relationships
PostStatus
has_many :posts
Post
belongs_to :post_status
Add values to your PostStatus table (if you have a live system with strings in the table you should match the existing post status strings here to allow you to convert the data (detailed below).
Change column name to post_status_id in the Post table, change its type to int. If this isn't live then just redo the migrate with the column as integer. If it is a live system you'll need to convert your data into a new column instead of just changing its type, the below is a suggested method.
add_column :posts, :post_status_id_tmp, :int
Post.reset_column_information # make the new column available to model methods
Post.all.each do |post|
# Assuming you have a string with the option text currently:
post.post_status_id_tmp = PostStatus.find_by_name(post.post_status).id
post.save
end
remove_column :posts, :post_status
rename_column :posts, :post_status_tmp, :post_status_id
In your post form add a selectbox.
<%= form.collection_select :post_status_id, PostStatus.all, :id, :name %>
That should at the least get you started!
You can try something like this:
class Post < ActiveRecord::Base
validates_inclusion_of :status, :in => [:draft, :pending, :publish]
def status
read_attribute(:status).to_sym
end
def status= (value)
write_attribute(:status, value.to_s)
end
end
where status is :string, limit: 20 (it's just for example) in migration
or you can try to use https://github.com/jeffp/enumerated_attribute
I'm building backend system, as written in Iain Hecker's tutorial: http://iain.nl/backends-in-rails-3-1 and I try to adapt it to MongoDB with Mongoid.
So when I need to write in backend/resourse_helper.rb
module Backend::ResourceHelper
def attributes
resource_class.attribute_names - %w(id created_at updated_at)
end
end
I get the following error:
undefined method `attribute_names' for Backend::User:Class
(I rooted backend to "backend/users#index").
Backend::User inherits from User:
class User
include Mongoid::Document
devise_for :users
field :name
field :address
end
I just need a list of fields for that User:Class, as I guess (i.e. ["email", "name", "address", ...]), but I broke my head trying to find how.
Mongoid already provides you the attributes for an object:
Model.new.attributes
To get the names for these attributes:
Model.fields.keys
Use the built-in method:
Model.attribute_names
# => ["_id", "created_at", "updated_at", "name", "address"]
One thing to take note of is that Model.fields.keys will only list the field keys that are defined in the Model class. If you use dynamic fields they will not be shown. Model.attributes.keys will also include the attribute keys for any dynamic fields you have used.
You're on the right track with attribute_names. I think you just need to make sure you're including your module in the proper place. For instance, if you had the same module:
module Backend::ResourceHelper
def attributes
resource_class.attribute_names - %w(id created_at updated_at)
end
end
Your class should look like so:
class User
include Mongoid::Document
extend Backend::ResourceHelper
devise_for :users
field :name
field :address
end
Then calling User.attributes should return ["name", "address"]
We are using thinkingtank gem and having trouble indexing model associations, even simple ones. For example, a profile belongs to an institution, which has a name – we would like to do something like:
class Profile < ActiveRecord::Base
#model associations
define_index do
indexes institution(:name), :as => :institution_name
end
end
but that doesn't work. This must be very simple – what am I doing wrong?
a possible solution to this issue would be adding a method returning the element to index. For the profile.institution.name case:
# profile.rb
# ...
belongs_to :institution
# ...
define_index do
indexes institution_name
end
def institution_name
self.institution.name
end
# ...
Also the ", :as => ..." syntax is not supported on thinkingtank.
I would also recommend giving a try to Tanker: https://github.com/kidpollo/tanker
Regards.
Adrian