I'm trying to build a staff rostering/scheduling app in Rails 5. The idea is for each staff member to have a user account where they can log in to enter the days they are available for work, then for the boss to book shifts based on availability. A good example of such a site is https://www.findmyshift.com/.
The main part of the site would be a calendar, with each user having their own instance of the calendar. Each user would have their own row in the calendar table, with each row displaying the days of the week.
I've looked at a few plugins, including FullCalendar and dhtmlxScheduler, but decided to try doing it from scratch. I've managed to build a basic calendar based on this Railscast, which works just fine. The 3 main parts of this are as follows:
home_controller.rb:
class HomeController < ApplicationController
def index
#date = params[:date] ? Date.parse(params[:date]) : Date.today
end
end
index.html.erb:
<div id="roster">
<h2 id="month">
<%= link_to "<", date: #date.prev_week %>
<%= #date.strftime("%B %Y") %>
<%= link_to ">", date: #date.next_week %>
</h2>
<%= calendar #date do |date| %>
<%= date.day %>
<% end %>
</div>
calendar_helper.rb:
module CalendarHelper
def calendar(date = Date.today, &block)
Calendar.new(self, date, block).table
end
class Calendar < Struct.new(:view, :date, :callback)
HEADER = %w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday]
START_DAY = :monday
delegate :content_tag, to: :view
def table
content_tag :table, class: "calendar" do
header + week_rows
end
end
def header
content_tag :tr do
HEADER.map { |day| content_tag :th, day }.join.html_safe
end
end
def week_rows
weeks.map do |week|
content_tag :tr do
week.map { |day| day_cell(day) }.join.html_safe
end
end.join.html_safe
end
def day_cell(day)
content_tag :td, view.capture(day, &callback), class: day_classes(day)
end
def day_classes(day)
classes = []
classes << "today" if day == Date.today
classes << "notmonth" if day.month != date.month
classes.empty? ? nil : classes.join(" ")
end
def weeks
first = date.beginning_of_week(START_DAY)
last = date.end_of_week(START_DAY)
(first..last).to_a.in_groups_of(7)
end
end
end
Again, all of this works fine. It displays 1 week row, with arrows to move backwards and forwards in time. What I would like is to have 1 week row PER USER. I'm aware that I could simply render a calendar for each user in my view template, but this would include the whole thing - header, week selector and week row. I just want 1 header + week selector, then a week row for each user as part of the same table.
My guess is that I should iterate over each user somewhere in calendar_helper.rb, adding a row per user, but I'm not too sure how to proceed. Any help would be much appreciated :)
EDIT:
The first image is what I have now, the second image is what I would like to achieve - multiple instances of the calendar week row, rendered dynamically based on number of users.
Related
I'm using chartkick to create some line charts to visualize data in my Rails app. At the moment, however, it shows ALL of the data in my database. Obviously this isn't always ideal. I'd like to give the user the ability to specify the date range of the data shown. I would use the created_at value of the record.
I cannot find any instructions in the documentation on how to do this.
https://github.com/ankane/chartkick.js?files=1
I have created a date picker which will give me a URL like http://localhost:3000/metrics?utf8=%E2%9C%93&search%5Bdate_from%5D=2019-11-01&search%5Bdate_to%5D=2019-11-17&commit=Search
Can chartkicker somehow access those parameters?
In my code a Club has many DailyMetrics, and I've created a metrics action in the clubs controller.
View
<%= javascript_include_tag "https://google.com/jsapi" %>
<div class="row">
<h1>Metrics</h1>
<div class="pull-right range-query">
<%= form_tag metrics_path, method: :get do %>
<%= text_field_tag 'search[date_from]', #search.date_from %>
<%= text_field_tag 'search[date_to]', #search.date_to %>
<%= submit_tag 'Search', class: 'btn search-button' %>
<% end %>
</div>
</div>
<h3>Total active students</h3>
<%= line_chart #club.daily_metrics.group(:created_at).sum(:total_active_students), library: { animation: { easing: 'easeOutQuad'}} %>
<h3>Lost students</h3>
<%= line_chart #club.daily_metrics.group(:created_at).sum(:lost_students), library: { animation: { easing: 'easeOutQuad'}} %>
<h3>New students</h3>
<%= line_chart #club.daily_metrics.group(:created_at).sum(:new_students), library: { animation: { easing: 'easeOutQuad'}} %>
Controller
def metrics
#club = current_club
#search = MetricSearch.new(params[:search])
#metrics = #search.scopeContr
end
Model for searching for metrics
class MetricSearch
attr_reader :date_from, :date_to
def initialize(params)
params ||= {}
#date_from = parsed_date(params[:date_from], 7.days.ago.to_date.to_s)
#date_to = parsed_date(params[:date_to], Date.today.to_s)
end
def scope
DailyMetric.where('created_at BETWEEN ? AND ?', #date_from, #date_to)
end
private
def parsed_date(date_string, default)
Date.parse(date_string)
rescue ArgumentError, TypeError
default
end
end
Is what I'm trying to do possible?
I posted a link to an great article in your comments, but you can use a gem called gem 'groupdate' as well.
# Gemfile
gem 'groupdate'
Assume your user model has_many :orders and you want to find out their spending over the past 6 month:
# order.rb
def self.monthly_spending
group_by_month(:date, last: 6, current: false).sum('orders.total')
end
In your view:
<%= line_chart current_user.orders.monthly_spending %>
Here is another tutorial how to group by date with chartkick and groupdate gem.
If you want to preselect a from-to-date for query you can setup a datepicker in your frontend and send the params to your controller
def show
orders = Order.where('created_at > %s AND created_at < %s', params[:start_date], params[:end_date])
end
call like above the group_by methods on your prefiltered list of orders.
I'm trying to make a date range form in my rails app, so the user can input two dates, and these dates will post to a query to return results from an API the application is connected to.
My model looks like this: It allows the first date and second date to default to create last week's date range, but it is supposed to accept options to overwrite the default...
class Day
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :first_day, :second_day
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def self.monday(options = {})
if options[:first_day]
return Date.parse(options[:first_day])
else
d = Date.today
seven_days_ago = (d - 7)
seven_days_ago.beginning_of_week
end
end
#defines the end of last week for the query
def self.sunday(options = {})
if options[:second_day]
return Date.parse(options[:second_day])
else
d = Date.today
seven_days_ago = (d - 7)
seven_days_ago.end_of_week
end
end
def persisted?
false
end
end
Form:
<%= form_for #day do |f| %>
<div class="field">
<%= f.label 'Beginning Date' %>
<%= f.datepicker :first_day, :size => 6 %>
<%= f.label 'End Date' %>
<%= f.datepicker :second_day, :size => 6 %>
</div>
<div class="actions">
<%= f.submit 'Submit' %>
</div>
<% end %>
And controller:
def new
#day = Day.new
end
# POST /days
# POST /days.json
def create
#day = Day.new(params[:day])
if #day.valid?
redirect_to root_url
end
end
I am currently accessing the Day.monday and Day.sunday methods in queries that live in some of my controllers. These values always end up being the default, even if the user posts other dates through the form (the idea is to overwrite the default values).
I would prefer a javascript option - the user inputs two dates, they stick to the methods without refreshing, then when the dates are reset it goes back to default.
Thanks for any help!
Hello I'm trying to make grouping news by date. I can't understand what is the problem?
feed_entries_controller.rb
def index
#feed_entries = FeedEntry.page(params[:page]).per_page(12).where('published_at < ?', DateTime.now)
#feed_entries_by_day = #feed_entries.group_by { |t| t.published_at.beginning_of_day }
end
index.html.erb
<% #feed_entries_by_day.each do |day, feed_entries| %>
<h3><%= h day.strftime("%d %B %Y") %></h3>
<%= render #feed_entries%>
<% end %>
Without seeing the data, I suspect our problem is the published_at column also contains time and we're just interested in the date part for the purposes of querying and grouping, and we need to reference the grouped data in the view..
If that's the case, we can amend our .where() clause like so:
feed_entries_controller.rb
def index
feed_entries = FeedEntry
.page(params[:page])
.per_page(12)
.where('published_at::date <= ?', Date.current)
#feed_entries_by_day = feed_entries.group_by { |t| t.published_at.to_date }
end
And in our view, we need to pick a different name to iterate through our grouped feed_entries (I've chosen entry)
index.html.erb
<% #feed_entries_by_day.each do |day, entry| %>
<h3><%= h day.strftime("%d %B %Y") %></h3>
<%= render entry%>
<% end %>
I'm using similar code to Railscast 213 to display a calendar with records.
The do line is causing a "getting wrong number of arguments (1 for 0):
<%= calendar #date do |date| %>
<%= date.day %>
<% if #wolabors_by_date[date] %>
<ul>
<% #wolabors_by_date[date].each do |wolabor| %>
<li><%= link_to wolabor.name, wolabor %></li>
<% end %>
</ul>
<% end %>
<% end %>
The calendar_helper.rb starts out with:
module CalendarHelper
def calendar(date = Date.today, &block)
Calendar.new(self, date, block).table
end
wolabors_controller.rb has
class WolaborsController < ApplicationController
def index
#wolabors = Wolabor.all
#wolabors_by_date = #wolabors.group_by(&:date)
#date = params[:date] ? Date.parse(params[:date]) : Date.today
end`
I think that first line is supposed to be
<% calendar_for #date do |date| %>
That railscast has been revised and it does not use that table_builder plugin in the new version.
http://railscasts.com/episodes/213-calendars-revised
I've found in the discussion about this Railscast , that the statement :
first = date.beginning_of_month.beginning_of_week(START_DAY)
gives the same arguments error . It seems methods
beginning_of_month
and
beginning_of_week
are Rails 3.2 specific and if you are on lower version , you should upgrade.
The following code is from a Ryan Bates' RailsCasts in which he turns the front page of a blog into a calendar, so that articles show up as links on days. The following helper module creates the Calendar. I have two questions about this code
In the day_cell method, he uses a method called capture. I found some docs on it but I still can't figure out how capture is working in this context. Also, what is the &callback that's passed as an argument to capture? Would it be the same :callback that's passed to Struct.new? If so, how does it get into capture? What is the :callback that's passed to Struct?
def day_cell(day)
content_tag :td, view.capture(day, &callback), class: day_classes(day)
end
source code
module CalendarHelper
def calendar(date = Date.today, &block)
binding.pry
Calendar.new(self, date, block).table
end
class Calendar < Struct.new(:view, :date, :callback)
HEADER = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]
START_DAY = :sunday
delegate :content_tag, to: :view
def table
content_tag :table, class: "calendar" do
header + week_rows
end
end
def header
content_tag :tr do
HEADER.map { |day| content_tag :th, day }.join.html_safe
end
end
def week_rows
weeks.map do |week|
content_tag :tr do
week.map { |day| day_cell(day) }.join.html_safe
end
end.join.html_safe
end
def day_cell(day)
content_tag :td, view.capture(day, &callback), class: day_classes(day)
end
def day_classes(day)
classes = []
classes << "today" if day == Date.today
classes << "notmonth" if day.month != date.month
classes.empty? ? nil : classes.join(" ")
end
def weeks
first = date.beginning_of_month.beginning_of_week(START_DAY)
last = date.end_of_month.end_of_week(START_DAY)
(first..last).to_a.in_groups_of(7)
end
end
end
I have done my research, and I've finally unraveled the mystery.
So, a couple of things to start with; as usual the documentation isn't very clear,
the capture(*args) method is supposed to grab a piece of template into a variable, but it doesn't dig deeper into explaining that you may pass variables to the grabbed piece of template, that of course comes in the form of a block
source code from Ryan Bate's Calendar Screen-cast:
<div id="articles">
<h2 id="month">
<%= link_to "<", date: #date.prev_month %>
<%= #date.strftime("%B %Y") %>
<%= link_to ">", date: #date.next_month %>
</h2>
<%= calendar #date do |date| %>
<%= date.day %>
<% if #articles_by_date[date] %>
<ul>
<% #articles_by_date[date].each do |article| %>
<li><%= link_to article.name, article %></li>
<% end %>
</ul>
<% end %>
<% end %>
</div>
In the code above, the block would be exclusively this part:
do |date| %>
<%= date.day %>
<% if #articles_by_date[date] %>
<ul>
<% #articles_by_date[date].each do |article| %>
<li><%= link_to article.name, article %></li>
<% end %>
</ul>
<% end %>
<% end %>
So, When he makes this call:
content_tag :td, view.capture(day, &callback), class: day_classes(day)
particularly:
view.capture(day, &callback)
What's happening here is that he's passing the day argument to the Block above as the |date| parameter (in the block).
What needs to be understood here, is that in the context of the Problem (making a 30-day Calendar); each day of the Month is passed to the capture method, along with the piece of template (&callback), doing so.. in consequence renders the block above for each day of a Given Month. The final step, of course is.. Placing that rendered content (for each day) as the content for the content_tag :td
A final note; Ryan is calling the capture method on a view variable, it isn't stated in the documentation either, but he does mention during the ScreenCast that he needs this view as a "proxy" to access the view, and of course the view is the only one that has access to ViewHelper methods.
So, in summary, it's very beautiful code, but it's only beautiful once you understand what it does, So, I agree is very confusing at first sight.
Hope this helps, it's the best explanation I could come up with. :)