# APPEALS-10565 Research We require a way to capture and track the requests for users who are requesting membership to VHA organizations. At the moment it is unclear what the best way to go about this would be as there do not seem to be any existing notification systems in Caseflow that are specific to user management (i.e. not tied to some other Caseflow business process i.e. appeals, hearings etc.). For this reason, although some sort of task system on the surface would seem to make sense - all existing tasks relate to one of the aforementioned business processes. Therefore attempting to utilize a task (at least as we currently do) would seem to present some hurdles to in terms of creating one that pertains only to user management. The ask of this research task is to investigate and document the feasibility of potential solutions for this new user management system that we'll need to implement. ## Feasibility of Using Tasks This section aims to address the feasibility of using the existing Tasks table/class to capture and track the existance of notifications Initial thoughts: Tasks, as the are currently used, are typically associated with appeals in almost all cases that I am aware of. In this ask, there would be no need for any type of appeal information to be generated or tracked, and therefore tasks might not be the best method to accomplish this. ### General Task Overview Below is a list of all the task associations and attributes ```ruby #Task Association Names [ :versions, :parent, :children, :assigned_to, :assigned_by, :cancelled_by, :appeal, :ama_appeal, :legacy_appeal, :supplemental_claim, :higher_level_review, :attorney_case_reviews, :task_timers, :cached_appeal ] #Task Attributes { "id"=>nil, "appeal_id"=>nil, "appeal_type"=>nil, "assigned_at"=>nil, "assigned_by_id"=>nil, "assigned_to_id"=>nil, "assigned_to_type"=>nil, "cancellation_reason"=>nil, "cancelled_by_id"=>nil, "closed_at"=>nil, "created_at"=>nil, "instructions"=>[], "parent_id"=>nil, "placed_on_hold_at"=>nil, "started_at"=>nil, "status"=>"assigned", "type"=>nil, "updated_at"=>nil } ``` From the list we can see that there are a lot of attibutes and associations that would be irrelevant for what we are trying to accomplish. The two associations we are most interested in are assigned_to and assigned_by as that can be used to capture the user that made the request and the organization the user is requesting to join. The attributes that we are most interested in (excluding keys and ids) are status, started_at, updated_at, and closed_at. These will probably be enough to identify the state of the request from initial request all the way through rejecting or accepting the request. The instructions field could also be potentially useful for conveying information between the two parties. ### Reusing an existing task The EngineeringTask is an example of a task created for a purpose that external to the normal appeals workflow. However, it is still tied to an appeal like most other tasks. QualityReviewTask is similar to the EngineeringTask for establishing precedent for tasks that aren't directly a part of the normal caseflow workflow. A TimeableTask might be reusable to schedule, process, and cleanup the requests to join an organization MailTask children keep track of email addresses and could potentially be reused in some way. #### Synopsis: There don't seem to be any existing tasks that would currently fit the ask and I don't recommend modifying or repurposing an existing existing task to accomplish this. ### Creating a new task Creating a new task to capture the effort is easy enough ```ruby # Simple inheritance with no specific functionality yet class JoinOrganizationTask < Task end ``` We can then use this new task in our form on the help page to create a request to an organization. We need to update the TasksController to include our new task type. Specifically adding the line below to this hash TASK_CLASSES_LOOKUP ```ruby JoinOrganizationTask: JoinOrganizationTask, ``` Now we should be able to create this new task through the UI with a post request with this payload **tbd update this so that it works with actual values** ```javascript // Create a post request to the url /tasks // Use the below payload to create the task { type: JoinOrganizationTask, assigned_to: VhaCamo.singleton!, assigned_by: User } ``` But before that let's go into the console and try to create one of our new task types ```ruby new_task = JoinOrganizationTask.create! ``` Whoops ![](https://i.imgur.com/BXmPneZ.png) Well, unfortunately due to this line in the Tasks.rb model you are always expected to have an Appeal ```ruby validates :assigned_to, :appeal, :type, :status, presence: true ``` So we have to hack our subclass a bit to dodge this validation ```ruby # Simple inheritance with no specific functionality yet class JoinOrganizationTask < Task # Removes the validators from the tasks superclass clear_validators! # Reapplys the validators to the subclass excluding appeal validates :assigned_to, :assigned_by, :type, :status, presence: true end ``` Let's try that again ```ruby new_task = JoinOrganizationTask.create!(assigned_to: VhaCamo.singleton, assigned_by: User.first) ``` And it fails! Again... ![](https://i.imgur.com/cxtfYQY.png) In the Tasks.rb file in verify_org_task_unique it is once again expecting an appeal. This is already requiring quite a few hacks to avoid the appeal association. So lets override the method in our subclass ```ruby def verify_org_task_unique # We could still use this to check if the org has other tasks of this type # The check would only allow one of these tasks with the assigned user # To this assigned org with the status of assigned or open ``` ![](https://i.imgur.com/ddMGT7g.png) There's a database constraint as well. That's yet one more pretty large change that has to be made to use tasks for this purpose. ```ruby! # rails generate migration RemoveNulls # Edit the script to look like this class RemoveNulls < ActiveRecord::Migration[5.2] def change change_column_null :tasks, :appeal_id, true change_column_null :tasks, :appeal_type, true end end #inside the Schema.rb should look like this create_table "tasks", force: :cascade do |t| t.integer "appeal_id" t.string "appeal_type" end ``` Finally, it will allow object creation! ![](https://i.imgur.com/163iYQO.png) TBD Test it somehow. Possibly remove csrf tokens or actually make the form on the page ### Help page form for testing it out We need some way to test out the controller and disabling the csrf token check is gross, so we might as well build out a simple form for vha help page to test the task since it's part of the ask. On the pages page at the url /help add the new vha help page link Add a new object to the pages array in HelpRootView.jsx file to create a new link on the page ```jsx { name: 'VHA Help', url: '/vha/help' } ``` Add a new route for the url above in the help/index.js file ```jsx! // Also import this at the top import VhaHelp from './components/VhaHelp'; <PageRoute exact path="/vha/help" title="Vha Help" component={VhaHelp} /> ``` Finally go create the VhaHelp.jsx file and export the VhaHelp Component ```jsx! import React from 'react'; class VhaHelp extends React.Component { render() { return <div className="cf-help-content"> <h1> Vha Help page</h1> </div>; } } export default VhaHelp; ``` Now that the javascript side is generated for now. We have to update the controller methods In application_controller.rb update the help_url method which is used for the routing ```ruby! def help_url { "certification" => certification_help_path, "dispatch-arc" => dispatch_help_path, "reader" => reader_help_path, "hearings" => hearing_prep_help_path, "intake" => intake_help_path, "vha" => vha_help_path }[application] || help_path end ``` In routes.rb add a vha help route ```ruby! # Add a route with this url to point to the help controller which will go up to application controller and use the method updated above to create the appropiate url to controller action mapping get 'vha/help' => 'help#vha' ``` Lastly go create a vha.html.erb file in the same location as the other help files and copy their content to the new file ```htmlmixed! <% content_for :full_page_content do %> <%= react_component("Help", props: { userDisplayName: current_user ? current_user.display_name : "", dropdownUrls: dropdown_urls, feedbackUrl: feedback_url, buildDate: build_date }) %> <% end %> ``` That should take care of all the routing and if we load the page we should see our new help page at the following url: /vha/help ![](https://i.imgur.com/sk9WmB2.png) If we add a simple form to the page we should be able to create an instance of our new task using the tasks controller by sending a post request to the /tasks url. Updating the VhaHelp.jsx to include a form and a submit function with the correct post data. ```javascript! import React from 'react'; import ApiUtil from 'app/util/ApiUtil'; class VhaHelp extends React.Component { constructor(props) { super(props); this.state = { results: '', }; this.handleSubmit = this.handleSubmit.bind(this); } async handleSubmit(event) { event.preventDefault(); const payload = { data: { tasks: [ { type: 'JoinOrganizationTask', assigned_to_id: 2000000222, assigned_to_type: 'BusinessLine' } ] } }; try { const results = await ApiUtil.post('/tasks', payload); this.setState({ results: JSON.stringify(results.body.tasks.data[0], undefined, 2) }); } catch (error) { console.log(error); // throw error; } } render() { return ( <div className="cf-help-content"> <h1> Vha Help page</h1> <form onSubmit={this.handleSubmit}> <h2>My Form?</h2> <pre><code>{this.state.results}</code></pre> <input type="submit" value="Submit" /> </form> </div> ); } } export default VhaHelp; ``` That is simple enough, but unfortunately it doesn't work without more hacks to the task controller and tasks model. Back in the tasks controller, since we don't have an external id or an appeal we have to change the create_params method ```ruby if task[:external_id] appeal = Appeal.find_appeal_by_uuid_or_find_or_create_legacy_appeal_by_vacols_id(task[:external_id]) end ``` But that's not enough, the Task.rb file method create_by_params also has to be adjusted for the lack of appeal and parent task. ```ruby def create_from_params(params, user) # This line has to be adjusted because .find will error if it is nil and since there is no parent it will always fail parent_task = Task.find_by(id: params[:parent_id]) # likewise this has to be skipped if parent_task fail Caseflow::Error::ChildTaskAssignedToSameUser if parent_of_same_type_has_same_assignee(parent_task, params) end # These steps also have to be skipped because there is no parent task if parent_task verify_user_can_create!(user, parent_task) params = modify_params_for_create(params) child = create_child_task(parent_task, user, params) parent_task.update!(status: params[:status]) if params[:status] else # This is the new else logic to handle this new task and potentially any others without an appeal params = modify_params_for_create(params) # New create_task_without_appeal method child = create_task_without_appeal(user, params) end child end # New method to handle creating this task without an appeal def create_task_without_appeal(current_user, params) Task.create!( type: name, assigned_by_id: current_user.id, assigned_to: params[:assigned_to] || child_task_assignee(parent, params), instructions: params[:instructions] ) end ``` But it still doesn't work after all of this because the tasks controller json_tasks(modified_tasks) line uses the tasks_serializer. Guess what doesn't work without an appeal? So the tasks serializer has to be updated with safe operators around every attribute that uses an appeal. A few examples, but all of the attributes with appeal have to be updated since this task isn't tied to an appeal. ```ruby attribute :veteran_file_number do |object| object.appeal&.veteran_file_number end attribute :closest_regional_office do |object| object.appeal&.closest_regional_office && RegionalOffice.find!(object.appeal.closest_regional_office) end attribute :external_appeal_id do |object| object.appeal&.external_id end ``` Finally after all that when we submit the form it will create an instance of our task and return the created task data to the page ![](https://i.imgur.com/yzjniVd.png) ### Overall Opinion for using a task There were no existing tasks that handled notifications that were not directly tied to an appeal. The notifications that existed were also almost entirely based around email notification which we could certainly reuse if that is our primary method of notification. The new JoinOrganizationTask did work as a method to store and track the User request to join the vha organization. **Pros:** * Can reuse existing controller logic and routing * No additional database tables need to be created * The ask closely resembles a normal task workflow since it is assigned and closed when the task is finished **Cons:** * Hacking Tasks.rb to make this work without an appeal or parent is a bit scary since Tasks are used for everything relating to an appeal in caseflow * It requires a lot of hacking to make this work * If we needed additional attributes we would have to add them to the tasks table which all other tasks use. * There are a lot of additional attributes and associations that we don't currently need for the ask that are present because of other tasks My recommendation based on the comparisons of pros and cons would be to avoid using Tasks as a method of tracking the user requests to join an organziation. ## Creating a customizable data record One method we could pursue to track this information is to simply create a new database table and active record model. I'm going to start from the model created in APPEALS-11024 ```ruby # frozen_string_literal: true class OrganizationMembershipRequest < CaseflowRecord attribute :created_at, :datetime, default: Time.zone.now validates :created_by, :organization, presence: true, on: :create belongs_to :created_by, class_name: "User" belongs_to :decision_by, class_name: "User" belongs_to :organization, class_name: "Organization" alias_attribute :requested_by, :created_by def approved? permitted_admittance || false end def denied? permitted_admittance == false end def pending? permitted_admittance.nil? end def apply_decision(admin_user, decision) # We may want a column for tracking whenever a decision is applied update!( decision_by: admin_user, permitted_admittance: decision ) end end ``` Testing out the new data type after migration and creation of the database table ![](https://i.imgur.com/KNtd1ez.png) Since it works we need a way to create this from the UI. We need to add a controller to handle the actions from the UI. We can start from the controller file in APPEALS-11024. We need to add a create method so we can send a post request to the controller in order to create the instance from the vha help page form. ```ruby # frozen_string_literal: true class OrganizationMembershipRequestsController < ApplicationController before_action :verify_org_admin, only: [:update_membership_request] def update_membership_request organization_membership_request render json: { org_request: organization_membership_request } end # New create method def create new_org_request = OrganizationMembershipRequest.create_from_params(create_params, current_user) # Might want to create a serializer render json: new_org_request end private def verify_org_admin Organization.find_by(url: params[:org_url]).user_is_admin?(current_user) end # Helper method for creating a new object def create_params new_org_task = params new_org_task = new_org_task.require(:organization_membership_request).permit(:organization_id) new_org_task end end ``` We also have to add a route in routes.rb to create this new object through the controller with a post request ```ruby resources :organization_membership_requests, only: [:create] do end ``` Lastly update the OrganizationMembershipRequests.rb model file to contain the method to create our new instance object from params and a user ```ruby # Class methods class << self def create_from_params(params, user) # Needs some error handling in true implementation organization = Organization.find_by(id: params[:organization_id]) create!( created_by: user, organization: organization ) end end ``` Now if we add a new submit function to the VhaHelp.jsx file to replace the old one for the tasks post form submission we can test out this new class as well. Create a new handle submit function there. ```javascript async handleSubmit(event) { event.preventDefault(); const payload = { data: { organization_membership_request: { organization_id: 2000000222 } } }; try { const results = await ApiUtil.post('/organization_membership_requests', payload); this.setState({ results: JSON.stringify(results.body, undefined, 2) }); } catch (error) { console.log(error); // throw error; } } ``` The submit works! Below is the returned json for the newly created OrganizationMembershipRequests object ![](https://i.imgur.com/ytGeKsG.png) ### Overall Opinion for creating a brand new datatype The new OrganizationMembershipRequests model works as a method to store and track the User's request to join the vha organization. **Pros:** * Flexibility - since this is a new database table and model we can expand it to do whatever it is we need to do * Low coupling - we don't have to worry about causing other failures by altering existing behavior **Cons:** * Have to create a new database table (Using a task would also require a migration however) * Have to create new routing, controller logic, and model logic. There's nothing to reuse since it is all brand new. * Currently untested behavior since it is a brand new workflow and model ## Other possibilities placeholder There are potentially more implementations that have not been discovered yet. ## Overall recommendation My overall recommendation is going to be creating a customizable data record. I can't see a situation in which it is ever going to be worth it to modify existing task behavior to such an extent that is required to make tasks work for the desired behavior. Tasks are heavily heavily coupled to appeals and parent tasks (e.g. Root task which has an appeal).