Rails join three models and compute the sum of a column - ruby-on-rails

I need some help over here to understand how the model relationship works on rails. Let me explain the scenario.
I have created 3 models.
Properties
Units
Rents
Here is the how relationship mapped for them.
Model #property.rb
class Property < ActiveRecord::Base
has_many :units
has_many :rents, :through=> :unit
end
Model #unit.rb
class Unit < ActiveRecord::Base
belongs_to :property
has_many :rents
end
Model #rent.rb
class Rent < ActiveRecord::Base
belongs_to :unit
end
here is the the schema
create_table "units", :force => true do |t|
t.integer "property_id"
t.string "number"
t.decimal "monthly_rent"
end
create_table "rents", :force => true do |t|
t.integer "unit_id"
t.string "month"
t.string "year"
t.integer "user_id"
end
OK, here is my problem. Let's say I have 2 properties
property A
property B
and
property A has unit A01,A02,A03
property B has unit B01,B02,B03
I need to generate a report which shows the SUM of all the outstanding rents based on the property and month
So here is how it should be looks like. (tabular format)
Property A - December - RENT SUM GOES HERE
Property B - December - RENT SUM GOES HERE
So I got all the properties first. But I really can't figure out a way to merge the properties and units (I guess we don't need the rents model for this part) and print them in the view. Can someone help me to do this. Thanks
def outstanding_by_properties
#properties = Property.find(:all)
#units = Unit.find(:all,:select=>"SUM(monthly_rent) as total,property_id",:conditions=>['property_id IN (?)',#properties])
end

I think something like this will work for you. Hopefully an SQL guru will come along and check my work. I'm assuming your Property model has a "name" field for "Property A," etc.--you should change it to whatever your field is called.
def outstanding_by_properties
Property.all :select => "properties.name, rents.month, SUM(units.monthly_rent) AS rent_sum",
:joins => { :units => :rents },
:group => "properties.id, rents.month, rents.year"
end
This should return an array of Property objects that have the attributes name, month, and rent_sum.
It basically maps to the following SQL query:
SELECT properties.name, rents.month, SUM(units.monthly_rent) AS rent_sum
FROM properties
JOIN units ON properties.id = units.property_id
JOIN rents ON units.id = rents.unit_id
GROUP BY properties.id, rents.month, rents.year
The JOINs connect rows from all three tables and the GROUP BY makes it possible to do a SUM for each unique combination of property and month (we have to include year so that e.g. December 2008 is not grouped together with December 2009).

Related

Referencing a column on a table to a column on another table Ruby on Rails

