-
Notifications
You must be signed in to change notification settings - Fork 5
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
Conversation
29e4c8b
to
1ba2a97
Compare
1ba2a97
to
3ae9fc6
Compare
3ae9fc6
to
4108690
Compare
4108690
to
1f9c7f3
Compare
1f9c7f3
to
292d722
Compare
|
||
delegate :investigation, :owner, :rationale, :user, :old_owner, to: :context | ||
|
||
def call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Method call
has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really see the complexity - probably moving the required params checking out will reduce this score but I think it will be more difficult to follow, and it's not a pattern I want to establish.
292d722
to
4891c92
Compare
@@ -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" |
There was a problem hiding this comment.
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.
psd-web/app/models/activity.rb
Outdated
end | ||
|
||
def subtitle(user) | ||
"#{subtitle_slug} by #{source&.show(user)}, #{pretty_date_stamp}" |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: rename this viewer
or similar, to make it a bit clearer who/what the user is?
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) |
There was a problem hiding this comment.
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
.
@@ -96,8 +97,7 @@ def important_owner_people | |||
|
|||
def past_owners |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now returns both teams and users; it is only actually used in one helper method.
entities = [owner] | ||
entities << old_owner if old_owner != user | ||
|
||
entities.map { |entity| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to extract this logic out to the Team
class in a later refactor.
@@ -0,0 +1,33 @@ | |||
class MoveUpdateOwnerActivityMetadata < ActiveRecord::Migration[5.2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tested this locally and originally intended it to be a Rake task but that would require taking the service into maintenance mode; I don't think it's necessary and this allows for the quickest deployment. Also there is precedent in our project for using migrations with no schema changes.
4891c92
to
164a1bd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Approved as I've just added comments / suggestions (a couple of which apply more widely than this PR).
psd-web/app/models/activity.rb
Outdated
end | ||
|
||
def subtitle(user) | ||
"#{subtitle_slug} by #{source&.show(user)}, #{pretty_date_stamp}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: rename this viewer
or similar, to make it a bit clearer who/what the user is?
def owner | ||
User.find_by(id: metadata["owner_id"]) || Team.find_by(id: metadata["owner_id"]) | ||
end |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
context.fail!(error: "No investigation supplied") unless investigation.is_a?(Investigation) | ||
context.fail!(error: "No owner supplied") unless owner.is_a?(User) || owner.is_a?(Team) | ||
context.fail!(error: "No user supplied") unless user.is_a?(User) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We’ve done this elsewhere, and I can see how it could be helpful, but it feels a bit un-Rubyish to do this kind of type checking and defensive programming (until Ruby gets type annotations?). I don't think we have any code which relies on these failures? We could considering dropping these lines, particularly if the method throws an exception if these are nil later on in the execution?
Happy to keep them if we decide they're helpful though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern is the input parameters aren't documented anywhere (normally you'd at least have a list of arguments to the method).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is one think I dislike about the Interactor gem. It’d be nice if you could define required keyword arguments in the call
method. It looks like there’s an open pull request to add this, but it's not been merged yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's see if we can come up with a better pattern which we can apply consistently in a follow up PR.
investigation.owner = owner | ||
investigation.save! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: combine with:
investigation.update!(owner: owner)
def email_body(viewer = nil) | ||
user_name = user.decorate.display_name(viewer: viewer) | ||
body = "Case owner changed on #{investigation.case_type} to #{owner.decorate.display_name(viewer: viewer)} by #{user_name}." | ||
body << "\n\nMessage from #{user_name}: #{inset_text_for_notify(rationale)}" if rationale | ||
body | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bit shocked that this doesn’t get built within the mailer job itself! One for a future refactor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I thought the same. Probably need a more tailored mailer method.
def self.build_metadata(owner, rationale) | ||
{ | ||
owner_id: owner.id, | ||
rationale: rationale | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
def self.build_metadata(owner) | ||
{ | ||
owner_id: owner.id | ||
} | ||
end |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. 🤔
There was a problem hiding this comment.
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.
164a1bd
to
9975282
Compare
…llbacks and dependency on User.current
9975282
to
642bdf0
Compare
* origin/master: Bump rubocop and rubocop-govuk in /psd-web (#704) Only show complainant details on case added activity if user is collaborating on the case (#696) Fix error when trying to display automatically updated case ownership change activity (#708) Remove User.current dependency from display_name and show methods (#701) Refactor update owner audit activity to store metadata, and remove callbacks and dependency on User.current (#700)
Description
This is a continuation of the previous refactoring work to:
Investigation
model to a service classUser.current
being setAdditionally, this PR introduces a simple example of a proposed pattern to improve the wizard controllers with the use of form objects for input validation. We still have to persist the previous page's inputs in the session, but the new implementation makes it possible to have multiple tabs open for different cases at the same time.
This owner updated activity was particularly crazy because the new owner ID was stored in the
title
column (presumably because it was the only place to put it). It made no sense, but with a metadata object we can store this sensibly.I fixed an apparent issue when the app sent notifications of the case owner being updated. Previously if a team had a email address set it was ignored and each user was emailed individually. Now it will just email the team's address if set.
I also fixed a number of issues with user names being displayed without passing in the
current_user
, which meant it fell back toUser.current
. This necessitated the activitytitle
, andsubtitle
needing theuser
to be supplied as an argument. This is only needed on a couple of activity types, but unfortunately because of the inheritance there is a need to have a consistent interface. This is mostly a short-term solution which can be improved on later.The current form also shows inactive users as potential case owners for selection, and also emails inactive users when the case is updated. I've fixed these bugs.
Checklist: