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

Refactor update owner audit activity #700

Merged
merged 1 commit into from
Jun 11, 2020
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
55 changes: 23 additions & 32 deletions psd-web/app/controllers/investigations/ownership_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,37 @@ class Investigations::OwnershipController < ApplicationController
include Wicked::Wizard
before_action :set_investigation
before_action :authorize_user
before_action :potential_owner, only: %i[show create]
before_action :store_owner, only: %i[update]

steps :"select-owner", :confirm

def show
@potential_owner = potential_owner&.decorate
@potential_owner = form.owner&.decorate
render_wizard
end

def new
clear_session
session[session_store_key] = nil
redirect_to wizard_path(steps.first, request.query_parameters)
end

def update
if owner_valid?
redirect_to next_wizard_path
else
render_wizard
end
return render_wizard unless form.valid?

session[session_store_key] = form.attributes.compact
redirect_to next_wizard_path
end

def create
@investigation.owner = potential_owner
@investigation.owner_rationale = params[:investigation][:owner_rationale]
@investigation.save
redirect_to investigation_path(@investigation), flash: { success: "#{@investigation.case_type.upcase_first} owner changed to #{potential_owner.decorate.display_name}" }
end
ChangeCaseOwner.call!(investigation: @investigation, owner: form.owner, rationale: form.owner_rationale, user: current_user)

private
session[session_store_key] = nil

def clear_session
session[:owner_id] = nil
message = "#{@investigation.case_type.upcase_first} owner changed to #{form.owner.decorate.display_name(viewer: current_user)}"
redirect_to investigation_path(@investigation), flash: { success: message }
end

private

def set_investigation
@investigation = Investigation.find_by!(pretty_id: params[:investigation_pretty_id]).decorate
end
Expand All @@ -46,7 +41,12 @@ def authorize_user
authorize @investigation, :change_owner_or_status?
end

def owner_params
def session_store_key
"update_case_owner_#{@investigation.pretty_id}_params"
Copy link
Contributor Author

@slorek slorek Jun 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously the key was :owner_id which is too generic. We now use the form name and case ID in the key so that the user could have multiple forms open without them conflicting.

end

def form_params
params[:investigation] ||= {}
params[:investigation][:owner_id] = case params[:investigation][:owner_id]
when "someone_in_your_team"
params[:investigation][:select_team_member]
Expand All @@ -59,23 +59,14 @@ def owner_params
else
params[:investigation][:owner_id]
end
params.require(:investigation).permit(:owner_id)
end

def store_owner
session[:owner_id] = owner_params[:owner_id]
params.require(:investigation).permit(:owner_id, :owner_rationale).merge(session_params)
end

def owner_valid?
if step == :"select-owner"
if potential_owner.nil?
@investigation.errors.add(:owner_id, :invalid, message: "Select case owner")
end
end
@investigation.errors.empty?
def session_params
session[session_store_key] || {}
end

def potential_owner
User.find_by(id: session[:owner_id]) || Team.find_by(id: session[:owner_id])
def form
@form ||= ChangeCaseOwnerForm.new(form_params)
end
end
2 changes: 1 addition & 1 deletion psd-web/app/controllers/investigations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,6 @@ def build_breadcrumbs
end

def set_suggested_previous_owners
@suggested_previous_owners = suggested_previous_owners
@suggested_previous_owners = suggested_previous_owners(@investigation)
end
end
28 changes: 28 additions & 0 deletions psd-web/app/forms/change_case_owner_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class ChangeCaseOwnerForm
include ActiveModel::Model
include ActiveModel::Attributes

attribute :owner_id
attribute :owner_rationale

validates_presence_of :owner_id
validate :new_owner_must_be_active_user_or_team, if: -> { owner_id.present? }

def owner
user || team
end

private

def new_owner_must_be_active_user_or_team
errors.add(:owner_id, :not_found) unless owner
end

def user
@user ||= User.active.find_by(id: owner_id)
end

def team
@team ||= Team.find_by(id: owner_id)
end
end
4 changes: 2 additions & 2 deletions psd-web/app/helpers/investigations_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ def user_ids_from_team(team)
[team.id] + team.users.map(&:id)
end

def suggested_previous_owners
all_past_owners = @investigation.past_owners + @investigation.past_teams
def suggested_previous_owners(investigation)
all_past_owners = investigation.past_owners
return [] if all_past_owners.empty? || all_past_owners == [current_user]

all_past_owners || []
Expand Down
11 changes: 8 additions & 3 deletions psd-web/app/models/activity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ def attachments
{}
end

def subtitle
"#{subtitle_slug} by #{source&.show}, #{pretty_date_stamp}"
# Can be overridden by child classes but sometimes need to pass in user
def title(_user)
super()
end

def subtitle(viewer)
"#{subtitle_slug} by #{source&.show(viewer)}, #{pretty_date_stamp}"
end

def search_index; end
Expand All @@ -31,7 +36,7 @@ def can_display_all_data?(_user)
true
end

def restricted_title
def restricted_title(_user)
# where necessary should be implemented by subclasses
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def self.build_email_address(correspondence)
output + "<br>"
end

def restricted_title
def restricted_title(_user)
"Email added"
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def activity_type
"meeting"
end

def restricted_title
def restricted_title(_user)
"Meeting added"
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def self.build_file_body(correspondence)
file.attached? ? "Attached: #{sanitize_text file.filename}<br>" : ""
end

def restricted_title
def restricted_title(_user)
"Phone call added"
end

Expand Down
2 changes: 1 addition & 1 deletion psd-web/app/models/audit_activity/document/add.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def email_update_text(viewer = nil)
"Document was attached to the #{investigation.case_type.upcase_first} by #{source&.show(viewer)}."
end

def restricted_title
def restricted_title(_user)
"Document added"
end

Expand Down
2 changes: 1 addition & 1 deletion psd-web/app/models/audit_activity/document/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def attached_image?
attachment.image?
end

def restricted_title; end
def restricted_title(_user); end

def can_display_all_data?(user)
can_be_displayed?(attachment, investigation, user)
Expand Down
2 changes: 1 addition & 1 deletion psd-web/app/models/audit_activity/document/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ def self.from(document, investigation)
super(document, investigation, title)
end

def restricted_title
def restricted_title(_user)
"Document deleted"
end

Expand Down
2 changes: 1 addition & 1 deletion psd-web/app/models/audit_activity/document/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def self.description_changed?(document, previous_data)
document.metadata[:description] != previous_data[:description]
end

def restricted_title
def restricted_title(_user)
"Document updated"
end

Expand Down
6 changes: 3 additions & 3 deletions psd-web/app/models/audit_activity/investigation/add.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def complainant

# title may change after investigation was created, so we retrieve from the
# metadata stored at the time of creation
def title
def title(_user)
return self[:title] if self[:title] # older activities stored this in the database

"#{investigation.case_type.titleize} logged: #{metadata['investigation']['title']}"
Expand All @@ -49,8 +49,8 @@ def can_display_all_data?(user)
end

# Only used for old records prior to metadata implementation
def restricted_title
title
def restricted_title(user)
title(user)
end

private
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
class AuditActivity::Investigation::AutomaticallyUpdateOwner < AuditActivity::Investigation::Base
def self.from(investigation)
title = investigation.owner.id.to_s
super(investigation, title)
def self.from(*)
raise "Deprecated - use DeleteUser.call instead"
end

def owner_id
# We store owner_id in title field, this is getting it back
# Using alias for accessing parent method causes errors elsewhere :(
AuditActivity::Investigation::Base.instance_method(:title).bind(self).call
def self.build_metadata(owner)
{
owner_id: owner.id
}
end
Comment on lines +6 to 10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this pattern elsewhere, but I wonder whether we could avoid the class method and just pass the owner_id when initializing the new object? (ie so you can do ActivityClass.create!(owner_id: owner_id, ...) rather than having to build the metadata object first?

Some possible implementations:

class AuditActivity::Investigation::AutomaticallyUpdateOwner < AuditActivity::Investigation::Base

  attr_writer :owner_id

  def metadata
    read_attribute(:metadata) || {
      owner_id: @owner_id
    }
  end

end

or overriding the initialiser:

class AuditActivity::Investigation::AutomaticallyUpdateOwner < AuditActivity::Investigation::Base
  attr_writer :owner_id

  def initialize(*args)
    super(args)
    return unless new_record?
    
    self.metadata = {
      owner_id: owner_id
    }
  end
end

or using the activerecord after_initialize callback to avoid overriding:

class AuditActivity::Investigation::AutomaticallyUpdateOwner < AuditActivity::Investigation::Base
  attr_writer :owner_id
  after_initialize :set_metadata

  def set_metadata
    return unless new_record?

    self.metadata = {
      owner_id: @owner_id
    }
  end
end

Worth exploring?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's an interesting idea. I think at the moment where we have dozens of classes inheriting from Activity there should be some consistent interface, but as we move over to services perhaps this is less important as I think it will only be accessed from the related service class. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to park this idea for this PR - we should try it separately and see how it looks.


# We store owner_id in title field, this is computing title based on that
def title
def title(user)
type = investigation.case_type.capitalize
new_owner = (User.find_by(id: owner_id) || Team.find_by(id: owner_id))&.decorate&.display_name
new_owner = owner.decorate.display_name(viewer: user)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing user in here eliminates a dependency on User.current.

"Case owner automatically changed on #{type} to #{new_owner}"
end

def subtitle
def subtitle(_viewer)
"Case owner automatically changed, #{pretty_date_stamp}"
end

def entities_to_notify
[]
end
private

def email_subject_text; end
def owner
User.find_by(id: metadata["owner_id"]) || Team.find_by(id: metadata["owner_id"])
end
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could avoid this by storing the owner team ID and the owner user ID under separate metadata keys? Especially as we'll soon be always storing the team ID as the "owner" on a case, with an optional user id as "assigned officer"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought storing the owner_type could avoid it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought initially, but then remembered that we had planned to change "owner" to always be the team, and optionally additionally store the user. Don't think we've done that on the investigation model yet though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would still need to store historical audit activity where it could be a user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially backfill those (by looking up the current team for that user), which is what I think we're planning to do for cases?

Happy to leave this to a follow-up PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started looking at this but I quickly realised it doesn't make a lot of sense to do this until the change to always be a team. Currently it can be either, and we can easily migrate the data at a later date.


def email_update_text(viewer = nil); end
# Do not send investigation_updated mail. This is handled by the DeleteUser service
def notify_relevant_users; end
end
4 changes: 2 additions & 2 deletions psd-web/app/models/audit_activity/investigation/team_added.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class AuditActivity::Investigation::TeamAdded < AuditActivity::Investigation::Base
def subtitle
"Team added by #{source&.show}, #{pretty_date_stamp}"
def subtitle(viewer)
"Team added by #{source&.show(viewer)}, #{pretty_date_stamp}"
end

private
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class AuditActivity::Investigation::TeamDeleted < AuditActivity::Investigation::Base
def subtitle
"Team removed by #{source&.show}, #{pretty_date_stamp}"
def subtitle(viewer)
"Team removed by #{source&.show(viewer)}, #{pretty_date_stamp}"
end

private
Expand Down
63 changes: 15 additions & 48 deletions psd-web/app/models/audit_activity/investigation/update_owner.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
class AuditActivity::Investigation::UpdateOwner < AuditActivity::Investigation::Base
include NotifyHelper

def self.from(investigation)
title = investigation.owner.id.to_s
body = investigation.owner_rationale
super(investigation, title, sanitize_text(body))
def self.from(*)
raise "Deprecated - use ChangeCaseOwner.call instead"
end

def owner_id
# We store owner_id in title field, this is getting it back
# Using alias for accessing parent method causes errors elsewhere :(
AuditActivity::Investigation::Base.instance_method(:title).bind(self).call
def self.build_metadata(owner, rationale)
{
owner_id: owner.id,
rationale: rationale
}
Comment on lines +6 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should also store the owner’s name, to guard against the user or team being deleted in future, and so that we can display the user/team’s name at the time they became the new owner? I think that’s what you'd expect to see when reading through the timeline of events, although it’s hard to know for sure. @tobyhughes @edwardhorsford?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure! We're disabling users, not deleting them - so this would mean name updates get reflected. On the other hand we could take the view that these audit entries are a point in time snapshot so could just store the value of the data at that time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users should not be deleted entirely, just marked with a deleted flag. Their names and team membership can change over time. Storing their team as well might be a good shout, but this wouldn't be used anywhere currently and we'd have to do some work refactoring the way we display activities to actually use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think displaying the name at-the-time would probably be ideal. But it's also not really essential, so happy to park this for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this'll need a fair bit of re-jigging which is going to tie this up for a while, would be pleased to see a follow up PR for this though.

end

def title
# We store owner_id in title field, this is computing title based on that
"Case owner changed to #{(User.find_by(id: owner_id) || Team.find_by(id: owner_id))&.decorate&.display_name}"
def title(user)
"Case owner changed to #{owner.decorate.display_name(viewer: user)}"
end

def email_update_text(viewer = nil)
body = []
body << "Case owner changed on #{investigation.case_type} to #{investigation.owner.decorate.display_name(viewer: viewer)} by #{source&.show(viewer)}."

if investigation.owner_rationale.present?
body << "Message from #{source&.show(viewer)}:"
body << inset_text_for_notify(investigation.owner_rationale)
end

body.join("\n\n")
def body
metadata["rationale"]
end

def email_subject_text
"Case owner changed for #{investigation.case_type}"
def owner
User.find_by(id: metadata["owner_id"]) || Team.find_by(id: metadata["owner_id"])
end

private
Expand All @@ -40,27 +28,6 @@ def subtitle_slug
"Changed"
end

def users_to_notify
compute_relevant_entities(model: User, compute_users_from_entity: proc { |user| [user] })
end

def teams_to_notify
compute_relevant_entities(model: Team, compute_users_from_entity: proc { |team| team.users })
end

def compute_relevant_entities(model:, compute_users_from_entity:)
return [] unless investigation.saved_changes.key?("owner_id")

previous_owner_id = investigation.saved_changes["owner_id"][0]
previous_owner = model.find_by(id: previous_owner_id)
new_owner = investigation.owner
owner_changed_by = source.user

old_users = previous_owner.present? ? compute_users_from_entity.call(previous_owner) : []
old_entities = previous_owner.present? ? [previous_owner] : []
new_entities = new_owner.is_a?(model) ? [new_owner] : []
return new_entities if old_users.include? owner_changed_by

(new_entities + old_entities).uniq
end
# Do not send investigation_updated mail. This is handled by the ChangeCaseOwner service
def notify_relevant_users; end
end
Loading