Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4473 expand reminder date possibilities #4606

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ gem "geocoder"
gem 'httparty'
# Generate .ics calendars for use with Google Calendar
gem 'icalendar', require: false
# Offers functionality for date reocccurances
gem "ice_cube"
# JSON Web Token encoding / decoding (e.g. for links in e-mails)
gem "jwt"
# Use Newrelic for logs and APM
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ DEPENDENCIES
guard-rspec
httparty
icalendar
ice_cube
image_processing
importmap-rails (~> 2.0)
jbuilder
Expand Down
12 changes: 12 additions & 0 deletions app/assets/stylesheets/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,15 @@
margin-top: 40px;
}
}

#week-day-fields, #date-fields {
display: none;
}

#toggle-to-week-day:checked ~ #week-day-fields {
display: block;
}

#toggle-to-date:checked ~ #date-fields {
display: block
}
6 changes: 4 additions & 2 deletions app/controllers/admin/organizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
class Admin::OrganizationsController < AdminController
def edit
@organization = Organization.find(params[:id])
@organization.get_values_from_reminder_schedule
end

def update
Expand Down Expand Up @@ -31,6 +32,7 @@ def index

def new
@organization = Organization.new
@organization.get_values_from_reminder_schedule
account_request = params[:token] && AccountRequest.get_by_identity_token(params[:token])

@user = User.new
Expand All @@ -46,7 +48,6 @@ def new

def create
@organization = Organization.new(organization_params)

