Traversal of Complex Data Structure in a Rails Application - ruby

I have a data structure in which Topics have subtopics, which again have subtopics, continuing down from the original Topic about six levels. Each of these topics has multiple subtopics.
I'm looking for a way to traverse this data and bring back data affiliated from each of the subtopics... as if pulling the data I want "downstream".
For example Given a topic structure:
Harry_Potter > Characters > Slitherin_House.videos
(Assuming that slitherin house has subtopics for each of the members, Malfoy, Crabbe, etc. ) I want the videos for each of the members to appear in the video lists for Slitherin_House, Characters, and Harry_Potter (each of the ancestors).
I've been looking around and stumbled across Ancestry and Acts As Tree and read through the code and tried my hand at using them, but they seem more oriented around the ancestor side of things, as opposed to accessing and pulling data from the children.
I also have tried my hand at using the associations
has_many :through, and has_and_belongs_to_many
but have been unsuccessful in my attempts to create a working traversal system. And can't seem to complete wrap my head around how to do this.
Does anyone have any ideas or suggestions on what to do given such a predicament? Or know of gems which provide for any such functionality?
Relationship class & model: (as it should flow like a stream)
class CreateStreams < ActiveRecord::Migration
def change
create_table :streams do |t|
t.integer :downstream_id
t.integer :upstream_id
t.timestamps
end
add_index :streams, :downstream_id
add_index :streams, :upstream_id
add_index :streams, [:downstream_id, :upstream_id], unique: true
end
end
# == Schema Information
#
# Table name: streams
#
# id :integer not null, primary key
# downstream_id :integer
# upstream_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Stream < ActiveRecord::Base
attr_accessible :downstream_id
belongs_to :subsidiary, class_name: "Topic"
belongs_to :tributary, class_name: "Topic"
validates :downstream_id, presence: true
validates :upstream_id, presence: true
end
Topic Model
# == Schema Information
#
# Table name: topics
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# slug :string(255)
# wiki :string(255)
# summary :string(255)
class Topic < ActiveRecord::Base
extend FriendlyId
attr_accessible :name, :wiki, :summary
has_many :streams, foreign_key: "downstream_id", dependent: :destroy
has_many :tributaries, through: :streams, source: :tributary
has_many :reverse_streams, foreign_key: "upstream_id",
class_name: "Stream",
dependent: :destroy
has_many :subsidiaries, :through => :reverse_streams, source: :subsidiary
friendly_id :name, use: :slugged
validates :name, presence: true, length: { maximum: 50 },
uniqueness: { case_sensitive: false }
def downstream?(other_topic)
streams.find_by_downstream_id(other_topic.id)
end
def flow!(other_topic)
streams.create!(downstream_id: other_topic.id)
end
def dam!(other_topic)
streams.find_by_downstream_id(other_topic.id).destroy
end
end
Note: I also want to be able to assign a subtopic, multiple parents. So that characters could potentially get put underneath of "Actors" as well for example.

if you want to set this up in a simple way i'd go for a recursive relation. This means a topic can belong to another topic (nested)
The database model of the topic would look like:
Topic
id
topic_id
title
the model would then be:
class Topic < ActiveRecord::Base
belongs_to :topic
has_many :topics
end
Now if you have a topic. you can access its parents by doing .topic and its childs with .topics. A Topic with no parent is a toplevel one and a Topic with no childs is a end node.

Related

Ruby finding all objects where has_one relation is true and is in array

I am currently trying to implement a has_one relationship where you are able to search through all of blogs active posts. My problem comes where even though i explicitly search through the active_post relation it will return everything in its posts instead and not just the active ones which is what i want.
# == Schema Information
#
# Table name: blog
#
# id :integer not null, primary key
# name :string(200)
# context :string(20)
# show_on_summary :boolean
#
class Blog < ApplicationRecord
has_many :posts, dependent: :destroy, autosave: true
has_one :active_post, -> { order('created_at DESC').where(posts: { status_code: 'active' }) }, class_name: 'Post', foreign_key: :blog_id
# == Schema Information
#
# Table name: posts
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# status_code :string(20)
# blog_id :integer
#
class Post < ApplicationRecord
STATUS_CODES = %w(active pending deactivated)
belongs_to :blog
and yet when i run the code below where i have a list of posts of any type of status_code and am trying to find the blog where only their active_post relation matches, it will return every blog that has a post in that list regardless of status_code.
Blog.where(active_post: list_of_posts)
When i am debugging i can go to the individual blogs and look at their 'active_post' relation and it will show the correct latest active post. if there is no active post it will return nil which is what i want.
I don't think you can do it without manually joining your table
Blog.joins(:active_post).where(posts: { id: list_of_posts })
When you join your table, you ensure that the relation is going to be used, and specifying the "posts"."id" column also ensures that it won't use a subquery to match the Blogs ids
I think you don't need to specify the :posts table in the where clause:
class Blog < ApplicationRecord
has_many :posts, dependent: :destroy, autosave: true
has_one :active_post, -> { order('created_at DESC').where(status_code: 'active') }, class_name: 'Post', foreign_key: :blog_id
#...
end
Blog.first.active_post
# => return the latest post

