iCalendar feed of plantings (#3588)

* Register mime type

* Add rough first pass

* Index predictions, and add links

* Trailing space

* Fix implementation

* Fix names, make public

* Fix names, make public

* Fix

* Fix tyyos

* Add date

* If there are no predictions, keep growing

* Add a todo

* Fix factories

* Specs
This commit is contained in:
Daniel O'Connor authored and GitHub committed 2024-01-27 00:38:33 +10:30
1 parent 254fd156f0
commit ad4a6e17e2
10 files changed
+99 -7

No files matched your search

+2
View File
@@ -127,6 +127,8 @@ gem 'faraday'
gem 'rack-cors'
gem 'icalendar'
# for signups as requested by email service
gem 'recaptcha'
+4
View File
@@ -315,6 +315,9 @@ GEM
rails-i18n
rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1)
icalendar (2.10.1)
ice_cube (~> 0.16)
ice_cube (0.16.4)
ice_nine (0.11.2)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
@@ -702,6 +705,7 @@ DEPENDENCIES
haml_lint (>= 0.25.1)
hashie (>= 3.5.3)
i18n-tasks
icalendar
jquery-rails
jquery-ui-rails!
jsonapi-resources
+1 -1
View File
@@ -3,6 +3,7 @@
class PlantingsController < DataController
after_action :update_crop_medians, only: %i(create update destroy)
after_action :update_planting_medians, only: :update
respond_to :ics, only: [:index] # TODO: This can be shifted up when all relevant controllers respond to ical
def index
@show_all = params[:all] == '1'
@@ -29,7 +30,6 @@ class PlantingsController < DataController
)
@filename = "Growstuff-#{specifics}Plantings-#{Time.zone.now.to_fs(:number)}.csv"
respond_with(@plantings)
end
+2
View File
@@ -30,6 +30,8 @@
= link_to (@owner ? member_plantings_path(@owner, format: format) : plantings_path(format: format)) do
= icon 'fas', format.to_s
= format.upcase
- if @owner
.badge.badge-info= link_to "iCal", member_plantings_path(@owner, format: 'ics', protocol: 'webcal', only_path: false)
.badge.badge-success= link_to 'API Methods', '/api-docs'
.col-md-10
+42
View File
@@ -0,0 +1,42 @@
<%
# TODO Refactor to a Planting <-> Ical view class?
cal = Icalendar::Calendar.new
cal.description = "Plantings by #{@owner.login_name}"
@plantings.each do |planting|
event = Icalendar::Event.new
lines = []
lines << "Quantity: #{planting['quantity'] ? planting['quantity'] : 'unknown' }"
lines << "Planted on: #{planting['planted_at'] ? planting['planted_at'] : 'unknown' }"
lines << "Sunniness: #{planting['sunniness'] ? planting['sunniness'] : 'unknown' }"
lines << "Planted from: #{planting['planted_from'] ? planting['planted_from'] : 'unknown' }"
lines << "First harvest from: #{planting['first_harvest_predicted_at'] ? planting['first_harvest_predicted_at'] : 'unknown' }"
lines << "Last harvest from: #{planting['last_harvest_predicted_at'] ? planting['last_harvest_predicted_at'] : 'unknown' }"
lines << "Finish predicted at: #{planting['finish_predicted_at'] ? planting['finish_predicted_at'] : 'unknown'}"
lines << "Finished at: #{planting['finished_at'] ? planting['finished_at'] : 'unknown' }"
lines << planting.description
finish_date = Date.parse(planting['finished_at'] || planting['finish_predicted_at'] || planting['last_harvest_predicted_at']) rescue nil
event.dtstart = Time.at(planting['created_at'])
event.dtend = finish_date || 1.day.from_now
event.summary = planting['crop_name']
event.description = lines.join("\n")
event.ip_class = "PUBLIC"
event.url = planting_url(slug: planting['slug'])
cal.add_event(event)
if finish_date && finish_date > Date.today
todo = Icalendar::Todo.new
todo.dtstart = planting['first_harvest_predicted_at'] || finish_date || Date.today
todo.due = finish_date
todo.summary = "Harvest #{planting['crop_name']}"
cal.add_todo(todo)
end
end
cal.publish
%>
<%= cal.to_ical %>
+1
View File
@@ -3,3 +3,4 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
Mime::Type.register "text/calendar", :ics
+1 -1
View File
@@ -49,7 +49,7 @@ FactoryBot.define do
crop { FactoryBot.create(:perennial_crop) }
end
factory :predicatable_planting do
factory :predictable_planting do
crop do
crop = FactoryBot.create(:annual_crop)
FactoryBot.create(:planting, crop:, planted_at: 10.days.ago)
+4 -4
View File
@@ -30,8 +30,8 @@ describe "Planting reminder email", :js do
context "when member has some plantings" do
# Bangs are used on the following 2 let blocks in order to ensure that the plantings are present
# in the database before the email is generated: otherwise, they won't be present in the email.
let!(:p1) { FactoryBot.create(:predicatable_planting, planted_at: 10.days.ago, garden: member.gardens.first, owner: member) }
let!(:p2) { FactoryBot.create(:predicatable_planting, planted_at: 30.days.ago, garden: member.gardens.first, owner: member) }
let!(:p1) { FactoryBot.create(:predictable_planting, planted_at: 10.days.ago, garden: member.gardens.first, owner: member) }
let!(:p2) { FactoryBot.create(:predictable_planting, planted_at: 30.days.ago, garden: member.gardens.first, owner: member) }
describe "lists plantings" do
it { expect(mail).to have_content "Progress report" }
@@ -50,8 +50,8 @@ describe "Planting reminder email", :js do
context "when member has some harvests" do
# Bangs are used on the following 2 let blocks in order to ensure that the plantings are present
# in the database before the spec is run.
let!(:p1) { FactoryBot.create(:predicatable_planting, garden: member.gardens.first, owner: member, planted_at: 20.days.ago) }
let!(:p2) { FactoryBot.create(:predicatable_planting, garden: member.gardens.first, owner: member) }
let!(:p1) { FactoryBot.create(:predictable_planting, garden: member.gardens.first, owner: member, planted_at: 20.days.ago) }
let!(:p2) { FactoryBot.create(:predictable_planting, garden: member.gardens.first, owner: member) }
let!(:h1) { FactoryBot.create(:harvest, owner: member, planting: p1, harvested_at: 1.day.ago) }
let!(:h2) { FactoryBot.create(:harvest, owner: member, planting: p2, harvested_at: 3.days.ago) }
+1 -1
View File
@@ -26,7 +26,7 @@ describe "Display a planting", :js do
end
context 'Annual with predicted finish' do
let(:planting) { FactoryBot.create(:predicatable_planting, planted_at: 2.weeks.ago) }
let(:planting) { FactoryBot.create(:predictable_planting, planted_at: 2.weeks.ago) }
it { expect(page).to have_text '28%' }
it { expect(page).to have_text '14/50 days' }
+41
View File
@@ -10,4 +10,45 @@ describe "Plantings" do
response.status.should be(200)
end
end
context "with a member" do
before do
@member = create(:interesting_member)
@predictable_planting = create(:predictable_planting, owner: @member)
@seedling_planting = create(:seedling_planting, owner: @member)
@seed_planting = create(:seed_planting, owner: @member)
@finished_planting = create(:finished_planting, owner: @member)
@annual_planting = create(:annual_planting, owner: @member)
@perennial_planting = create(:perennial_planting, owner: @member)
Planting.reindex
end
describe "GET /members/x/plantings.ics" do
it "works!" do
get member_plantings_path(@member, format: "ics")
calendar = Icalendar::Parser.new(response.body, true).parse.first
expect(calendar.description[0].to_s).to eq "Plantings by #{@member.login_name}"
events = calendar.events
expect(events.length).to eq 6 # There are 7, but finished plantings aren't included
# TODO: Better date comparison
# Predicted finish should be used
expect(events[1].summary.to_s).to include @predictable_planting.crop.name
expect(events[1].dtstart.to_datetime.to_i).to be_within(1.second).of @predictable_planting.created_at.to_i
expect(events[1].dtend.to_date).to eq @predictable_planting.finish_predicted_at
# Actual finish should be used
# expect(events[4].dtend.to_date).to be_within(1.second).of @finised_planting.finished_at
# Otherwise, tomorrow should be used
expect(events[2].dtend.to_date).to eq 1.day.from_now.to_date
# TBA: Perennial and annual crops predictions of 'next' harvest date don't really fit
response.status.should be(200)
end
end
end
end