if @organization.save
Organization.seed_items(@organization)
@user = UserInviteService.invite(name: user_params[:name],
Expand Down Expand Up @@ -82,7 +83,8 @@ def destroy

def organization_params
params.require(:organization)
.permit(:name, :short_name, :street, :city, :state, :zipcode, :email, :url, :logo, :intake_location, :default_email_text, :account_request_id, :reminder_day, :deadline_day,
.permit(:name, :short_name, :street, :city, :state, :zipcode, :email, :url, :logo, :intake_location, :default_email_text, :account_request_id,
:date_or_week_day, :date, :day_of_week, :every_nth_day, :deadline_day,
users_attributes: %i(name email organization_admin), account_request_attributes: %i(ndbn_member_id id))
end

Expand Down
7 changes: 4 additions & 3 deletions app/controllers/organizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ def show

def edit
@organization = current_organization
@organization.get_values_from_reminder_schedule
end

def update
@organization = current_organization

if OrganizationUpdateService.update(@organization, organization_params)
redirect_to organization_path, notice: "Updated your organization!"
else
Expand Down Expand Up @@ -92,13 +92,14 @@ def organization_params
:name, :short_name, :street, :city, :state,
:zipcode, :email, :url, :logo, :intake_location,
:default_storage_location, :default_email_text,
:invitation_text, :reminder_day, :deadline_day,
:invitation_text, :reminder_schedule, :deadline_day,
:repackage_essentials, :distribute_monthly,
:ndbn_member_id, :enable_child_based_requests,
:enable_individual_requests, :enable_quantity_based_requests,
:ytd_on_distribution_printout, :one_step_partner_invite,
:hide_value_columns_on_receipt, :hide_package_column_on_receipt,
:signature_for_distribution_pdf,
:signature_for_distribution_pdf, :date_or_week_day, :date, :day_of_week,
:every_nth_day,
partner_form_fields: [],
request_unit_names: []
)
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/partner_groups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def create

def edit
@partner_group = current_organization.partner_groups.find(params[:id])
@partner_group.get_values_from_reminder_schedule
@item_categories = current_organization.item_categories
end

Expand All @@ -33,6 +34,7 @@ def update
private

def partner_group_params
params.require(:partner_group).permit(:name, :send_reminders, :deadline_day, :reminder_day, item_category_ids: [])
params.require(:partner_group).permit(:name, :send_reminders, :reminder_schedule,
:deadline_day, :date_or_week_day, :date, :day_of_week, :every_nth_day, item_category_ids: [])
end
end
91 changes: 87 additions & 4 deletions app/models/concerns/deadlinable.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,98 @@
module Deadlinable
extend ActiveSupport::Concern

MIN_DAY_OF_MONTH = 1
MAX_DAY_OF_MONTH = 28
EVERY_NTH_COLLECTION = [["First", 1], ["Second", 2], ["Third", 3], ["Fourth", 4], ["Last", -1]].freeze
WEEK_DAY_COLLECTION = [["Sunday", 0], ["Monday", 1], ["Tuesday", 2], ["Wednesday", 3], ["Thursday", 4], ["Friday", 5], ["Saturday", 6]].freeze

included do
attr_accessor :date_or_week_day, :date, :day_of_week, :every_nth_day
attr_reader :every_nth_collection, :week_day_collection, :date_or_week_day_collection
validates :deadline_day, numericality: {only_integer: true, less_than_or_equal_to: MAX_DAY_OF_MONTH,
greater_than_or_equal_to: MIN_DAY_OF_MONTH, allow_nil: true}
validates :reminder_day, numericality: {only_integer: true, less_than_or_equal_to: MAX_DAY_OF_MONTH,
greater_than_or_equal_to: MIN_DAY_OF_MONTH, allow_nil: true}
validate :reminder_on_deadline_day?, if: -> { date.present? }
validate :reminder_is_within_range?, if: -> { date.present? }
validates :date_or_week_day, inclusion: {in: %w[date week_day]}, if: -> { date_or_week_day.present? }
validates :day_of_week, if: -> { day_of_week.present? }, inclusion: {in: %w[0 1 2 3 4 5 6]}
validates :every_nth_day, if: -> { every_nth_day.present? }, inclusion: {in: %w[1 2 3 4 -1]}
end

def convert_to_reminder_schedule(day)
schedule = IceCube::Schedule.new
schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(day)
schedule.to_ical
end

def show_description(ical)
schedule = IceCube::Schedule.from_ical(ical)
schedule.recurrence_rules.first.to_s
end

def from_ical(ical)
return if ical.blank?
schedule = IceCube::Schedule.from_ical(ical)
rule = schedule.recurrence_rules.first.instance_values
date = rule["validations"][:day_of_month]&.first&.value

results = {}
results[:date_or_week_day] = date ? "date" : "week_day"
results[:date] = date
results[:day_of_week] = rule["validations"][:day_of_week]&.first&.day
results[:every_nth_day] = rule["validations"][:day_of_week]&.first&.occ
results
rescue
nil
end

def get_values_from_reminder_schedule
return if reminder_schedule.blank?
results = from_ical(reminder_schedule)
return if results.nil?
self.date_or_week_day = results[:date_or_week_day]
self.date = results[:date]
self.day_of_week = results[:day_of_week]
self.every_nth_day = results[:every_nth_day]
end

private

def reminder_on_deadline_day?
if date_or_week_day == "date" && date.to_i == deadline_day
errors.add(:date, "Reminder must not be the same as deadline date")
end
end

def reminder_is_within_range?
# IceCube converts negative or zero days to valid days (e.g. -1 becomes the last day of the month, 0 becomes 1)
# The minimum check should no longer be necessary, but keeping it in case IceCube changes
if date_or_week_day == "date" && date.to_i < MIN_DAY_OF_MONTH || date.to_i > MAX_DAY_OF_MONTH
errors.add(:date, "Reminder day must be between #{MIN_DAY_OF_MONTH} and #{MAX_DAY_OF_MONTH}")
end
end

def should_update_reminder_schedule
if reminder_schedule.blank?
return date_or_week_day.present?
end
sched = from_ical(reminder_schedule)
date_or_week_day != sched[:date_or_week_day].presence.to_s ||
date != sched[:date].presence.to_s ||
day_of_week != sched[:day_of_week].presence.to_s ||
every_nth_day != sched[:every_nth_day].presence.to_s
end

validates :reminder_day, numericality: {other_than: :deadline_day}, if: :deadline_day?
def create_schedule
schedule = IceCube::Schedule.new(Time.zone.now.to_date)
return nil if date_or_week_day.blank?
if date_or_week_day == "date"
return nil if date.blank?
schedule.add_recurrence_rule(IceCube::Rule.monthly(1).day_of_month(date.to_i))
else
return nil if day_of_week.blank? || every_nth_day.blank?
schedule.add_recurrence_rule(IceCube::Rule.monthly(1).day_of_week(day_of_week.to_i => [every_nth_day.to_i]))
end
schedule.to_ical
rescue
nil
end
end
8 changes: 7 additions & 1 deletion app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# name :string
# one_step_partner_invite :boolean default(FALSE), not null
# partner_form_fields :text default([]), is an Array
# reminder_day :integer
# reminder_schedule :string
# repackage_essentials :boolean default(FALSE), not null
# short_name :string
# signature_for_distribution_pdf :boolean default(FALSE)
Expand Down Expand Up @@ -110,6 +110,12 @@ def upcoming
end
end

before_save do
if should_update_reminder_schedule
self.reminder_schedule = create_schedule
end
end

after_create do
account_request&.update!(status: "admin_approved")
end
Expand Down
22 changes: 13 additions & 9 deletions app/models/partner_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
#
# Table name: partner_groups
#
# id :bigint not null, primary key
# deadline_day :integer
# name :string
# reminder_day :integer
# send_reminders :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# organization_id :bigint
# id :bigint not null, primary key
# deadline_day :integer
# name :string
# reminder_schedule :string
# send_reminders :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# organization_id :bigint
#
class PartnerGroup < ApplicationRecord
has_paper_trail
Expand All @@ -19,7 +19,11 @@ class PartnerGroup < ApplicationRecord
has_many :partners, dependent: :nullify
has_and_belongs_to_many :item_categories

before_save do
self.reminder_schedule = create_schedule
end

validates :organization, presence: true
validates :name, presence: true, uniqueness: { scope: :organization }
validates :deadline_day, :reminder_day, presence: true, if: :send_reminders?
validates :deadline_day, presence: true, if: :send_reminders?
end
20 changes: 15 additions & 5 deletions app/services/partners/fetch_partners_to_remind_now_service.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
module Partners
class FetchPartnersToRemindNowService
def fetch
current_day = Time.current.day
current_day = Time.current
deactivated_status = ::Partner.statuses[:deactivated]

partners_with_group_reminders = ::Partner.left_joins(:partner_group)
.where(partner_groups: {reminder_day: current_day})
.where.not(partner_groups: {reminder_schedule: nil})
.where.not(partner_groups: {deadline_day: nil})
.where.not(status: deactivated_status)

# where partner groups have reminder schedule match
filtered_partner_groups = partners_with_group_reminders.select do |partner|
sched = IceCube::Schedule.from_ical partner.partner_group.reminder_schedule
sched.occurs_on?(current_day)
end

partners_with_only_organization_reminders = ::Partner.left_joins(:partner_group, :organization)
.where(partner_groups: {reminder_day: nil})
.where(partner_groups: {reminder_schedule: nil})
.where(send_reminders: true)
.where(organizations: {reminder_day: current_day})
.where.not(organizations: {deadline_day: nil})
.where.not(organizations: {reminder_schedule: nil})
.where.not(status: deactivated_status)

(partners_with_group_reminders + partners_with_only_organization_reminders).flatten.uniq
filtered_organizations = partners_with_only_organization_reminders.select do |partner|
sched = IceCube::Schedule.from_ical partner.organization.reminder_schedule
sched.occurs_on?(current_day)
end
(filtered_partner_groups + filtered_organizations).flatten.uniq
end
end
end
3 changes: 1 addition & 2 deletions app/views/admin/organizations/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@
<%= f.input :city %>
<%= f.input :state, collection: us_states, class: "form-control", placeholder: "state" %>
<%= f.input :zipcode %>
<%= f.input :reminder_day, class: "form-control", placeholder: "Reminder day" %>
<%= f.input :deadline_day, class: "form-control", placeholder: "Deadline day" %>
<%= render 'shared/deadline_day_fields', f: f %>
<%= f.simple_fields_for :account_request do |account_request| %>
<%= account_request.input :ndbn_member, label: 'NDBN Membership', wrapper: :input_group do %>
<%= account_request.association :ndbn_member, label_method: :full_name, value_method: :id, label: false %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/organizations/_details.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@
</p>
</div>
<div class="mb-4">
<h3 class='font-bold'>Reminder day</h3>
<h3 class='font-bold'>Reminder Schedule</h3>
<p>
<%= fa_icon "calendar" %>
<%= @organization.reminder_day.blank? ? 'Not defined' : "The #{@organization.reminder_day.ordinalize} of each month" %>
<%= @organization.reminder_schedule.blank? ? 'Not defined' : @organization.show_description(@organization.reminder_schedule) %>
</p>
</div>
<div class="mb-4">
Expand Down
6 changes: 4 additions & 2 deletions app/views/partners/_partner_groups_table.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@
</td>
<td>
<% if pg.send_reminders %>
<span>Reminder emails are sent on the <strong class='text-bold text-red-600'><%= pg.reminder_day.ordinalize %></strong> of every month. </span>
<br>
<% if pg.reminder_schedule.present? %>
<span>Reminder emails are sent <strong class='text-bold text-red-600'><%= pg.show_description(pg.reminder_schedule) %></strong>. </span>
<br>
<% end %>
<span>Deadlines are the <strong class='text-bold text-red-600'><%= pg.deadline_day.ordinalize %></strong> of every month. </span>
<% else %>
<span class='text-gray-600 text-bold font-italic'>No</span>
Expand Down
Loading