Rails view segment dependent on udpated_at/created_at time? - ruby-on-rails

I can do this in a view to add labels to news stories:
<% if article.urgency == 'majorbreaking' && article.updated_at.today? %>
<span class="breaking">
…
It works out of the box with 'today', but how do I do the same thing with (say) "last six hours" (in a view, I know how to to it with scopes and controllers to select the articles, but this is after selection, just for the view).
Thanks.

Because this is after selection, we can simply use like this:
article.updated_at >= 6.hours.ago
Simple & pretty

You can minimize the hard-coded logic that your view is processing and potential DRY up your code (if in fact you want to process multiple time spans) by creating a method in your article model. For example:
class Article < ActiveRecord::Base
def recent?(hours_ago=24)
created_at >= hours_ago.hours.ago
end
end
Then in your view:
For within the last day:
<% if article.urgency == 'majorbreaking' && article.recent? %>
<span class="breaking">
For within the last 6 hours:
<% if article.urgency == 'majorbreaking' && article.recent?(6) %>
<span class="breaking">

If you want to replace that at_today with the last 6 hours use:
<% if article.urgency == 'majorbreaking' && article.updated_at > (DateTime.now - 6.hours) %>
<span class="breaking">
…
check out time helpers
http://api.rubyonrails.org/classes/DateTime.html
http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html
http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html

Related

Filling in blanks for DATE_TRUNC grouping with rails

The following query to the postgres database
#interventos_2 = Intervento.where(['previsto >= ?', start_date]).group("DATE_TRUNC('week', previsto)").count
generates a hash from which the view can extract the data as follows
<% #interventos_2.each do |w| %>
<%= w[0].strftime('%Y-%W') %> <%= w[1] %><br />
<% end %>
However if there is a blank (count = 0) in the range of weeks we are concerned with, the sequence of years and commercial weeks will look weird and or misleading.
What is an efficient way to declare the range and then fill in the blank weeks with zero?
Update the query is being run via specific sql for performance reasons as the data set is expected to be sufficiently large, frequently changing (cache may not help all that much) and frequently asked.
If I understand you correctly, you want to present a full range of YYYY-WW labels and counts, starting with start_date regardless of whether there's data for a given week. You didn't mention if previsto is a Date, a Time, or a DateTime; I'll assume it's a Time just for maximum inconvenience. =]
I think the main challenge you're struggling with is that you're conflating the dataset with the presentation. I like handling the two separately: first, get the data and put it into a year-week format; then present the range.
For getting the data, I like a more Railsy, less database-specific solution. Your mileage may vary, especially if you've got a large dataset and/or need to make the database do the heavy lifting. Here's a query that gets only the previsto field for each record while also forcing the database to evaluate the date range. Probably the most concise query without having to break out SQL:
#interventos_2 = Intervento.select(:previsto).
where(previsto: (start_date..Time.now)).
map {|iv| iv.previsto.strftime('%Y-%W')}
Note that this also maps the result down to a simple array of YYYY-WW. Speaking of which, let's map out that range of YYYY-WW now:
# make sure the end_date a clean multiple of 7 days from start_date
end_date = Date.today + (7 - (Date.today - start_date.to_date) % 7)
#timespan = (start_date.to_date..end_date).step(7).map {|date| date.strftime('%Y-%W')}
(There are probably much tidier ways to write that)
Given those bits, here's a version of your view code that presents the full range of weeks and the count for that week, even if it's 0:
<% #timespan.each do |yearweek| %>
<%= yearweek %> <%= #interventos_2.count(yearweek) %><br />
<% end %>
In bocca al lupo!
Update: Your update notes that your use case requires the direct SQL query, so here's the same general approach with that in mind:
#interventos_2 = Intervento.where(['previsto >= ?', start_date]).
group("DATE_TRUNC('week', previsto)").count.
map {|timestamp,count| [timestamp.strftime('%Y-%W'), count]}.to_h
# make sure the end_date a clean multiple of 7 days from start_date
end_date = Date.today + (7 - (Date.today - start_date.to_date) % 7)
#timespan = (start_date.to_date..end_date).step(7).map {|date| date.strftime('%Y-%W')}
<% #timespan.each do |yearweek| %>
<%= yearweek %> <%= #interventos_2[yearweek] || 0 %><br />
<% end %>
With the following controller method, the range of weeks and the data required for the view is generated (beginning_of_week for comparing with the has data, in addition to the data itself).
#weeks = []
while start_date < final_date
#weeks[start_date.year] = [] unless #weeks[start_date.year]
#weeks << [start_date.beginning_of_week, start_date.cweek]
start_date += 1.week
end
#weeks.reject!{|a| a.blank?}
Thus the view can generate a layout item for each week and compare it to the hash, and where there is nil, generate zero.
<% #weeks.each do |week| %>
<% if !week.nil? %>
<%= week[0] %>:
<% z = #interventos_2.detect {|f| f[0] == week[0] } %>
<% if !z.nil? %>
<%= z.to_a[1] %>
<% else %>
0
<% end %>
<% end %>

Returning a specific number of datetime results closest to now

I am building a quiz application. Let's say I have four quizzes (that should run at 5 PM, 6 PM, 7 PM, and 8 PM), I represent it by setting a .datetime called game_start in the Quiz model.
Next, I want to show the user when the next 3 quizzes are coming up by datetime. I know I can do that by comparing game_start to Time.now like so:
<p>Upcoming Game #1: <%= Quiz.where("game_start >= ?", Time.now).order(game_start: :asc).first.topic %></p>
<p>Upcoming Game #2: <%= Quiz.where("game_start >= ?", Time.now).order(game_start: :asc).second.topic %></p>
I found someone approaching a similar problem in this answer, but they were not trying to return multiple matches.
So it sort of works, but that does not follow DRY. Is it possible to specify that I want to return the first three results closest to Time.now instead of doing them individually?
You can use limit to retrieve only 3. In your controller:
#quizzes = Quiz.where("game_start >= ?", Time.now).order(game_start: :asc).limit(3)
Then in your view iterate over #quizzes to display each quiz:
<% #quizzes.each_with_index do |quiz, index| %>
<p>Upcoming Game #<%= index + 1 %>: <%= quiz.topic %></p>
<% end %>

Adding dates to the database that can be used to specify an active week

I am in the process of creating an competition app that needs to be pretty much autonomous. The app has a resource called 'Weeks' and each week has many 'Entries'. Users can add an entry to the week, and then they can be voted on.
The week has a database column called 'start_date' and if I populate this with a string, eg "16-03-2015" then on the Show view I can pull in the weeks params and compare the string to todays date and set whether the vote buttons are active or not.
In my weeks controller I use a before action called set_dates and I set a couple of variables
def set_dates
#today = Date.today
#weekstart = Date.parse(#week.active_date)
#weekend = Date.parse(#week.active_date)+7
end
I can then set if and else statements on the view to show the vote buttons if the week is 'active', ie. todays date is within the active_date + 7 days
<% #entries.each do |entry| %>
<div class="entry">
<%= image_tag(entry.photo(:thumb)) %>
<%= entry.cached_votes_up %>
<% if (#weekstart..#weekend).include? #today %>
<%= link_to "Upvote", upvote_week_entry_path(entry.week_id, entry) %>
<%= link_to "Downvote", downvote_week_entry_path(entry.week_id, entry) %>
<% end %>
</div>
<% end %>
This was fine until I realised I needed to have an 'active week' outside of the show view. I want to put a link into the Navigation Bar in the application_layout to 'This Weeks Comp', and I need this week to point to whichever week voting is active on. But without finding the week by its ID, then cross referencing it's active_date, I am unsure how to do this. Any help would be greatly appreciated.
Thanks!
If anyone else is find this looking to solve a similar problem, I solved the issue by adding an end_date column to the weeks resource, and then assigned a variable to an arel query in the application controller
App-Controller
before_action :active_week
protected
def active_week
#aweek = Week.where("? BETWEEN start_date AND end_date", Date.today)
#active = #aweek.first
end
Application Layout
<%= link_to "This Week", #active %>

Rails 3.2 - Query Performance (Postgres)

Background:
I'm creating a dashboard as a project and I have a query that I think is going to be a big performance issue:
<% for outlet in #outlets %>
<% if Monitoring.where(:outlet_id => outlet.id).where('date(created_at) = ?', Date.today).exists? %>
<li>
<a class="done" href="<%= outlet_url(outlet) %>" rel="tooltip" title="<%= outlet.name %>"></a>
</li>
<% else %>
<li>
</li>
<% end %>
<% end %>
What I'm trying to achieve is a series of dots on a page. If the anchor tag has a class of done, it will display as green, if not it will be red (done through CSS).
Aside from the obvious DRY issues here, this query is very heavy, so I'm looking at ways to improve it.
An Outlet is Monitored at least once a day (An Outlet has_many :monitorings). For each outlet I need to check if it has been monitored on that particular day, and output the HTML accordingly.
If anyone could help me with this it would be fantastic.
(Also, any advice on caching this would be appreciated).
Cheers in advance :).
You might make a conditioned association for current monitors, then use includes to fetch the associated current monitorings on the original query.
class Outlet
has many :current_monitorings, :class_name => "Monitoring",
:conditions => proc { [ 'monitorings.created_at > ?', Time.now.midnight ] }
end
#outlets = Outlet.includes(:current_monitorings)
#outlets.each do |outlet|
if outlet.current_monitorings.empty?
# the no monitor today case
else
# it's been monitored today
end
end
At the Postgres level, you'll likely benefit from an index on monitorings(outlet_id,created_at) to support the outer join implied by #includes.
BTW, it's bad style to be executing database queries in your view. Put the domain logic in your models, and have your controller execute the query and supply the results to the presentation layer.
Maybe try:
<% #outlets.includes(:monitorings).each do |outlet| %>
<% css_class = outlet.monitorings.any? { |m| m.created_at == Date.today } ? 'done' : '' %>
<li><%= link_to '', outlet_url(outlet), :class => css_class, :rel => "tooltip", :title => outlet.name %></li>
<% end %>
It'll perform 1 big query.
A good way to cache this would be denormalizing your database with the use of callbacks. In your Outlet model, you could add a field called last_monitored_on; anytime a Monitor is saved, update the appropriate Outlet model with the date. Then, you wouldn't have to query Monitors at all.
You could also consider caching that page fragment, and let it expire daily.