How should I use attributes on the "through" object in a has_many: through: ActiveRecord relation?

class Reservation << ApplicationRecord
has_many :charges_reservations, :dependent => :destroy
has_many :charges, :through => :charges_reservations
monetize :price_cents
def associate_charge(charge, portion)
# looking for help here
end
def owing
price - amount_paid
end
def amount_paid
# looking for help here
end
end
class ChargesReservation << ApplicationRecord
belongs_to :charge
belongs_to :reservation
monetize :portion_cents
end
class Charge << ApplicationRecord
has_many :charges_reservations
has_many :reservations, :through => :charges_reservations
monetize :amount_cents
validates :state, inclusion: {in: %w(failed successful pending)}
def successful?
state == "successful"
end
end
What I want to know is, how to access the portion attribute on
ChargesReservation both when associating the charges with the reservations,
and when asking whether the reservation is fully paid. Both Charge and
Reservation are created at different points in the user flow.
So my question is twofold:
what's the best way to create the association once I have a charge?
how do I get the sum of a reservation's portions of all successful charges
associated with it, as a model method. I know, roughly, how to achieve this
in SQL (I'm rusty, but I'd get there) but I'm stumped in ActiveRecord.

Error creating new method of nested resources

I have my resources:
resources :flows do
resources :fmodules
end
the new method in fmodules controller:
# /flows/1/fmodules/new
def new
#flow = Flow.find(params[:flow_id])
#fmodule = #flow.fmodules.build
end
the models:
class Flow < ApplicationRecord
has_many :fmodules, dependent: :destroy
validates :code, presence: true, length: { maximum: 5 }
validates :name, presence: true
end
class Fmodule < ApplicationRecord
belongs_to :flow
end
When i try to go at /flows/1/fmodules/new ruby says unknown attribute 'flow_id' for Fmodule.
I dont know what is wrong
Here is the migration of Fmodel in addition
class CreateFmodules < ActiveRecord::Migration[5.1]
def change
create_table :fmodules do |t|
t.string :code
t.string :name
t.string :f_code
t.timestamps
end
add_foreign_key :fmodules, :flows, column: :f_code
end
end
So, the problem is that you don't have a flow_id in your fmodules table. In rails, by convention the foreign key is build automatically inferring column name from an argument you pass to belongs_to. That's why rails think that foreign key for flows table is flow_id and it raises exception not finding it. You can overwrite the default with foreign_key option like the following
class Fmodule < ApplicationRecord
belongs_to :flow, foreign_key: : f_code
end

Configuring the proper join column in Rails Admin

