Updating a 2-dimensional form - ruby-on-rails

Hey guys. I'm new-ish to rails development and have hit a bit of a wall. The application I'm working on is a scheduling solution that requires updating a join model, but not in a simple 1:1 sort of way.
The app is laid out as follows:
class Route < ActiveRecord::Base
has_many :markers, :foreign_key => 'source_id'
has_many :schedules
accepts_nested_attributes_for :markers, :allow_destroy => true, :reject_if => proc { |a| a['name'].blank? }
accepts_nested_attributes_for :schedules, :allow_destroy => true, :reject_if => proc { |a| a['name'].blank? }
end
class Schedule < ActiveRecord::Base
has_many :arrivals
has_many :markers, :through => :arrivals
accepts_nested_attributes_for :arrivals, :allow_destroy => true, :reject_if => :all_blank
end
class Marker < ActiveRecord::Base
has_many :arrivals
has_many :schedules, :through => :arrivals
end
class Arrival < ActiveRecord::Base
belongs_to :marker
belongs_to :schedule
end
... so a basic has_many :through ... or so I would think :P
When you create a route, you can create 1..n schedules, and 1..n markers. Editing a schedule should allow you to add 1..n arrival entries for each marker defined in the route. THIS is what's causing me grief.
Through the magic of ascii-art, this is what I want the app to look like:
/views/routes/edit.html.erb (works already)
ROUTE
-----
...
SCHEDULES
---------
[Add]
* Schedule 1 [Edit][Delete]
* Schedule 2 [Edit][Delete]
...
MARKERS
-------
[Add]
* Marker 1 [Edit][Delete]
* Marker 2 [Edit][Delete]
* Marker 3 [Edit][Delete]
* Marker 4 [Edit][Delete]
...
/views/schedules/edit.html.erb
SCHEDULE X
----------
[Add Col.]
Marker 1 [ ] [ ]
Marker 2 [ ] [ ]
Marker 3 [ ] [ ]
Marker 4 [ ] [ ]
[x] [x]
(the [x] should remove a column)
EDIT (09NOV04):
I've removed the incomplete view code I originally had posted, but would like to update the question a bit.
I think part of the confusion here (for myself, and possibly for anyone who might be able to help) is that I haven't explained the relationships properly.
markers have many arrivals
schedules have many markers
routes have many schedules
That's the basics.
Having a form that would update arrivals for a single marker wouldn't be difficult, as that's a basic form. What I'm hoping to do is to provide a form that updates all markers at the same time.
When you click on "Add Entry", it should add a new arrival for each marker that's currently available. Under each "column", there should be a "remove" button, that will remove each arrival for that particular column (so from each marker).
I'm not sure if that clears it up any :P

When you create a route, you can
create 1..n schedules, and 1..n
markers. Editing a schedule should
allow you to add 1..n arrival entries
for each marker defined in the route.
THIS is what's causing me grief.
There is nothing linking markers to routes as far as schedules are concerned. The way you've laid things out any schedule can add any number arrival entries for each marker defined in your database.
You need make a few changes to get the functionality you want. I'm assuming that a schedule belongs_to a route. I've also left out the accepts_nested_attributes_for lines out, to conserve space.
class Route < ActiveRecord::Base
has_many :markers, :foreign_key => 'source_id'
has_many :schedules
...
end
class Schedule < ActiveRecord::Base
has_many :arrivals
belongs_to :route
has_many :markers, :through => :arrivals
has_many :route_markers, :through => :route, :source => :markers
...
end
class Marker < ActiveRecord::Base
has_many :arrivals
has_many :schedules, :through => :arrivals
...
end
class Arrival < ActiveRecord::Base
belongs_to :marker
belongs_to :schedule
...
end
Now #schedule.route_markers returns a list of markers in the route linked to the schedule.
You can use those to generate your grid. Then create arrival objects to establish a marker in a specific schedule.
Then it's just a matter of #schedule.markers= list_of_markers and rails takes care of creating/deleting entries in the join table.
Sorry but without knowing more, I'm not going to speculate on what the view will look like.

Related

Ruby/rails: saving a sample of has_many relationship to database?

