Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Naming/VariableNumber:
Style/Documentation:
Enabled: false

Style/EmptyMethod:
Enabled: false

Style/FetchEnvVar:
Enabled: false

Expand Down
10 changes: 9 additions & 1 deletion app/controllers/donations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ def create
end

def show
@donation = Donation.active_with_includes.find(params[:id])
@donation =
if params[:allow_deleted] == "true"
Donation.with_includes.find(params[:id])
else
Donation.active_with_includes.find(params[:id])
end

redirect_to donations_path unless current_user.can_view_donation?(@donation)
end

Expand Down Expand Up @@ -67,6 +73,7 @@ def destroy

begin
@donation.soft_delete
Notification.notify_deleted_donation(current_user, @donation)
flash[:success] = "Donation '#{@donation.id}' deleted!"
rescue DeletionError => e
flash[:error] = e.message
Expand All @@ -83,6 +90,7 @@ def destroy
def destroy_closed
donation = Donation.find(params[:id])
donation.soft_delete_closed
Notification.notify_deleted_donation(current_user, donation)
flash[:success] = "Closed donation '#{donation.id}' deleted!"
redirect_to closed_donations_path
end
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/items_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@ def update # rubocop:disable Metrics/AbcSize
item_params[:value]&.delete!(",")
item_event_params = params.require(:item).permit(:edit_amount, :edit_method, :edit_reason, :edit_source)

raise PermissionError if item_event_params[:edit_reason] == "transfer_external" && !current_user.root_admin?

@item.assign_attributes item_params
@item.mark_event item_event_params
@item.update_bins!(params)

if @item.save
flash[:success] = "'#{@item.description}' updated"
Notification.notify_spoilage(current_user, @item, item_event_params)
redirect_to items_path(category_id: @item.category.id)
else
redirect_to :back, alert: @item.errors.full_messages.to_sentence
Expand Down
24 changes: 24 additions & 0 deletions app/controllers/notifications_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class NotificationsController < ApplicationController
require_permission :can_subscribe_to_notifications?

def index
end

def show
@notification = current_user.notifications.find(params[:id])
end

def update
@notification = current_user.notifications.find(params[:id])

if params[:mark_read] == "true"
@notification.update!(completed_at: Time.zone.now) unless @notification.read?
redirect_to notifications_path, flash: { success: "Marked message as read" }
elsif params[:mark_unread] == "true"
@notification.update!(completed_at: nil) unless @notification.unread?
redirect_to notifications_path, flash: { success: "Marked message as unread" }
else
raise "Update method undefined!"
end
end
end
9 changes: 9 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ def external_link(object, prefix: nil)
end
end

def notifications_badge
return unless current_user.can_subscribe_to_notifications?

@notifications_badge_count ||= current_user.unread_notification_count
return unless @notifications_badge_count > 0

tag.span(class: "badge") { @notifications_badge_count.to_s }
end

def showing_tab?(tab_id)
params[:show_tab] == tab_id || @show_tab == tab_id
end
Expand Down
14 changes: 14 additions & 0 deletions app/helpers/notifications_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module NotificationsHelper
def notification_reference_link(notification)
return unless notification.reference

case notification.reference
when Donation
link_to "Donation #{notification.reference.id}", donation_path(notification.reference, allow_deleted: true)
when Item
link_to notification.reference.description, edit_stock_item_path(notification.reference)
else
"Reference link unable to be determined"
end
end
end
6 changes: 5 additions & 1 deletion app/models/concerns/users/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ module Users
module Info
extend ActiveSupport::Concern

def root_admin?
role == "root"
end

def super_admin?
role == "admin"
root_admin? || role == "admin"
end

def report_admin?
Expand Down
48 changes: 38 additions & 10 deletions app/models/concerns/users/user_manipulator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ module UserManipulator
extend ActiveSupport::Concern
attr_reader :original_email

def can_subscribe_to_notifications?(_type = nil)
# nil type means general notification subscription check, explicit type
# asks if can subscribe to that type. Right now, they are the same thing.
root_admin?
end

def super_edit_user_access?(user)
super_admin? && role_object >= user.role_object
end

def can_invite_user?
super_admin? || admin?
end
Expand All @@ -11,8 +21,14 @@ def can_invite_user_at?(organization)
super_admin? || admin_at?(organization)
end

def can_delete_user?
super_admin?
def can_delete_user?(user = nil)
if !user
# Checking if this user has general access to delete other users
super_admin?
else
# Deleting a specific other user
super_edit_user_access?(user)
end
end

def can_update_user?(user = nil)
Expand All @@ -24,7 +40,7 @@ def can_update_user?(user = nil)
true
else
# Updating a specific user requires update user permission at that organization
super_admin? || user.organizations.any? { |organization| can_update_user_at?(organization) }
super_edit_user_access?(user) || user.organizations.any? { |organization| can_update_user_at?(organization) }
end
end

Expand All @@ -33,20 +49,20 @@ def can_force_password_reset?(user = nil)
# Checking if this user has general access to force password resets
super_admin? || admin?
else
super_admin? || user.organizations.any? { |organization| can_force_password_reset_at?(organization) }
super_edit_user_access?(user) || user.organizations.any? { |organization| can_force_password_reset_at?(organization) }
end
end

def can_update_user_details?(user)
super_admin? || user == self
super_edit_user_access?(user) || user == self
end

def can_update_user_role?(user)
super_admin? || user.organizations.any? { |organization| can_update_user_role_at?(organization) }
super_edit_user_access?(user) || user.organizations.any? { |organization| can_update_user_role_at?(organization) }
end

def can_update_password?(user)
super_admin? || user == self
super_edit_user_access?(user) || user == self
end

def can_update_user_at?(organization)
Expand Down Expand Up @@ -74,11 +90,21 @@ def invite_user(params)
invitation.invite_mailer(self).deliver_now
end

def update_user(params)
def update_user(params) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
user = transaction do
user = User.find(params[:id])
raise PermissionError unless can_update_user?(user)
user.update_details(permitted_params(params)) if can_update_user_details?(user) && params[:user].present?

if can_update_user_details?(user) && params[:user].present?
user_permitted_params = permitted_params(params)
raise PermissionError if user_permitted_params[:role] == "root" && !root_admin?
user.update_details(user_permitted_params)
end

if can_subscribe_to_notifications? && user == self
user.update_subscriptions(params.require(:subscriptions).permit(Notification::SUBSCRIPTION_TYPES.keys))
end

user.update_roles(self, params) if can_update_user_role?(user)
user.update_password(self, params) if can_update_password?(user)
user
Expand All @@ -99,7 +125,9 @@ def destroy_user(params)
raise PermissionError unless can_delete_user?

transaction do
User.find(params[:id]).organization_users.each(&:destroy!)
user_to_delete = User.find(params[:id])
raise PermissionError unless can_delete_user?(user_to_delete)
user_to_delete.organization_users.each(&:destroy!)
end
end

Expand Down
1 change: 1 addition & 0 deletions app/models/donation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Donation < ApplicationRecord

validate :not_changing_after_closed

scope :with_includes, -> { includes(:county, :user, donor: :addresses, donation_details: { item: :category }) }
scope :active_with_includes, -> { active.includes(:county, :user, donor: :addresses, donation_details: { item: :category }) }
scope :closed_with_includes, -> { closed.includes(:county, :user, donor: :addresses, donation_details: { item: :category }) }
scope :deleted_with_includes, -> { deleted.includes(:county, :user, donor: :addresses, donation_details: { item: :category }) }
Expand Down
8 changes: 5 additions & 3 deletions app/models/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ def self.for_category(category_id)
end
end

def self.selectable_edit_reasons
@selectable_edit_reasons ||= edit_reasons.reject do |x|
%w[donation donation_adjustment adjustment order_adjustment purchase reconciliation transfer].include?(x)
UNSELECTABLE_EDIT_REASONS = Set.new(%w[donation donation_adjustment adjustment order_adjustment purchase reconciliation transfer])

def self.selectable_edit_reasons(user)
edit_reasons.reject do |x|
UNSELECTABLE_EDIT_REASONS.include?(x) || (x == "transfer_external" && !user.root_admin?)
end
end

Expand Down
75 changes: 75 additions & 0 deletions app/models/notification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

class Notification < ApplicationRecord
belongs_to :user
belongs_to :reference, polymorphic: true, optional: true
belongs_to :triggered_by_user, class_name: "User", optional: true

# Notification types
SPOILAGE = "spoilage"
DELETED_DONATIONS = "deleted_donations"
DELETED_PURCHASES = "deleted_purchases"

SUBSCRIPTION_TYPES = {
spoilage: {
type: SPOILAGE,
label: "Notify me when spoilages are added"
}.freeze,
deleted_donations: {
type: DELETED_DONATIONS,
label: "Notify me when donations are deleted"
}.freeze,
deleted_purchases: {
type: DELETED_PURCHASES,
label: "Notify me when purchases are deleted"
}.freeze
}.freeze

def self.notify_deleted_donation(current_user, donation)
notify!(Notification::DELETED_DONATIONS, title: "Donation deleted", message: <<~MESSAGE, triggered_by_user: current_user, reference: donation)
Donation ##{donation.id} was deleted.
MESSAGE
end

# TODO: Purchases can only be canceled... do we notify on this or drop this notification?
def self.notify_deleted_purchase(current_user, purchase)
notify!(Notification::DELETED_PURCHASES, title: "Purchase deleted", message: <<~MESSAGE, triggered_by_user: current_user, reference: purchase)
Purchase ##{purchase.id} was deleted.
MESSAGE
end

def self.notify_spoilage(current_user, item, params)
return unless params[:edit_amount] && params[:edit_method] && params[:edit_reason]
return unless params[:edit_reason] == "spoilage"

notify!(Notification::SPOILAGE, title: "Spoilage for item #{item.description}", message: <<~MESSAGE, triggered_by_user: current_user, reference: item) # rubocop:disable Layout/LineLength
Spoilage update for item ##{item.id} (#{item.category.description} - #{item.description}):

Stock #{params[:edit_method]} by #{params[:edit_amount]}.

Reason: #{params[:edit_source]}
MESSAGE
end

def self.notify!(type, title:, message:, triggered_by_user: nil, reference: nil)
caught_error = nil

NotificationSubscription.includes(:user).where(notification_type: type, enabled: true).find_each do |subscription|
next unless subscription.user.can_subscribe_to_notifications?(type)

create!(title: title, message: message, user: subscription.user, triggered_by_user: triggered_by_user, reference: reference)
rescue StandardError => e
caught_error = e
end

raise caught_error if caught_error
end

def unread?
completed_at.blank?
end

def read?
completed_at.present?
end
end
3 changes: 3 additions & 0 deletions app/models/notification_subscription.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class NotificationSubscription < ApplicationRecord
belongs_to :user
end
27 changes: 27 additions & 0 deletions app/models/role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Role
include Comparable
attr_reader :value

def initialize(value)
@value = value
end

def <=>(other)
to_i <=> other.to_i
end

def to_i
case value
when "root"
3
when "admin"
2
when "report"
1
when "none"
0
else
raise "Unknown role value: #{value.inspect}"
end
end
end
Loading