Rails: Grouping Partials Based on an Attribute

I've got an application that uses a calendar, and I'd like to provide a calendar summary on login. The summary should be formatted so it shows events for Today, Tomorrow, and This Week, like this:
Events For:
Today
Event 1
Tomorrow
Event 2
Event 3
Event 4
This Week
Event 5
Event 6
Event 7
Event 8
How do I render the partials so that they are grouped together in the right way, based on their date?
First one could add a model method for_dates(start_date,end_date) which would contain:
where([:date_column >= ? and :date_column <= ?, start_date, end_date])
Then use:
Model.for_dates(Date.today, Date.today)
Model.for_dates(Date.today+1, Date.today+1)
Default 'week' is Sunday to Monday. Add an offset if you wish different days, e.g.
Monday to Friday is
Model.for_dates(Date.today.beginning_of_week+1, Date.today.end_of_week+1)
Supposing your model is called Event and it has a date attribute, and your collection of these is called #events, here's one idea: Put this in your view...
<ul>
<%= render #events, locals: { events: #events } %>
</ul>
...then you would have a partial called _event.html.erb that looks like this:
<% last_event = event_counter == 0 ? nil : events[event_counter - 1]
next_event = events[event_counter + 1]
%>
<% if last_event && last_event.date != event.date
# add a new date header and start a new nested list of events
%>
<li><h3><%= event.date %></h3>
<ul>
<% end %>
<li><%= link_to event %></li>
<% if next_event && next_event.date != event.date
# end the nested list and make way for the next date header and set of events
%>
</ul>
</li>
<% end %>
What's happing is in the render call we're passing in the whole #events collection as a local called events. Then inside the partial we use the automatically-generated event_counter method to look up the previous (events[event_counter - 1]) and next (events[event_counter + 1]) events in the collection. Then if the date of the current event is different from the date of last_event (meaning it's the first one for that date) we start a new set with a new date heading, and if the date of event is different from the date of next_event (i.e. it's the last one for that date) we end the set.
It's a little ugly, and there are more elegant ways to do it for sure, but it gets the job done.

Resources