Asking this question re: a student project I'm working on and I have tinkered for too long without being able to come up with a solution.
I have a class called Game, a game has many quotes. If I create a game #game = Game.create and then associate it with characters #game.characters = [#character1, #character2]. Because of my associations, I have access to all of the quotes of both characters with #game.quotes (hundreds of objects returned).
I'd like to be able to grab a sample of 10 of the quotes, something like #game.ten_quotes (an array of objects) will return a random sample of #game.quotes. I also want #game.ten_quotes to be saved to the database.
My first thought is that I need a new attribute for Game in the migration:
class CreateGames < ActiveRecord::Migration[5.1]
def change
create_table :games do |t|
t.text :state, default: "[]"
t.boolean :completed, default: false
t.something :ten_quotes
# what would this look like if I'm saving an array of objects?
t.timestamps
end
end
end
In my rails controller below I was able to generate the ten quotes but I feel that I'm working in the wrong direction:
class Game < ApplicationRecord
has_many :game_logs
has_many :characters, through: :game_logs
has_many :quotes, through: :characters
def generate_quotes
if self.ten_quotes == []
x = quotes.shuffle.sample(10)
self.ten_quotes = x
else
return false
end
end
end
How can I get a sample of quotes, associate that sample with a game instance and then save the game instance to the database one time with no chance to overwrite in the future? Do I need a new model?
Thanks in advance if you'd like to assist. Otherwise, have a great day!
class AddTenQuotesIdsColumnToGames < ActiveRecord::Migration[5.1]
def change
add_column :games, :ten_quotes_ids, :text
end
end
class Game < ApplicationRecord
has_many :game_logs
has_many :characters, through: :game_logs
has_many :quotes, through: :characters
serialize :ten_quotes_ids, Array
def ten_quotes
if ten_quotes_ids.length == 10
quotes.where(id: ten_quotes_ids)
else
ten_quotes = quotes.order('RANDOM()').limit(10) # for MySQL use `quotes.order('RAND()').limit(10)`
update ten_quotes_ids: ten_quotes.map(&:id)
ten_quotes
end
end
end
Add a column of type text to your Game model called ten_quotes_ids. In your model, serialize the ten_quotes_ids column as an Array. This lets you store arrays of Ruby objects to the database. You could store instances of quotes, but they will not be kept in sync with your database if there are changes, so better to just store the ids so you can fetch the current records on demand.
In your ten_quotes method you're checking if ten_quotes_ids has 10 elements, and if so querying the Game's quotes based on those ids and returning them, or else selecting a random set of 10 quotes belonging to the Game from the database, updating the ten_quotes_ids attribute, and returning the ten quotes.
edit 2
I removed my first idea, based your comment, as your information "a quote should belong to many samples" and "ten quotes were for a game that happened a week ago", my idea creating new model for samples with this relation as follow
class Game < ApplicationRecord
# -> quotes -> samples
has_many :quotes, :dependent => :destroy
accepts_nested_attributes_for :quotes, :allow_destroy => :true
has_many :samples, through: :quotes
end
class Quote < ApplicationRecord
belongs_to :game
has_many :samples, :dependent => :destroy
accepts_nested_attributes_for :samples, :allow_destroy => :true
end
class Sample < ApplicationRecord
belongs_to :quote
scope :just_last_week, lambda { where('created_at >= ?', 1.week.ago)}
end
I add accepts_nested_attributes_for and allow_destroy to make easier you create child record from parent
here is link in case you want to know more
from game controller
# if you want get samples and
# for specific game
#game = Games.find(params[:id])
#samples = #game.samples.limit(10) # this will get 10 samples from samples
#samples = #game.samples.just_last_week.limit(10) # this will get 10 samples created last week
# for samples that created last week no matter what game is
#samples = Sample.all.just_last_week

Ruby apply association on each item of collection

I am trying to access specific objects through associations applied one after the other on a collection. For example, one of my database request would be :
get_current_user.readable_projects.cards.find(params[:card_id]).tasks
get_current_user returns a unique User, readable_projects a collection of projects that this user can read. So far, so good. However, each project has many cards, and I'd like to retrieve all the cards from each project I have in the collection, so that in this result I can do a find for a specific card, and then retrieve its tasks, the association between Card and Task being also a has_many.
I could iterate through the projects with a each, but the problem is I want to use the default behavior of a find, so that if in all the cards from all the projects I don't find the one I was looking for, the default RecordNotFound routine is triggered. I could also use find_by and raise the exception manually, doing something like that :
#projects = get_current_user.readable_projects
#projects.each do |p|
#found = p.cards.find_by(id: params[:card_id])
break if #found.present?
end
if #found.present?
#tasks = #found.tasks
else
raise ActiveRecord::RecordNotFound
end
However my main objective is to get this card in a way anyone reading the code could easily understand what I am doing here.
All my model relationships are what follow :
User.rb :
has_many :reader_projects, -> { where "memberships.status = #{Membership::ACTIVE} AND memberships.role_id >= #{Membership::READER} " },
through: :memberships, :class_name => 'Project', :source => :project
has_many :contributor_projects, -> { where "memberships.status = #{Membership::ACTIVE} AND memberships.role_id >= #{Membership::CONTRIBUTOR} " },
through: :memberships, :class_name => 'Project', :source => :project
has_many :admin_projects, -> { where "memberships.status = #{Membership::ACTIVE} AND memberships.role_id >= #{Membership::ADMIN} " },
through: :memberships, :class_name => 'Project', :source => :project
def readable_projects
self.reader_projects + self.contributable_projects
end
def contributable_projects
self.contributor_projects + self.administrable_projects
end
def administrable_projects
self.admin_projects
end
Project.rb :
has_many :cards, inverse_of: :project, dependent: :destroy
Card.rb :
has_many :tasks, inverse_of: :card, dependent: :destroy
My question is : is there a way to do such kind of request in one very understandable line ?
Thank you in advance for your help.

