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})
Related
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.
Two Models: An Owner and a Dog:
owner.rb
class Owner < ActiveRecord::Base
has_one :dog
end
dog.rb
class Dog < ActiveRecord::Base
belongs_to :owner
end
And here is the schema:
schema.rb
ActiveRecord::Schema.define(version: 123) do
create_table "dogs", force: true do |t|
t.string "name"
t.integer "energy"
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "dogs", ["owner_id"], name: "index_dogs_on_owner_id"
create_table "owners", force: true do |t|
t.string "name"
t.string "energy"
t.datetime "created_at"
t.datetime "updated_at"
end
end
Pretty simple setup.
I want an owner to take his dog for a walk. When the walk ends, the owner's energy will drop by 5, AND the dog's energy will drop by 20.
Clearly this walk_the_dog action/method, wherever it is going to live, is effecting two objects: an owner object and a dog object (and of course this dog object happens to be associated to this owner).
I don't know where to put this code. I know I could simply create an action within the owners_controller.rb, but that seems like a bad idea. It would look something like this:
owners_controller.rb
class OwnersController < ApplicationController
def walk_the_dog
#owner = Owner.find(params[:id])
#owner.energy -= 5
#owner.dog.energy -= 20 # this line in particular seems like bad OO design
#owner.save
#owner.dog.save
end
...
end
As I understand it, objects should only change state for themselves and shouldn't change the state of other objects. So this appears to be a bad idea because within the owner controller we are changing the state of not just the owner object, but the associated dog object as well.
I have read about services. It seems like walk_the_dog is an excellent case for a service, because services, as I understand it, allow interactions between objects and state changes for multiple objects. I just don't know how to do it/implement it.
Should there be a service object called walk_the_dog? Should their just be a file within a services directory with a bunch of service methods -- one of which is called walk_the_dog and the owners_controller.rb simply utilizes this method in it's controller? I don't know what the next step is.
Note: I can see someone saying "who cares if this breaks OO design. Just do it and if it works, it works." Unfortunately this is not an option. The application I am working on now followed that mindset. The application grew very large, and now maintaining it is very difficult. I want to get this situation down for the major redesign of the app.
Here are the few things that I would do if I were to refactor this code:
Writing numbers in your code is a bad thing, either you have defined them as constants like ENERGY_PER_WALK_FOR_DOG = 20 or a better way is to define a field in the table of Dog model. This way, it will be much better to manage and assign those values.
add_column :dogs, energy_per_walk, :integer, default: 20
add_column :owners, energy_per_walk, :integer, default: 5
I'd create a method in ApplicationController class:
def walk(resources = [])
resources.each do |resource|
resource.lose_walk_energy # you can refine it more!
end
end
In the folder, app/models/concerns, I would write the following module:
module Walkable
extend ActiveSupport::Concern
# subtract energy_per_walk form the energy saved in db
def lose_walk_energy
self.energy -= self.energy_per_walk
save
end
end
And now, your method reduces to the following method:
def walk_the_dog
#owner = Owner.find(params[:id])
walk([#owner, #owner.dog])
end
I would say that this should be a method in the Owner model. You also need to do both operations in one transaction, to ensure, that both models have been updated.
class Owner
has_one :dog
def walk_the_dog
return false if dog.nil?
transaction do
decrement!(:energy, 5)
dog.decrement!(:energy, 20)
end
end
end
I am a bit confused about creating and inserting data in the table.
For example, I have user-table, where all users are saved. A payment-table should save all payments made by user A. The payments will not be updated. Once user A is registered, he fills up a form where he types his payments and there are saved and nothing more. ( I know it sounds strange but my database is based on statistical analysis of the files. Once a file is loaded, analysis is done and the data from the analysis (simple numbers) have to be stored. So pro file there are about 1000 numbers to be stored and nothing will be updated). Reference key is user's id.
So, it should be something like this:
class PaymentsTable < ActiveRecord::Migration
def change
create_table :payments do |t|
t.integer :user_id
t.float :sum
end
end
end
My problem is that i do not understand how I can save 10 payemnts of user A only if I specified t.float :sum only one time.
Thanks in advance
you want to make entry of each of payments made by a user and you want to sum those payments then your current way is wrong.
you can create table of payments like this,
class PaymentsTable < ActiveRecord::Migration
def change
create_table :payments do |t|
t.integer :user_id
t.float :amount
end
end
so if you sum this amount column by query like
user_payment = Payment.where(:user_id=>params[:user_id)).first.sum(&:amount)
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?
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).