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?
Related
We're creating a flight scheduling program that schedules employees and planes for flights.
Here are the models that currently exist
Airport
Pilot
FlightAttendant
Aircraft
We are starting with only four airports. We want to fill in the four airports in the Airports table as soon as the app launches, and then make a self-join table from the Airports that lists all possible origin-destination combinations and their duration.
How would we do that?
I've seen some stuff online about it but it looks like its done when we create the models, but our models are already made, so I can't figure out the migration and how to fill in that table automatically. We know the durations and just need to feed them in.
EDIT:
In response to the flight times: we plan to store them as an integer which is the number of minutes it takes to complete a flight. The airports are in Midwestern cities chosen at random in the Central Time Zone.
Lincoln and Iowa City: 32 mins
Lincoln and Evanston: 57 mins
Lincoln and West Lafayette: 62 mins
Iowa City and Evanston: 24 mins
Iowa City and West Lafayette: 31 mins
Evanston and West Lafayette: 13 mins
For further detail
This is the specific migration which created the Airports table
class CreateAirports < ActiveRecord::Migration[5.2]
def change
create_table :airports do |t|
t.string :full_name
t.string :flight_code
t.timestamps
end
end
end
full_name is simply the name like Evanston. flight_code is the the three letter code to represent it, like EVA.
The model is currently empty. Do I need to add something in it first before I add the association columns, or do I need to generate the migration to create the join table and then alter the Airport model?
You can run ruby code after the migration, Rails code in this case:
class CreateAirports < ActiveRecord::Migration[5.2]
def change
create_table :airports do |t|
t.string :full_name
t.string :flight_code
t.timestamps
end
Airport.create(params_for_airport1)
Airport.create(params_for_airport2)
Airport.create(params_for_airport3)
Airport.create(params_for_airport4)
end
end
Then, for the durations table, I imagine you'll have something like a "FlightDuration" using a table with 3 columns: airport_from_id, airport_to_id, duration. You can do the same as before, you create the table and right after the create_table just create the objects with the data.
You'll have to add the relationships to Airport and to FlightDuration, something like:
class Airport < ApplicationRecord
has_many :flight_durations_from, class_name: 'FlightDuration', inverse_of: :airport_from
has_many :flight_durations_to, class_name: 'FlightDuration', inverse_of: :airport_to
end
class FlightDuration < ApplicationRecord
belongs_to :airport_from, class_name: 'Airport', foreign_key: :airport_from_id
belongs_to :airport_to, class_name: 'Airport', foreign_key: :airport_to_id
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)
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})
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.
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).