rails 4 many to many through pivot table

My models:
class Entrant < ActiveRecord::Base
has_many :events, :through => :event_maps
has_many :event_maps, :foreign_key => "entrant_id"
accepts_nested_attributes_for :events, :reject_if => :all_blank
end
class Event < ActiveRecord::Base
has_many :event_maps, :foreign_key => "event_id"
has_many :entrants, :through => :event_maps
accepts_nested_attributes_for :entrants, :reject_if => :all_blank
end
class EventMap < ActiveRecord::Base
belongs_to :event, foreign_key: "event_id"
belongs_to :entrant, foreign_key: "entrant_id"
end
My mappings are correct as far as I can tell, on the console I can do following:
create a new event and add a new entrant:
#event = Event.new(name: 'my event');
#event.save
#event.entrants_attributes = [{name: 'Jack'}]
#event.save
create a new entrant and add a new event:
#entrant = Entrant.new(name: 'Peter')
#entrant.save
#entrant.events_attributes = [{name: 'Great concert'}]
#entrant.save
Now how would I map Peter to my event or Jack to Great concert?
Meaning
I want to register an existing Entrant to an existing Event,
Add a new Entrant to an existing Event or vice versa.
As I said, the many 2 many seems to work both ways, but adding data to the pivot table on existing objects is not really clear to me. Thanks for the help.
Edit: ok I got 2.
#event.entrants.new(name: "hello") #adds a new Entrant works
You can do (as j03w suggested)
#event.entrants << #entrant
The << is the Binary Left Shift Operator which
Binary Left Shift Operator. The left operands value is moved left by
the number of bits specified by the right operand.
It's also used for arrays to push the given object on to the end of this array. This expression returns the array itself, so several appends may be chained together.
$: [] << 'a'
-> ['a']

Multiple entries in a :has_many through association

I need some help with a rails development that I'm working on, using rails 3.
This app was given to me a few months ago just after it's inception and I have since become rather fond of Ruby.
I have a set of Projects that can have resources assigned through a teams table.
A team record has a start date and a end date(i.e. when a resource was assigned and de-assigned from the project).
If a user has been assigned and deassigned from a project and at a later date they are to be assigned back onto the project,
instead of over writting the end date, I want to create a new entry in the Teams table, to be able to keep a track of the dates that a resource was assigned to a certain project.
So my question is, is it possible to have multiple entries in a :has_many through association?
Here's my associations:
class Resource < ActiveRecord::Base
has_many :teams
has_many :projects, :through => :teams
end
class Project < ActiveRecord::Base
has_many :teams
has_many :resources, :through => :teams
end
class Team < ActiveRecord::Base
belongs_to :project
belongs_to :resource
end
I also have the following function in Project.rb:
after_save :update_team_and_job
private
def update_team_and_job
# self.member_ids is the selected resource ids for a project
if self.member_ids.blank?
self.teams.each do |team|
unless team.deassociated
team.deassociated = Week.current.id + 1
team.save
end
end
else
self.teams.each do |team|
#assigning/re-assigning a resource
if self.member_ids.include?(team.resource_id.to_s)
if team.deassociated != nil
team.deassociated = nil
team.save
end
else
#de-assigning a resource
if team.deassociated == nil
team.deassociated = Week.current.id + 1
team.save
end
end
end
y = self.member_ids - self.resource_ids
self.resource_ids = self.resource_ids.concat(y)
self.member_ids = nil
end
end
end
Sure, you can have multiple associations. has_many takes a :uniq option, which you can set to false, and as the documentation notes, it is particularly useful for :through rel'ns.
Your code is finding an existing team and setting deassociated though, rather than adding a new Team (which would be better named TeamMembership I think)
I think you want to just do something like this:
add an assoc for active memberships (but in this one use uniq: => true:
has_many :teams
has_many :resources, :through => :teams, :uniq => false
has_many :active_resources,
:through => :teams,
:class_name => 'Resource',
:conditions => {:deassociated => nil},
:uniq => true
when adding, add to the active_resources if it doesn't exist, and "deassociate" any teams that have been removed:
member_ids.each do |id|
resource = Resource.find(id) #you'll probably want to optimize with an include or pre-fetch
active_resources << resource # let :uniq => true handle uniquing for us
end
teams.each do |team|
team.deassociate! unless member_ids.include?(team.resource.id) # encapsulate whatever the deassociate logic is into a method
end
much less code, and much more idiomatic. Also the code now more explicitly reflects the business modelling
caveat: i did not write a test app for this, code may be missing a detail or two

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources