I am searching for the best way to model recurring events. I am using fullcalendar to display events. But I guess recurring events are best handled on the rails backend.
I already looked at other questions and existing example code but I didn't find anything which fits.
It should behave similar like google calendar. So it should be possible to delete/modify single events of the recurring event series. But saving all events of the event series in the database seems inefficient. Also it should be possible to create single events without any recurrence.
What would be a good model architecture?
My event model right now looks like that (without additional attributes):
# Table name: events
#
# id :integer not null, primary key
# employee_id :integer
# created_at :datetime
# updated_at :datetime
# starts_at :datetime
# ends_at :datetime
#
class Event < ActiveRecord::Base
attr_accessible :starts_at, :ends_at
end
Here is how I would model this. I haven't used Google Calendar much, so I'm basing the functionality off of iCal's recurring events.
All models should have the usual id, created_at, updated_at properties. Listed are the custom properties. If the property is another model, you will implement it an association such as has_one or belongs_to.
RecurrencePeriod
Event base_event # has_one :base_event, :class_name'Event'
Time end_date # may be nil, if it recurs forever
WeeklyRecurrence recurrence # has_one :recurrence, :as=>:recurrence
Array[OccurrenceOverride] overrides # has_many :overrides, :class_name=>'OccurrenceOverride'
The RecurrencePeriod starts on the date that its base_event starts. Also, I assume that an Event's employee_id refers to the employee that created that event. A RecurrencePeriod will also belong to the employee that created the base_event.
The model depends on how flexibly you want to be able to specify recurrences. Are you going to support "Tuesday and Thursday every two weeks from 10 AM to 11 AM and from 2 PM to 3 PM" or just "repeats weekly"? Here's a model that supports just "repeats weekly", "repeats every two weeks", etc.; you can expand it if you need to.
WeeklyRecurrence
Integer weeks_between_recurrences
RecurrencePeriod recurrence_period # belongs_to :recurrence, :polymorphic=>true
I use polymorphic associations here, because I think they might be useful if you want more than one type of recurrence, such both WeeklyRecurrence and DailyRecurrence. But I'm not sure that they're the correct way to model that, so if they turn out not to be, just use has_one :weekly_recurrence and belongs_to :recurrence_period instead.
The Ice cube library seems like it might be useful for calculating recurrences. If WeeklyRecurrence above isn't powerful enough, you might just want to store an Ice cube Schedule object in a model, replacing WeeklyRecurrence. To store a Schedule object in a model, save it as an attribute "schedule", put serialize :schedule in the model definition, and generate a text column "schedule" in the database.
OccurrenceOverride handles the case of a single instance of a recurring event being edited.
OccurrenceOverride
RecurrencePeriod recurrence_period_to_override # belongs_to :recurrence_period_to_override, :class_name=>'RecurrencePeriod'
Time original_start_time # uniquely identifies which recurrence within that RecurrencePeriod to replace
Event replacement_event # has_one :replacement_event, :class_name=>'Event'; may be nil, if that recurrence was deleted instead of edited
Instead of storing each occurrence of an event individually, generate them temporarily when you need to show them in the view. In RecurrencePeriod, create a method generate_events_in_range(start_date, end_date) that generates Events, not to save in the database, but just to pass to the view so it can show them.
When a user edits a recurrence, they should have the option to modify all occurrences, all future occurrences, or just that event. If they modify all occurrences, modify the RecurrencePeriod's base_event. If they modify all future occurrences, use a method you should implement on RecurrencePeriod that splits itself into two RecurrencePeriods on either side of a certain date, and then save the changes to just the second period. If they modify only that event, create an OccurrenceOverride for the time that they are overriding, and save the changes to the override's replacement_event.
When a user says a certain event should now recur every two weeks for the foreseeable future, you should create a new RecurrencePeriod with that event as base_event and a nil end_date. Its recurrence should be a new WeeklyRecurrence with weeks_between_recurrence=2, and it should have no OccurrenceOverrides.
In my case I did something like this :
# Holds most of my event's data; name, description, price ...
class Event < ActiveRecord::Base
has_many :schedules
has_many :occurrences
attr_accessible :started_at, :expired_at # expired_at is optional
end
# Holds my schedule object
class Schedule < ActiveRecord::Base
belongs_to :event
attr_accessible :ice_cube_rule # which returns my deserialized ice_cube object
end
# Holds generated or manually created event occurrences
class Occurrence < ActiveRecord::Base
belongs_to :event
attr_accessible :started_at, :expired_at
attr_accessible :generated # helps me tell which occurrences are out of an ice_cube generated serie
attr_accessible :canceled_at
end
From there, I used ice_cube to manage the occurrences calculation and stored the results in the occurrences table. I first tried to work without the Occurrence model, but no matter how advanced the rule engine, you'll always have exceptions, so storing the occurrences in their own model gives you flexibility.
Having an Occurrence model makes it a lot easier to display the events on a calendar or with date search filters as you just need to query for occurrences and then display the related event's data instead of gathering all the events in a given date range and then having to filter out the events where the schedule(s) don't match.
Also you can flag an event occurrence as canceled or modify it (setting the generated attribute at false so it does not get cleaned up when editing an ice_cube schedule... or whatever your business need is)
Of course if you have events that repeat indefinitely, you'll want to limit how far in the future you want those occurrences to be generated and use automated rake tasks to clean up the old ones and generate occurrences for the next year or so.
So far this pattern works pretty well for me.
Also, take a look at the recurring_select gem which is a pretty neat ice_cube form input.
Just an opinion off the top of my head, perhaps comenters will point out a problem I'm not thinking of at the moment:
I would make a RecurringEvent model (or whatever you want to call it) that has_many :events.
Let's say each event is created by an employee (based on your notes), then RecurringEvent would also belong_to :employee. You could then build a has_many :through relationship where an employee has many events and has many recurring events.
The RecurringEvent model could have a start date and a pattern, and it could initially use this pattern to create the individual occuring events. Then on any event that is part of the recurring series you could modify or delete that individual occurance, but you could also 'regenerate the series', deleting all events in the series (or all future events in the series) and rebuilding them based on a new pattern, so for instance move the meeting from "every tuesday" to "every thursday".
One other kind of nice thing about this is you could create an at-a-glance list of recurring events, which might give you some nice insight into people's major obligations.
Like I said, off the top of my head that's how I would approach it, but this is just an idea and I haven't built anything like that so I don't know if there are any big gotchas in the approach I'm suggesting.
Good luck, please post what you end up doing!
I m quite new to Rails, your solution sounds interesting. To create the schedule and associated occurences, do you use conditionnal callbacks in Event model?
In my case, users would be able to create events, weekly recurring or not. So I was thinking about a recurring boolean field in event model. So I guess you would have a first callback to create the schedule:
before_save :create_weekly_schedule, if: :recurring
and basically a second one to create the occurences:
after_save :create_occurences_if_recurring
def create_occurences_if_recurring
schedules.each do |sched|
occurences.create(start_date: sched.start_time, end_date: sched.end_time)
end
end
Does this sound logical with your solution?
Thx
Related
I have an existing live application with an Event model that looks like the following:
class Event < ActiveRecord::Base
has_many :tickets
has_many :orders
has_many :products, through: :orders
has_many :rsvps, through: :orders
end
My problem is that I now need to add in the functionality that will let a user specify that the event needs to be recurring (for which they'll set a schedule). There are a couple of approaches I've thought of as follows:
Have a schedule and parent_event property on the Event model, for which I will trigger a background job after the event is created to create duplicate events in the DB with the parent_event pointing to the main event so that I can aggregate statistics on the main event (like total tickets bought, etc...). This is the easiest route as all of the models references (tickets, rsvps, orders) don't need to be altered.
Create a Schedule model that is a habtm relationship with events, and make tickets, rsvps, etc... belong to that instead. So I'd be creating one event with many Schedules attached to it for recurring events.
Option 1 seems really dirty, especially if users want to delete an event (I'd have to delete all of the duplicates then as well), but it would require the least amount of code-changes. Option 2 seems like it could be the most scalable approach, but I feel like I may be overcomplicating this. Any advice / examples would be appreciated.
I think the first option is the way to go. I have done some tests with Google Calendar and I think their model is similar to the one you are proposing. You can read Google Calendar documentation (I provide some links below). As far as I could read:
A recurring event has many instances
Instances are events
Instances have a link to their parent event (recurringEventId)
When you modify or delete a recurring event, you are asked if you want to modify or delete only this instance, this an the following instances or all instances.
Recurring events have information about the recurrence (as specified in RFC5545). Recurrence information is including in the parent event (it is omitted for single events or instances of recurring events).
An event is an object associated with a specific date or time range. Events are identified by an ID that is unique within a calendar. Besides a start and end date-time, events contain other data such as summary, description, location, status, reminders, attachments, etc.
Types of events: Google Calendar supports single and recurring events:
A single event represents a unique occurrence.
A recurring event defines multiple occurrences.
Instances
A recurring event consists of several instances: its particular occurrences at different times. These instances act as events themselves.
Recurring event modifications can either affect the whole recurring event (and all of its instances), or only individual instances. Instances that differ from their parent recurring event are called exceptions. For example, an attendee can be invited to just one instance of a recurring event.
Official information about Google Calendar:
https://developers.google.com/google-apps/calendar/concepts/events-calendars
https://developers.google.com/google-apps/calendar/v3/reference/events
Currently I am building a homepage for my local soccer club and I want to save training times for each team. The training times will look something like this:
Tuesday 18:00-19:30
Thursday 18:30-20:00
What is the best way to store these values inside the database in rails?
I recommend creating a Practice model, with each practice having a :start and :end attribute, each typed as a :datetime. If you generate a migration like so:
rails generate model Practice start_time:datetime finish_time:datetime
That will build a migration for your database, adding the columns you need. Be sure to run rake db:migrate to update your development database.
You would also have to link this new model to your Team model. The relationship between teams and practices seems to be one-to-many, so you'd add this to your Team class
class Team < ActiveRecord::Base
has_many :practices
end
and add the corresponding relationship to your Practice model
class Practice < ActiveRecord::Base
belongs_to :team
end
(Note the different use of singular and plural class names in these methods.)
You might then build a method within your Practice class to render a formatted date and time range from these two attributes.
def practice_time
formatted_str = start_time.strftime("%A %H:%M") + " " + finish_time.strftime("%A %H:%M")
end
You can see more options for the strftime method here
Once you have stored your date information in a logical way, feel free to develop model and/or helper methods to return your data in a more useful form. You might also consider some add-ons like ActiveAdmin to make entering and searching for dates easier.
Here's my solution(may or may not sounds good to you). Create two time columns each represents From and To timings of a day.for example
for Monday it goes like this in your database
|monday_from(time property)|monday_to(time property)|and so on
+--------------------------+------------------------+
Hope this helps you.
Edit: I considered that the training events are repeated in time regularly, so you don't have to stick to a particular date.
You can simply save it to the database as a string.
You can also make a new model (or table) with attributes, start time and end time. Then you can save a Time object to the database.
When loading these Time objects, you can create a helper for view to format the time. It can look the following:
def format_time(time)
time.strftime("%A %H:%M")
end
In my Rails app I have an Invoice model with the attributes date and due_date.
For reasons of simplicity I don't want the user to manually enter the due_date but rather simply enter the number of days that should be added to the date.
This is why I set up a virtual attribute days_allowed.
class Invoice < ActiveRecord::Base
belongs_to :user
before_save :save_date
attr_accessor :days_allowed
def days_allowed # attribute reader (I need that too!)
(due_date - date).to_i
end
def save_date
self.due_date = date + days_allowed.days
end
end
However, when a user picks a date in the form and enters a number of days, e.g. 10, I get wrong results because the save_date callback refers to the days_allowed method rather than the attribute of the same name.
The key problem seems to be that I am using the callback on two different attributes that depend on each other (date and days_allowed).
Can anybody tell me how to solve this puzzle?
Thanks for any help.
How about this approach (no before_save is necessary):
class Invoice < ActiveRecord::Base
belongs_to :user
def days_allowed
(due_date - date).to_i
end
def days_allowed=(days)
self.due_date = date + days
end
end
EDIT Not supposed to work with mass assignment when both date and days_allowed are present unless date always goes first.
I think you need to store all three fields, just not provide any form attributes for editing the calculated one.
While your use-case at it is might seem like this is a violation of 'don't repeat yourself', I think this is an instance of coincidental duplication. Let me explain:
Consider your next problem - you are going to start having due dates on weekends, holidays, etc. For exactly that reason I wrote the business_time gem:
https://github.com/bokmann/business_time
so in your solution where you add 10 days, you can now add 10 business days. The exact due date can now fluctuate across weekends, 3-day weekends created by holidays, etc. And if you have to deal with snow days, like most of the East Coast of the U.S. has had to lately, you can just add tham as a holiday and recalculate. The number of business days is still 10, but the due date has changed. The number of business days was a critical piece of data to save, not a piece of data to calculate. The due data needs to be saved, because its a piece of information calculated in a context that you'll want to save for querying rather than try to recalculate every time.
In my current application, I need the ability to track points on a weekly basis so that the point totals for the user reset back to zero each week. I was planning on using the gem merit: https://github.com/tute/merit to track points.
In my users profile I have a field that is storing the points. What I have been unable to locate is how I can have rails on an auto basis for all users clear this field.
I have come across some information Rails reset single column I think this may be the answer in terms of resetting it every Sunday at a set time -- but I am uncertain on this last part and in addition where the code would go (model or controller)
Also, would welcome any suggestions if their is a better method.
You'd be better making a Point model, which belongs_to :user
This will allow you to add any points you want, and can then query the table based on the created_at column to get a .count of the points for the timespan you want
I can give you more info if you think it appropriate
Models
One principle we live by is to extend our models as much as possible
You want each model to hold only its data, thus ensuring more efficient db calls. I'm not super experienced with databases, but it's my opinion that having a lot of smaller models is more efficient than one huge model
So in your question, you wanted to assign some points to a user. The "right" way to do this is to store all the points perpetually; which can only be done with its own model
Points
#app/models/point.rb
Class Point < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :points
end
Points table could look like this:
points
id | user_id | value | created_at | updated_at
Saving
To save the points, you will literally just have to add extra records to the points table. The simplest way to achieve this will be to merge the params, like this:
#app/controllers/points_controller.rb
class PointsController < ApplicationController
def new
#points = Point.new
end
def create
#points = Point.new(points_params)
#points.save
end
private
def points_params
params.require(:points).permit(:value).merge(:user_id => current_user.id)
end
end
You can define the "number" of points by setting in the value column (which I'd set as default 1). This will be how StackOverflow gives different numbers of points; by setting the value column differently ;)
Counting
To get weekly countings, you'll have to create some sort of function which will allow you to split the points by week. Like this:
#app/models/point.rb -> THIS NEEDS MORE WORK
def self.weekly
where(:created_at => Time.now.next_week..Time.now.next_week.end_of_week)
end
That function won't work as it is
I'll sort out the function properly for you if you let me know a little more about how you'd like to record / display the weekly stats. Is it going to be operated via a cron job or something?
Based on your description, you might want to simply track the users points and the time that they got them. Then you can query for any 1 week period (or different periods if you decide you want all-time, annual, etc) and you won't lose historical data.
I have a situation that involves Companies, Projects, and Employees who write Reports on Projects.
A Company owns many projects, many reports, and many employees.
One report is written by one employee for one of the company's projects.
Companies each want different things in a report. Let's say one company wants to know about project performance and speed, while another wants to know about cost-effectiveness. There are 5-15 criteria, set differently by each company, which ALL apply to all of that company's project reports.
I was thinking about different ways to do this, but my current stalemate is this:
To company table, add text field criteria, which contains an array of the criteria desired in order.
In the report table, have a company_id and columns criterion1, criterion2, etc.
I am completely aware that this is typically considered horrible database design - inelegant and inflexible. So, I need your help! How can I build this better?
Conclusion
I decided to go with the serialized option in my case, for these reasons:
My requirements for the criteria are simple - no searching or sorting will be required of the reports once they are submitted by each employee.
I wanted to minimize database load - where these are going to be implemented, there is already a large page with overhead.
I want to avoid complicating my database structure for what I believe is a relatively simple need.
CouchDB and Mongo are not currently in my repertoire so I'll save them for a more needy day.
This would be a great opportunity to use NoSQL! Seems like the textbook use-case to me. So head over to CouchDB or Mongo and start hacking.
With conventional DBs you are slightly caught in the problem of how much to normalize your data:
A sort of "good" way (meaning very normalized) would look something like this:
class Company < AR::Base
has_many :reports
has_many :criteria
end
class Report < AR::Base
belongs_to :company
has_many :criteria_values
has_many :criteria, :through => :criteria_values
end
class Criteria < AR::Base # should be Criterion but whatever
belongs_to :company
has_many :criteria_values
# one attribute 'name' (or 'type' and you can mess with STI)
end
class CriteriaValues < AR::Base
belongs_to :report
belongs_to :criteria
# one attribute 'value'
end
This makes something very simple and fast in NoSQL a triple or quadruple join in SQL and you have many models that pretty much do nothing.
Another way is to denormalize:
class Company < AR::Base
has_many :reports
serialize :criteria
end
class Report < AR::Base
belongs_to :company
serialize :criteria_values
def criteria
self.company.criteria
end
# custom code here to validate that criteria_values correspond to criteria etc.
end
Related to that is the rather clever way of serializing at least the criteria (and maybe values if they were all boolean) is using bit fields. This basically gives you more or less easy migrations (hard to delete and modify, but easy to add) and search-ability without any overhead.
A good plugin that implements this is Flag Shih Tzu which I've used on a few projects and could recommend.
Variable columns (eg. crit1, crit2, etc.).
I'd strongly advise against it. You don't get much benefit (it's still not very searchable since you don't know in which column your info is) and it leads to maintainability nightmares. Imagine your db gets to a few million records and suddenly someone needs 16 criteria. What could have been a complete no-issue is suddenly a migration that adds a completely useless field to millions of records.
Another problem is that a lot of the ActiveRecord magic doesn't work with this - you'll have to figure out what crit1 means by yourself - now if you wan't to add validations on these fields then that adds a lot of pointless work.
So to summarize: Have a look at Mongo or CouchDB and if that seems impractical, go ahead and save your stuff serialized. If you need to do complex validation and don't care too much about DB load then normalize away and take option 1.
Well, when you say "To company table, add text field criteria, which contains an array of the criteria desired in order" that smells like the company table wants to be normalized: you might break out each criterion in one of 15 columns called "criterion1", ..., "criterion15" where any or all columns can default to null.
To me, you are on the right track with your report table. Each row in that table might represent one report; and might have corresponding columns "criterion1",...,"criterion15", as you say, where each cell says how well the company did on that column's criterion. There will be multiple reports per company, so you'll need a date (or report-number or similar) column in the report table. Then the date plus the company id can be a composite key; and the company id can be a non-unique index. As can the report date/number/some-identifier. And don't forget a column for the reporting-employee id.
Any and every criterion column in the report table can be null, meaning (maybe) that the employee did not report on this criterion; or that this criterion (column) did not apply in this report (row).
It seems like that would work fine. I don't see that you ever need to do a join. It looks perfectly straightforward, at least to these naive and ignorant eyes.
Create a criteria table that lists the criteria for each company (company 1 .. * criteria).
Then, create a report_criteria table (report 1 .. * report_criteria) that lists the criteria for that specific report based on the criteria table (criteria 1 .. * report_criteria).