How to store attributes in Postgres with Rails? - ruby-on-rails

I have a model called Page. The users are able to specify attributes whenever they create a Page. Ideally, they will choose from a list of attributes and assign them to the Page.
My first though was to create as many columns as attributes the user is able to select, and basically marking them as true/false if it has been selected.
However, I have a feeling that I might be missing a better approach. I have also used at HSTORE, but at the end of the day, I would also need to do something like:
attributes: { 'attribute1' => 'true', 'attribute2' => 'false' } and I am not sure if that is appropriate.
I will use those attributes in order to select pages, so I should be able to say something like:
Page.where(attribute1 is true).where(attribute2 is false).where(...)
How should I store those attributes in my Page model?

The acts_as_taggable_on gem might be of help to you.
You would declare your model as taggable on your attributes as follows:
class Page < ActiveRecord::Base
acts_as_ordered_taggable_on :attributes
end
And your example query would be something like this:
Page.tagged_with(["attribute1"]).tagged_with(["attribute2"], :exclude => true)
The Gem is very well documented here.

Related

Single Table inheritance with CRUD and forms in Rails

I'm a bit confused about STI in rails.
My situation:
I have a Contact model that has description and data string fields, to store some contact like phone, fax, email, etc.
Now when I have some specific contact type like phone number of email address I want to walidate the data format in different way and I want to make some different formating on output.
I decided to use STI as all the models have the same data with just different behaviour. And I have some questions regarding forms and CRUD operations as I don't want to go against Rails conventions.
How do I make a dropdown list in form with model type? Should I hardcode it or is there some more automated way?
How do I create a record? Should I use switch statement and according to received type create new model of according instance?
How should I update it if I'm going to change the model type? Cast the object to new class? Or create a new object and destroy the previous one?
I'll be very thankfull for your help!
Yes, should do a hardcore as there no default store for your STI models.
Generally, yes. But With Rails you could just use camelize.constantize to get class from string. Another way is just use parent model, and set type field manually. As with STI all records are in the same table and then all are of the parent class.
If you wish to update, just update type field. Then you could re-query to force Rails to get new object of different type.
You could create a model like this :
Type < ActiveRecord::Base
has_many :contacts
end
You could use this command rails g model Type name:string, add a type_id column in your contact and migrate the database.
end change your contact's model like this :
Contact < ActiveRecord::Base
belongs_to :type
end
Now, in your form, you could use this :
select("type", "type_id", Type.all.collect {|t| [ t.name, t.id ] }, { :include_blank => true })
It should resolve your problem.
Now you can do something like this :
#emails = Type.find_by_name('email').contacts
Or use scopes.

Rails 3.1 Validations defined in database

is there any way how to execute validation directives saved in database ?
See, i don't have staticaly defined attributes by database columns but are defined as row-type.
Examle:
Have groups and attributes. Every group should have different attributes.
Groups: id, name
Attributes: id, name
GroupAttributes: id, group_id, attribute_id, validation(serialize hash as string, same as rails validations)
Articles: id, group_id, attribute_id, value
Form is dynamicaly created based on which group is defined. Group 1 may have different validation for Attribute 1 than Group 1 for same Attribute.
But big problem are validations. I can't implicit define it in Model. Because i dont know which attributes are want to valide.
I have an idea evaluate string saved in database. But it is critical when there be injected code (which is very unespected).
Do you know any way how to resolve this ?
EDIT:
Principle is: I will write about CMS, it could be more clearer.
CMS have Articles/Posts and Post belongs to Group(Category).
Article should have many Attributes (saved in Attributes db table) like title, content, excerpt, thumbnail image, etc. But these attributes are not fixed in everyone article.
This is because Article belongs to Group. For example: news, posts, properties, reviews, etc.
And there is what i want. When i write some review, it is not neccessary put all fields(attributes) for all groups, only these which are needs.
So my idea is, that is easier to define attributes as "row type" instead "column type". Any time i'm able to add new, remove old for any Group. There could be tens of attributes (and different) for any group.
After neccessary joins all Models i want to define validation for each one attribute (column "validation" on top) and be used only for these attributes which are associated with group (GroupAttributes).
Give these attribute validations into the Model, like standard Rails "validates". Without complicated "if" and "unless" conditional statements.
Hope is more detailed than first one.
You can try make custom validator and then loop attributes for group and evaluate their validation parameters on the article. But you will need a proper hash for rails validations.
GroupAttributes.first.validation => {:presence => true,
:length => {:minimum => 3, :maximum => 254},
:uniqueness => true}
#app/models/article.rb
class Article << ActiveRecord::Base
vlidates_with ArticleValidator
end
#lib/article_validator.rb
class ArticleValidator << ActiveModel::Validator
def validate(record)
record.group.group_attributes.each do |group_attribute|
record.validates group_attribute.attribute.name, group_attribute.validation
end
end
end
I am not sure if does this works but hope it will be helpfull.
UPDATE: I was wrong it didn't work because validates is ActiveModel method not ActiveRecord like I thought. The right answer is you have to add before_validation callback and then setup the right validators.
class Article << ActiveRecord::Base
before_validation :set_validators
validates :group_id, :presence => true
privat
def set_validators
self.group.group_attributes.each do |group_attribute|
Article.validates group_attribute.attribute.name, group_attribute.validation
end
end
end

Dynamic scope for accessing Model Attributes

I'm currently using the mass assignment security baked into rails 3 to scope what level of users can update about their model. For example this code allows me to protect attributes based on the user level.
class Customer
attr_accessor :name, :credit_rating
attr_accessible :name
attr_accessible :name, :credit_rating, :as => :admin
end
I would like to be able to use this same idea for which attributes appear when I do a find. For example I would like to be able to say
Customer.all.as(:admin)
and get back the credit rating. Compare this to doing
Customer.all
and getting back all the attributes except the credit_rating
Is this something rails supports and I've missed?
attr_accessible is used to filter incoming attributes on mass assignment. This is a convenience method created so that a developer does not need to manually clean the incoming hash of params, something he does not control.
When displaying information a developer is in full control of what he/she desires to show, so there seems to be no reason to limit the read functionality.
However, rails allows you to "select" the attributes you desire in a query: see http://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields
You could easily create a scope with the name admin that would limit the selected values.
If you do not desire to have the full models, but only the values, you could use the generated sql. e:g.
ActiveRecord::Base.connection.select_values(Customer.select('name').to_sql)

has_many through blowing away the association's metadata on mass association

Hey,
Not a Rails noob but this has stumped me.
With has many through associations in Rails. When I mass assign wines to a winebar through a winelist association (or through) table with something like this.
class WineBarController
def update
#winebar = WineBar.find(params[:id])
#winebar.wines = Wine.find(params[:wine_bar][:wine_ids].split(",")) // Mass assign wines.
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
This will delete every winelist row associated with that winebar. Then it finds all of the wines in wine_ids, which we presume is a comma separated string of wine ids. Then it inserts back into the winelist a new association. This would be expensive, but fine if the destroyed association rows didn't have metadata such as the individual wine bar's price per glass and bottle.
Is there a way to have it not blow everything away, just do an enumerable comparison of the arrays and insert delete whatever changes. I feel like that's something rails does and I'm just missing something obvious.
Thanks.
Your problem looks like it's with your first statement in the update method - you're creating a new wine bar record, instead of loading an existing record and updating it. That's why when you examine the record, there's nothing showing of the relationship. Rails is smart enough not to drop/create every record on the list, so don't worry about that.
If you're using the standard rails setup for your forms:
<% form_for #wine_bar do |f| %>
Then you can call your update like this:
class WineBarController
def update
#winebar = WineBar.find(params[:id])
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
You don't need to explicitly update your record with params[:wine_bar][:wine_ids], because when you updated it with params[:wine_bar], the wine_ids were included as part of that. I hope this helps!
UPDATE: You mentioned that this doesn't work because of how the forms are setup, but you can fix it easily. In your form, you'll want to rename the input field from wine_bar[wine_ids] to wine_bar[wine_ids_string]. Then you just need to create the accessors in your model, like so:
class WineBar < ActiveRecord::Base
def wine_ids_string
wines.map(&:id).join(',')
end
def wine_ids_string= id_string
self.wine_ids = id_string.split(/,/)
end
end
The first method above is the "getter" - it takes the list of associated wine ids and converts them to a string that the form can use. The next method is the "setter", and it accepts a comma-delimited string of ids, and breaks it up into the array that wine_ids= accepts.
You might also be interested in my article Dynamic Form Elements in Rails, which outlines how rails form inputs aren't limited to the attributes in the database record. Any pair of accessor methods can be used.