I was reading another question on here regarding referencing columns from two separate tables but was a little confused if it addressed my issue. What's going on is I have two tables, Destination and Booking. The Destination table has a column for location_id, and the Booking has a column for location, and I am trying to reference location in Booking table from location_id column in Destination table.
Here is my table for Booking(migration)
class CreateBookings < ActiveRecord::Migration[6.1]
def change
create_table :bookings do |t|
t.string :name
t.string :start_date
t.string :end_date
t.string :email
t.integer :location
t.timestamps
end
end
end
and here is my table(Migration) for Destination
class CreateDestinations < ActiveRecord::Migration[6.1]
def change
create_table :destinations do |t|
t.string :address
t.string :city
t.string :state
t.string :zip
t.integer :location_id
t.timestamps
end
end
end
My Models are setup currently as
class Booking < ApplicationRecord
# belongs_to :reservation, optional: true
has_many :destinations, :class_name => 'Destination', :foreign_key=> 'location_id'
validates :name, :start_date, :end_date, :email, presence: true
end
and
class Destination < ApplicationRecord
has_many :bookings, :class_name => 'Booking', :foreign_key=> 'location'
end
Am I currently referencing the columns correctly, or is there something else I should be doing?
How you should write your migrations depends on the association between your models. Foreign keys go onto tables that have a belongs_to association.
Can a single Booking have multiple Destinations? If the answer is no, you need to change the association in your Booking model to belongs_to :destination and then put a :destination_id on your bookings table (you can give it a custom name like :location_id if you want but the convention is to use the model name).
If a single Booking can have multiple Destinations, and surely a single Destination can have multiple Bookings, then you have a many-to-many relationship. In that case you will not put foreign keys on the destinations table, nor the bookings table. Instead you will need a join table between them and that's where the foreign keys go.
Rails gives 2 different ways to declare many-to-many relationships. See https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many.
If you want to use has_and_belongs_to_many, your models would look like this:
class Booking < ApplicationRecord
has_and_belongs_to_many :destinations
end
class Destination < ApplicationRecord
has_and_belongs_to_many :bookings
end
And the migration would look like this:
class CreateBookingsAndDestinations < ActiveRecord::Migration[6.0]
def change
create_table :bookings do |t|
# ...
end
create_table :destinations do |t|
# ...
end
create_table :bookings_destinations, id: false do |t|
t.belongs_to :booking
t.belongs_to :destination
end
end
end
Caveat: Based on your question I'm assuming you want a booking to have a destination. If you want a destination to many bookings and vise-versa, Sean's answer is great.
I think you're misunderstanding how foreign keys / associations work in databases.
It sounds like you want a column in the bookings table to "reference" a value column in the destinations table (or maybe the opposite), as in:
bookings.location -> destinations.location_id or maybe destinations.location_id -> bookings.location.
That's not typically what we mean by "reference" in a relational database. Instead, when you say that a table (for example, a 'comments' table) references another table (for example, a comments table references a user table), what we typically mean is that we're storing the primary key column of the referenced table (e.g. the user's id) in a column in the first table (e.g. comments.user_id --> users.id).
From an english language standpoint I expect that you want a booking to refer to a destination, so I'm going to assuming we want a the booking table to reference/refer to the destinations table, like this:
booking.location -> destinations.id
In Ruby on Rails, the convention is to name a column that stores an association with the same as the table it references, plus _id, like so the convention would be this:
booking.destination_id -> destinations.id
A common way to create this in a migration would be with:
add_reference :bookings, :destination
When adding a reference in a database you almost always want to index by that value (so that you can do Bookings.where(destination_id: #destination.id) and not kill your database). I am also a strong advocate for letting your database enforce referential integrity for you, so (if your database supports it) i'd recommend the following:
add_reference :destinations, :booking, index: true, foreign_key: true
This would prevent someone from deleting a destination that has a booking associated with it.

How do I sort records based on sum of a particular attribute in a set of associations in Ruby on Rails?

guys! I would like to sort comments based on the total ratings score where the total rating score is the sum of the ratings' score attributes for each comment.
class Rating < ActiveRecord::Base
belongs_to :comment, :class_name => 'Comment', :foreign_key => 'comment_id'
end
class Comment < ActiveRecord::Base
has_many :ratings
end
Rating schema
create_table "ratings", force: true do |t|
t.integer "user_id"
t.integer "comment_id"
t.integer "score"
t.datetime "created_at"
t.datetime "updated_at"
end
Thanks for your help!
You should be able to order by the sum of associated records columns like this
Comment.joins(:ratings).group('comments.id').order('sum(ratings.score) desc')
Take a look at this answer for doing the count through a select call.
Order Players on the SUM of their association model. This would be the suggested way.
Another way would be to include a method to sum all the rating's scores in your comment model.
def rating_score_sum
ratings.sum(:score)
end
Then, you can sort your collection using that method.
Comment.all.sort_by(&:rating_score_sum)
Though this will calculate the score sum of all ratings for each comment every time and may become an issue as your database grows. I would consider saving that sum on the comments table and updating it on every new rating.
Cheers!
You should be able to do this easily with:
Comment.joins(:ratings).select('comments.*, SUM(ratings.score) as rating_score').order('rating_score DESC')
You could also try using includes instead of joins or even eager_load as it will preload the association (ratings) and improve the performance of this query.

Improve my data pull for a simple "announcements" rails app

I'm working through the RailsTutorial but making an "Announcements" webapp for the middle school I teach at (tweaking the given Twitter clone).
When users create an announcement, they use check boxes to determine which grades it should be displayed to (1-3 grades could be true). This is working correctly, with me storing grades as booleans.
create_table "announcements", :force => true do |t|
t.string "content"
t.integer "user_id"
t.boolean "grade_6"
t.boolean "grade_7"
t.boolean "grade_8"
t.date "start_date"
t.date "end_date"
t.datetime "created_at"
t.datetime "updated_at"
end
My users also have a grade field, which is an integer. I want to use this to make each user's home page show the announcements for their grade.
Example: An 8th grade teacher has grade = 8. When they log in, their home page should only show announcements which have grade_8 = TRUE.
Example: An principal has grade = 0. When they log in, their home page should show all announcements.
I'm struggling with how to translate the integer user.grade value into boolean flags for pulling announcements from the model.
The code I'm writing is working, but incredibly clunky. Please help me make something more elegant! I'm not tied to this db model, if you have a better idea. (In fact, I really don't like this db model as I'm hardcoding the number of grades in a number of locations).
# Code to pull announcements for the home page
def feed
case grade
when 6
grade_6
...
else
grade_all
end
end
# Example function to pull announcements for a grade
def grade_6
Announcement.where("grade_6 = ? AND start_date >= ? AND end_date <= ?",
TRUE, Date.current, Date.current)
the correct way to set this type of relationship up would be to use a many-to-many relationship via has_many through:
class Announcement < ActiveRecord::Base
has_many :announcement_grades
has_many :grades, :through => :announcement_grades
end
class AnnouncementGrades < ActiveRecord::Base
belongs_to :grade
belongs_to :announcement
end
class Grade < ActiveRecord::Base
has_many :announcement_grades
has_many :announcements, :through => :announcement_grades
end
then your migrations will be:
create_table :announcements, :force => true do |t|
t.date :start_date
t.date :end_date
t.timestamps #handy function to get created_at/updated_at
end
create_table :announcement_grades, :force => true do |t|
t.integer :grade_id
t.integer :announcement_id
t.timestamps
#start and end date might be more appropriate here so that you can control when to start and stop a particular announcement by grade rather than the whole announcement globally, depending on your needs.
end
create_table :grades, :force => true do |t|
t.timestamps
#now you have a bona-fide grade object, so you can store other attributes of the grade or create a relationship to teachers, or something like that
end
so, now you can simply find your grade then call announcements to filter:
#grade = Grade.find(params[:id])
#announcements = #grade.announcements
so, that's the correct way to do it from a modeling perspective. there are other considerations to this refactor as you will have to make significant changes to your forms and controllers to support this paradigm, but this will also allow for much greater flexibility and robustness if you decide you want to attach other types of objects to a grade besides just announcements. this railscast demonstrates how to manage more than one model through a single form using nested form elements, this will help you keep the look and feel the same after you apply the changes to your models. I hope this helps, let me know if you need more help doing this, it'll be a bit of work, but well worth it in the end.
Chris's example is theoretically superior. However, your original schema may be more practical if 1) you know your app won't become more complicated, and 2) the US's k-12 system is here to stay (i would bet on it...). If you would prefer to stick with the schema that you already have, here some improvements you could make to the code:
Let's add a 'grade' scope to your Announcement model
class Announcement < ActiveRecord::Base
....
scope :grade, lambda do |num|
num > 0 ? where("grade_#{num} = ?", true) : where('1=1')
end
....
end
This would allow for much simpler coding such as
teacher = User.find(user_id)
announcements = Announcement.grade(teacher.grade).where('start_date >= :today AND end_date <= :today', {:today => Date.today})

Ruby on Rails: How to model recurring times (weekly activities)?

In my Ruby on Rails application, I have a "Group" model that has weekly recurring "activities". Some activities occur only one day a week (Friday at 6:00pm) while some occur multiple times a week (Mon-Fri at 8:00am, or Tues/Thurs at 10:00am).
I am having trouble trying to figure out how to model this data, and how to use Rails to create a form to create/update the data. Do I create an "Activities" table that has a datetime field? Or do I separate the day of the week from the time of day into two separate fields? What about the activities that occur multiple times a week?
Any ideas or advice would be appreciated. Also, I would appreciate knowing if you know of a Gem that helps with this so I don't have to re-invent the wheel.
Update:
For Example, if I needed to display something like this:
Special Group A's Activities
Monday at 10pm - Football
Tues/Thurs at 8am - Tennis
Special Group B's Activities
Monday-Friday at 12pm - Lunch
Saturday at 8am - Breakfast
Sunday at 6pm - Dinner
What steps would I need to take in order to model and display this data, using Ruby on Rails?
Models
group.rb
class Group < ActiveRecord::Base
validates_presence_of :name
has_many :activities, :through => :group_activity
end
activity.rb
class Activity < ActiveRecord::Base
validates_presence_of :name
belongs_to :group
has_many :occurances, :through => :activity_occurance
end
occurance.rb
class Activity < ActiveRecord::Base
validates_presence_of :date
belongs_to :activity
end
Migrations (separate or all together)
add_everything.rb
class AddEverything < ActiveRecord::Migration
def self.up
create_table :groups, :force => true do |t|
t.string :name
t.timestamps
end
create_table :group_activity, :force => true do |t|
t.integer :group_id, :activity_id
t.timestamps
end
create_table :activities, :force => true do |t|
t.string :name
t.timestamps
end
create_table :activity_occurance, :force => true do |t|
t.integer :activity_id, :occurance_id
t.timestamps
end
create_table :occurance, :force => true do |t|
t.datetime :date
t.timestamps
end
end
def self.down
drop_table :groups
drop_table :activities
drop_table :occurances
drop_table :group_activity
drop_table :activity_occurance
end
end
That take's care of your model work. In your groups _form view I would add your associated group name, and fields_for for your activity name and fields_for occurance. In your occurance, use this handy jQuery datetime picker that is an extension off of the jQuery date picker, to populate your occurance field:
http://puna.net.nz/timepicker.htm
You should also have separate views to manage activities separately with it's own respective form. In your show page displaying other fields is pretty standard, but for the occurances you can have something like (haml syntax):
= #group.name
- for activity in #group.activities
= activity.name
- for occurance in activity.occurances
= occurance.date.strftime("%A at %r")
Hope this at least gets you started. You can add additional logic for checking activity.occurances.size to format accordingly if you want to display something day1/day2/day3
https://github.com/jimweirich/texp Jim Weirich's Temporal expressions library is an excellent resource for querying these sorts of things in ruby.
If you don't need to query this set other than looking at all of them in batch, then just serializing that datastructure would probably work for you.
But in the end you'll probably just use a has_many :occurances where occurances start off as date_time homebaked-recurrence-pattern pairs and iterate from there depending on what feature set you need.
If you think it straight, there is a great number of possibilites for you to represent and persist diverse date and time formats and intervals on a database, also you probably are going to change it to add some options to users or to remove options accordingly to the growth of your website.
I would go with creating two fields like "date_start" and "date_end", and one string field containing a code that represents the frequency. Something like 3 chars per code, first can be M for monthly, W weekly; second can be F for first, L for last; third char can be a number for a specific number of the week, F for friday.
The point here is that you can (encode and) decode that programatically so if you add features you won't have to recreate your database relations.
In the same way a group can have multiple activities, I think an activity can have multiple occurances. I would suggest trying to model your database that way, with a occurance table.
Regarding the form, what about a "master - detail" form with activity as the master, and occurance as the detail?

Display column name on view if value present in the column -- Ruby on Rails

I have column LTD in my Company model. After retrieving value from model using
Company.find
If any value is present in LTD column, then I have to display the text "Limited" on the view. I have many columns in the model which are in the abbreviated form and when value is present their long form are displayed on the view. Therefore writing conditions on the view is not feasible.
I was thinking whether writing a custom rails config file containing application constants will do. But I don't have quantitative and qualitative information on this.
Please help. Thanks in advance.
You could create a separate Abbreviation model that your Company model could be associated with through a join model CompanyAbbreviation. Then there would be one join table record for each column in a specific company record. Rather than having each abbreviation as a column in your companies table you would have secondary keys in your company_abbreviations table referring to the associated company and abbreviation records.
Something like the following:
class Company < ActiveRecord::Base
has_many :company_abbreviations
has_many :abbreviations, :through => :company_abbreviations
end
class Abbreviation < ActiveRecord::Base
has_many :company_abbreviations
end
class CompanyAbbreviation < ActiveRecord::Base
belongs_to :company
belongs_to :abbreviation
end
class CreateAbbreviations < ActiveRecord::Migration
def self.up
create_table :abbreviations do |t|
t.string :abbr
t.string :description
end
add_index :abbreviations, :abbr
end
end
class CreateCompanyAbbreviations < ActiveRecord::Migration
def self.up
create_table :company_abbreviations do |t|
t.references :company
t.references :abbreviation
end
add_index :company_abbreviations, :company_id
add_index :company_abbreviations, :abbreviation_id
end
end
In db/seeds.db you could pre-populate your abbreviations table.
You add new associations like this:
#company.company_abbreviations.create(:abbreviation => Abbreviation.find_by_abbr("LTD"))
In your view you can reference the expanded abbreviation columns cleanly like this:
<% #company.abbreviations.each do |abbr| %>
<%= abbr.description %>
<% end %>
You may also want to control the display order in some fashion, say by a sort column in the join table,
This works for me perfectly.
I have declared a global hash in config/environment.rb which maintains the list of all the column name short-forms and long-forms and on the view I just check if value is present in the column I search for the corresponding key value pair from the global hash and display the long-form.
Thanks guyz for giving your time to help me.

Resources