I have two models, which associate with each other through a has_and_belongs_to_many relationship.
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: :stage_number
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands
end
Assume both tables have an id field, and that stage has a stage_name field.
They're related to each other through a table called bands_stages, with a schema that looks similar to this:
create_table :bands_stages, id: false do |t|
t.integer :band_id
t.integer :stage_number
end
My intention is to use Rails Admin to allow us to modify certain fields on the Stage, but every time that runs, I get an SQL error doing so:
column stages.id does not exist
It seems that Rails Admin is picking the wrong column by default to join on. How would I inform Rails Admin that I want it to join on a column that actually exists in my join table?
Note that I can't actually make use of the ID in the stages table. The intention is that only ten stages exist at any given time, denoted by their stage number, but every band can visit each stage. Since an ID would automatically increment, it seems to be safer and more explicit to its intent to leverage the more concrete :stage_number field instead.
I'm sure that it's not a problem of rails admin but habtm association.
To make habtm use the right column in sql primary key must be specified for stage model and foreign key for association.
And it is the only way to make it works right.
class Stage < ActiveRecord::Base
self.primary_key = "stage_number"
has_and_belongs_to_many :bands, foreign_key: :stage_number
end
But I think the best way is to use joint model and has_many/belongs_to because for has_many/belongs_to it's possible to set any column to be used as primary key via :primary_key option.
class BandStageLink < ActiveRecord::Base
self.table_name = "bands_stages"
belongs_to :band
belongs_to :stage, foreign_key: :stage_number, primary_key: :stage_number
end
class Band < ActiveRecord::Base
has_many :band_stage_links
has_many :stages, through: :band_stage_links, foreign_key: :stage_number
end
class Stage < ActiveRecord::Base
has_many :band_stage_links, primary_key: :stage_number, foreign_key: :stage_number
has_many :bands, through: :band_stage_links
end
Update: Note that in this case there is still no need to specify any primary keys for stage table. For instance my migration is:
class CreateStageBandTables < ActiveRecord::Migration
def change
create_table :bands_stages, id: false do |t|
t.integer :band_id
t.integer :stage_number
end
create_table :bands do |t|
t.string :name
end
create_table :stages, id: false do |t|
t.integer :stage_number
t.string :name
end
end
end
I tested both cases for rails 4.2.5 and everything works just fine.
Edit - I did mis-understand the primary key bit, I think the desire was to tell Rails to use different attribute as PK, which should be less problematic than re-purposing the auto-increment-by-default PK ID. In that case, the Stage model should include self.primary_key = "stage_number", and the rest of the details at the bottom of this answer relating to HABTM alongside that. Of course has-many-through would still be my preferred solution here.
I think there's a bigger problem with the models and approach, than Rails Admin.
If I understand what you're trying to do, then you'd also need to turn off auto-increment for the primary key in stages table, to hold arbitrary numbers (representing stage numbers) as primary key IDs. It could end badly very quickly, so I'd advise against it.
If the data is genuinely static (10 stages ever), you could even keep it as a constant in the Band model and scrap Stage completely (unless there's more there), e.g.
class Band < ActiveRecord::Base
POSSIBLE_STAGES = [1, 2, ...]
validates :stage, inclusion: { in: POSSIBLE_STAGES, message: "%{value} is not a stage we know of!" }
end
For a table-based approach, I would suggest has-many-through, it'll save you a lot of pain in the future (even if you don't need additional attributes on the join table, things like nested forms are a little easier to work with than in HABTM). Something like this:
class Band < ActiveRecord::Base
has_many :events
has_many :stages, through :events
# band details go into this model
end
class Event < ActiveRecord::Base
belongs_to :band
belongs_to :stage
# you could later add attributes here, such as date/time of event, used_capacity, attendee rating, and
# even validations such as no more than X bands on any given stage at the same time etc.
end
class Stage < ActiveRecord::Base
has_many :events
has_many :bands, through :events
# stage number/details go into this model
end
The migration for that could look something like this:
create_table :bands do |t|
t.string :bandname
# whatever else
end
create_table :events do |t|
t.belongs_to :band
t.belongs_to :stage
# you could add attributes here as well, e.g. t.integer :used_capacity
end
create_table :stages do |t|
t.integer :number
t.integer :total_capacity
# whatever else
end
As you can see primary key IDs are not touched here at all, and I would always avoid storing business data in Rails' and databases' plumbing of any sort (which is what I consider IDs to be, they're there to ensure relation/integrity of the data in a relational database, as well as nice and consistent mapping to ActiveRecord - all business data should be beside that, in actual attributes, not plumbing used to connect models).
If you still want HABTM and re-purposing of primary ID, then I think Stage should include a foreign_key statement to "advertise" itself to the bands_stages join table as having a custom key name (in bands_stages only), while keeping the association_foreign_key on the Band end to show what you want to query in the join table to reach the other side. The stages table would still utilise id though as its primary key, you'd just want to turn off auto-increment with something like t.integer :id, :options => 'PRIMARY KEY' (might be dependent on the database flavour - and again, I would advise against this).
Your models would look like this:
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: "stage_number"
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands, foreign_key: "stage_number"
end
The connection between bands and bands_stages would be bands.id = bands_stages.band_id, for which many bands_stages.stage_number would be found, and each would be connected to stage via bands_stages.stage_number = stages.id (where stages.id has been re-purposed to represent business data at a likely future peril).
Change the association_foreign_key value to be a string instead of symbol.
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: 'stage_number'
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands, foreign_key: 'stage_number'
end

Storing a boolean bit for a many-to-many relation

I'm writing a forum system (in Ruby, using Sequel), and one of the requirements is for users to be able to "star" a thread, which is vaguely equivalent to "subscription" features most forums support. I'm unsure about how to store the starring in the database, and especially on how to query for starred/unstarred threads for a given user, or checking whether a thread is starred.
Any tips would be greatly appreciated, and if you happen to know your way around Sequel, an example model would be absolutely grand.
This is very simple to implement:
First your migration:
create_table(:subscriptions, ignore_index_errors: true) do
primary_key :id
column :created_at, 'timestamp with time zone'
foreign_key :user_id, :users, null: false, key: [:id], index: true, on_delete: :cascade
foreign_key :thread_id, :threads, null: false, key: [:id], index: true, on_delete: :cascade
end
Your Models:
app/models/subscription.rb
class Subscription < Sequel::Model
many_to_one :user
many_to_one :thread
end
app/models/user.rb
class User < Sequel::Model
one_to_many :subscriptions
many_to_many :subscribed_threads,
class: :Thread,
join_table: :subscriptions,
left_key: :user_id,
right_key: :thread_id
end
app/models/thread.rb
class Thread < Sequel::Model
one_to_many :subscriptions
many_to_many :subscribers,
class: :User,
join_table: :subscriptions,
left_key: :thread_id,
right_key: :user_id
end
Query as follows
# all threads a user is subscribed to
user.subscribed_threads
# all subscribers to a thread
thread.subscribers
# all subscriptions to a thread in the last 3 days
thread.subscriptions.where{created_at >= Date.today - 3}
I suggest also configuring the dataset associations plugin on all your models:
# Make all model subclasses create association methods for datasets
Sequel::Model.plugin :dataset_associations
You can then compose and chain queries through associations with conditions more conveniently:
# all new subscribers for a thread in the last 3 days who are moderators
thread.subscriptions.where{created_at >= Date.today - 3}.user.where(moderator: true)
There are some powerful filtering and querying possibilities:
http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
http://sequel.jeremyevans.net/rdoc/files/doc/querying_rdoc.html

Resources