Signaling validation errors in assigning a virtual attribute?

This is a Rails/ActiveRecord question.
I have a model which basically has to represent events or performances. Each event has many attributions: an attribution is basically something like "In this event, Person X had Role Y".
I concluded that the best way to allow a user to edit this data is by providing a free text field which expects a structured format, which I'll call a role string:
singer: Elvis Costello, songwriter: Paul McCartney, ...
where I use autocompletion to complete on both the names of roles (singer, songwriter...) and the names of people. Both roles and people are stored in the database.
To implement this, I created a virtual attribute in the Event model:
def role_string
# assemble a role string from the associations in the model
end
def role_string=(s)
# parse a string in the above role string format,
# look up the People and Events mentioned, and update
# what's in the database
end
This is all fine. The whole thing works quite well, when the role string is well-formed and the associations given by the role string all check out.
But what if the role string is malformed? Okay, I figure, I can just use a regex together with standard validation to check the format:
validates_format_of :role_string, :with => /(\w+:\s*\w+)(,\s*\w+:\s*\w+)*/
But what if the associations implied by the role string are invalid? For example, what happens if I give the above role string, and Elvis Costello doesn't reference a valid person?
I thought, well, I could use validates_each on the attribute :role_string to look up the associations and throw an error if one of the names given doesn't match anything, for example.
My questions are two: first, I don't like this approach, since to validate the associations I would have to parse the string and look them up, which duplicates what I'd be doing in role_string= itself, except for actually saving the associations to the database.
Second, ... how would I indicate that an error's occurred in assigning to this virtual attribute?
First of all, you're attributing the Person to the Event incorrectly. You should instead pass a Person's ID to the event, rather than a string of the person's name. For instance, what if a Person with an ID of 230404 and a name of "Elvis Costello" changes his name to "Britney Spears?" Well, if that were to happen, the ID would remain the same, but the name would change. However, you would not be able to reference that person any longer.
You should set up your associations so that the foreign key references people in multiple cases:
has_one :singer, :class_name => "Person", :foreign_key => "singer_id"
has_one :songwriter, :class_name => "Person", :foreign_key => "songwriter_id"
This way, you can have multiple people associated with an Event, under different roles, and you can reference multiple attributes that Person may have. For example:
Event.first.singer.name # => "Elvis Costello"
Event.first.songwriter.name # => "Britney Spears"
You can research the available validations for associations (validates_associated), as well as whether or not an ID is present in a form (validates_presence_of). I would recommend creating your own custom validation to ensure that a Person is valid before_save. Something like (untested):
def before_save
unless Person.exists?(self.songwriter_id)
self.errors.add_to_base("Invalid songwriter. Please try again!")
return false
end
end
Also, I noticed that you're looking for a way for users to select a user which should be used for the roles in your Event. Here is what you can do in your form partial:
select(:event, :singer_id, Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => 'None' })
Hope this helps!

Resources