I have a nested form set up that will allow me to create up to 7 "Schedule" instances at one time. Each instance will allow a user to assign a schedule_start_time(datetime), schedule_end_time(datetime), and notes(String) field value. When I submit the form with a few of these completed, the parameters array looks exactly as I would expect:
{"utf8"=>"✓",
"authenticity_token"=>"HEoylzovRgr7BCZH47iNRPfizDHeVFMLTEmIiNeudcw=",
"workout"=>{"id"=>"2",
"schedules_attributes"=>{
"0"=>{"scheduled_start_time"=>"06/01/2011",
"scheduled_end_time"=>"06/02/2011",
"notes"=>"Notes 1"},
"1"=>{"scheduled_start_time"=>"",
"scheduled_end_time"=>"",
"notes"=>""},
"2"=>{"scheduled_start_time"=>"06/03/2011",
"scheduled_end_time"=>"06/04/2011",
"notes"=>"Notes 2"},
"3"=>{"scheduled_start_time"=>"",
"scheduled_end_time"=>"",
"notes"=>""},
"4"=>{"scheduled_start_time"=>"06/16/2011",
"scheduled_end_time"=>"06/30/2011",
"notes"=>"Notes 3"},
"5"=>{"scheduled_start_time"=>"",
"scheduled_end_time"=>"",
"notes"=>""},
"6"=>{"scheduled_start_time"=>"",
"scheduled_end_time"=>"",
"notes"=>""}}}, "commit"=>"Submit"}
In the controller, I filter those "schedules" with a blank start_date. My params list then looks like this:
{"utf8"=>"✓",
"authenticity_token"=>"HEoylzovRgr7BCZH47iNRPfizDHeVFMLTEmIiNeudcw=",
"workout"=>{"id"=>"2",
"schedules_attributes"=>{
"0"=>{"scheduled_start_time"=>"06/01/2011",
"scheduled_end_time"=>"06/02/2011",
"notes"=>"Notes 1"},
"2"=>{"scheduled_start_time"=>"06/03/2011",
"scheduled_end_time"=>"06/04/2011",
"notes"=>"Notes 2"},
"4"=>{"scheduled_start_time"=>"06/16/2011",
"scheduled_end_time"=>"06/30/2011",
"notes"=>"Notes 3"}}},
"commit"=>"Submit",
"action"=>"create",
"controller"=>"schedules"}
The SQL that gets generaated is not what I would expect however:
(0.1ms) BEGIN WARNING: Can't
mass-assign protected attributes: id
SQL (0.2ms) INSERT INTO schedules
(created_at, notes,
scheduled_end_time,
scheduled_start_time, updated_at,
workout_id) VALUES ('2011-06-29
03:23:45', 'Notes 1', '2011-02-06
00:00:00', '2011-01-06 00:00:00',
'2011-06-29 03:23:45', 2)
SQL
(0.1ms) INSERT INTO schedules
(created_at, notes,
scheduled_end_time,
scheduled_start_time, updated_at,
workout_id) VALUES ('2011-06-29
03:23:45', 'Notes 2', '2011-04-06
00:00:00', '2011-03-06 00:00:00',
'2011-06-29 03:23:45', 2)
SQL
(0.2ms) INSERT INTO schedules
(created_at, notes,
scheduled_end_time,
scheduled_start_time, updated_at,
workout_id) VALUES ('2011-06-29
03:23:45', 'Notes 3', NULL, NULL,
'2011-06-29 03:23:45', 2) (0.5ms)
COMMIT
Some of the valid date values are in the params array, but are being filtered out before the SQL commits.
Here is the controller code:
def create
#workout = Workout.find(params[:workout][:id])
7.times do |count|
#schedule = params[:workout][:schedules_attributes]["#{count}"]
if (#schedule[:scheduled_start_time].blank?)
params[:workout][:schedules_attributes].delete count.to_s.to_sym
end
end
if #workout.update_attributes(params[:workout])
redirect_to schedules_url, :notice => "Successfully updated schedule."
else
render :action => 'new'
end
end
And the Workout Model
class Workout < ActiveRecord::Base
belongs_to :team, :class_name => "Team", :foreign_key => "team_id"
has_many :exercise_instances, :dependent => :destroy
validates :name,
:presence => true
has_many :schedules, :dependent => :destroy
accepts_nested_attributes_for :schedules
end
And the Schedule Model
class Schedule < ActiveRecord::Base
attr_accessible :workout_id, :scheduled_start_time, :scheduled_end_time, :notes
belongs_to :workout
end
Any direction would be welcome. I'm suspecting caching at some level, but I'm just not sure where to start looking. Thanks!
I think you are reinventing the wheel here and it's the source of the problem. Try the following:
class Workout < ActiveRecord::Base
has_many :schedules, :dependent => :delete_all
accepts_nested_attributes_for :schedules, :allow_destroy => true, :reject_if => proc { |schedule| schedule[:scheduled_start_time].blank? }
end
You will still need to restrict the number of schedules to 7. I would recommend checking out existing solutions for this behavior; there are a lot of solid patterns on this already.
Personally I would do something like
params[:workout][:schedules_attributes].each do |sched|
#workout.schedules << #workout.schedules.build(sched) if sched[:scheduled_start_time].present?
end
if #workout.save # etc
And not use nested_attributes_for. This will guarantee that you will only get what was sent.
I've found with nested_attributes that it's often best to delete and recreate on edit every time as well, which may or may not be what you want.
I'm sure others with better nested_attributes 'fu' than me may have better solutions.
Related
Consider the following setup:
class Task
has_many :users, through: :task_users
end
class User
has_many :tasks, through: :tasks_users
end
class TaskUser
# Joins table
has_many :comments, dependent: :destroy
end
class Comment
belongs_to :task_user
end
Now if I perform a standard #destroy command, such as:
tu = TaskUser.first
tu.destroy
Then all comments associated to the task-user will also be destroyed.
However, suppose you want to update a user's tasks via #collection_singular_ids=, like so:
u = User.first
puts u.task_ids # => [1, 2, 3]
u.task_ids = [1, 2]
Doing this (without even calling #save explicitly!) will trigger SQL like:
(0.3ms) BEGIN
SQL (0.4ms) DELETE FROM `task_users` WHERE `task_users`.`task_id` = 3 AND `task_users`.`user_id` = 1
(2.0ms) COMMIT
...So the associated Comments get orphaned.
The same issue occurs if you use #attributes=:
u.attributes = { task_ids: [1, 2] }
Is there a clean way to ensure that the associated Comments will always be destroyed (i.e. never orphaned)?
Thanks to #engineersmnky for pointing me in the right direction.
This may not be the prettiest solution, but a viable option is to define a callback on the association, such as:
class User
has_many :tasks,
through: :tasks_users,
before_remove: ->(user, task) do
task_user = TaskUser.find_by!(user: user, task: task)
task_user.comments.each(&:destroy!)
end
# Or alternatively, this can be defined as a method:
has_many :tasks,
through: :tasks_users,
before_remove: :destroy_task_user_comments
private
def destroy_task_user_comments(task)
task_user = TaskUser.find_by!(user: self, task: task)
task_user.comments.each(&:destroy!)
end
end
Note that I have used bang (!) methods within the block, so that the entire transaction will rollback if the an exception is raised - as per the documentation:
if any of the before_remove callbacks throw an exception, the object will not be removed from the collection.
I have created Rails(version 3.2) application with mysql database. I have table Message with column content. I can update the following data using rails console but I can't using run same code from seeds.rb.
data: "Join **** Audio / Video Meeting. This is an online meeting by ****, the community marketplace to find products and services in your neighbourhood. http://*****.dev:3000/conferences/80"
I have following code in my seeds.rb file
all_messages = Message.all
all_messages.each do |message|
message_content = message.content
if message_content.present? && message_content[/\/(.*)\/(.*)\/conferences/,2].present?
message_content.slice! (message_content[/\/(.*)\/(.*)\/conferences/,2]+'/')
if message.update_attributes!(content: message_content)
puts message.content
else
puts "nothing"
end
end
end
It does't show any errors, but data is not updated in db and not shown any errors.
This is my model file
class Message < ActiveRecord::Base
attr_accessible :content
after_save :update_conversation_read_status
belongs_to :sender, :class_name => "Person"
belongs_to :conversation
has_one :request
validates_presence_of :sender_id
validates_presence_of :content
def update_conversation_read_status
conversation.update_attribute(:last_message_at, created_at)
conversation.participations.each do |p|
last_at = p.person.eql?(sender) ? :last_sent_at : :last_received_at
p.update_attributes({ :is_read => p.person.eql?(sender), last_at => created_at})
end
end
end
Note: content is text type in db.
So I have a bit of a server response time issue - which I think is caused due to obsolete queries. One major query chain that I have takes up to 370ms, which is obviously causing an issue.
Here are the requirements:
5 Different languages
There are several Product Categories (i.e. Cat 1, Cat 2, Cat 3, etc.)
Categories displayed depend on language. For example whilst category 1 is displayed in all languages, category 2 is only displayed in Germany and France but not in the UK
Each category contains x number of items (has_many belongs_to relationship). Again some items are displayed in certain languages others are not. For example even category 2 is displayed in France and Germany, only in Germany you can buy Item 1 and hence Item 1 should not be displayed in France but Germany.
The categories and items do have boolean fields named after the locale. This way I can set via flag whether or not to display the category and item in a specific language.
My solution:
Building the solution is quiet easy. In controller I read out all the categories for the current locale:
application_controller.rb (since it is used on every single page)
#product_categories = ProductCategory.where("lang_" + I18n.locale.to_s + " = ?", true)
And in the view (the navigation) I do the following:
layouts/navs/productnav.html.haml
- #product_categories.each do |category|
...
- category.products.includes(:product_teasers).where("lang_" + I18n.locale.to_s + " = ? AND active = ?", true, true).in_groups_of(3).each do |group|
...
The issue with this solution is that each time I fire a lot of queries towards the database. Using "includes" does not solve it as I can not specify what items to pull. Furthermore I require the in_groups_of(3) in my loop to display the items correctly on the page.
I was also looking into memchached solutions to have the queries cached all together - i.e. Dalli however, this would require me to change a lot of code as I am guessing I would require to query all categories for each language and cache them. In addition to it I have to query each item for each langugage depending on language and store that somehow in an array ?!
My question:
How to approach this ? There must be a simpler and more efficient solution. How to efficiently query respectively cache this?
Thank you!
UPDATE:
As requested here are my two Models:
1.) ProductCategory
class ProductCategory < ActiveRecord::Base
translates :name, :description, :slug, :meta_keywords, :meta_description, :meta_title, :header, :teaser, :fallbacks_for_empty_translations => true
extend FriendlyId
friendly_id :name, :use => [:globalize, :slugged]
globalize_accessors :locales => [:at, :de, :ch_de, :ch_fr, :fr, :int_en, :int_fr], :attributes => [:slug]
has_paper_trail
has_many :products, :dependent => :destroy
validates :name, presence: true, uniqueness: { case_sensitive: false }
default_scope { includes(:translations) }
private
def slug_candidates
[
[:name]
]
end
end
2.) Product
And every product Category can have 0..n Products, and each Product must belongs to one category.
class Product < ActiveRecord::Base
translates :slug, :name, :meta_keywords, :meta_description, :meta_title, :teaser, :power_range, :product_page_teaser, :product_category_slider_teaser, :fallbacks_for_empty_translations => true
extend FriendlyId
friendly_id :name, :use => :globalize
before_save :change_file_name
searchable do
text :name, :teaser, :product_page_teaser, :product_category_slider_teaser
integer :product_category_id
end
belongs_to :product_category
has_many :product_teasers, :dependent => :destroy
has_many :product_videos, :dependent => :destroy
has_many :product_banners, :dependent => :destroy
has_many :product_documents, :dependent => :destroy
has_many :product_tabs, :dependent => :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
has_many :references
has_and_belongs_to_many :contacts
accepts_nested_attributes_for :product_teasers, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_tabs, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_videos, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_banners, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_documents, :reject_if => :all_blank, :allow_destroy => true
has_paper_trail
validates :name, presence: true, uniqueness: { case_sensitive: false }
default_scope {includes(:translations)}
.... a lot more going on here ...
end
Please note: That category contains language flags (booleans), i.e lang_at, lang_de, lang_fr, etc. and if set then this category is displayed in that particualar language. SAME applies to products, as certain products are not displayed in all langauges altough the category might be.
Examples:
#product_categories = ProductCategory.where("product_categories.lang_" + I18n.locale.to_s + " = ?", true)
#products = Product.where("product_categories.lang_" + I18n.locale.to_s + " = ?", true)
I skipped any includes on purpose above - it is just to demonstrate the language logic.
UPDATE
The system have spent a lot of times to loop data in nested loop. Avoid to fetch data in nested loop. You have to use join or includes to catch your data more effective. For example:
Controller
#products = Product.includes(:product_category).where("product_categories.lang_" + I18n.locale.to_s + " = ? AND product_categories.active = ?", true, true).group_by(&:category)
View
- #products.each do |category, products|
<%= category.name %>
- products.each do |product|
<%= product.title %>
It needs to fix with your necessary code. I just help the main query. For example: active = ? is for products field or product_categories field. I hope It can help you.
I'm trying to make a RoR application for a Hospital so it has patients, doctors, offices, etc.
The problem I'm having is that, at the patient "Sign-up", I'm not able to save the new patient in the database. In fact, despite I've checked that the attributes are ok (It's just a name and a personal ID), once the method is excecuted, in the database only appears a new row with "<null>" instead of the actual attribute values. Here's the method:
def pat_create
pName = params[:name].to_s
id = params[:pid].to_s.to_i
pat = Patient.where(:pID => id).first
if pat == nil
pat = Patient.new(:name => pName, :pID =>id)
pat.save
end
end
Also, This is the query that it constructs:
INSERT INTO `patients` (`created_at`, `name`, `pID`, `updated_at`) VALUES ('2013-05-20 02:04:28', NULL, NULL, '2013-05-20 02:04:28')
This method is called after some other view collects the :name and :pid information in this form:
<%= form_tag('/page/pat_create') do %>
<%= text_field_tag :name, nil%>
<%= text_field_tag :pid, nil%>
<%= button_tag(type: "submit", class: "btn btn-success") do %>
Register<i class="icon-white icon-plus"></i>
<%end%>
<%end%>
Needless to say, pat.errors.empty? is true, and so is pat.save.
Any idea why this happens?
Here's Patient Model:
class Patient < ActiveRecord::Base
attr_accessible :name, :pID
attr_accessor :name, :pID
has_many :appointments
validates_presence_of :name
validates :name, :format => {:with=> /^[a-zA-Z\s\D]+$/}
validates_presence_of :pID
validates_numericality_of :pID
validates_inclusion_of :pID, :in => 100000..9999999999
end
Remove the following line in class Patient:
attr_accessor :name, :pID
What happened was that attr_accessor replaced the two database column attributes :name and :pID (which were automatically generated) with its own, resulting in two virtual attributes, :name and :pID.
Thus, the virtual attributes were being set and validated instead of the corresponding database attributes, which resulted in no errors yet null values in the database.
Can you show us how this method is called? Also are you sure that params[:name] and params[:pid].
You have used the column :pid and :pID, as below
pat = Patient.where(:pID => id).first
if pat == nil
pat = Patient.new(:name => pName, :pID =>id) # should use pat = Patient.new(:name => pName, :pid =>id)
pat.save
end
So in your controller your params are nil but you call .to_s and .to_s.to_i which results in an empty string "" and 0 (zero). You then save them into your database. A couple recommendations:
def pat_create
pat = Patient.new(:name => params[:name], :pid =>params[:pid])
pat.save
end
In addition to the uniqueness validation I would make sure your db column has a unique index on it to insure no duplicate patients.
class Patient < ActiveRecord::Base
attr_accessible :name, :pid
attr_accessor :name, :pid
has_many :appointments
validates :name, presence: true,
length: { maximum: 50 },
format: {:with=> /^[a-zA-Z\s\D]+$/}
validates :pid, presence: true,
numericality: { only_integer: true,
greater_than_or_equal_to: 100000,
less_than_or_equal_to: 9999999999 },
uniqueness: true
end
If you are getting valid values this will work and if not you will see why it is not.
I'm writing a facebook style messaging system for a Rails App and I'm having trouble selecting the Messages for the inbox (with will_paginate).
The messages are organized in threads, in the inbox the most recent message of a thread will appear with a link to it's thread. The thread is organized via a parent_id 1-n relationship with itself.
So far I'm using something like this:
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
has_many :children, :class_name => "Message", :foreign_key => "parent_id"
belongs_to :thread, :class_name => "Message", :foreign_key => "parent_id"
end
class MessagesController < ApplicationController
def inbox
#messages = current_user.received_messages.paginate :page => params[:page], :per_page => 10, :order => "created_at DESC"
end
end
That gives me all the messages, but for one thread the thread itself and the most recent message will appear (and not only the most recent message). I can also not use the GROUP BY clause, because for the thread itself (the parent so to say) the parent_id = nil of course.
Anyone got an idea on how to solve this in an elegant way? I already thought about adding the parent_id to the parent itself and then group by parent_id, but I'm not sure if that works.
Thanks
My solution would be to get a list of threads (which I'm assuming could be obtained by messages with no parent id). Then on the Message model, add a method that will find the latest message in the thread and return it. You can then use that method to obtain the latest method in each thread and put in a link to the head of the thread easily.
(Pseudo-)code:
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
has_many :children, :class_name => "Message", :foreign_key => "parent_id"
belongs_to :thread, :class_name => "Message", :foreign_key => "parent_id"
def get_last_message_in_thread()
last_message = self
children.each do |c|
message = c.get_last_message_in_thread()
last_message = message if message.created_at > last_message.created_at
end
return last_message
end
end
class MessagesController < ApplicationController
def inbox
#messages = current_user.received_messages.find_by_parent_id(Null).paginate :page => params[:page], :per_page => 10, :order => "created_at DESC"
end
end
You could probably do a lot better than having a recursive function to find the last message in the thread, but it's the simplest solution I can think of to demonstrate the idea. I'm also not sure I have the correct syntax for finding unset parent id's in the inbox function, which is why I marked the code as pseudo code :)
giving the parent itself as parent makes it very easy to create queries that operate on the whole thread, because you can group (or anything similar) by parent_id.
if you handle the parents differently, all your queries have to take care of this too
The only efficient way would be to have a Thread model and use GROUP BY as you mentioned - Anything else would require iteration over the messages.
read update in comments
I figured the only good solution is using a second model to store the most recent messages for every thread (because of performance issues when using GROUP BY with a subselect, see my comments). It won't take a lot of space in the DB because we're only storing id's and no text or even blobs.
The RecentMessages Model would look something like this:
create_table :recent_messages do |t|
t.integer :sender_id
t.integer :recipient_id
t.integer :message_id
t.integer :message_thread_id
t.timestamps
end
class RecentMessage < ActiveRecord::Base
belongs_to :message
belongs_to :message_thread, :class_name => 'Message'
belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
end
The main idea is: All the messages are stored in one model (Messages). Whenever a new message is added to a thread (or a thread is created), two things happen (e.g. with a after_save callback):
Store the new message in the RecentMessages model (that means sender_id, recipient_id, message_id, message_thread_id (= parent_id || id))
Get the most recent message (from this thread in messages), where sender_id == recipient_id and vice versa (note: This only works if the message model should only support messages between 2 users) and store it in the RecentMessages model as well (if found and if it's not already there)
Of course there should only be max. 2 recent_messages stored in the DB for every message_thread at any given time.
If one wants to show i.e. the inbox, the following has to happen:
#messages = current_user.recent_received_messages.paginate :page => params[:page], :per_page => 10, :order => "created_at DESC", :include => :message
That's the best I figured out so far. I still think it's ugly but it's fast and it works. If anyone comes up with a better solution, I'll be gratefull!
I don't know how to accomplish this in Rails, but this is how I did it directly in MySQL:
select * from messages where message_id in (
select max(message_id) from messages where to_uid = 51 group by thread_id
) order by timestamp desc
I used a subquery to grab the most recent message in a thread and then the main query to grab all of the fields for the messages found in the subquery.