From 1158b8e8a78c68e649d35a99c98d0ee0262fb085 Mon Sep 17 00:00:00 2001 From: Nader Kutub Date: Mon, 21 Oct 2024 12:32:07 -0700 Subject: [PATCH] Release/fy25 q1.1.4 (#23287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/appeals 49670 49667 phase ii release tested (#23257) * APPEALS-36688- Build out Decision Review Created API route & Controller (#20569) * added skeleton for api route * removed duplicate code * removed development envs for api and moved to creating an ApiKey * removing any changes to development.rb * removed extra auth code * removed before action --------- Co-authored-by: TuckerRose * APPEALS-38232 -Build out Decision Review Created Event Failure API route & Controller (#20601) * APPEALS-38232 - renamed controller to AC req, added decision_review_created_error endpoint, added decision_review_created_error method, and added RSpec for new method * APPEALS-38232 - add comments to RSpec * Jonathan/appeals 36684 (#20516) * initial Events migration and model creation * created model for DecisionReviewCreatedEvent * updated comment with example * added spec for DRCE model * APPEALS-36684 created event_records migration and added polymorphic associations to specific models, and added rspec for the event_record model * APPEALS-36684 - Updated RSpec tests and updated variables and got unhappy path to pass * cleaned lint * saving DRCE spec changes * fixed spec test * changed has_many to has_one * updated event model spec * added validation for ER poly associations * changed association to has_one * added new method and updated tests * added foreign key after running checks * some PR comment changes * refactored methods in EventConcern --------- Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde * added migration, scopes and specs for events (#20707) * fixed migration to update existing events table * rollbacked to fix schema * schema fixes --------- Co-authored-by: TuckerRose * Jr/APPEALS-38926 (#20714) * APPEALS-38926 - Created DecisionReviewCreatedError Service Class, added logic for handling service error, updated DecisonReviewCreatedController, and DecisionReviewCreatedController spec, with the updated service logic * APPEALS-38926- created RSpec Test for DecisionReviewCreatedError Service Class and edited the Rails.logger for the service class * APPEALS-38926 Added new info column to update transaction and added it to the RSpec test * APPEALS-38926 - added comments to the Service Class * APPEALS-38926 - Code Changes from TL Code Review, added rescues and fails * APPEALS-38926 - fixed lint * Jonathan/appeals 36689 (#20671) * created new service class * add rspec test cases * service class methods * controller action and spec * controller update * CC fixes * removed accidental line * changed to find_or_create_by * reworked error for redis lock * additional rspec for controller * fixed test * rspec fix * delete lock key afterwards * moved Event creation back into lock block --------- Co-authored-by: Jonathan Tsang * APPEALS-39663 Create CreateUserOnEvent service class and add logic to create user if needed (#20838) * added user creation class & test * removed extra lines * add comment to class * added context for args from avro --------- Co-authored-by: Jonathan Tsang * Jr/APPEALS-39664 (#20898) * APPEALS-39664 - Created updated_vacols_on_optin module class, and removed extra private * APPEALS-39664 - created UpdatedVacolsOnOptin module, RSpec file, as well as sudo code for SOC and SSOC optin check in main service class DecisionReviewCreated * APPEALS-39664 - Created RSpec Test - PASS, Updated method name. * APPEALS-39664 - Added Error Handling to Sub Service Class * APPEALS-39664 - Removed un-needed comments * APPEALS-39664 - added include for the module UpdateVacolsOnOptin inside decision_review_created service * APPEALS-39664 - Added Custom Error , and updated all .perform! to .process! * APPEALS-39664 - Updated RSpec Test to reflect changes - all pass * Updated comment for decision_review_created Service Class * Will/appeals 36691 (#20909) * Created attribute for failed claims on event and displaying failed claim * passing all failed events back to serializer * added controller tests for failed_claims and added class method for finding claims on events * renamed failed_claims to claim_errors --------- Co-authored-by: TuckerRose * JR/APPEALS-40954 Create CreateIntake service class and add logic to create Intake (#20967) * APPEALS-40954 - Added Sudo Code for CreateIntake Logic * APPEALS-40954 - added logic to CreateIntake module * APPEALS-40954 - added Create Intake spec with error handling - All pass * APPEALS-40954 - Added CreateIntake Module to DecisionReviewCreated Main Service * APPEALS-40954 - updated folder name and namespace * APPEALS-40954 - Updated RSpec to match folder * APPEALS-40954- Upated decision review created servie include to match folder * Fixed failing test due to folder structure change * Jonathan/appeals 40950 (#20965) * Added new veteran creation module * saving test changes * rspec * fixed datetime assign * renamed var --------- Co-authored-by: Jonathan Tsang * APPEALS-40950 - update var veteran to vbms_veteran (#21050) * APPEALS-40950 - update var veteran to vbms_veteran * updated private method in controller to match happy path params dcr to drc * Create CreateClaimantOnEvent service class and add logic to create claimant if needed (#21044) * created service class and basic unit tests * added conditionals for veteran claimants and will create veteran claimant now * modified create claimant to use eventing data * change to a class * creating claimant correctly * test fixes * removed old comments * fixed type for veteran_is_not_claimant * changed specs to match pulling out hash params and removed event.reference id * moved back to dot notation * updated comments * fixed create claimant issue * updated process to use bang method and returning claimant --------- Co-authored-by: TuckerRose * APPEALS-41968 Modify Issues Endpoints & Update Metric Service Logic (#21108) * moved metricsService call to top of method * modified rspec case * moved metric logging spec case higher up --------- Co-authored-by: Jonathan Tsang * Resolved merge conflicts while merging master into feature/APPEALS-28… (#21167) * Jonathan/APPEALS-41957 (#21171) * added interface + parser class * edited veteran parse methods * renamed var to "payload" * started refactor * more refactor + rspec * added EPE attr * dateTime conversions * added class comments * rubocop lint changes * fixed test case --------- Co-authored-by: Jonathan Tsang * Jr/appeals 41931 (#21192) * APPEALS-41931 - Created create_ep_establishment file and class * APPEALS-41931 - added process! method that creates epe from payload * APPEALS-41931 - added logic for EventRecord being created and error handling * APPEALS-41931 - added comments to process method * APPEALS-41931 - set up rspec test * APPEALS-41931 - removed lint * APPEALS-41931-created Rspec and Test Pass * APPEALS-41931 - cleaned lint and added error test 100% code coverage * APPEALS-41931- fixed lint * APPEALS-41931- fixed lint and fixed %100 code cov * APPEALS-41931 - cleaned up lint and warn for Service Class * APPEALS-41931 - Added CreateEpEstablishment.process! to the decision_review_created Parent Service Class * APPEALS-41931 - refactor code to implement new parser * fixed linting issues * APPEALS-41931 - Updated Comments for CreateEpEstablishment * APPEALS-41931 - moved logical date int to parser and refactored code in Class and RSepc * edits * Will/appeals 41929 (#21205) * added an error and commented out call for createclaim * added new parser logic * merge request changes * fixed rubocop issues and added checks for claim review attributes --------- Co-authored-by: TuckerRose * APPEALS-42631- Create Rspec for Parser with sample payload method, and add additional parser methods (#21294) * APPEALS-42631 - create example.json * APPEALS-42631 - implemented example_response and load_example method and works as expected * APPEALS-42631 - created RSpec for DecisionReviewCreatedParser * APPEALS-42631 - Refactored parser and Added RSpec for current praser * APPEALS-42631 - added methods to parser for intake, claimant, and claim_review and added matching rspec for new methods * APPEALS-42631 - updated code per TL Comments * APPEALS-421631- added additional comments and fixing lint * APPEALS-41934 (#21251) * initial commit * implementation & error * renamed method * rspec * saving refactor progress * finished refactoring class * added comment * minor parser/rspec updates --------- Co-authored-by: Jonathan Tsang * added RI parser methods to rspec (#21322) * added RI parser methods to rspec * updated config for Consumer --------- Co-authored-by: Jonathan Tsang * Update DecisionReviewCreated to make all calls and link all intake records (#21334) * updated decision review created to uncomment actions and updated specs * remove binding.pry * removed comments * fixed a bunch of broken tests * fixed last broken tests * fixed params for methods and specs --------- Co-authored-by: TuckerRose * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * Jr/ama controller refactor (#21365) * fixed test and refactored controller * saving changes * init commit passing all the way to request_issues * all pass and functions as expected * lint * lint * updated initializer to use deep_symbolize_keys * updated headers to be more dry * added missing Intake attributes * fixed failing tests * updated createIntake test * fixed veteran rspec * fixed RI test * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * updated intake * modified intake --------- Co-authored-by: Jonathan Tsang * redo init commit * updated imp. logic * attorney widget fix * Create end to end , happy path rspec's for Decision Created event Feature (#21395) * updated more tests and fixed user creation * added in person creation * corrected headers and fixed broken tests * added type * fixed failing spec * creating event when person is created * updated spec to account for both events being created * ignored long lines for spec file * linting fixes * fixed more linting errors/ ignored long lines --------- Co-authored-by: TuckerRose * Jonathan/appeals 43589 (#21397) * saving * saving user class error progress * error handling for user creation * updated error handling/raises * validator methods * validations * update logical date converter * changed veteran service class to use file_number * added fields to hlr * updated epe data * removed byebug * fixed typos * Edit 5: adding detail_id to Intake * EDIT 6: add claimant_participant_id to epe * Edit 8: Intake is correctly linked to veteran * Edit 7: RI additions * fixed spacing * fixed typo * saving rspec changes * rspec updates * added datetime conversion for person dob * fixed rspec * remove unused methods --------- Co-authored-by: Jonathan Tsang * feature/APPEALS-35707-29633-29632 (uat) (#21435) * 🔀 Squash merge AlecK/APPEALS-35707 - Replace `database_cleaner` with `database_cleaner-active_record` * 🔀 Squash merge jcroteau/APPEALS-29632-fix-deprecation-action-view-base-instances * 🔀 Squash merge jcroteau/APPEALS-29633-fix-deprecation-warning-active_record-result-to_hash * awillis/APPEALS-45152 (#21506) * APPEALS-45152 Updated the logic in create_claimant_on_event.rb to always generate a claimant record. Updated RSPEC. * APPEALS-45152 Cleaned up RSPEC. * APPEALS-45152 Fixed Failing RSPEC within decision_review_created_spec.rb * APPEALS-45152 Updated RSPEC within decision_review_created_spec.rb with addtional checks. * APPEALS-44319 (#21449) (#21541) * added logic for legacy issues in DRC * more legacy logic * updated rspec context lang * error cov --------- Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang * Konstantin/APPEALS-45175 (#21517) * logical_date_converter re-written to work with yyyymmdd numbers from json payload * comments removed * comments removed2 * method rewitten * json example fixed * date in scenario_b_spec.rb replaced by valid ones, additional check for empty string parameter added to the logical_date_converter * removed filter for non rating request issue dropdown (#21480) * removed filter for non rating request issue dropdown * removed excluded types as we don't need to check this anymore * fixed broken test * added category back * no longer need test to check for missing categories since we're returning them all. --------- Co-authored-by: TuckerRose * Column added to RequestIssues table (#21578) * added migration and simple spec to test * fixed schema deleted issues * updated spec to verify data being set * error was no longer trigger from update. removed check and passed in correct variable --------- Co-authored-by: TuckerRose Co-authored-by: TuckerRose * added bgs_source to parser, controller and serializer along with specs (#21621) Co-authored-by: TuckerRose * Konstantin/appeals 45180 (#21591) * bug is fixed, condition added to DecisionReviewCreatedController * rspec test added * json message updated * comment added * Update decision_review_created_controller.rb comment updated --------- Co-authored-by: Nader Kutub * cmartine/APPEALS-42621 (#21599) * Add remove_comp_and_pen_intake feature toggle to intakes controller * Add new compensation and pension removal message to COPY.json * Disable comp and pen radio buttons if remove_comp_and_pen_intake toggle is on * Frontend test updates * Add tooltip checks for review page integration tests * fix frontend test lint errors * frontend tests changes * Fix linter errors --------- Co-authored-by: kshiflett88 Co-authored-by: root * nrithner/APPEALS-45263 (#21617) * Add remove_comp_and_pen_intake feature toggle to intakes controller * Add new compensation and pension removal message to COPY.json * Disable comp and pen radio buttons if remove_comp_and_pen_intake toggle is on * Frontend test updates * Add tooltip checks for review page integration tests * fix frontend test lint errors * frontend tests changes * Add banner for non-vha * Add banner for VHA employee * Refactor conditionals * Refactor logic for banners to display * Add dangerouslySetHtml to banner * Add test for no VHA employee with vha_claim_review_establishment disabled. * Add VHA employee scenario to display REMOVE_INTAKE_COMP_AND_PEN * Complete non vha user scenarios * Complete VHA employee scenarios * Remove comments * Fix linter errors * Remove spaces preceding question marks * Correct linting errors --------- Co-authored-by: Chris-Martine Co-authored-by: kshiflett88 Co-authored-by: root * Konstantin/appeals 45149 (#21644) * RequestIssueSerializer attributes added * development_item_reference_id, same_office, legacy_opt_in_approved attributes were added * merge conflicts resolved * merge conflict resolved * fixed * all datapoints were added * empty line removed * comment removed * refactored * app/controllers/api/docs/v3/ama_issues.yaml updated with new attributes * example of request issue updated app/controllers/api/docs/v3/ama_issues.yaml * null to nil edited in example ama_issues.yaml * example ama_issues.yaml updated * example ama_issues.yaml updated2 * linters fixed * linters fixed2 * linters are fixed * APPEALS-45883 - Remove End Product Establishment records within the Event Records table in Caseflow (#21745) * APPEALS-45883 - changes to remove the creating of Event Records for epe * APPEALS-45883 - remove removed event params from method calls * APPEALS-45883 removed Event Double * APPEALS-48553 updated event_record spec * removed eventing from create claim review (#21718) * removed eventing from create claim review * checking why file is not being found for event_record * removed event var and require dependency * spec fixes * updated specs * fixed revert --------- Co-authored-by: TuckerRose Co-authored-by: Nader Kutub * Nrithner/appeals 45913 (#21716) * Convert from milliseconds to datetime * Add expection to have datetime after parsing * Fix linting issues * APPEALS - 45914 & 45915 (#21744) * Decision review parser and spec changes * remove byebug * Added db seed and testing (#21284) * Added db seed and testing * Change ApiKey seed name to ConsumerApiKey, and associated changes --------- Co-authored-by: Matt Ray Co-authored-by: Chris-Martine Co-authored-by: Nader Kutub * re-merge Cmartine/appeals 46861 (#21855) * Edit DecisionReviewCreatedController to respond with ok status if event exists and is completed, edit test * Move checking if event exists and is completed into event model, edit tests * Fix linter errors --------- Co-authored-by: Chris-Martine * Nrithner/appeals 46860 (#21856) * Set event info back to default default state for info is an empty json object * Correct linting errors * Test happy path with rspec * Refactor rspec example * Converte module to class * Remove module import --------- Co-authored-by: Nico Rithner * Jonathan/appeals 45899/45878 (#21714) * add "from event" value to Store * updated setup guide for M3 * added new badges * added jest * storybook file * additional story * bugfix * snap update * Konstantin/appeals 46175 (#21768) * draft processor method done * process_nonrating method moved to DecisionReviewCreated service * json example added, process_nonrating method updated * 4 rspec test added * useless json example removed * commented lines removed * condition added * condition added. * test updated * first condition moved out of the method * process_nonrating method call moved down * 1 comment removed, 1 added * process nonrating method rafactored, logic transferred into the parser * method refactored, rspec tests updated * rspec test slightly refactored --------- Co-authored-by: Nader Kutub * Edit Issues screen update (#21907) * Edit Issues screen update * relocated row * updated capybara test * updated capybara to check for new added text * cmartine/APPEALS-46905 (#21884) * Remove cache updating call for veteran when veteran is being created for an event * Change seeds api_key.rb to consumer_api_key.rb --------- Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example * Change edited_by_css_id to return users css_id * Konstantin/appeals 48306 (#21998) * ama_eventing_enabled feature check added * toggle renamed * rspec tests added * 2 comments added * toggle renamed, logic trasferred into controller, tests updated * empty line removed --------- Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * 930 code updates * remove byebug * Change edited_by_css_id to return users css_id (#22062) Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * Nrithner/appeals 49245 (#22107) * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example --------- Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> * fix extra commas from merge * APPEALS-51223: Disable ClaimantValidator for DecisionReviewCreatedEvents (#22132) * APPEALS-51223 refactored the claimant_validator class method claimant_details_required? to consider the conditional statement * APPEALS-51223 updated Rspec to confirm claimant address_line_1 is nil and error is empty * APPEALS-51223 made changes to to claimant_validator to have all Unit test passing * APPEALS-51223 added suggested changes * Revert "fix merge conflict with AMA" This reverts commit 6775316c85e2c3d8cd94c1baae6d29d75503163d, reversing changes made to 66cf311d1ddb18d11137b173b94ecbafaa022e2b. * added comments * Delete docker-bin/build.sh, I didnt make these changes * Revert "Delete docker-bin/build.sh, I didnt make these changes" This reverts commit 03a3830c5df26e9d4830fcf1d66ea77af4703346. --------- Co-authored-by: nader k * Jonathan/appeals 51926 (#22215) * Hide UI changes behind featureFlag * fixed lint spacing warnings * code climate spec fix (#22211) * code climate spec fix * up argument count * created a parser helper for reusable methods * reset codeclimate back to original and added issue parser * modified methods to take in a hash instead of multiple params * reduced create params down to 3 params * added space inside * swapped to safe navigator for intake check * fixed spec * fixed linting issue * fixed failing spec * fixed merge conflicts with code changes for helper --------- Co-authored-by: TuckerRose Co-authored-by: TuckerRose Co-authored-by: Nader Kutub * Konstantin/appeals 52321 (#22363) * utc conversion method added in app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date method updated in RequestIssue model, updated contested_rating_issue_profile_date in DecisionIssue model * decision issue updated, tests added * tests added, converters improve app/models/decision_issue.rb app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date restored in request issue model * uneccessary logic removed * 3 more tests added * test added, error handling aaded in format_rating_profile_date method * Update decision_issue.rb * Update request_issue.rb * Appeals 52317 (#22321) * wip * added the parser to skeleton * removed commented code * added in parser logic * added dru_error route, error handling and params * added rspecs and fixed routing for update error to be a post * added empty spec * updated dru params to match intake json * rubocop fixes * more rubocop fixes * fixed routing to post now and updated specs --------- Co-authored-by: TuckerRose * fixed logic for AMA issues API serializer attributes (#22358) * fixed logic for added_by fields * methods for fetching removed_by user * withdrawn_by methods * methods for edited_by & instance vars * more logic for added_by methods * fixed failing tests * rspec updates * rspec for RIU addition * small changes * fix test failure * Initial commit (#22494) * Konstantin/appeals 52318 (#22528) * very loose prototype of class created * hopeless variant removed * updated parser per new design and updated specs * request_issues_updated_event updated * some improvments added * unused methods removed, some methods updated * after issues added * tests added * edited issues method returned, invalid tests removed * tests updated * 9 working tests added, RequestIssuesUpdateEvent variable instanciated * tests improved, attribute writers added * more test cases added, error handling in process! method added * added new version of parser and tests to it from Will's PR * uneccesary overrided classes removed * process_job call removed * process_job! restored, 3 more tests added * validate before perfom removed * linter issues addressed * linter issues addressed2 --------- Co-authored-by: TuckerRose * Bug Fix for APPEALS-44115 (#22538) * running ci * testing category * ci with conditional * set back to false * reverted to original spec * running ci * updated intake helpers to account for using none of these match option * ignored metrics abc size * removed from helper * removed comments * updated spec to check on each category * removed single quotes * fixed a flakey test that was failing on a duplicate key * linting fix * pushed up feature flag * lint clean up * swapped to different featureflag * fixing spec feature toggle param * added feature flags back * checking if label is there * flipped feature test to correct order --------- Co-authored-by: TuckerRose * cmartine/APPEALS-50366 (#22618) * Create EditDisabledBanner * Add banner displaying logic and pageroute to IntakeEditFrame * Add IntakeEditFrame jest tests for removeCompAndPenIntake feature toggle * Edit displayEditDisabledBanner to use the disableEditingForCompensationAndPension method * Re-add intakeEditFrame jest tests, re-add and move testProps * Refactor intakeEditFrame jest tests * Edit change testProps into static variable * Add another test for intakeEditFrame * Change testProps back into function return * Change comp and pen removal text * Change Intake Remove string back, add Intake Edit Disabled string, changed banner and tests to use this * Ksarlett/appeals 50368 - Disable the remove issue, withdraw issue & edit issue options from drop down (#22637) * ksarlett/APPEALS-50368 - Disable dropdown * rermove console.log * spec refactor * spec fix * spec refactor * change test name --------- Co-authored-by: Matt Ray * nrithner/APPEALS 50367 (#22640) * Add disabled logic to add issue and edit claim label buttons * Add test buttons are disabled When benefit is compensation * Test buttons are disabled in supplemental_claim Add issue and Edit claim label are disabled when benefits pension or compensation are present * Test buttons are disabled in HLR Add issue and Edit claim label are disabled when benefits pension or compensation are present * Refactor to remove redundancy * Test refactoring --------- Co-authored-by: Matt Ray Co-authored-by: kshiflett88 * APPEALS-53923 (#22727) * new service class & error message * rspec * Update update_informal_conference_spec.rb * Konstantin/appeals 52982 (#22715) * eligibility-related methods added * methods fixed, date conversion for closed_at added * decision_review_updated_issue_parser created, json example updated, request_issues_update_event updated * tests for parser fixed after json example update * new attribute vbms_id added to request_issues table, decision_review_updated_issue_parser update respectively * request_issues_update_event attr_writers added, decision_review_created_issue_parser class name update and call of that parser updated * veteran removed from DecisionReviewUpdatedParser initilizer, methods marked for potential deletion * decision_review_updated_issue_parser updated and test added for it, comments added to request_issues_update_event * not necessary methods removed from parser, tests for them commented out for now * attributes_from_intake_data method overrided * :id replced by vbms_id, json example, parsers and test for it updated * parsers added, test updated * css_id, station and detail_type commented from the parser * parser cleaned, test for parser fixed, request_issues_update_event cleaned * process_issues! overriden in app/models/request_issues_update_event.rb, linter offenses fixed, testtypo fixed * vbms_id renamed to reference_id * comment typo fixed * Create Audit Tables for SC, HLR, Request Issues Appeals 55403 (#22689) * migration added * schema updated correctly * added audit service and call to update evenr records * logic for update type * fixed typo in audit file and added spec * added insert * moved Insert DRUA to create request issues for created ones * updated event record to take in update type * passing in correct info attribute now * disabled unused params, can enable once we use them * remove << Co-authored-by: TuckerRose * fix merge conflic in seeds.rb * remove cusoner seed files * Konstantin/appeals 57291 (#22853) * decision_review_created/decision_review_created_parser.rb updated * rspec tests for decision_review_created parser added * issue parser updated, tests added * decision review updated parsers updated, tests added * linter issue addressed * Konstantin/appeals 57283 (#22851) * reference_id added to parser, json example and create_request_issues.rb * rspec tests for decision_review_created_issue_parser created * reference_id null error handling added, tests for create_request_issues.rb added * linter offenses fixed * new param permitted added in app/controllers/api/events/v1/decision_review_created_controller.rb * feature tests fixed * APPEALS-55402 (#22768) * service class and error * adding to DRU class method * linter line too long fix * small fix to riue * added rspec * fixed typo * Konstantin/appeals 57279 (#22859) * DecisionReviewUpdateMissingIssueError handling is created * initializer added, typos fixed in request_issues_update_event.rb,more error handlings added, tests updated and added * little optimizationd added to app/models/request_issues_update_event.rb * attributes_from_intake_data fixed * commented code removed * review defined in decision_review_updated.rb * formatted a bit * useless comments removed * linter offenses cleaned, useless comments removed * Update request_issues_update_event.rb Result of auto conflict resolution fixed * useless comment removed * Update decision_review_updated.rb Lines 21,22 uncommented. * rubocop disable/enable added * additional positive tests added, RequestIssuesUpdateEvent updated * 2 tests added * linter issue fixed * Jonathan/appeals 58619 (#22875) * service class and error * adding to DRU class method * linter line too long fix * small fix to riue * added rspec * fixed typo * Added new service class + fixed typo * typos * more typo fixes * additional fields to epe.update * updated sample payload * unit test + time conversion --------- Co-authored-by: Nader Kutub * Process newly added fields passed for DecissionReviewUpdated (#22876) * added parser changes * removed empty line * updated json to match updated design and removed unused methods * remove ep code category * removed unused methods * fixing specs and linting * linting fix * updated json example to match * added edited_description to spec * added new json payload changes that required spec updates * removed binding.pry --------- Co-authored-by: TuckerRose * Refactor Created Events Logic to Handle Contested Issues (#22852) * wip * added check for contested with specs * removed newline * fixed contested setting to if not eligible and fixed specs * removed setter for contested --------- Co-authored-by: TuckerRose * issue parser applied (#22904) * issue parser applied * refactor request_issues_update_event * refactor request_issues_update_event- remove temp files * fix lint issues * fix linting issues * fix eligibility fields * add gaurd for missing participant in request issues serializer * add description fields to eligibility updates * bug fixes with controller --------- Co-authored-by: Nader Kutub Co-authored-by: Nader Kutub * renamed epe to singular method call (#22914) Co-authored-by: TuckerRose * Edit DecisionReviewUpdatedController dru_params (#22939) * API fix + epe feature test (#22956) * API fix + epe feature test * add specs --------- Co-authored-by: Nader Kutub * Feature/appeals 59173 (#22995) * fix bug reatletd to new issues * fix DRU bugs * fix linting issue * fix event records bug * fix event records spec * bug fixes for error logs, audits, claim id (#23015) * bug fixes for error logs, audits, claim id * fix redi lock on created event * add reference id to AMA API (#23022) * add reference id to AMA API * add withdrawn audit * audit spec fix * Konstantin/appeals 58609 (#22970) * draft test * typo fixed, byebug inside find_or_build_request_issue_from_intake_data * typo fixed, edited_description added to attributes in app/controllers/api/events/v1/decision_review_updated_controller.rb * test file renamed * commented lines removed * Update spec_helper.rb * Update scenario_edited_issues_spec.rb typo * success message updated, any updates check added, edited_description before check added * audit find_by fix and add create scenario --------- Co-authored-by: Nader Kutub Co-authored-by: Nader Kutub * Konstantin/withdrawn issues feature test (#23028) * withdrawn feature test added * more checks added to test * temp change * request_issue.reload removed * fix ama api and closed rspec * remove byebug and fix closed statu rspec * Jonathan/appeals 59166 (#22972) * feature test, update hlr/sc optin value * featuretoggle change * featuretoggle * fix linting issues * fix DRU removed and DRUs for eventing feature, added feature tests an… (#23055) * fix DRU removed and DRUs for eventing feature, added feature tests and RIUs checks * fix linting errors * fix mutex error spec * lint fixes * disable DRU audit (#23064) * Nader/audit sync fix (#23079) * remove sync attemps on RequestIssuesUpdateEvent and add auditing, fix nil discription * add audit record check on add issue scenario spec * fix changes? call * ermove audit mock * fix HIGHER_LEVEL_REVIEW check * Revert "fix HIGHER_LEVEL_REVIEW check" This reverts commit 1499727ceb3211d4b2f910ab6a42dd5483fa3481. * Konstantin/appeals 59181 (#22992) * eligible to ineligible test added * eligible_to_ineligible test added * one added, 2 updated * empty line removed * spec_helper restored * success message updated * empty line removed * succes message expectation update in tests * wording updated * typo fixed * linter addressed * scenario_create_issues_spec.rb deleted, it was an invalid draft accidently pushed in previous PR * add last sync to RIU's, update contention updated and remoted at , ac… (#23092) * add last sync to RIU's, update contention updated and remoted at , account for CF intaked RIs based on original_casflow_request_issue_id * fix rspec * fix discription texts * fix missing descriptions * fix epe's getting created and error on missing ineligible_due_to (#23103) * update contention text fix for eligibility changes * fix the unidentified_text and contention_refference_id (#23119) * fix the unidentified_text and contention_refference_id * fix rspec and lint * Jonathan/dru eventrecord dupe fix (#23162) * consolidating review update into 1 service class * removing double find * including test scenario for claimreview * updating test * Nader/cont ref id unid issu txt fix (#23164) * fix the missing contention reference id and unidentified issue text * fix spec * fix before issues query * fix filed asignments when nil * fix lint issue * refactor update logic to not depend on RequestIssuesUpdate (#23187) * refactor update logic to not depend on RequestIssuesUpdate * fix rspec * Konstantin/prevent check for before ama! (#23180) * issue_from_event? check added into close_if_ineligible! * alternative approach added * check if appeal added * comments removed * status ok changed to created, close_if_ineligible! updated, useless methods removed * close_if_ineligible! updated * cleaned * adding "contested" to UI text --------- Co-authored-by: Jonathan Tsang * fix the remove of issues created from vbms, add before audit, all fie… (#23202) * fix the remove of issues created from vbms, add before audit, all fields saved for all evenets except edit * final fixes and changes to veteran_participant_id and legacy_issue processing call * add legacy processing for eligibility change * add more logic for eligibility changes with legacy issues * fix verify_contentions * Add disabling of edit contention title button on the edit issues page when feature toggle enabled (#23194) * Pass disableEditingForCompAndPen toggle to EditContentionTitle, enable/disable edit button depending on toggle status * add feature tests for contention title disable * remove byebug --------- Co-authored-by: kshiflett88 * remove _ext_ table from schema * add saftey operator for missing request_issues_update * fix optin check to pull from parser not review * Revert "fix optin check to pull from parser not review" This reverts commit c9cd5a908239795bee8ae9c39e570d911697f85d. * Revert "add saftey operator for missing request_issues_update" This reverts commit 7f2fd96cb2a8859f526b710a71479dc44fb3017a. * Revert "remove _ext_ table from schema" This reverts commit 63758f21ac6f1240a9c294046eabce1bb431d66c. * remove _ext_ table from schema * add back _ext_ table --------- Co-authored-by: Will Love Co-authored-by: TuckerRose Co-authored-by: Enrilo Ugalde <71367882+Jruuuu@users.noreply.github.com> Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde Co-authored-by: isaiahsaucedo Co-authored-by: Calvin Co-authored-by: Craig Reese Co-authored-by: Jeremy Croteau Co-authored-by: (Jeffrey) Aaron Willis <98484567+Aaron-Willis@users.noreply.github.com> Co-authored-by: Konstantin Shevtsov <166068160+KonstantinShevtsov@users.noreply.github.com> Co-authored-by: TuckerRose Co-authored-by: Chris-Martine <135330019+Chris-Martine@users.noreply.github.com> Co-authored-by: kshiflett88 Co-authored-by: root Co-authored-by: nicorithner-bah <148365039+nicorithner-bah@users.noreply.github.com> Co-authored-by: Chris-Martine Co-authored-by: Matt Ray Co-authored-by: Nico Rithner Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> Co-authored-by: Jonathan Tsang * hotfix/APPEALS-59737 and APPEALS-59667 (RC) (#23263) * Have time slots handle aliased timezones properly * Add Indianapolis alias * Pluck RTL version of the ScheduleVeteranForm from the Node 18 upgrade branch. Co-authored-by: AdamShawBAH * Add unrecognized timezones to tz initializer * Add spec file for initializer * Add Jest test coverage * Fix undefined expectations * Update snapshots * Leverage timezone aliases * Remove defunct TZ mapping overrides * Add missing alias * Refactor ReadOnlyHearingTimeWithZone's Jest test file to utilize beginsAt values for hearing days that are formatted in the way they are in actuality within the app. * WIP: Add service specs * Add HearingDatetimeService tests * Remove extra line of whitespace * Make date consistent in tests * Add legacy tests * Update tz initializer spec * Fix typo where I used 'subject' in place of 'is_expected' * Fix use of wrong variable name * Fix legacy hearing staging * Add specs for HearingTimeService * Add icol service test * Fix hearing time service time_to_string method * Change time expectations to meet actual behavior * Fix mistake where I had accidentally set the expectation for the Louisville, KY RO to be a time in the wrong timezone. --------- Co-authored-by: Matthew Thornton Co-authored-by: AdamShawBAH * APPEALS-26734 and APPEALS-25121 HRT Phases 1 and 2 (#23258) * APPEALS-25117: rspec fix + add docket and id as conference id * APPEALS-25117: final service updates * APPEALS-25117: adds webex service rspec and comments to create conference job spec, needs further review * APPEALS-25117 spec update * APPEALS-25117 update test * APPEALS-25117 finish spec update * APPEALS-25117 correct schema * APPEALS-34501 update arguments * APPEALS-34501 added port env var * APPEALS-34501 updated the feature toggle * APPEALS-34501 update spec * Add helper methods to ExternalApi::WebexService::CreateRespons * Add create_webex_conference * Conference ID to string * Set conference_id back * Rename LinkService to PexipLinkService * Remove conference_id setting since we dont use it * Add env vars to controller spec * Fix failing tests in pexip_conference_link_spec * Remove nil check * Fix pexip_link_service_spec failures * Clean up test file a bit * APPEALS-34517 render correct data guestlinks * Fix test link * Add fixes and fiel changes to create_conference_job_spec * Whittle down create_conference_job * Remove env vars that can come back out * Add FTs to conf job * APPEALS-34517 updated tests and guestLink DailyDocket * Lint * Remove unused mocks * Fix conf link tests * Maybe fix some tests * Controller spec fixes * Add fix to virtual hearing spec and update factory and model * Remove defunct check for host link * Update link_generation_initialized trait across app * Resolve daily docket spec with new trait name * Adjust StuckVirtualHearingsChecker * Adjust daily docket * Fix async pending tests * APPEALS-34673: added conditional based on hearing conference provider to display links * Add update for links * Update to guestLink rendering * APPEALS-34673: update snapshot tests, add hearing links to test runs * APPEALS-34673: revert caseworkerindex snapshot and remove comment * APPEALS-34673: update snapshot - passes locally * APPEALS-34673: updates to snapshot * APPEALS-34673: updates to snapshot * APPEALS-34673: updates to snapshot * APPEALS-34880: added nbf and exp methods to hearing day and virtual hearing * APPEALS-34880: added nbf exp rspec to virtual hearing spec * APPEALS-34880: added hearing day spec test for nbf and exp and removed comments * APPEALS-34880: removed comments * APPEALS-34880: removed comments * APPEALS-34880: removed spacing in rspec * APPEALS-34908: added one liners * APPEALS-34908 update resp * APPEALS-34908 update json parse * APPEALS-34908 update send_webex method nil * APPEALS-34908 update webex_conference_link to string * APPEALS-34908 update create_response.rb sym to string * DRY up generate_conference_information * APPEALS-34908: service and fakes updates * APPEALS-34908: update request body * Change param passing * APPEALS-34908 rspec test fix * APPEALS-34908: updated webex service spec * APPEALS-34908 converted hash to json in fake * Change some names * APPEALS-35033: change casing for Nbf Exp to nbf exp * Correct spec file name * Remove .gitignore diff * Use .to_json * APPEALS-35195: update sub to use unique hearing day id * APPEALS-35195: update hearing day spec to have specific id for hearing day * APPEALS-35195: change date format * APPEALS-35728: initial updates to daily docket row * APPEALS-35728: update logic for type vs conferenceProvider * APPEALS-35728: updates to test, props, store, snapshot * Test: Change hearing links label (#20082) * Change hearing links label * Update snapshot * Fix tests * More snapshots --------- Co-authored-by: Matthew Thornton * Merge branch 'feature/APPEALS-26734' into feature/APPEALS-35295 Resolves [Jira Issue Title](https://jira.devops.va.gov/browse/JIRA-12345) # Description Please explain the changes you made here. ## Acceptance Criteria - [ ] Code compiles correctly ## Testing Plan 1. Go to [Jira Issue/Test Plan Link](https://jira.devops.va.gov/browse/JIRA-12345) or list them below - [ ] For feature branches merging into master: Was this deployed to UAT? # Frontend ## User Facing Changes - [ ] Screenshots of UI changes added to PR & Original Issue BEFORE|AFTER ---|--- ## Storybook Story *For Frontend (Presentation) Components* * [ ] Add a [Storybook](https://github.com/department-of-veterans-affairs/caseflow/wiki/Documenting-React-Components-with-Storybook) file alongside the component file (e.g. create `MyComponent.stories.js` alongside `MyComponent.jsx`) * [ ] Give it a title that reflects the component's location within the overall Caseflow hierarchy * [ ] Write a separate story (within the same file) for each discrete variation of the component # Backend ## Database Changes *Only for Schema Changes* * [ ] Add typical timestamps (`created_at`, `updated_at`) for new tables * [ ] Update column comments; include a "PII" prefix to indicate definite or potential [PII data content](https://github.com/department-of-veterans-affairs/appeals-team/blob/master/caseflow-team/0-how-we-work/pii-handbook.md#what-is-pii) * [ ] Have your migration classes inherit from `Caseflow::Migration`, especially when adding indexes (use `add_safe_index`) (see [Writing DB migrations](https://github.com/department-of-veterans-affairs/caseflow/wiki/Writing-DB-migrations)) * [ ] Verify that `migrate:rollback` works as desired ([`change` supported functions](https://edgeguides.rubyonrails.org/active_record_migrations.html#using-the-change-method)) * [ ] Perform query profiling (eyeball Rails log, check bullet and fasterer output) * [ ] For queries using raw sql was an explain plan run by System Team * [ ] Add appropriate indexes (especially for foreign keys, polymorphic columns, unique constraints, and Rails scopes) * [ ] Run `make check-fks`; add any missing foreign keys or add to `config/initializers/immigrant.rb` (see [Record associations and Foreign Keys](https://github.com/department-of-veterans-affairs/caseflow/wiki/Record-associations-and-Foreign-Keys)) * [ ] Add `belongs_to` for associations to enable the [schema diagrams](https://department-of-veterans-affairs.github.io/caseflow/task_trees/schema/schema_diagrams) to be automatically updated * [ ] Document any non-obvious semantics or logic useful for interpreting database data at [Caseflow Data Model and Dictionary](https://github.com/department-of-veterans-affairs/caseflow/wiki/Caseflow-Data-Model-and-Dictionary) ## Integrations: Adding endpoints for external APIs * [ ] Check that Caseflow's external API code for the endpoint matches the code in the relevant integration repo * [ ] Request: Service name, method name, input field names * [ ] Response: Check expected data structure * [ ] Check that calls are wrapped in MetricService record block * [ ] Check that all configuration is coming from ENV variables * [ ] Listed all new ENV variables in description * [ ] Worked with or notified System Team that new ENV variables need to be set * [ ] Update Fakes * [ ] For feature branches: Was this tested in Caseflow UAT # Best practices ## Code Documentation Updates - [ ] Add or update code comments at the top of the class, module, and/or component. ## Tests ### Test Coverage Did you include any test coverage for your code? Check below: - [ ] RSpec - [ ] Jest - [ ] Other ### Code Climate Your code does not add any new code climate offenses? If so why? - [ ] No new code climate issues added ## Monitoring, Logging, Auditing, Error, and Exception Handling Checklist ### Monitoring - [ ] Are performance metrics (e.g., response time, throughput) being tracked? - [ ] Are key application components monitored (e.g., database, cache, queues)? - [ ] Is there a system in place for setting up alerts based on performance thresholds? ### Logging - [ ] Are logs being produced at appropriate log levels (debug, info, warn, error, fatal)? - [ ] Are logs structured (e.g., using log tags) for easier querying and analysis? - [ ] Are sensitive data (e.g., passwords, tokens) redacted or omitted from logs? - [ ] Is log retention and rotation configured correctly? - [ ] Are logs being forwarded to a centralized logging system if needed? ### Auditing - [ ] Are user actions being logged for audit purposes? - [ ] Are changes to critical data being tracked ? - [ ] Are logs being securely stored and protected from tampering or exposing protected data? ### Error Handling - [ ] Are errors being caught and handled gracefully? - [ ] Are appropriate error messages being displayed to users? - [ ] Are critical errors being reported to an error tracking system (e.g., Sentry, ELK)? - [ ] Are unhandled exceptions being caught at the application level ? ### Exception Handling - [ ] Are custom exceptions defined and used where appropriate? - [ ] Is exception handling consistent throughout the codebase? - [ ] Are exceptions logged with relevant context and stack trace information? - [ ] Are exceptions being grouped and categorized for easier analysis and resolution? * APPEALS-31618 add migration files * APPEALS-31618 rmove migration * APPEALS-31618 0fficial add co_host_hearing_link * APPEALS-31618 Add co_host_link to conference_links * APPEALS-31618 added code for co-host * APPEALS-31618 remove attribute conferencelinks * APPEALS-31618 update tests * APPEALS-31618 update lint * APPEALS-31618 update VirtualHearingsFields snapshot * APPEALS-31618 update details.test.js snapshot * APPEALS-31618 update create_conference_job_spec * APPEALS-31618 updated Marcs sugestions rnd 1 * jefftmarks/APPEALS-31619 (#20515) * APPEALS-31619 Created migration * APPEALS-31619 Add xls column to migration * APPEALS-31619 Update index name * APPEALS-31619 Change null options on migration * APPEALS-31619 Refactor transcription files table * APPEALS-31619 Remove old migration * APPEALS-31618 updated migration inheritance * Min/APPEALS-31621 (#20523) * APPEALS-31621 added the table * APPEALS-31621 moved model under hearings folder * APPEALS-31621 reverted schema back from testing table * APPEALS-31621 added transcriptions association * APPEALS-31621 fixed typo * APPEALS-31621 removed migration file * APPEALS-31621 added additional template methods in model and comments * APPEALS-31621 fixed transcription typo * APPEALS-31621 added additional template methods * APPEALS-31621 changed name of model and controller * APPEALS-31621 removed transcript association * hotfix/APPEALS-35296-flaky-conference-job-test (#20555) * akonhilas/APPEALS-31820 (#20510) * APPEALS-31820: create transcription transactions serializer with outline until model complete * APPEALS-31820: update aws link attributes * APPEALS-31820: removed methods and has many relationship, change file name * APPEALS-31820: updated file tree * Revert "APPEALS-31820: updated file tree" This reverts commit 4b58d5a93c3922582b97447e6b40c5cb1b087083. * APPEALS-31820: updated serializer to match new db columns and updated AC * APPEALS-35177 adjust spacing * APPEALS-35177 fix lint * APPEALS-35177 made final positioning adjustments * APPEALS-35176 - Pull radio buttons closer and tidy up the CSS * jefftmarks/APPEALS-31619 Remove unique contraints on indexes (#20620) * APPEALS-31619 Created migration * APPEALS-31619 Add xls column to migration * APPEALS-31619 Update index name * APPEALS-31619 Change null options on migration * APPEALS-31619 Refactor transcription files table * APPEALS-31619 Remove old migration * APPEALS-31619 Remove unique contraints on indexes * APPEALS-31619 Change index order * APPEALS-31619 Move docket number in index * Allow NonVirtual Webex hearings to display the HC link on the Hearing Details page * APPEALS-36652 Backend changes * Display only Virtual Hearing Links for virtual hearings and only Hearing Links for nonvirtual hearings * APPEALS-36652 * APPEALS-35176 - Update html and css to better match the figma design * Refine conference provider search so that correct links show up for pexip conferences * APPEALS-35176 - Update radio button to align better with label * Add daily docket conference links to legacy appeals * Add daily docket conference link to the nonvirtual webex hearing links * Show non virtual webex HC link * APPEALS-35176 - Make margins more consistent across browsers * Update feature tests to remove old conditions * APPEALS-36652 heearing_spec update * Updated jest testing in progress * Add nonvirtual webex test - in Progress * Add test with default props back into test file * Remove unnecessary test and resolve testing errors with webex hearinglinks tests * Show hearing links titles and clickable link * Update jest snapshots * Capitalize webex hearing styling * akonhilas/APPEALS-38906 (#20664) * APPEALS-38906: updates to service, created new file for job * APPEALS-38906: response, updates to query, finalizing job, fakes * APPEALS-38906: add query to call * APPEALS-38906: add scheduled jobs, cgi formatting, exponential backoff, add get post to service, service spec updates * APPEALS-38906: added get webex recording job spec, changed timing for to and from params in job * APPEALS-38906: pushing failing tests for visibility * APPEALS-38906: pushing tests for visibility * APPEALS-38906: final test updates * APPEALS-38906: remove comments, time updates, va ops email comment * APPEALS-38906: fixed rspec and removed unnecessary error raise from job * jefftmarks/APPEALS-38907 (#20659) * APPEALS-31821 created job file and template * APPEALS-38907 Create download transcription file job * min/APPEALS-31821 made new helper method templates for use on perform * APPEALS-38907 Continue prototype * APPEALS-31821 basic webvtt to rtf conversion and tmp folder initializers * APPEALS-38907 Complete basic structure for job * APPEALS-38907 Update error handling * APPEALS-31821 gathering info for the front page * APPEALS-38907 Add error handling for invalid file name * APPEALS-38907 Add error handling for invalid file type * APPEALS-38907 Fix linting * APPEALS-38907 Open URI CodeQL fix * APPEALS-31821 generating a table for the front page info * APPEALS-31821 generating header info for first page cell * APPEALS-38907 Update initializer to create subdirectory for each file type * APPEALS-38907 Update code comments * APPEALS-31821 most info appearing on first page now * APPEALS-31821 stashing regexes for future use * APPEALS-31821 vtt fully converted to rtf * APPEALS-38907 Implement S3 upload * APPEALS-38907 Update comments * APPEALS-31821 page numbers and footers now displaying * APPEALS-31821 texts from the same speaker is now consolidated * APPEALS-31821 refactored footer creation on document * APPEALS-38907 Complete implementation of job minus va ops email * APPEALS-38907 Update S3 bucket and folder names * APPEALS-31821 S3 download and upload implemented and tmp file clean up * APPEALS-31821 S3 download and upload implemented and tmp file clean up * APPEALS-38907 Polish error handling before moving on to tests * APPEALS-31821 added error handling and logging * APPEALS-31821 repurposed class into a workflow * APPEALS-31821 removed mailer * APPEALS-38907 Connect job with TranscriptionTransformer workflow * APPEALS-38907 Small fixes after writing xray tests * APPEALS-31821 added rspec tests and refactored workflow * APPEALS-38907 Finish spec files * APPEALS-31821 removed the new datadog metrics that was made * APPEALS-31821 removed the new datadog metrics that was made * APPEALS-31821 added back old datadog metrics * APPEALS-31821 removed ext_claim schema * APPEALS-31821 moved transcription_transformer to workflow folder * APPEALS-38907 Complete spec * APPEALS-31821 adjusted rspec tests and fixed minor edgecase for empty string identifiers * APPEALS-31821 fixed minor formatting issues * APPEALS-31821 edited spacing * APPEALS-31821 added error class back * APPEALS-38907 Fix argument being passed to TranscriptionTransformer * APPEALS-38907 Fix footer on rtf layout * APPEALS-38907 Fix spec to mock TranscriptionTransformer correctly * APPEALS-38907 Change URI.open call to be more safe --------- Co-authored-by: Minhazur Rahaman Co-authored-by: msteele Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * Refactor double returns to a single return * Update snapshots * Remove unnecessary commented out code * Update linting issues * Fix linting error * Update snapshots for jest testing * jefftmarks/APPEALS-40657 (#20851) * APPEALS-40657 Refactor DownloadTranscriptionFileJob * APPEALS-40657 Refactor TranscriptionFile * APPEALS-40657 Refactor UploadTranscriptionFileToS3 workflow * APPEALS-40657 Refactor DownloadTranscriptionFileJob spec * APPEALS-40657 Update migration and model for TranscriptionFile to associate with hearing instead of appeal * APPEALS-40657 Update DownloadTranscription job to save records with hearing instead of appeal attributes * APPEALS-40657 Update subject for conference to include hearing attributes instead of appeal * APPEALS-40657 Schema * APPEALS-40657 Fix failing spec in virtual hearing spec * APPEALS-40657 Update comment in migration * APPEALS-40657 Update schema * Min/APPEALS-39905 (#20853) * APPEALS-39905 audible text will now be displayed as [INAUDIBLE] * APPEALS-39905 csv getting made and uploaded when inaudibles are found * APPEALS-39905 turned tmp clean up back on * APPEALS-39905 adjusted rspec tests * APPEALS-39905 adjusted transformer rspec tests * APPEALS-39905 added more rspec tests to transformer workflow * APPEALS-39905 added guards for nil values in hearing info * added byebug * removed byebug * APPEALS-39905 Resolve merge conflicts in download transcription job spec * APPEALS-39905 changed inaudible display * APPEALS-39905 changed test url back * APPEALS-39905 converts input vtt to readable format if invalud utf is found * APPEALS-39905 adjusted rspec tests --------- Co-authored-by: Jeff Marks * akonhilas/APPEALS-31620 (#20788) * APPEALS-31620: initial commit * APPEALS-31620: finished all webex service/response/fakes/spec updates * APPEALS-31620: adding recording details job * APPEALS-31620: cannot get past rspecs here * APPEALS-31620: update rspecs * APPEALS-31620: add file * APPEALS-31620: finalized rspecs, removed Hearings prepend - unnecessary after rebuild * APPEALS-31620: resolve webex service rspec * APPEALS-31630: code cleanup * APPEALS-31620: remove hearings prepend, perform_later, scheduled_for, regex * APPEALS-31620: rspec * jefftmarks/APPEALS-40909 (#20951) * APPEALS-40909 Create migration and write spec tests * APPEALS-40909 Update conference_link model to associate with polymorphic hearing * APPEALS-40909 Write spec for add index to conference links * APPEALS-40909 Add spec to test conference link factory after null constraint removed * APPEALS-40909 Uncomment out code in migration to see if tests pass on GHA * APPEALS-40909 Implement database cleaner fix and write down test for index migration * APPEALS-40909 Test conference links can belong to hearings and hearing days * APPEALS-40909 Remove migration tests * APPEALS-40909 Refactor delete conference link job spec to avoid static record count * APPEALS-40909 Fix duplicate examples in download transcription file spec --------- Co-authored-by: Adam Ducker * APPEALS-39907 (#20952) * APPEALS-39907 Initial commit * APPEALS-39907 update convention * APPEALS-39907 update render call * APPEALS-39907 update to/from within .erb * APPEALS-39907 added to and from attributes * APPEALS-39907 to/from update * APPEALS-39907 direction update * APPEALS-39907 conditional added * APPEALS-39907 update conditional * b_reed/APPEALS-39907 added mailing in error handling for DownloadTranscriptionFileJob and GetWebexRecordingDetailsJob * APPEALS-39907 added more mailing code for error handling * APPEALS-39907 adjusted rspec tests * APPEALS-39907 Unit tests complete * APPEALS-39907 adjusted lint * APPEALS-39907 update class ActionMailer * APPEALS-39907 added comments * APPEALS-39907 removed redundant unit test * APPEALS-39907 update demo url * APPEALS-39907 seperated env instances * APPEALS-39907 update id fetch method ln: 209 * APPEALS-39907 remove filename parameter ln: 32 * APPEALS-39907 added filename argument back * APPEALS-39907 added the lint disabler back * APPEALS-39907 updated demo env test * b_reed/APPEALS-39907 fixed minor issue with argument names * APPEALS-39907 updated how appeal ids are found * APPEALS-39907 more minor fixes * APPEALS-39907 * APPEALS-39907 email address to send mail and cc now changes depending on rails env * APPEALS-39907 fixed rspec tests * APPEALS-39907 Update html doc layout * APPEALS-39907 adjusted email addresses and some refactors * APPEALS-39907 adjusted rspec tests * APPEALS-39907 update case statements * APPEALS-39907 email preview template * APPEALS-39907 update values * APPEALS-39907 added appeal id * APPEALS-39907 previewer lint * APPEALS-39907 remove blank space * APEALS-39907 disable actionMailer lint * APPEALS-39907 fixed rspecs * APPEALS-39907 config file update --------- Co-authored-by: Minhazur Rahaman * akonhilas/APPEALS-40915 (#20948) * APPEALS-40915: initial commit * APPEALS-40909 Create migration and write spec tests * APPEALS-40909 Update conference_link model to associate with polymorphic hearing * APPEALS-40909 Write spec for add index to conference links * APPEALS-40909 Add spec to test conference link factory after null constraint removed * APPEALS-40909 Uncomment out code in migration to see if tests pass on GHA * APPEALS-40915: updated comments to job * APPEALS-40909 Implement database cleaner fix and write down test for index migration * APPEALS-40915: added error catching, updated hearing model, removed webex hearing day link creation * APPEALS-40915: updated existing rspec tests to include/exclude new functionality * APPEALS-40909 Test conference links can belong to hearings and hearing days * APPEALS-40915: prepping branch for merge * APPEALS-40915: job creates a hearing conference link * APPEALS-40915: updating rspecs * APPEALS-40915: finished rspecs * APPEALS-40915: nullifying link values to resolve frontend errors * APPEALS-40915: added create nv conf call to correct spot, removed old code/rspecs * APPEALS-40915: final rspec addition * APPEALS-40915: remove migration specs * APPEALS-40915: add code review updates --------- Co-authored-by: Jeff Marks Co-authored-by: Adam Ducker * Missed rubocop merge conflict * akonhilas/APPEALS-40921 (#21116) * APPEALS-40921: added method to concern and reference to both hearing serializers * APPEALS-40921: change get to fetch, conference link find by hearing, fix rubocop lint errors * APPEALS-40921: change get to fetch, conference link find by hearing, fix rubocop lint errors * Min/APPEALS-37605 (#21151) * APPEALS-37605 added route and test json response * APPEALS-37605 file now gets downloaded onto local computer * APPEALS-37605 added user verification * min/APPEALS-37605 added rspec and factory for transcription_file * APPEALS-37605 finished writing rspec tests * APPEALS-37605 finished writing rspec tests * APPEALS-37605 added more error handling * APPEALS-37605 Move controller back to hearings folder * APPEALS-37605 Remove unused methods from controller * APPEALS-37605 Move transcription_file_spec to hearings folder --------- Co-authored-by: msteele * jefftmarks/APPEALS-37292-37293 (#21140) * APPEALS-37292 Create transcription files table * APPEALS-37292 Adjust table styling * APPEALS-37292 Update legacy conditional to show transcription details * APPEALS-37292 Conditionally render different sections of transcription details * APPEALS-37292 Implement useState and useEffect to reduce renders on table * APPEALS-37293 Populate table with updated transcription file serializer * APPEALS-37292-37293 * APPEALS-37293 Comment code * APPEALS-37293 Update snapshot * APPEALS-37292-37293 Update snapshot * APPEALS-37293 Revert transcription files controller * APPEALS-37293 Update Details jest test * APPEALS-37293 Add date attribute to hearings data for jest test * APPEALS-37293 Update imports on Details.jsx jest test * APPEALS-37293 Update hearing concern with transcription file by recording method * APPEALS-37292 Update LegacyHearing model and serializer * APPEALS-37293 Update feature test for details page * APPEALS-37293 Fix typo in table id * APPEALS-37293 Add aws link to factory * APPEALS-37293 Fix hearing links snapshot to pass jest test * APPEALS-37293 Fix hearing factory * APPEALS-37293-37293 * APPEALS-37293 Fix transcription files controller spec * APPEALS-37293 Update to group files on table by docket number * APPEALS-37293 Refactor route/controller without responds_to and move css * APPEALS-37293 Fix feature test to reflect route update * APPEALS-37293 Fix linting on css * APPEALS-37293 scss lint * BelongsToPolymorphicHearingConcern updates from Rails 6.0 * jefftmarks/APPEALS-40907 (#21249) * APPEALS-40907 Create polymorphic hearing concerns specific to conference links and transcription files and update models * APPEALS-40907 Remove unnecessary polymorphic concerns * APPEALS-40907 Update daily docket for one to one nonvirtual webex conference links * APPEALS-40907 Update models * APPEALS-40907 Update models * APPEALS-40907 Update and remove jest tests * Min/APPPEALS-42711 (#21285) * APPEALS-42711 adjusted query for webex recordings list * APPEALS-42711 added rspec tests for webex_conference_link * APPEALS-42711 some code climate refactors * APPEALS-42711 fixed code climate issues and webex service argument refactor * APPEALS-42711 fixed som broken rspec tests * APPEALS-42711 fixed a few code climate issues with mailer * APPEALS-42711 refactored more code climate issues with mailer and webex implementation * APPEALS-42711 fixed broken rspec tests and finishing up with clearing up viable code climate issues * APPEALS-42711 fixed some broken rspec tests and more code climate issues * APPEALS-42711 changed names of classes and methods to fix code climate issue * APPEALS-42711 changed names of classes and methods to fix code climate issue * APPEALS-42711 changed names of classes and methods to fix code climate issue * APPEALS-42711 added max value back in to webex recordings * APPEALS-42711 added max value back in to webex recordings * min/APPEALS-42711 adjusted query * APPEALS-42711 refactored some code * akonhilas/APPEALS-40906 (#21165) * APPEALS-40906: remove sub nbf exp from hearing_day * APPEALS-40906: updates to serializer, coHostHearingLink to coHostLink, proper conditionals for virtual vs non virtual * APPEALS-40906: Update so many jest tests and test data * APPEALS-40906: merge latest from feature * APPEALS-40906: Update snapshots after merge * APPEALS-40906: updates to feature test * APPEALS-40906 change serializer debug attempt * APPEALS-40906 Revert previous debugging commit and add isVirtual back to VirtualHearingLink.jsx * APPEALS-40906 Return references of conference_links to conference_link * APPEALS-40906 Duplicate changes from hearing_serializer to legacy_hearing_serializer * APPEALS-40906 Update failing spec and serializer * APPEALS-40906 Allow for null links prop in LinkContainer in HearingLinks.jsx * APPEALS-40906 Ensure correct classname and button text for hearing coordinator link * Fix hearing_day_spec to user singular conference link * Update hearing details snap shots * APPEALS-40906 Update Details.test.js snapshot * APPEALS-40906 Fix hearing details feature by adding was virtual to HearingLinks.jsx * APPEALS-40906 Remove hearing links from central and video details page * Uncomment hearing details feature spec * APPEALS-40906 Remove hearing links from details if hearing type video and hearing in past * APPEALS-40906 Add bang operator * APPEALS-40906 Update Hearing Links jest to not use redux store * APPEALS-40906 Remove console.log * APPEALS-40906 Remove console.log * APPEALS-40906 Ensure hearing day links replace virtuali nks when converted from virtual to video * APPEALS-40906 Update snapshots * APPEALS-40906 Update hearing serializers so attributes conform across virtual and nonvirtual conference links * APPEALS-40908 Update HearingLinks UI to show copy link button at all times * APPEALS-40906 Update hearing details feature test to reflect hearing links for all hearing types * APPEALS-40906 Fix jest test props for HearingLinks test * APPEALS-40906 Uncomment out jest config * APPEALS-40906: remove unused code from hearing links test * APPEALS-40906: move non virtual conference link to hearing concern --------- Co-authored-by: msteele Co-authored-by: Jeff Marks Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * Min/APPEALS-43214 (#21344) * APPEALS-43214 added new error handling for file upload and new param to custom exception * APPEALS-43214 wrote rspec tests and made some adjustment to error handlers * rerun checks * rerun checks * APPEALS-43214 adjusted failing rspec * APPEALS-43214 Linting fix * APPEALS-43214 Indentation fixes * APPEALS-43214 Update mailer to handle different transcription file jobs * APPEALS-43214 Added beginning of hour to webex list calls * APPEALS-43214 Update mailer to show download link and fix failing test * APPEALS-43214 Refactor transcription file issues mailer * APPEALS-43214 Remove old spec file * APPEALS-43214 Refactor content section of mailer * APPEALS-43214 Add missing comma * APPEALS-43212 Refactor mailer to render nested lists * APPEALS-43214 Update prodtest email and demo vs dev distinction * APPEALS-43214 Linting and fixes to create conference job * APPEALS-43214 Call job#extra * APPEALS-43214 job#log_error * APPEALS-43214 Remove extra method * APPEALS-43214 Fix log error issue * APPEALS-43214 Fix typo * APPEALS-43214 Incorporate action direction into job action hash * APPEALS-43214 Remove byebug --------- Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> Co-authored-by: msteele Co-authored-by: Jeff Marks * b_reed/APPEALS-34071-v2 (#21422) * Add rcredstash and dynamodb * Update ports * Fix m1 docker compose * Add kms * Add initializers * Upgrade aws-sdk * Lint roll * Update table name * Add back VirtualHearings::RefreshWebexAccessTokenJob * Add refresh_access_token method to ExternalApi::WebexService * Add back AccessTokenRefreshResponse * Add back in error handling in Webex Response class * Add new job to SCHEDULED_JOBS * Add WebexInvalidTokenError error class * Add refresh_access_token to fake WebexService * Add back in specs * APPEALS-34071 bundle install additions gemfile.lock * APPEALS-34071 slim out test * Fix the 'levers exists' message overwritting test output --------- Co-authored-by: Matthew Thornton * Move methods from hearing.rb to hearing concern and add default aws region to resolve error * Match yarn.lock to master * Update ref for caseflow-commons * Update gemfile.lock * Update scheduled jobs key and move job to different folder * Update job to set user manually and pass in updated_by * msteele/APPEALS-45182 Fix transcription_files relationships on Hearings/LegacyHearings (#21492) * APPEALS-45182 Reinforce has_many relationships * APPEALS-45182 Add missing comma * Resolve lint issue --------- Co-authored-by: Matthew Thornton * Hotfix/APPEALS-45218 (#21503) * APPEALS-45218 reverted webex config arguments * APPEALS-45218 reverted webex config arguments * APPEALS-25218 Update WebexService to remove config in favor of individual arguments * APPEALS-45218 Format comment --------- Co-authored-by: msteele * B_reed/hotfix_APPEALS-45285 (#21531) * APPEALS-45285 Bug Fix * APPEALS-45285 update pexip_service_spec * APPEALS-45285 remove white space * Hotfix/APPEALS-45218-v2 (#21522) * APPEALS-45218 reverted webex config arguments * APPEALS-45218 reverted webex config arguments * APPEALS-45218 added query argument to webex conference link * APPEALS-45218: webex recordings endpoint correction * APPEALS-45218 adjusted arguments during inititalizing of refresh tokens * APPEALS-45218 fixed rspec * APPEALS-45218: added error catching on webex service response * APPEALS-45218: remove tested code * APPEALS-45218 Go back to leveraging HTTPI response inherited methods --------- Co-authored-by: Ariana Konhilas Co-authored-by: msteele * msteele/APPEALS-45349 (#21539) * APPEALS-45349 Check for error on HTTPI object, then create Webex response object and fail with specific error * APPEALS-45349 Flip conditionals * APPEALS-45349 Fix linting errors * hotfix/APPEALS-45399-45401-45472 (#21555) * APPEALS-45399-45401 Update fetch webex list and details to retrieve apikey from cred stash * APPEALS-45399-45401 Remove CGI escape from fetch webex list job * APPEALS-445399-45401 Update spec files for webex list and details job * APPEALS-45399-45401 Include error handling for transcription issue mailer failed delivery * Revert "APPEALS-45399-45401 Include error handling for transcription issue mailer failed delivery" This reverts commit d9ce585c59444f9793e681e58060d71872057e4c. * hotfix/APPEALS-45472 (#21562) * APPEALS-45472 Refactor error handling for transcription * APPEALS-45472 Include missing provider in error details for fetch recording list job and update comment for mailer * Hotfix/APPEALS-45818 (#21632) * APPEALS-45818: parsing fix, remove topic param, fix response * APPEALS-45818: revert list and details job back to original state, update tests --------- Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * msteele/APPEALS-45285-v2 Fix delete_conference in PexipService (#21634) * APPEALS-45285 update ExternalApi::PexipService#delete_conference to accept virtual_hearing as the only argument * APPEALS-45285 Update spec to use virtual_hearing * APPEALS-45285 Linting fixes * hotfix/APPEALS-45828 (#21625) * APPEALS-45828 Include call to create webex conference links in base hearing update form * APPEALS-45828 Update details feature spec to expect webex link creation on conversion from virtual * Fix needing to reload to display co host link on details page * Fix incorrect method name * Remove temporary rendering of default pexip link for webex hearings after conversion * APPEALS-45828 Fix hearing mailer spec context issues and null check for BAD_VIRTUAL_LINK_TEXT * APPEALS-45828 Remove file name argument from fetch details job spec * APPEALS-45828 Fail on bad hearing link if link nil or legacy link * APPEALS-45828 Sanitize url string * APPEALS-45828 Undo url sanitize * Remove unused styles after merge * APPEALS-45285 added conditional to handled nil conference ids (#21766) Co-authored-by: Minhazur Rahaman * feature/APPEALS-45998 Webex Rooms API Workaround (#21810) * Min/APPEALS-46009 (#21690) * APPEALS-46005: job, job logic, env placeholder, schedule update * APPEALS-46011 Add fetch rooms list and fetch room details methods to webex service * APPEALS-46011 Update fakes * APPEALS-46005: merge 46011 logic updates * APPEALS-46009 added the new webex room details job * APPEALS-46011 Update specs * commenting out code * commenting code back in * APPEALS-46009 adjusted rspec tests * APPEALS-46009 adjusted rspec tests * APPEALS-46005: add rspec, comment out rooms details call, final filter * APPEALS-46009 adjusted rspec tests * APPEALS-46005: rough filter test added to rooms list spec commented out * APPEALS-46009 adjusted rspec tests * APPEALS-46009: filter rspec, filter in rooms list job, add fake data * APPEALS-46009: resolved comments --------- Co-authored-by: Ariana Konhilas Co-authored-by: Jeff Marks * akonhilas/APPEALS-46006 (#21734) * APPEALS-46006: initial updates to recordings list job, recordings list resp, recordings details job, room meeting details job * APPEALS-46006: webex service spec update, fake update, recordings list response update * APPEALS-46006: recordings details job and spec updates, lint fix recordings list job * APPEALS-46006: recordings list spec and job updates * APPEALS-46006: room meeting details spec updates * APPEALS-46006: final updates to fake data * APPEALS-46006: changing topic to actual webex response * APPEALS-46006 create and use self.acceses_token method in WebexService classes * APPEALS-46006 Update failing rspec for Details and List Jobs * APPEALS-46006: update mailer, preview, specs, lint error --------- Co-authored-by: msteele --------- Co-authored-by: minhazur9 <65432922+minhazur9@users.noreply.github.com> Co-authored-by: Ariana Konhilas Co-authored-by: Jeff Marks Co-authored-by: Ariana Konhilas <109693628+konhilas-ariana@users.noreply.github.com> * Hotfix/APPEALS-46083 (#21832) * Min/APPEALS-46009 (#21690) * APPEALS-46005: job, job logic, env placeholder, schedule update * APPEALS-46011 Add fetch rooms list and fetch room details methods to webex service * APPEALS-46011 Update fakes * APPEALS-46005: merge 46011 logic updates * APPEALS-46009 added the new webex room details job * APPEALS-46011 Update specs * commenting out code * commenting code back in * APPEALS-46009 adjusted rspec tests * APPEALS-46009 adjusted rspec tests * APPEALS-46005: add rspec, comment out rooms details call, final filter * APPEALS-46009 adjusted rspec tests * APPEALS-46005: rough filter test added to rooms list spec commented out * APPEALS-46009 adjusted rspec tests * APPEALS-46009: filter rspec, filter in rooms list job, add fake data * APPEALS-46009: resolved comments --------- Co-authored-by: Ariana Konhilas Co-authored-by: Jeff Marks * APPEALS-46083 hearing details page now shows NA when hearing is converted from virtual to non or if postponed * APPEALS-46083 fixed conditional for cancelled hearings and virtual hearings --------- Co-authored-by: Ariana Konhilas Co-authored-by: Jeff Marks * akonhilas/APPEALS-46130 (#21804) * APPEALS-43160: Fix linting errors in OrgUsers * APPEALS-43160: fix margins and move divider * APPEALS-43160: remove margin top on search bar styling * APPEALS-43160: fix webex lint errors * APPEALS-43160: move radio button style div inside visibility check * APPEALS-46130: move css inline style to scss * APPEALS-46130: fix lint errors, remove gray line * Hotfix/APPEALS-48161 (#21917) * APPEALS-48161: remove, refactor, fake update, specs, filename update * APPEALS-48161: kickoff gha on right branch merge * APPEALS-48161: filter title call only once * APPEALS-46385: remove s, add aws-sdk, bundle install, move rcredstash up (#21941) * Adjust spacing in gemfile * Hotfix/APPEALS-49560 (#22015) * APPEALS-49560 moved non-virtual conference link creation to hearing concern * APPEALS-49560 moved non_virtual conference creation inside of reschedule method * APPEALS-49560 added a new spec test to test link creation * APPEALS-49560 refactored rspec * APPEALS-49560 removed additional method in after_create to prevent risk of duplication * APPEALS-49560 removed additional method in after_create to prevent risk of duplication * Hotfix/APPEALS-49624 (#22016) * APPEALS-49624: update api_call in fetch jobs for error emails, change i to breaks * APPEALS-49624: add rooms and room details to mailer preview, correct api url * APPEALS-49624: cleaning up rspecs * APPEALS-49624: update query format, add mailer tests * APPEALS-49624: update mailer keys * APPEALS-49624: fix lint errors * APPEALS-49624 Remove rubocop ignores and fix query format --------- Co-authored-by: msteele * Remove extra newline for Rubocop * Hotfix/Appeals-49560-v2 (#22052) * APPEALS-49560 moved non-virtual conference link creation to hearing concern * APPEALS-49560 moved non_virtual conference creation inside of reschedule method * APPEALS-49560 added a new spec test to test link creation * APPEALS-49560 refactored rspec * APPEALS-49560 removed additional method in after_create to prevent risk of duplication * APPEALS-49560 removed additional method in after_create to prevent risk of duplication * APPEALS-49560 added extra filtering for conference link querying and same fix applied to HearingPostponementRequestMailTask * Update app/models/concerns/hearing_concern.rb Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> --------- Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> * Remove extra newline after merge conflict * Recover lost code to compress multiple lines by same speaker into one large block * Reintroduce code after bad merge conflict resolution * Min/APPEALS-50859 (#22081) * remediated code climate issues * remediated code climate issues * remediated code climate issues * fixed duplicate code issues * fixed duplicate code issues * fixed duplicate code issues * APPEALS-50859 added new concern to abstract duplicate code * APPEALS-50859 fixed new concern climate issues * APPEALS-50859 added new config method for rooms endpoints to concern * APPEALS-50859 added new config method for rooms endpoints to concern * APPEALS-50859 added new config method for instant connect to concern * APPEALS-50859 added new config method for instant connect to concern * Update bundler version * Remove duplicate method * Fix bad merge conflict resolution * Remove x86_64 platforms, add ruby platform * Remove bad metrics line * akonhilas/APPEALS-59152 (#22973) * APPEALS-59152: removed unnecessary logic, color.primary * APPEALS-59152: update snapshot tests * Merge hotfix/APPEALS-59907 into feature/APPEALS-25121 * Reapply hotfix/APPEALS-60945 to feature/APPEALS-25121 * Edit bad down migration to use SQL --------- Co-authored-by: Ariana Konhilas Co-authored-by: breedbah Co-authored-by: mchbidwell <122634362+mchbidwell@users.noreply.github.com> Co-authored-by: Matthew Thornton Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> Co-authored-by: Jeff Marks Co-authored-by: Jeff Marks <106996298+jefftmarks@users.noreply.github.com> Co-authored-by: minhazur9 <65432922+minhazur9@users.noreply.github.com> Co-authored-by: Ariana Konhilas <109693628+konhilas-ariana@users.noreply.github.com> Co-authored-by: Adam Ducker Co-authored-by: 631862 Co-authored-by: Minhazur Rahaman Co-authored-by: breedbah <123968373+breedbah@users.noreply.github.com> Co-authored-by: Jeff Marks * hotfix/appeals 59143: Transcription vendor swap (#23261) * hotfix/APPEALS-59143: Remove ravens group and add vet reporting Co-authored-by: Alexandra Ferencz Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> * Update Jest test * Update jest test --------- Co-authored-by: Alexandra Ferencz Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> * hotfix/appeals 59867: Remove Chattanooga and Knoxville RO (#23260) * hotfix/APPEALS-59867 Remove locations * Remove locations and update jest tests * Trigger Build --------- Co-authored-by: Alexandra Ferencz Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> * Update jest test * delete snapshot file --------- Co-authored-by: Alexandra Ferencz Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> Co-authored-by: Ronald Wabukenda * Feature/appeals 49670 49667 phase ii release pt 1.1.4 -bug fixes (#23264) * add saftey operator for missing request_issues_update * fix optin check to pull from parser not review * Reader prototype bugfixes (#23266) * Hotfix/appeals 57826 (#22946) * Add fix for filtered docs bug * Revert component import paths to full path * Revert glamor styles changes * Update lodash imports to reduce size * Replace lodash/get with javascript optional chaining * Refactor previous/next doc logic without lodash * Remove unused import from ReaderFooter * Delete lodash logic in documentUtil and move ReaderToolbar functions to ReaderToolbar component * Update prototype-footer css * Adding regression tests for filtered document order. * Adding label to filtered icon. * Adding test for unfiltered content. * Moving test up to previous. * Fix merge conflict * Fix merge conflict * Fix empty arrow function lint error in ReaderFooter.test.js * Add previous/next navigation with left/right arrow keys --------- Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> Co-authored-by: Brian Bommarito * Remove alpha attribute from Page component to fix black boxes renderi… (#22947) Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> * Hotfix/appeals 57877 (#22948) * Add fix for filtered docs bug * Revert component import paths to full path * feature/APPEALS-34124-43428-29105-28925-33581 - Rails 6.1 upgrade (release) (#22813) (#22817) * 🔧 Assume defaults for `config.action_dispatch.use_cookies_with_metadata` and `config.action_mailer.delivery_job` The following config settings are not backwards compatible: - config.action_dispatch.use_cookies_with_metadata - config.action_mailer.delivery_job Now that Rails 6.0 is stable on production, we can assume their default values going forward. * ✅ Fix flakey spec * 🔧 Assume default for `config.action_dispatch.use_authenticated_cookie_encryption` Since we are making other cookie configuration changes in this PR for Rails 6.0, this is an opportune time to migrate this Rails 5.2 cookie setting to its default value as well. * ⏪️ Restore overrides for `config.action_dispatch.use_authenticated_cookie_encryption` and `config.action_dispatch.use_cookies_with_metadata` While testing in PreProd, we discovered that, without these cookie config overrides, re-authentication was broken -- after logging out, a user could not log back in. Since the default settings are still optional going forward, we can restore these overrides and devise a solution to migrate cookies later. For more details, see Jira story APPEALS-54897: https://jira.devops.va.gov/browse/APPEALS-54897 * ✨ Add new utility module for adding DB indexes concurrently Introduces `Caseflow::Migrations::AddIndexConcurrently` as a replacement for `Caseflow::Migration` for migrations on ActiveRecord 6.0 and beyond, since `Caseflow::Migration` is forever coupled to ActiveRecord 5.1 due to its extensive use on legacy migrations and should be deprecated moving forward. * 🗑️ Deprecate `Caseflow::Migration` * 🔧 Add instructive error message for non-concurrent `add_index` migrations * 🚨 Address linter / codeclimate complaints * ✨ Introduce `SslRedirectExclusionPolicy` To be used in the environment configuration settings for excluding exempt request paths from SSL redirects when `config. force_ssl = true` * ♻️ Replace deprecated controller-level `force_ssl` Replace deprecated controller-level `force_ssl` with equivalent configuration settings in preparation for the Rails 6.1 upgrade. * 🔥 Remove deprecated config setting `config.active_record.sqlite3.represent_boolean_as_integer` This will have no implications for Caseflow, since we are only using the `sqlite3` adapter nominally for the `demo_vacols` database, which is not actually being used in our demo environments (demo environments are deployed as `development` envs). * ⬆️ Update `caseflow-commons` to resolve sub-dependency conflicts Removes unneeded gems `bourbon` and `neat`, which had a sub-dependency conflict on `thor`. * ⬆️ Update rails and other gems as necessary * 🐛 Fix 'uninitialized constant' error when loading app * ⬆️ bin/rails app:update - Apply relevant changes * 🔧 Override default for `config.active_record.has_many_inversing` * 🔧 Assume default for `config.active_storage.track_variants` We're not currently using ActiveStorage in Caseflow, so it is safe to just assume the default here. * 🔧 Override default for `config.active_job.retry_jitter` The default jitter is probably safe, however, I'm not 100% sure that we don't have any jobs that need to be requeued with exact wait times. So we let's override this for now to stay on the safe side. * 🔧 Assume default for `config.active_job.skip_after_callbacks_if_terminated` We're not currently using `throw :abort` within any `before_enqueue`/`before_perform`  callbacks on existing Caseflow jobs, so the default should be fine here. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#4c60 * 🔧 Assume default for `config.action_dispatch.cookies_same_site_protection` This setting controls the `SameSite` optional attribute for the `Set-Cookie` header. `SameSite=Lax` means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link). This is the default behavior if the SameSite attribute is not specified. `Lax` is currently the default assumed by both Chrome and Edge browsers when this attribute is left unspecified, so assuming this value should be sensible. It allows us to have our cake (blocking CSRF attacks) and eat it too (providing a logged-in experience when users navigate to Caseflow across origins). For more background, see - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#1f15 * 🔧 Assume default for `config.action_controller.urlsafe_csrf_tokens` * 🔧 Assume default for `ActiveSupport.utc_to_local_returns_utc_offset_times` We're not using `ActiveSupport::TimeZone.utc_to_local` anywhere, so the default is safe to assume here. * 🔧 Assume default for `config.action_dispatch.ssl_default_redirect_status` The default is safe to assume. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#4c3e * 🔧 Assume default for `config.active_record.legacy_connection_handling` The default should be safe to assume here, as we do not do any role or shard switching on database connections. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#8007 * 🔧 Assume default for `config.action_view.form_with_generates_remote_forms` We don't use the `form_with` helper anywhere, so this behavior change is inconsequential for us, and we can safely assume the new default. * 🔧 Assume default for `config.active_storage.queues.analysis` We do not use ActiveStorage, so the default is safe to assume here. * 🔧 Assume default for `config.active_storage.queues.purge` We do not use ActiveStorage, so the default is safe to assume here. * 🔧 Assume default for `config.action_mailbox.queues.incineration` We don't use ActionMailbox, so the new default is safe to assume here. * 🔧 Assume default for `config.action_mailbox.queues.routing` We do not use ActionMailbox, so the default is safe to assume here. * 🔧 Assume default for `config.action_mailer.deliver_later_queue_name` We're not using `ActionMailer::MessageDelivery #deliver_later` anywhere, so the default is safe to assume. * 🔧 Assume default for `config.action_view.preload_links_header` This flag can be safely uncommented. Browsers that support Link headers will get a performance boost. Browsers that don’t will ignore them. We override in `development` environments to avoid an edge case leading to an HTTP response header overflow. For more background, see https://lilyreile.medium.com/rails-6-1-new-framework-defaults-what-they-do-and-how-to-safely-uncomment-them-c546b70f0c5e#3679 * 🔥 Remove 'new_framework_defaults_6_1.rb' * 🔧 Load defaults for Rails 6.1 * ♻️ Extract constant * ♻️ Migrate to new Rails deprecation config where applicable * ♻️ Push members down now that there is only one subclass * 🩹 Add forgotten disallowed deprecation warning This deprecation warning was addressed by the following PR, but we forgot to add it to the list of disallowed deprecation warnings: https://github.com/department-of-veterans-affairs/caseflow/pull/21614 * 💡 Update comment Task `rake routes` has been replaced with `rails routes` * ✅ Update test to account for change to `ActionDispatch::Response#content_type`  `ActionDispatch::Response#content_type` now returns the full Content-Type header * 🚨 Exclude 'config.ru' from Rubocop cops * 🚚 Move 'db/etl/migrate' to 'db/etl_migrate' * 🚚 Move 'db/etl/schema.rb' to 'db/etl_schema.rb' * ♻️ Arrange 'database.yml' configs by environment Group DB configs by environment in anticipation of reformatting for Rails 6+ multi-DB configuration. * 🔧 Reformat 'database.yml' to Rails 6+ multi-DB conventions * 🔧 Add etl migration paths to DB config * 🔧 Update DB connection names in 'database_cleaner' config * ♻️ Use new database-specific rake tasks After migrating to the Rails 6+ native multi-database configuration, the behavior of some DB management tasks, such as `rake db:migrate` changed such that they now act on ALL databases and not just the primary database. So we must replace the invocations of these tasks with their new, database-specific counterparts. * ➖ Remove 'multiverse' gem Now that we have fiully transitioned to Rails-native multi-database support, we are no longer reliant on the 'multiverse' gem and can remove it. * 🗃️ Prohibit execution of vacols DB and non-DB-specific rake tasks After transitioning to Rails-native multi-DB support, the behavior of some DB tasks changed such that they will now act on ALL databases and not just the primary database (ex. `rake db:migrate` will now migrate ALL databases). To avoid accidents, we re-define these tasks here to no-op and output a helpful message to redirect developers toward using their new database-specific counterparts instead. * ♻️ Create new environment for GH workflow 'Make-docs-to-webpage' Instead of performing a bunch of hard-to-maintain `sed` gymnastics to modify the existing 'test' environment, let's create a new 'make_docs' environment (based off of 'test') and configure it appropriately for use by the 'Make-docs-to-webpage' GH workflow. * 💚 Remove redundant DB migrations from CI workflow Task `db:schema:load` already loads the checked in schema, so there should be no need to run `db:migrate` afterwards. * 🐛 Fix `spec/mailers/hearing_mailer_spec.rb` - `NoMethodError` Addresses the following error: NoMethodError: undefined method `build_lookup_context' for ActionView::Base:Class * 🐛 Fix `spec/workflows/post_decision_motion_updater_spec.rb` - `FrozenError` Addresses the following error: FrozenError: can't modify frozen Hash: {} * ✅ Add test for `RoSchedulePeriod` * 🐛 Fix `spec/models/schedule_period_spec.rb` - `ActiveRecord::RecordInvalid` Apparently, there were some changes to the inner workings of `ActiveModel::Errors` in Rails 6.1, causing a model to be considered invalid in the case that `errors[:base] == [[]]`. This makes sense, as `[[]]` is not considered "empty". Unfortunately, this was causing `RoSchedulePeriod #validate_spreadsheet` to inadvertently mark the model as invalid upon creation. `HearingSchedule::ValidateRoSpreadsheet #validate` returns an empty array (`[]`) when valid, which gets pushes onto the `RoSchedulePeriod` `errors[:base]` array, resulting in a non-empty array (`[[]]`) and an erroneously invalid disposition. Furthermore, calling `<<` to an `ActiveModel::Errors` message array in order to add an error is a deprecated, so we can take this opportunity to use the new `#add` API to hit two birds with one stone. The change implemented here is not a pure refactoring, however the end-user experience is unchanged in terms of how errors are presented when attempting to upload a spreadsheet with multiple non-conformities. Down the road, we may want to consider moving `HearingSchedule::ValidateRoSpreadsheet` toward using `ActiveModel::Validations` in order to leverage the full `ActiveModel::Errors` API and construct the errors object in the prescribed manner. For more details see - https://api.rubyonrails.org/v6.1.7.7/classes/ActiveModel/Validations.html - https://api.rubyonrails.org/v6.1.7.7/classes/ActiveModel/Errors.html * 🐛 Fix `spec/mailers/hearing_mailer_spec.rb` - `ActionView::Template::Error` * ✅ Fix `spec/models/veteran_spec.rb` * ✅ Fix `spec/sql/ama_cases_sql_spec.rb` Addresses failures such as the below: 0) AMA Cases Tableau data source expected report calculates age and AOD based on person.dob Failure/Error: expect(aod_case["aod_veteran.age"]).to eq("76") expected: "76" got: 0.76e2 * ✅ Fix multiple specs - `Minitest::UnexpectedError` Test helper method `#perform_enqueued_jobs` now wraps exceptions in an `Minitest::UnexpectedError`: https://github.com/rails/rails/blob/914caca2d31bd753f47f9168f2a375921d9e91cc/activejob/lib/active_job/test_helper.rb#L591 So, to assert that a specific exception is raised during execution of the `#perform_enqueued_jobs` block, we must rescue the `Minitest::UnexpectedError` and make the assertion on its error message instead. * ✅ Fix `spec/lib/helpers/association_wrapper_spec.rb` * ✅ Fix `spec/controllers/api/v1/jobs_controller_spec.rb` In Rails 6.1, `ActiveJob #perform_now` was changed to behave as it did once before (at the behest of GitHub), returning the value fo the job instead of true/false. See related GH issue: https://github.com/rails/rails/issues/38040 * 🐛 Fix `spec/controllers/appeals_controller_spec.rb` - `NoMethodError` Addresses error: NoMethodError: undefined method `workflow' for # 0) AppealsController GET appeals when current user is a System Admin when request header does not contain Veteran ID responds with an error Failure/Error: errors: errors.messages[:workflow], NoMethodError: undefined method `workflow' for # # ./app/workflows/case_search_results_base.rb:31:in `search_call' * 🐛 Fix `CaseSearchResultsBase` validations Addresses test failures in `spec/controllers/appeals_controller_spec.rb` similar to the below: AppealsController GET appeals when current user is a System Admin when request header does not contain Veteran ID responds with an error Failure/Error: expect(response_body["errors"][0]["title"]).to eq "Veteran file number missing" expected: "Veteran file number missing" got: nil Using `ActiveModel::Errors` to store error data in an arbitrary format may have been somewhat permissible in the past, but it is an abuse of the object's intended use and is also proving incompatible with the more formalized `ActiveModels::Errors` API in Rails 6.1. In order to preserve the existing response shape of the affected JSON endpoints, we need to move away from the `ActiveModel::Validations` implementation on `CaseSearchResultsBase` (and its descendent classes) to a more bespoke method of performing validations and aggregating errors, since Rails 6.1 `ActiveModel::Errors` is no longer appropriate for our needs here. * ✅ Fix `spec/controllers/application_controller_spec.rb` -- Cache-Control error Addresses the test failure below: ApplicationController no cache headers when toggle set sets Cache-Control etc Failure/Error: expect(response.headers["Cache-Control"]).to eq "no-cache, no-store" expected: "no-cache, no-store" got: "no-store" (compared using ==) # ./spec/controllers/application_controller_spec.rb:59:in `block (4 levels) in ' In Rails 6.1, the `no-store` directive is exclusive of any others that are set on the `Cache-Control` header, which makes sense given the specification https://datatracker.ietf.org/doc/html/rfc7234#section-3 This change was implemented in PR https://github.com/rails/rails/pull/39461 Since it no longer makese sense to set both `no-store` and `no-cache` directives, we will only set `no-store` here, as that is the stronger of the two. * 🐛 Fix multiple specs - `ActiveRecord::EagerLoadPolymorphicError` Addresses multiple test failures caused by the error below: QueueConfig.to_hash title when assigned to an org is formatted as expected Failure/Error: tasks.with_assignees.group("assignees.display_name").count(:all).each_pair.map do |option, count| label = self.class.format_option_label(option, count) self.class.filter_option_hash(option, label) end ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :appeal # ./app/models/queue_column.rb:110:in `assignee_options' * 🐛 Fix `spec/models/task_spec.rb` - `update_all` clears query cache In Rails 6.1.7.7, the method `ActiveRecord::Relation #update_all` will now clear any records cached by the calling relation. This was altering the behavior of `Task #cancel_task_and_child_subtasks` and causing the following test failure: Task#cancel_task_and_child_subtasks cancels all tasks and child subtasks Failure/Error: expect(second_level_tasks[0].versions.count).to eq(initial_versions + 2) expected: 3 got: 2 (compared using ==) # ./spec/models/task_spec.rb:368:in `block (3 levels) in ' To remedy, we will now cache the necessary Task records in an Array, which can be used for generating PaperTrail versions both before and after the `update_all`. * 🐛 Fix `spec/services/hearings/calendar_service_spec.rb` - template rendering error Addresses the following test failure: Hearings::CalendarService.confirmation_calendar_invite returns appropriate iCalendar event Failure/Error: expect(ical_event.description).to eq(expected_description) expected: "You're scheduled for a virtual hearing with a Veterans Law Judge of the Board of Veterans' Appeals.\...to reschedule or cancel your virtual hearing, contact us by email at bvahearingteamhotline@va.gov\n" got: # * 🐛 Fix YAML syntax error caused by whitespace in ENV var Address the following error, found during demo deployment: rake aborted! Cannot load database configuration: YAML syntax error occurred while parsing /caseflow/config/database.yml. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Error: (): could not find expected ':' while scanning a simple key at line 49 column 5 * ⬆️ Update `caseflow-commons` dependency to latest ref Removes `bourbon` and `neat` dependencies. * Add arrow keys fix * Pull filtered docs and doc navigation changes from laurenyj/APPEALS-58255 and add browser tab fix * Update Document Viewer document title logic so document title updates when user clicks Next or Previous buttons * Revert glamor styles changes * Update lodash imports to reduce size * Replace lodash/get with javascript optional chaining * Refactor previous/next doc logic without lodash * Remove unused import from ReaderFooter * Delete lodash logic in documentUtil and move ReaderToolbar functions to ReaderToolbar component * Update prototype-footer css --------- Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> Co-authored-by: Jeremy Croteau * Hotfix/appeals 57831 (#22949) * Add fix for the outer scroll bar in the DocumentList page * Remove unnecessary conditional for outer scroll bar logic in DocumentViewer --------- Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> * Hotfix/appeals 57827 57832 57836 (#22950) * Add fix for filtered docs bug * Revert component import paths to full path * Revert glamor styles changes * Update lodash imports to reduce size * Replace lodash/get with javascript optional chaining * Refactor previous/next doc logic without lodash * Remove unused import from ReaderFooter * Delete lodash logic in documentUtil and move ReaderToolbar functions to ReaderToolbar component * Update prototype-footer css * Adding regression tests for filtered document order. * Adding label to filtered icon. * Adding test for unfiltered content. * Moving test up to previous. * Fix merge conflict * Fix merge conflict * Fix empty arrow function lint error in ReaderFooter.test.js --------- Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> Co-authored-by: Brian Bommarito * Andre/appeals 57834 (#22951) * APPEALS-57834 Sidebar is defaulting to "close" instead of "open" * Add test to detect sidebar is open * Category test fixes/props cleanup. * WIP for readersidebar * finish up test to verify sidebar state persist to next document --------- Co-authored-by: Andre Pollard Co-authored-by: Davy Wentworth * Andre/appeals 57833 (#22878) * Update scaleFraction to allow single column on load * create scaling equation for large screens * Started test to detect single or two column loading on default * update css to breakpoint at 1654px for most docs * remove test as not changing code only css * Andre/appeals 57834 (#22858) * APPEALS-57834 Sidebar is defaulting to "close" instead of "open" * Add test to detect sidebar is open * Category test fixes/props cleanup. * WIP for readersidebar * finish up test to verify sidebar state persist to next document --------- Co-authored-by: Davy Wentworth * Removed Line 30 (#22953) Co-authored-by: Anusha Palliyil * Davywentwortht2it/appeals 57817 (#22954) * APPEALS-57817. Rework Reader prototype Page logic to minimize race conditions when scaling/scrolling. * APPEALS-57817. Minimal test coverage for Page component. --------- Co-authored-by: Davy Wentworth * Update documentation replacing master with main (#22960) * Update WINDOWS_11.md replacing master with main * References to master are changed to main where applicable * Add doc navigation test to ReaderFooter.test.js (#22969) * APPEALS-54874: Update PG and Ruby-oci8 gems (#22506) * update minimum version in gemfile and ran bundle install (#22503) * require ruby-oci8 in config/boot.rb to fix SIGV fault * APPEALS-36292: Conditionally render banner alerts based on which environment it is deployed on (#21367) * Conditionally render alerts based on which environment is deployed * setting Env alert banner sitewide * separate colors for different envs using the same alert type * lighter reds for prodtest alert so the text is more readable * give demo banner more contrast * adjust specific language for demo env * adjust logic to accept proper env var. Add style for preprod (yellow) * isolate env alert styles * remove duplicate 'demo' Alert * remove development env banner to fix tests * making conditional logic a little more bulletproof against 'production' * update CaseWorkerIndex snapshot * fix env var * update snapshot from hard 'uat' setting * conditionally render env alert on only the home page * sticky env header statuses * getting development alert banners to work * target correct frontend dependency for NavigationBar changes * connecting new frontend package with better prod exclusion logic * make the prodtest env badge a more readable color * cleaning up testing code * fix linting issues * Remove unnecessary space * new NavigationBar update * yarn install of new FE dependency * remove extra package.json * remove development env logic due to NODE_ENV bug * fixing broken logo link padding and colors * hardening logic to exclude other inputs * correcting operator logic * remove testing var * point caseflow-frontend to master * point at most recent caseflow-frontend master * linting issues fix * update Route and CaseWorker snapshot * update snapshot for process.env.DEPLOY_ENV --------- Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> * Merge feature/APPEALS-41559 into release/FY24Q4.6.0 (#22979) * Start feature work tracking PR with a TODO comment * Remove TODO comment for CodeClimate * SeanC/APPEALS-42315 | Create separate Remand table, model and seed data (#22220) * added migration file for adding type column to supplemental claims table * created stub for remand model * added type to supplemental claim factory and new remand trait * added remand sc's to vha seeds * updated migration name and fixed typo * Changed migration to inherit from Caseflow::Mirgration * schema changes * reverting schema * changed the setting of the type column * fixed schema and renamed migration file * fixed migration and schema * updated the remands factory and seeds * fixed comment in schema --------- Co-authored-by: Robert Travis Pierce * TYLERB/APPEALS-42455: Display new and migrated Remands in Decision Review Queue tabs (#22206) * Added support for remands to the business_line.rb model and the index.js util file that parses the decision review type filters for the decision review queue. * Updated the business line spec file, the decision reviews controller spec tests, and the reviews spec feature test to support remands. * Added a decision review polymorphic helper to DRY up some of the polymorphic appeal concerns. * Overrode the remand model association to request issues so active record joins will work correctly. * Added another helper for sti polymorphic relationships. Fixed filtering for STI decision review types for remands. Updated tests and added factories for testing. * Code climate fixes and test fixes. * Fixed more test failures. Removed the remand request issues relationship since it was incorrectly implemented. Added a scope to supplemental claims for remands. Fixed an issue where board grant effectuation tasks without request issues were missing from the new task type count method. * Changed the UNION ALL to a UNION in the new task_type_count method to prevent duplicate row counts. Updated tests and fixed an issue where the board grant effectuation tasks were being counted for pending task queries. * JHoang/APPEALS-42318 (#22270) * some preliminary remand & sc model updates * typo * some preliminary hlr checks? * undo seed change * decision issue and sc updates * attempt code climate fixes * another code climate fix * testing changes * useless variable.. * fix conditional in decision issue * fix undefined find_by method * attempt rspec fix * rspec fix attempt #2 * supplemental_claim_spec fix? * remove comma * code climate fix * added comment for transparency on remand creation, attempt fix for remands route * updated specs for decision_issue, hlr_spec, appeal_spec to account for remand creation type * fix lint error * fix lint error #2 --------- Co-authored-by: Jonathan Hoang * APPEALS-42458 (#22365) * disable edit issues link for remands * add banners explaining that remands are not editable also makes sure that remands are not editable on the edit issues page * TYLERB/APPEALS-44922: Update Generate task report page UI to include Remand in conditions (#22356) * Initial commit. Changed the DecisionReviewType condition filter to a searchable dropdown and added the remands option to it as well. Updated the report page filter parsing to work with the searchable dropdown instead of checkboxes for the decision review type condition filter. * Updated the ReportPage jest test and the report page feature test to work with the decision review type filter as a searchable dropdown instead of checkboxes. * SeanC/APPEALS-42317 | Migrate existing Supplemental Claim Remands to Remand records via script (#22359) * initial verison of update script * remanded migration file * updated sc spec with type * schema * fixed a few issues in the migration * small fix to migration file * moved the type column check into a seperate it block * fixed wrong column in migration file * created a new migration for creating the view * updated the backfill migration file --------- Co-authored-by: Brandon Lee Dorner * J hoang/appeals 44940 (#22428) * initial * some factory and spec updates * change history query update and spec fixes * code climate/lint fix * change @claim_type identifier * fix claim_history_event_spec.rb * test coverage for change history with Remand claim type * spec lint fixes * refactor claim_type filtering to account for remand subtype * business_line_spec fix * decision_reviews_controller_spec update for remand claim type * lint fixes for decision_reviews_controller_spec.rb * comment updates in business_line.rb * renamed mock data in change_history_reporter_spec * trigger GA * refactor claim type filter for less conditional handling * business line spec fix --------- Co-authored-by: Jonathan Hoang * Initial commit for the brakeman sql injection warning fixes. (#22598) * Updated the business line reporter class to properly display Remands as a seperate type than SupplementalClaims. Also added additional testing logic for the business line reporter for hlr and scs since it was only covering appeals. (#22664) Co-authored-by: Robert Travis Pierce * Fixed a bug where the frontend would error due to calling .toLowerCase() on a null or undefined value which could happen sometimes if the api filter options are in an incorrect state. (#22796) Co-authored-by: Robert Travis Pierce * Updated the remand migration files to work with the rails 6 upgrade and the changes to Caseflow::Migration. (#22825) * added fix for failing test in decision_reviews_controller_spec (#22829) --------- Co-authored-by: Robert Travis Pierce Co-authored-by: Sean Craig <110493538+seancva@users.noreply.github.com> Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> Co-authored-by: jonathanh-va <111081469+jonathanh-va@users.noreply.github.com> Co-authored-by: Jonathan Hoang Co-authored-by: Brandon Lee Dorner Co-authored-by: Clay Sheppard * APPEALS-50679 | Reassign Cases to Camo Change History (#22982) * Al/APPEALS-45335 (#22228) * added additional option to check box list * fixed some tests and code clean up * fixed more tests * fixed jest tests * changes per Pr feedback * added custom error display * fixed style for error message --------- Co-authored-by: Sean Craig * adding logic to bring in issue-modification request and updating the … (#22238) * adding logic to bring in issue-modification request and updating the csv logic * refactor logic to get pending status and null to request addition * pending status filter work fixing * adding withdrawal date and adding logic to display request issue components * getting edit of request rows and fixing cancelled data * fixing linter and fixing the query to make test happy * fixing specs failures * removing byebug * removing frontend changes, adding test, fixing the query to replace higherlevel review,adding new field in serializer * making code climate happy by doing some minor refactor * fixing the serializer spec failure * After merging APPEALS-45335, filter for Requested issues were broken, so refactored it and removed the unnecessary methood that i created * fixing code climate and spec was failing because of hard coded date * refactor the edit of request logic to take from version, refactored business line query to remove coalesce and fix test * fixing codeclimate, remove unnecessary field * refactor to make first comment appear first in edit of request * fixing linter issue, spec and refactor to fix the bug * fixing pending status filter issue and adding test for business line * fixing broken spec failure * adding user facilty * fixing the lead and lag function to prevent entry of pending status multiple times * fixing an issue in which in progress event was not being made * adding decision reason that was missed and fixing the event logic one more time * fixing the multiple issue modification request after decision has been made * refactor inprogress logic for approved data * fixing spec failure * fixing the first version logic to be more simpler and fixing name and adding spec * fixing more inprogess event * removing the disabled complexity and taking out the boolean condition to a separate method * Al/APPEALS-45334 Individual change history table which includes Reassign Cases to CAMO events (#22483) * added logic for claim history events table * fixed linting errors * fixing test failures * added change Pr feedback * PR feedback round 2 * changes from round 2 of PR feedback * Added logic event data versions and tests to cover more edgecases * changed RequestedIssueFragment * Updated the individual claim history spec file for test failures. Updated an attribute in the IndividualClaimHistory page that was using the wrong getter. Fixed a code climate linting error. * Removing unnecessary clear filter * Combined some of the individual claim history tests into one test so it will run more quickly. * fixing the withdrawal date versioning issue for different level changes * fixing the spec failure and linter --------- Co-authored-by: Brandon Lee Dorner Co-authored-by: = Co-authored-by: Prajwal Amatya * TYLERB/APPEALS-57367: change-history-updates (#22479) * Initial commit with new tests cases for issue modification requests for change history. * Added two cte queries to the business line change history query to properly work with a lead for the issue modification requests. Added new methods into the claim history event to work with this new lead and modified the existing issue modification in progress and pending event generation methods to better reflect the state of a claim that is going through the issue modficiation process. * Cleaned up unused code and todos. * Updated the claim history event create_imr_in_progress_status_event? with even more logic and edge cases. * Fixed a possible error when with a greater than operator could be used on a null value. * Fixed a few rspec tests. * Renamed a method and fixed a few code climate issues. * Fixed a bug in event status generation if there is a task with versions but no versions that change status. * Updated tests to work with new logic and remove a todo statement. * Fixed code complexity warnings. * Updated serializer spec to work with the bug fix. * Adjusted the pending method to prevent possible race conditions. * Fixed a bug where the incorrect decision event user would be shown for an issue modification request creation if the request has been decided when it should be the requestor as the event user. * Added the correct css ids to a few IMR change history events so that the filtering would work correctly. Also added some additional sql to properly filter based on css id and station id for requestor and decider joins. * Updated the IMR business line logic so that the decided at fields would always account for cancelled status since it uses updated at instead of decided at to determine when the IMR was closed. Fixed a bit of logic in the claim history event IMR in progress event generation to work another edge case. * Added in previous IMR edit versions for displaying it to the user. * Added a test for the change history service class to verify that request edit events have the correct attributes from the previous edits. * Fixed a typo for one of the table aliases in the business line query. * Fixed another typo where the facilities filter was using css id instead of station id from a copy/paste error. * adding changes for previous state array and some test modification * adding test for previous data * Fixed some outstanding test failures. * Fixed a linting error. Removed some duplicated code that happened when merging in the feature branch. Updated a factorybot method that was causing tests to fail. * Fixed a few code climate warnings. * Removed some puts statements in the individual claim history feature test. --------- Co-authored-by: Prajwal Amatya * TYLERB/APPEALS-58656: Change History CSV and Version Parsing fix (#22874) * Fixed a modification request claim history in progress event generation bug that could occur when 3-4 pending requests were all created at the same time and then later cancelled at the same time. Also fixed an issue count bug used when building the issue type filter due to additional duplicate rows being inserted into issue type count query due to the left join to the issue modification requests table. * Altered the version parsing for change history to work with commas in the version strings since that was causing parsing errors. Altered the business line query to use '|||' as a delimeter instead of comma and also changed the array_agg functions to string agg to avoid the '{}' array type casting that was happening between postgres and rails. Updated tests to work with this new parsing. * Updated vha seed data to have associations to intake data so those seeded claims can correctly generate change history. * TYLERB/CHANGE-HISTORY-YAML-QUOTE-FIX (#22910) * Made an update to the claim history parse versions method to work with double quotations after the array_agg to string_agg change. * Updated the tests to work with the new string agg and version parsing method. * Skip flakey tests These are possibly flakey now due to the rails upgrade. We can come back and fix these once the work has been confirmed in UAT. * Add rubocop disables for line length These should be removed when the skips are removed. * Updated the report_page feature test to reduce flakyness in github actions. Also updated the DownloadHelpers module. --------- Co-authored-by: Brandon Dorner * Fix testing error from merge conflict --------- Co-authored-by: almorbah <149511814+almorbah@users.noreply.github.com> Co-authored-by: Sean Craig Co-authored-by: Prajwal Amatya <122557351+pamatyatake2@users.noreply.github.com> Co-authored-by: = Co-authored-by: Prajwal Amatya Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> * Hotfix/appeals 57826.1 - Fix for alt+C placing comment and moving icon with arrow keys (#22985) * Fix for icon appearing on alt+c and arrow key support for moving icon. * Add test coverage * Andre/appeals 57833 1 (#22990) * Added test for checking the view layout based on the width of screen * Add in test for default column * Hotfix/appeals 37269 #2 (#22886) * adds script and tests for the script * adjust the way scheduled_for is created since the previous method was removed * refactor some of the script * rename test file to include _spec * push empty commit to rerun tests --------- Co-authored-by: Zackary Borges-Rowe Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> * APPEALS-59294: Hearings seed file is creating DistributionTasks for legacy appeals (#22983) * set parent task for legacy appeal hearing tasks to root task * fix whitespace issue * Fix alt+m hotkey for opening and closing sidebar (#22986) * Fix alt+m hotkey for opening and closing sidebar * Add fix for double search results bug and revert current page refactor * Hotfix/appeals 57826.1 - Fix for alt+C placing comment and moving icon with arrow keys (#22985) * Fix for icon appearing on alt+c and arrow key support for moving icon. * Add test coverage * Andre/appeals 57833 1 (#22990) * Added test for checking the view layout based on the width of screen * Add in test for default column * Fix failing tests in ReaderFooter.test.js --------- Co-authored-by: davywentwortht2it Co-authored-by: andrecolinone <146746795+andrecolinone@users.noreply.github.com> * Making document reads persist. (#22936) * Making document reads persist. * Adding test to insure api is called. * Change the store to use rootReducer. * Fixing linting issues. * Adding a test to make sure previous/next work. * Hotfix/appeals 57830 (#22952) * Persist zoom level between Reader documents * component tests * code cleanup --------- Co-authored-by: Daniel Mage * Add loading text in ReaderFooter (#23007) * fix for persisting reader sidebar state between documents * Fix failing tests; add factory for documents. * bug fix for sidebar toggle persisting across documents * remove commented out test to clean up pr * Metrics collection for Reader Prototype * Adding tests for both complete and incomplete document loads. * Deleting PdfDocument test. * Adding new test for metric logging. * bug fix unsaved comment removed on doc change * Fix for incorrect document title. * Fix for ctrl-F not working on Windows. * Ensure document title persists even when documents have the same type. * Update paths in test due to new file location. * Use ctrl as meta key for 'find next' behavior. (#23214) * Use ctrl as meta key for 'find next' behavior. * Bugfix * Fix text layer scale bug. (#23216) * remove code from bad merge --------- Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> Co-authored-by: Brian Bommarito Co-authored-by: Jeremy Croteau Co-authored-by: Andre Pollard Co-authored-by: Davy Wentworth Co-authored-by: andrecolinone <146746795+andrecolinone@users.noreply.github.com> Co-authored-by: Anusha Palliyil Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: seanrpa <155660052+seanrpa@users.noreply.github.com> Co-authored-by: Robert Travis Pierce Co-authored-by: Robert Travis Pierce Co-authored-by: Sean Craig <110493538+seancva@users.noreply.github.com> Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> Co-authored-by: jonathanh-va <111081469+jonathanh-va@users.noreply.github.com> Co-authored-by: Jonathan Hoang Co-authored-by: Brandon Lee Dorner Co-authored-by: Clay Sheppard Co-authored-by: almorbah <149511814+almorbah@users.noreply.github.com> Co-authored-by: Sean Craig Co-authored-by: Prajwal Amatya <122557351+pamatyatake2@users.noreply.github.com> Co-authored-by: = Co-authored-by: Prajwal Amatya Co-authored-by: zborgesva <145717155+zborgesva@users.noreply.github.com> Co-authored-by: Zackary Borges-Rowe Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> Co-authored-by: Daniel Mage * Feature/appeals 49670 49667 phase ii release tested (#23257) (#23269) (#23281) * APPEALS-36688- Build out Decision Review Created API route & Controller (#20569) * added skeleton for api route * removed duplicate code * removed development envs for api and moved to creating an ApiKey * removing any changes to development.rb * removed extra auth code * removed before action --------- * APPEALS-38232 -Build out Decision Review Created Event Failure API route & Controller (#20601) * APPEALS-38232 - renamed controller to AC req, added decision_review_created_error endpoint, added decision_review_created_error method, and added RSpec for new method * APPEALS-38232 - add comments to RSpec * Jonathan/appeals 36684 (#20516) * initial Events migration and model creation * created model for DecisionReviewCreatedEvent * updated comment with example * added spec for DRCE model * APPEALS-36684 created event_records migration and added polymorphic associations to specific models, and added rspec for the event_record model * APPEALS-36684 - Updated RSpec tests and updated variables and got unhappy path to pass * cleaned lint * saving DRCE spec changes * fixed spec test * changed has_many to has_one * updated event model spec * added validation for ER poly associations * changed association to has_one * added new method and updated tests * added foreign key after running checks * some PR comment changes * refactored methods in EventConcern --------- * added migration, scopes and specs for events (#20707) * fixed migration to update existing events table * rollbacked to fix schema * schema fixes --------- * Jr/APPEALS-38926 (#20714) * APPEALS-38926 - Created DecisionReviewCreatedError Service Class, added logic for handling service error, updated DecisonReviewCreatedController, and DecisionReviewCreatedController spec, with the updated service logic * APPEALS-38926- created RSpec Test for DecisionReviewCreatedError Service Class and edited the Rails.logger for the service class * APPEALS-38926 Added new info column to update transaction and added it to the RSpec test * APPEALS-38926 - added comments to the Service Class * APPEALS-38926 - Code Changes from TL Code Review, added rescues and fails * APPEALS-38926 - fixed lint * Jonathan/appeals 36689 (#20671) * created new service class * add rspec test cases * service class methods * controller action and spec * controller update * CC fixes * removed accidental line * changed to find_or_create_by * reworked error for redis lock * additional rspec for controller * fixed test * rspec fix * delete lock key afterwards * moved Event creation back into lock block --------- * APPEALS-39663 Create CreateUserOnEvent service class and add logic to create user if needed (#20838) * added user creation class & test * removed extra lines * add comment to class * added context for args from avro --------- * Jr/APPEALS-39664 (#20898) * APPEALS-39664 - Created updated_vacols_on_optin module class, and removed extra private * APPEALS-39664 - created UpdatedVacolsOnOptin module, RSpec file, as well as sudo code for SOC and SSOC optin check in main service class DecisionReviewCreated * APPEALS-39664 - Created RSpec Test - PASS, Updated method name. * APPEALS-39664 - Added Error Handling to Sub Service Class * APPEALS-39664 - Removed un-needed comments * APPEALS-39664 - added include for the module UpdateVacolsOnOptin inside decision_review_created service * APPEALS-39664 - Added Custom Error , and updated all .perform! to .process! * APPEALS-39664 - Updated RSpec Test to reflect changes - all pass * Updated comment for decision_review_created Service Class * Will/appeals 36691 (#20909) * Created attribute for failed claims on event and displaying failed claim * passing all failed events back to serializer * added controller tests for failed_claims and added class method for finding claims on events * renamed failed_claims to claim_errors --------- * JR/APPEALS-40954 Create CreateIntake service class and add logic to create Intake (#20967) * APPEALS-40954 - Added Sudo Code for CreateIntake Logic * APPEALS-40954 - added logic to CreateIntake module * APPEALS-40954 - added Create Intake spec with error handling - All pass * APPEALS-40954 - Added CreateIntake Module to DecisionReviewCreated Main Service * APPEALS-40954 - updated folder name and namespace * APPEALS-40954 - Updated RSpec to match folder * APPEALS-40954- Upated decision review created servie include to match folder * Fixed failing test due to folder structure change * Jonathan/appeals 40950 (#20965) * Added new veteran creation module * saving test changes * rspec * fixed datetime assign * renamed var --------- * APPEALS-40950 - update var veteran to vbms_veteran (#21050) * APPEALS-40950 - update var veteran to vbms_veteran * updated private method in controller to match happy path params dcr to drc * Create CreateClaimantOnEvent service class and add logic to create claimant if needed (#21044) * created service class and basic unit tests * added conditionals for veteran claimants and will create veteran claimant now * modified create claimant to use eventing data * change to a class * creating claimant correctly * test fixes * removed old comments * fixed type for veteran_is_not_claimant * changed specs to match pulling out hash params and removed event.reference id * moved back to dot notation * updated comments * fixed create claimant issue * updated process to use bang method and returning claimant --------- * APPEALS-41968 Modify Issues Endpoints & Update Metric Service Logic (#21108) * moved metricsService call to top of method * modified rspec case * moved metric logging spec case higher up --------- * Resolved merge conflicts while merging master into feature/APPEALS-28… (#21167) * Jonathan/APPEALS-41957 (#21171) * added interface + parser class * edited veteran parse methods * renamed var to "payload" * started refactor * more refactor + rspec * added EPE attr * dateTime conversions * added class comments * rubocop lint changes * fixed test case --------- * Jr/appeals 41931 (#21192) * APPEALS-41931 - Created create_ep_establishment file and class * APPEALS-41931 - added process! method that creates epe from payload * APPEALS-41931 - added logic for EventRecord being created and error handling * APPEALS-41931 - added comments to process method * APPEALS-41931 - set up rspec test * APPEALS-41931 - removed lint * APPEALS-41931-created Rspec and Test Pass * APPEALS-41931 - cleaned lint and added error test 100% code coverage * APPEALS-41931- fixed lint * APPEALS-41931- fixed lint and fixed %100 code cov * APPEALS-41931 - cleaned up lint and warn for Service Class * APPEALS-41931 - Added CreateEpEstablishment.process! to the decision_review_created Parent Service Class * APPEALS-41931 - refactor code to implement new parser * fixed linting issues * APPEALS-41931 - Updated Comments for CreateEpEstablishment * APPEALS-41931 - moved logical date int to parser and refactored code in Class and RSepc * edits * Will/appeals 41929 (#21205) * added an error and commented out call for createclaim * added new parser logic * merge request changes * fixed rubocop issues and added checks for claim review attributes --------- * APPEALS-42631- Create Rspec for Parser with sample payload method, and add additional parser methods (#21294) * APPEALS-42631 - create example.json * APPEALS-42631 - implemented example_response and load_example method and works as expected * APPEALS-42631 - created RSpec for DecisionReviewCreatedParser * APPEALS-42631 - Refactored parser and Added RSpec for current praser * APPEALS-42631 - added methods to parser for intake, claimant, and claim_review and added matching rspec for new methods * APPEALS-42631 - updated code per TL Comments * APPEALS-421631- added additional comments and fixing lint * APPEALS-41934 (#21251) * initial commit * implementation & error * renamed method * rspec * saving refactor progress * finished refactoring class * added comment * minor parser/rspec updates --------- * added RI parser methods to rspec (#21322) * added RI parser methods to rspec * updated config for Consumer --------- * Update DecisionReviewCreated to make all calls and link all intake records (#21334) * updated decision review created to uncomment actions and updated specs * remove binding.pry * removed comments * fixed a bunch of broken tests * fixed last broken tests * fixed params for methods and specs --------- * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * Jr/ama controller refactor (#21365) * fixed test and refactored controller * saving changes * init commit passing all the way to request_issues * all pass and functions as expected * lint * lint * updated initializer to use deep_symbolize_keys * updated headers to be more dry * added missing Intake attributes * fixed failing tests * updated createIntake test * fixed veteran rspec * fixed RI test * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * updated intake * modified intake --------- * redo init commit * updated imp. logic * attorney widget fix * Create end to end , happy path rspec's for Decision Created event Feature (#21395) * updated more tests and fixed user creation * added in person creation * corrected headers and fixed broken tests * added type * fixed failing spec * creating event when person is created * updated spec to account for both events being created * ignored long lines for spec file * linting fixes * fixed more linting errors/ ignored long lines --------- * Jonathan/appeals 43589 (#21397) * saving * saving user class error progress * error handling for user creation * updated error handling/raises * validator methods * validations * update logical date converter * changed veteran service class to use file_number * added fields to hlr * updated epe data * removed byebug * fixed typos * Edit 5: adding detail_id to Intake * EDIT 6: add claimant_participant_id to epe * Edit 8: Intake is correctly linked to veteran * Edit 7: RI additions * fixed spacing * fixed typo * saving rspec changes * rspec updates * added datetime conversion for person dob * fixed rspec * remove unused methods --------- * feature/APPEALS-35707-29633-29632 (uat) (#21435) * 🔀 Squash merge AlecK/APPEALS-35707 - Replace `database_cleaner` with `database_cleaner-active_record` * 🔀 Squash merge jcroteau/APPEALS-29632-fix-deprecation-action-view-base-instances * 🔀 Squash merge jcroteau/APPEALS-29633-fix-deprecation-warning-active_record-result-to_hash * awillis/APPEALS-45152 (#21506) * APPEALS-45152 Updated the logic in create_claimant_on_event.rb to always generate a claimant record. Updated RSPEC. * APPEALS-45152 Cleaned up RSPEC. * APPEALS-45152 Fixed Failing RSPEC within decision_review_created_spec.rb * APPEALS-45152 Updated RSPEC within decision_review_created_spec.rb with addtional checks. * APPEALS-44319 (#21449) (#21541) * added logic for legacy issues in DRC * more legacy logic * updated rspec context lang * error cov --------- * Konstantin/APPEALS-45175 (#21517) * logical_date_converter re-written to work with yyyymmdd numbers from json payload * comments removed * comments removed2 * method rewitten * json example fixed * date in scenario_b_spec.rb replaced by valid ones, additional check for empty string parameter added to the logical_date_converter * removed filter for non rating request issue dropdown (#21480) * removed filter for non rating request issue dropdown * removed excluded types as we don't need to check this anymore * fixed broken test * added category back * no longer need test to check for missing categories since we're returning them all. --------- * Column added to RequestIssues table (#21578) * added migration and simple spec to test * fixed schema deleted issues * updated spec to verify data being set * error was no longer trigger from update. removed check and passed in correct variable --------- * added bgs_source to parser, controller and serializer along with specs (#21621) * Konstantin/appeals 45180 (#21591) * bug is fixed, condition added to DecisionReviewCreatedController * rspec test added * json message updated * comment added * Update decision_review_created_controller.rb comment updated --------- * cmartine/APPEALS-42621 (#21599) * Add remove_comp_and_pen_intake feature toggle to intakes controller * Add new compensation and pension removal message to COPY.json * Disable comp and pen radio buttons if remove_comp_and_pen_intake toggle is on * Frontend test updates * Add tooltip checks for review page integration tests * fix frontend test lint errors * frontend tests changes * Fix linter errors --------- * nrithner/APPEALS-45263 (#21617) * Add remove_comp_and_pen_intake feature toggle to intakes controller * Add new compensation and pension removal message to COPY.json * Disable comp and pen radio buttons if remove_comp_and_pen_intake toggle is on * Frontend test updates * Add tooltip checks for review page integration tests * fix frontend test lint errors * frontend tests changes * Add banner for non-vha * Add banner for VHA employee * Refactor conditionals * Refactor logic for banners to display * Add dangerouslySetHtml to banner * Add test for no VHA employee with vha_claim_review_establishment disabled. * Add VHA employee scenario to display REMOVE_INTAKE_COMP_AND_PEN * Complete non vha user scenarios * Complete VHA employee scenarios * Remove comments * Fix linter errors * Remove spaces preceding question marks * Correct linting errors --------- * Konstantin/appeals 45149 (#21644) * RequestIssueSerializer attributes added * development_item_reference_id, same_office, legacy_opt_in_approved attributes were added * merge conflicts resolved * merge conflict resolved * fixed * all datapoints were added * empty line removed * comment removed * refactored * app/controllers/api/docs/v3/ama_issues.yaml updated with new attributes * example of request issue updated app/controllers/api/docs/v3/ama_issues.yaml * null to nil edited in example ama_issues.yaml * example ama_issues.yaml updated * example ama_issues.yaml updated2 * linters fixed * linters fixed2 * linters are fixed * APPEALS-45883 - Remove End Product Establishment records within the Event Records table in Caseflow (#21745) * APPEALS-45883 - changes to remove the creating of Event Records for epe * APPEALS-45883 - remove removed event params from method calls * APPEALS-45883 removed Event Double * APPEALS-48553 updated event_record spec * removed eventing from create claim review (#21718) * removed eventing from create claim review * checking why file is not being found for event_record * removed event var and require dependency * spec fixes * updated specs * fixed revert --------- * Nrithner/appeals 45913 (#21716) * Convert from milliseconds to datetime * Add expection to have datetime after parsing * Fix linting issues * APPEALS - 45914 & 45915 (#21744) * Decision review parser and spec changes * remove byebug * Added db seed and testing (#21284) * Added db seed and testing * Change ApiKey seed name to ConsumerApiKey, and associated changes --------- * re-merge Cmartine/appeals 46861 (#21855) * Edit DecisionReviewCreatedController to respond with ok status if event exists and is completed, edit test * Move checking if event exists and is completed into event model, edit tests * Fix linter errors --------- * Nrithner/appeals 46860 (#21856) * Set event info back to default default state for info is an empty json object * Correct linting errors * Test happy path with rspec * Refactor rspec example * Converte module to class * Remove module import --------- * Jonathan/appeals 45899/45878 (#21714) * add "from event" value to Store * updated setup guide for M3 * added new badges * added jest * storybook file * additional story * bugfix * snap update * Konstantin/appeals 46175 (#21768) * draft processor method done * process_nonrating method moved to DecisionReviewCreated service * json example added, process_nonrating method updated * 4 rspec test added * useless json example removed * commented lines removed * condition added * condition added. * test updated * first condition moved out of the method * process_nonrating method call moved down * 1 comment removed, 1 added * process nonrating method rafactored, logic transferred into the parser * method refactored, rspec tests updated * rspec test slightly refactored --------- * Edit Issues screen update (#21907) * Edit Issues screen update * relocated row * updated capybara test * updated capybara to check for new added text * cmartine/APPEALS-46905 (#21884) * Remove cache updating call for veteran when veteran is being created for an event * Change seeds api_key.rb to consumer_api_key.rb --------- * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example * Change edited_by_css_id to return users css_id * Konstantin/appeals 48306 (#21998) * ama_eventing_enabled feature check added * toggle renamed * rspec tests added * 2 comments added * toggle renamed, logic trasferred into controller, tests updated * empty line removed --------- * 930 code updates * remove byebug * Change edited_by_css_id to return users css_id (#22062) * Nrithner/appeals 49245 (#22107) * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example --------- * fix extra commas from merge * APPEALS-51223: Disable ClaimantValidator for DecisionReviewCreatedEvents (#22132) * APPEALS-51223 refactored the claimant_validator class method claimant_details_required? to consider the conditional statement * APPEALS-51223 updated Rspec to confirm claimant address_line_1 is nil and error is empty * APPEALS-51223 made changes to to claimant_validator to have all Unit test passing * APPEALS-51223 added suggested changes * Revert "fix merge conflict with AMA" This reverts commit 6775316c85e2c3d8cd94c1baae6d29d75503163d, reversing changes made to 66cf311d1ddb18d11137b173b94ecbafaa022e2b. * added comments * Delete docker-bin/build.sh, I didnt make these changes * Revert "Delete docker-bin/build.sh, I didnt make these changes" This reverts commit 03a3830c5df26e9d4830fcf1d66ea77af4703346. --------- * Jonathan/appeals 51926 (#22215) * Hide UI changes behind featureFlag * fixed lint spacing warnings * code climate spec fix (#22211) * code climate spec fix * up argument count * created a parser helper for reusable methods * reset codeclimate back to original and added issue parser * modified methods to take in a hash instead of multiple params * reduced create params down to 3 params * added space inside * swapped to safe navigator for intake check * fixed spec * fixed linting issue * fixed failing spec * fixed merge conflicts with code changes for helper --------- * Konstantin/appeals 52321 (#22363) * utc conversion method added in app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date method updated in RequestIssue model, updated contested_rating_issue_profile_date in DecisionIssue model * decision issue updated, tests added * tests added, converters improve app/models/decision_issue.rb app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date restored in request issue model * uneccessary logic removed * 3 more tests added * test added, error handling aaded in format_rating_profile_date method * Update decision_issue.rb * Update request_issue.rb * Appeals 52317 (#22321) * wip * added the parser to skeleton * removed commented code * added in parser logic * added dru_error route, error handling and params * added rspecs and fixed routing for update error to be a post * added empty spec * updated dru params to match intake json * rubocop fixes * more rubocop fixes * fixed routing to post now and updated specs --------- * fixed logic for AMA issues API serializer attributes (#22358) * fixed logic for added_by fields * methods for fetching removed_by user * withdrawn_by methods * methods for edited_by & instance vars * more logic for added_by methods * fixed failing tests * rspec updates * rspec for RIU addition * small changes * fix test failure * Initial commit (#22494) * Konstantin/appeals 52318 (#22528) * very loose prototype of class created * hopeless variant removed * updated parser per new design and updated specs * request_issues_updated_event updated * some improvments added * unused methods removed, some methods updated * after issues added * tests added * edited issues method returned, invalid tests removed * tests updated * 9 working tests added, RequestIssuesUpdateEvent variable instanciated * tests improved, attribute writers added * more test cases added, error handling in process! method added * added new version of parser and tests to it from Will's PR * uneccesary overrided classes removed * process_job call removed * process_job! restored, 3 more tests added * validate before perfom removed * linter issues addressed * linter issues addressed2 --------- * Bug Fix for APPEALS-44115 (#22538) * running ci * testing category * ci with conditional * set back to false * reverted to original spec * running ci * updated intake helpers to account for using none of these match option * ignored metrics abc size * removed from helper * removed comments * updated spec to check on each category * removed single quotes * fixed a flakey test that was failing on a duplicate key * linting fix * pushed up feature flag * lint clean up * swapped to different featureflag * fixing spec feature toggle param * added feature flags back * checking if label is there * flipped feature test to correct order --------- * cmartine/APPEALS-50366 (#22618) * Create EditDisabledBanner * Add banner displaying logic and pageroute to IntakeEditFrame * Add IntakeEditFrame jest tests for removeCompAndPenIntake feature toggle * Edit displayEditDisabledBanner to use the disableEditingForCompensationAndPension method * Re-add intakeEditFrame jest tests, re-add and move testProps * Refactor intakeEditFrame jest tests * Edit change testProps into static variable * Add another test for intakeEditFrame * Change testProps back into function return * Change comp and pen removal text * Change Intake Remove string back, add Intake Edit Disabled string, changed banner and tests to use this * Ksarlett/appeals 50368 - Disable the remove issue, withdraw issue & edit issue options from drop down (#22637) * ksarlett/APPEALS-50368 - Disable dropdown * rermove console.log * spec refactor * spec fix * spec refactor * change test name --------- * nrithner/APPEALS 50367 (#22640) * Add disabled logic to add issue and edit claim label buttons * Add test buttons are disabled When benefit is compensation * Test buttons are disabled in supplemental_claim Add issue and Edit claim label are disabled when benefits pension or compensation are present * Test buttons are disabled in HLR Add issue and Edit claim label are disabled when benefits pension or compensation are present * Refactor to remove redundancy * Test refactoring --------- * APPEALS-53923 (#22727) * new service class & error message * rspec * Update update_informal_conference_spec.rb * Konstantin/appeals 52982 (#22715) * eligibility-related methods added * methods fixed, date conversion for closed_at added * decision_review_updated_issue_parser created, json example updated, request_issues_update_event updated * tests for parser fixed after json example update * new attribute vbms_id added to request_issues table, decision_review_updated_issue_parser update respectively * request_issues_update_event attr_writers added, decision_review_created_issue_parser class name update and call of that parser updated * veteran removed from DecisionReviewUpdatedParser initilizer, methods marked for potential deletion * decision_review_updated_issue_parser updated and test added for it, comments added to request_issues_update_event * not necessary methods removed from parser, tests for them commented out for now * attributes_from_intake_data method overrided * :id replced by vbms_id, json example, parsers and test for it updated * parsers added, test updated * css_id, station and detail_type commented from the parser * parser cleaned, test for parser fixed, request_issues_update_event cleaned * process_issues! overriden in app/models/request_issues_update_event.rb, linter offenses fixed, testtypo fixed * vbms_id renamed to reference_id * comment typo fixed * Create Audit Tables for SC, HLR, Request Issues Appeals 55403 (#22689) * migration added * schema updated correctly * added audit service and call to update evenr records * logic for update type * fixed typo in audit file and added spec * added insert * moved Insert DRUA to create request issues for created ones * updated event record to take in update type * passing in correct info attribute now * disabled unused params, can enable once we use them * remove << Co-authored-by: TuckerRose Co-authored-by: Enrilo Ugalde <71367882+Jruuuu@users.noreply.github.com> Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde Co-authored-by: isaiahsaucedo Co-authored-by: Calvin Co-authored-by: Craig Reese Co-authored-by: Jeremy Croteau Co-authored-by: (Jeffrey) Aaron Willis <98484567+Aaron-Willis@users.noreply.github.com> Co-authored-by: Konstantin Shevtsov <166068160+KonstantinShevtsov@users.noreply.github.com> Co-authored-by: TuckerRose Co-authored-by: Chris-Martine <135330019+Chris-Martine@users.noreply.github.com> Co-authored-by: kshiflett88 Co-authored-by: root Co-authored-by: nicorithner-bah <148365039+nicorithner-bah@users.noreply.github.com> Co-authored-by: Chris-Martine Co-authored-by: Matt Ray Co-authored-by: Nico Rithner Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> Co-authored-by: Jonathan Tsang * APPEALS-61414: Judge misspelled on Case Distribution Controls page (#23276) * Feature/appeals 49670 49667 phase ii release tested (#23257) (#23269) * APPEALS-36688- Build out Decision Review Created API route & Controller (#20569) * added skeleton for api route * removed duplicate code * removed development envs for api and moved to creating an ApiKey * removing any changes to development.rb * removed extra auth code * removed before action --------- * APPEALS-38232 -Build out Decision Review Created Event Failure API route & Controller (#20601) * APPEALS-38232 - renamed controller to AC req, added decision_review_created_error endpoint, added decision_review_created_error method, and added RSpec for new method * APPEALS-38232 - add comments to RSpec * Jonathan/appeals 36684 (#20516) * initial Events migration and model creation * created model for DecisionReviewCreatedEvent * updated comment with example * added spec for DRCE model * APPEALS-36684 created event_records migration and added polymorphic associations to specific models, and added rspec for the event_record model * APPEALS-36684 - Updated RSpec tests and updated variables and got unhappy path to pass * cleaned lint * saving DRCE spec changes * fixed spec test * changed has_many to has_one * updated event model spec * added validation for ER poly associations * changed association to has_one * added new method and updated tests * added foreign key after running checks * some PR comment changes * refactored methods in EventConcern --------- * added migration, scopes and specs for events (#20707) * fixed migration to update existing events table * rollbacked to fix schema * schema fixes --------- * Jr/APPEALS-38926 (#20714) * APPEALS-38926 - Created DecisionReviewCreatedError Service Class, added logic for handling service error, updated DecisonReviewCreatedController, and DecisionReviewCreatedController spec, with the updated service logic * APPEALS-38926- created RSpec Test for DecisionReviewCreatedError Service Class and edited the Rails.logger for the service class * APPEALS-38926 Added new info column to update transaction and added it to the RSpec test * APPEALS-38926 - added comments to the Service Class * APPEALS-38926 - Code Changes from TL Code Review, added rescues and fails * APPEALS-38926 - fixed lint * Jonathan/appeals 36689 (#20671) * created new service class * add rspec test cases * service class methods * controller action and spec * controller update * CC fixes * removed accidental line * changed to find_or_create_by * reworked error for redis lock * additional rspec for controller * fixed test * rspec fix * delete lock key afterwards * moved Event creation back into lock block --------- * APPEALS-39663 Create CreateUserOnEvent service class and add logic to create user if needed (#20838) * added user creation class & test * removed extra lines * add comment to class * added context for args from avro --------- * Jr/APPEALS-39664 (#20898) * APPEALS-39664 - Created updated_vacols_on_optin module class, and removed extra private * APPEALS-39664 - created UpdatedVacolsOnOptin module, RSpec file, as well as sudo code for SOC and SSOC optin check in main service class DecisionReviewCreated * APPEALS-39664 - Created RSpec Test - PASS, Updated method name. * APPEALS-39664 - Added Error Handling to Sub Service Class * APPEALS-39664 - Removed un-needed comments * APPEALS-39664 - added include for the module UpdateVacolsOnOptin inside decision_review_created service * APPEALS-39664 - Added Custom Error , and updated all .perform! to .process! * APPEALS-39664 - Updated RSpec Test to reflect changes - all pass * Updated comment for decision_review_created Service Class * Will/appeals 36691 (#20909) * Created attribute for failed claims on event and displaying failed claim * passing all failed events back to serializer * added controller tests for failed_claims and added class method for finding claims on events * renamed failed_claims to claim_errors --------- * JR/APPEALS-40954 Create CreateIntake service class and add logic to create Intake (#20967) * APPEALS-40954 - Added Sudo Code for CreateIntake Logic * APPEALS-40954 - added logic to CreateIntake module * APPEALS-40954 - added Create Intake spec with error handling - All pass * APPEALS-40954 - Added CreateIntake Module to DecisionReviewCreated Main Service * APPEALS-40954 - updated folder name and namespace * APPEALS-40954 - Updated RSpec to match folder * APPEALS-40954- Upated decision review created servie include to match folder * Fixed failing test due to folder structure change * Jonathan/appeals 40950 (#20965) * Added new veteran creation module * saving test changes * rspec * fixed datetime assign * renamed var --------- * APPEALS-40950 - update var veteran to vbms_veteran (#21050) * APPEALS-40950 - update var veteran to vbms_veteran * updated private method in controller to match happy path params dcr to drc * Create CreateClaimantOnEvent service class and add logic to create claimant if needed (#21044) * created service class and basic unit tests * added conditionals for veteran claimants and will create veteran claimant now * modified create claimant to use eventing data * change to a class * creating claimant correctly * test fixes * removed old comments * fixed type for veteran_is_not_claimant * changed specs to match pulling out hash params and removed event.reference id * moved back to dot notation * updated comments * fixed create claimant issue * updated process to use bang method and returning claimant --------- * APPEALS-41968 Modify Issues Endpoints & Update Metric Service Logic (#21108) * moved metricsService call to top of method * modified rspec case * moved metric logging spec case higher up --------- * Resolved merge conflicts while merging master into feature/APPEALS-28… (#21167) * Jonathan/APPEALS-41957 (#21171) * added interface + parser class * edited veteran parse methods * renamed var to "payload" * started refactor * more refactor + rspec * added EPE attr * dateTime conversions * added class comments * rubocop lint changes * fixed test case --------- * Jr/appeals 41931 (#21192) * APPEALS-41931 - Created create_ep_establishment file and class * APPEALS-41931 - added process! method that creates epe from payload * APPEALS-41931 - added logic for EventRecord being created and error handling * APPEALS-41931 - added comments to process method * APPEALS-41931 - set up rspec test * APPEALS-41931 - removed lint * APPEALS-41931-created Rspec and Test Pass * APPEALS-41931 - cleaned lint and added error test 100% code coverage * APPEALS-41931- fixed lint * APPEALS-41931- fixed lint and fixed %100 code cov * APPEALS-41931 - cleaned up lint and warn for Service Class * APPEALS-41931 - Added CreateEpEstablishment.process! to the decision_review_created Parent Service Class * APPEALS-41931 - refactor code to implement new parser * fixed linting issues * APPEALS-41931 - Updated Comments for CreateEpEstablishment * APPEALS-41931 - moved logical date int to parser and refactored code in Class and RSepc * edits * Will/appeals 41929 (#21205) * added an error and commented out call for createclaim * added new parser logic * merge request changes * fixed rubocop issues and added checks for claim review attributes --------- * APPEALS-42631- Create Rspec for Parser with sample payload method, and add additional parser methods (#21294) * APPEALS-42631 - create example.json * APPEALS-42631 - implemented example_response and load_example method and works as expected * APPEALS-42631 - created RSpec for DecisionReviewCreatedParser * APPEALS-42631 - Refactored parser and Added RSpec for current praser * APPEALS-42631 - added methods to parser for intake, claimant, and claim_review and added matching rspec for new methods * APPEALS-42631 - updated code per TL Comments * APPEALS-421631- added additional comments and fixing lint * APPEALS-41934 (#21251) * initial commit * implementation & error * renamed method * rspec * saving refactor progress * finished refactoring class * added comment * minor parser/rspec updates --------- * added RI parser methods to rspec (#21322) * added RI parser methods to rspec * updated config for Consumer --------- * Update DecisionReviewCreated to make all calls and link all intake records (#21334) * updated decision review created to uncomment actions and updated specs * remove binding.pry * removed comments * fixed a bunch of broken tests * fixed last broken tests * fixed params for methods and specs --------- * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * Jr/ama controller refactor (#21365) * fixed test and refactored controller * saving changes * init commit passing all the way to request_issues * all pass and functions as expected * lint * lint * updated initializer to use deep_symbolize_keys * updated headers to be more dry * added missing Intake attributes * fixed failing tests * updated createIntake test * fixed veteran rspec * fixed RI test * APPEALS-43446- Change name of BackfillRecord polymorphic model to EventedRecord (#21382) * APPEALS-43446 - renamed columns for backfill record to evented record migrate and rollback work as intended * APPEALS-43446 replaced all backfill_record with evented_record within the models associations and updated Rspec tests * APPEALS-43446 fixed error message * updated intake * modified intake --------- * redo init commit * updated imp. logic * attorney widget fix * Create end to end , happy path rspec's for Decision Created event Feature (#21395) * updated more tests and fixed user creation * added in person creation * corrected headers and fixed broken tests * added type * fixed failing spec * creating event when person is created * updated spec to account for both events being created * ignored long lines for spec file * linting fixes * fixed more linting errors/ ignored long lines --------- * Jonathan/appeals 43589 (#21397) * saving * saving user class error progress * error handling for user creation * updated error handling/raises * validator methods * validations * update logical date converter * changed veteran service class to use file_number * added fields to hlr * updated epe data * removed byebug * fixed typos * Edit 5: adding detail_id to Intake * EDIT 6: add claimant_participant_id to epe * Edit 8: Intake is correctly linked to veteran * Edit 7: RI additions * fixed spacing * fixed typo * saving rspec changes * rspec updates * added datetime conversion for person dob * fixed rspec * remove unused methods --------- * feature/APPEALS-35707-29633-29632 (uat) (#21435) * 🔀 Squash merge AlecK/APPEALS-35707 - Replace `database_cleaner` with `database_cleaner-active_record` * 🔀 Squash merge jcroteau/APPEALS-29632-fix-deprecation-action-view-base-instances * 🔀 Squash merge jcroteau/APPEALS-29633-fix-deprecation-warning-active_record-result-to_hash * awillis/APPEALS-45152 (#21506) * APPEALS-45152 Updated the logic in create_claimant_on_event.rb to always generate a claimant record. Updated RSPEC. * APPEALS-45152 Cleaned up RSPEC. * APPEALS-45152 Fixed Failing RSPEC within decision_review_created_spec.rb * APPEALS-45152 Updated RSPEC within decision_review_created_spec.rb with addtional checks. * APPEALS-44319 (#21449) (#21541) * added logic for legacy issues in DRC * more legacy logic * updated rspec context lang * error cov --------- * Konstantin/APPEALS-45175 (#21517) * logical_date_converter re-written to work with yyyymmdd numbers from json payload * comments removed * comments removed2 * method rewitten * json example fixed * date in scenario_b_spec.rb replaced by valid ones, additional check for empty string parameter added to the logical_date_converter * removed filter for non rating request issue dropdown (#21480) * removed filter for non rating request issue dropdown * removed excluded types as we don't need to check this anymore * fixed broken test * added category back * no longer need test to check for missing categories since we're returning them all. --------- * Column added to RequestIssues table (#21578) * added migration and simple spec to test * fixed schema deleted issues * updated spec to verify data being set * error was no longer trigger from update. removed check and passed in correct variable --------- * added bgs_source to parser, controller and serializer along with specs (#21621) * Konstantin/appeals 45180 (#21591) * bug is fixed, condition added to DecisionReviewCreatedController * rspec test added * json message updated * comment added * Update decision_review_created_controller.rb comment updated --------- * cmartine/APPEALS-42621 (#21599) * Add remove_comp_and_pen_intake feature toggle to intakes controller * Add new compensation and pension removal message to COPY.json * Disable comp and pen radio buttons if remove_comp_and_pen_intake toggle is on * Frontend test updates * Add tooltip checks for review page integration tests * fix frontend test lint errors * frontend tests changes * Fix linter errors --------- * nrithner/APPEALS-45263 (#21617) * Add remove_comp_and_pen_intake feature toggle to intakes controller * Add new compensation and pension removal message to COPY.json * Disable comp and pen radio buttons if remove_comp_and_pen_intake toggle is on * Frontend test updates * Add tooltip checks for review page integration tests * fix frontend test lint errors * frontend tests changes * Add banner for non-vha * Add banner for VHA employee * Refactor conditionals * Refactor logic for banners to display * Add dangerouslySetHtml to banner * Add test for no VHA employee with vha_claim_review_establishment disabled. * Add VHA employee scenario to display REMOVE_INTAKE_COMP_AND_PEN * Complete non vha user scenarios * Complete VHA employee scenarios * Remove comments * Fix linter errors * Remove spaces preceding question marks * Correct linting errors --------- * Konstantin/appeals 45149 (#21644) * RequestIssueSerializer attributes added * development_item_reference_id, same_office, legacy_opt_in_approved attributes were added * merge conflicts resolved * merge conflict resolved * fixed * all datapoints were added * empty line removed * comment removed * refactored * app/controllers/api/docs/v3/ama_issues.yaml updated with new attributes * example of request issue updated app/controllers/api/docs/v3/ama_issues.yaml * null to nil edited in example ama_issues.yaml * example ama_issues.yaml updated * example ama_issues.yaml updated2 * linters fixed * linters fixed2 * linters are fixed * APPEALS-45883 - Remove End Product Establishment records within the Event Records table in Caseflow (#21745) * APPEALS-45883 - changes to remove the creating of Event Records for epe * APPEALS-45883 - remove removed event params from method calls * APPEALS-45883 removed Event Double * APPEALS-48553 updated event_record spec * removed eventing from create claim review (#21718) * removed eventing from create claim review * checking why file is not being found for event_record * removed event var and require dependency * spec fixes * updated specs * fixed revert --------- * Nrithner/appeals 45913 (#21716) * Convert from milliseconds to datetime * Add expection to have datetime after parsing * Fix linting issues * APPEALS - 45914 & 45915 (#21744) * Decision review parser and spec changes * remove byebug * Added db seed and testing (#21284) * Added db seed and testing * Change ApiKey seed name to ConsumerApiKey, and associated changes --------- * re-merge Cmartine/appeals 46861 (#21855) * Edit DecisionReviewCreatedController to respond with ok status if event exists and is completed, edit test * Move checking if event exists and is completed into event model, edit tests * Fix linter errors --------- * Nrithner/appeals 46860 (#21856) * Set event info back to default default state for info is an empty json object * Correct linting errors * Test happy path with rspec * Refactor rspec example * Converte module to class * Remove module import --------- * Jonathan/appeals 45899/45878 (#21714) * add "from event" value to Store * updated setup guide for M3 * added new badges * added jest * storybook file * additional story * bugfix * snap update * Konstantin/appeals 46175 (#21768) * draft processor method done * process_nonrating method moved to DecisionReviewCreated service * json example added, process_nonrating method updated * 4 rspec test added * useless json example removed * commented lines removed * condition added * condition added. * test updated * first condition moved out of the method * process_nonrating method call moved down * 1 comment removed, 1 added * process nonrating method rafactored, logic transferred into the parser * method refactored, rspec tests updated * rspec test slightly refactored --------- * Edit Issues screen update (#21907) * Edit Issues screen update * relocated row * updated capybara test * updated capybara to check for new added text * cmartine/APPEALS-46905 (#21884) * Remove cache updating call for veteran when veteran is being created for an event * Change seeds api_key.rb to consumer_api_key.rb --------- * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example * Change edited_by_css_id to return users css_id * Konstantin/appeals 48306 (#21998) * ama_eventing_enabled feature check added * toggle renamed * rspec tests added * 2 comments added * toggle renamed, logic trasferred into controller, tests updated * empty line removed --------- * 930 code updates * remove byebug * Change edited_by_css_id to return users css_id (#22062) * Nrithner/appeals 49245 (#22107) * Update process_nonrating to process_nonrationg_issue_category Remove extra logic * Update rspec. Remove unused example --------- * fix extra commas from merge * APPEALS-51223: Disable ClaimantValidator for DecisionReviewCreatedEvents (#22132) * APPEALS-51223 refactored the claimant_validator class method claimant_details_required? to consider the conditional statement * APPEALS-51223 updated Rspec to confirm claimant address_line_1 is nil and error is empty * APPEALS-51223 made changes to to claimant_validator to have all Unit test passing * APPEALS-51223 added suggested changes * Revert "fix merge conflict with AMA" This reverts commit 6775316c85e2c3d8cd94c1baae6d29d75503163d, reversing changes made to 66cf311d1ddb18d11137b173b94ecbafaa022e2b. * added comments * Delete docker-bin/build.sh, I didnt make these changes * Revert "Delete docker-bin/build.sh, I didnt make these changes" This reverts commit 03a3830c5df26e9d4830fcf1d66ea77af4703346. --------- * Jonathan/appeals 51926 (#22215) * Hide UI changes behind featureFlag * fixed lint spacing warnings * code climate spec fix (#22211) * code climate spec fix * up argument count * created a parser helper for reusable methods * reset codeclimate back to original and added issue parser * modified methods to take in a hash instead of multiple params * reduced create params down to 3 params * added space inside * swapped to safe navigator for intake check * fixed spec * fixed linting issue * fixed failing spec * fixed merge conflicts with code changes for helper --------- * Konstantin/appeals 52321 (#22363) * utc conversion method added in app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date method updated in RequestIssue model, updated contested_rating_issue_profile_date in DecisionIssue model * decision issue updated, tests added * tests added, converters improve app/models/decision_issue.rb app/serializers/api/v3/issues/ama/request_issue_serializer.rb * set_contested_rating_issue_profile_date restored in request issue model * uneccessary logic removed * 3 more tests added * test added, error handling aaded in format_rating_profile_date method * Update decision_issue.rb * Update request_issue.rb * Appeals 52317 (#22321) * wip * added the parser to skeleton * removed commented code * added in parser logic * added dru_error route, error handling and params * added rspecs and fixed routing for update error to be a post * added empty spec * updated dru params to match intake json * rubocop fixes * more rubocop fixes * fixed routing to post now and updated specs --------- * fixed logic for AMA issues API serializer attributes (#22358) * fixed logic for added_by fields * methods for fetching removed_by user * withdrawn_by methods * methods for edited_by & instance vars * more logic for added_by methods * fixed failing tests * rspec updates * rspec for RIU addition * small changes * fix test failure * Initial commit (#22494) * Konstantin/appeals 52318 (#22528) * very loose prototype of class created * hopeless variant removed * updated parser per new design and updated specs * request_issues_updated_event updated * some improvments added * unused methods removed, some methods updated * after issues added * tests added * edited issues method returned, invalid tests removed * tests updated * 9 working tests added, RequestIssuesUpdateEvent variable instanciated * tests improved, attribute writers added * more test cases added, error handling in process! method added * added new version of parser and tests to it from Will's PR * uneccesary overrided classes removed * process_job call removed * process_job! restored, 3 more tests added * validate before perfom removed * linter issues addressed * linter issues addressed2 --------- * Bug Fix for APPEALS-44115 (#22538) * running ci * testing category * ci with conditional * set back to false * reverted to original spec * running ci * updated intake helpers to account for using none of these match option * ignored metrics abc size * removed from helper * removed comments * updated spec to check on each category * removed single quotes * fixed a flakey test that was failing on a duplicate key * linting fix * pushed up feature flag * lint clean up * swapped to different featureflag * fixing spec feature toggle param * added feature flags back * checking if label is there * flipped feature test to correct order --------- * cmartine/APPEALS-50366 (#22618) * Create EditDisabledBanner * Add banner displaying logic and pageroute to IntakeEditFrame * Add IntakeEditFrame jest tests for removeCompAndPenIntake feature toggle * Edit displayEditDisabledBanner to use the disableEditingForCompensationAndPension method * Re-add intakeEditFrame jest tests, re-add and move testProps * Refactor intakeEditFrame jest tests * Edit change testProps into static variable * Add another test for intakeEditFrame * Change testProps back into function return * Change comp and pen removal text * Change Intake Remove string back, add Intake Edit Disabled string, changed banner and tests to use this * Ksarlett/appeals 50368 - Disable the remove issue, withdraw issue & edit issue options from drop down (#22637) * ksarlett/APPEALS-50368 - Disable dropdown * rermove console.log * spec refactor * spec fix * spec refactor * change test name --------- * nrithner/APPEALS 50367 (#22640) * Add disabled logic to add issue and edit claim label buttons * Add test buttons are disabled When benefit is compensation * Test buttons are disabled in supplemental_claim Add issue and Edit claim label are disabled when benefits pension or compensation are present * Test buttons are disabled in HLR Add issue and Edit claim label are disabled when benefits pension or compensation are present * Refactor to remove redundancy * Test refactoring --------- * APPEALS-53923 (#22727) * new service class & error message * rspec * Update update_informal_conference_spec.rb * Konstantin/appeals 52982 (#22715) * eligibility-related methods added * methods fixed, date conversion for closed_at added * decision_review_updated_issue_parser created, json example updated, request_issues_update_event updated * tests for parser fixed after json example update * new attribute vbms_id added to request_issues table, decision_review_updated_issue_parser update respectively * request_issues_update_event attr_writers added, decision_review_created_issue_parser class name update and call of that parser updated * veteran removed from DecisionReviewUpdatedParser initilizer, methods marked for potential deletion * decision_review_updated_issue_parser updated and test added for it, comments added to request_issues_update_event * not necessary methods removed from parser, tests for them commented out for now * attributes_from_intake_data method overrided * :id replced by vbms_id, json example, parsers and test for it updated * parsers added, test updated * css_id, station and detail_type commented from the parser * parser cleaned, test for parser fixed, request_issues_update_event cleaned * process_issues! overriden in app/models/request_issues_update_event.rb, linter offenses fixed, testtypo fixed * vbms_id renamed to reference_id * comment typo fixed * Create Audit Tables for SC, HLR, Request Issues Appeals 55403 (#22689) * migration added * schema updated correctly * added audit service and call to update evenr records * logic for update type * fixed typo in audit file and added spec * added insert * moved Insert DRUA to create request issues for created ones * updated event record to take in update type * passing in correct info attribute now * disabled unused params, can enable once we use them * remove << Co-authored-by: TuckerRose Co-authored-by: Enrilo Ugalde <71367882+Jruuuu@users.noreply.github.com> Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde Co-authored-by: isaiahsaucedo Co-authored-by: Calvin Co-authored-by: Craig Reese Co-authored-by: Jeremy Croteau Co-authored-by: (Jeffrey) Aaron Willis <98484567+Aaron-Willis@users.noreply.github.com> Co-authored-by: Konstantin Shevtsov <166068160+KonstantinShevtsov@users.noreply.github.com> Co-authored-by: TuckerRose Co-authored-by: Chris-Martine <135330019+Chris-Martine@users.noreply.github.com> Co-authored-by: kshiflett88 Co-authored-by: root Co-authored-by: nicorithner-bah <148365039+nicorithner-bah@users.noreply.github.com> Co-authored-by: Chris-Martine Co-authored-by: Matt Ray Co-authored-by: Nico Rithner Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> Co-authored-by: Jonathan Tsang * Update seed and test data files to fix typo --------- Co-authored-by: Nader Kutub Co-authored-by: Will Love Co-authored-by: TuckerRose Co-authored-by: Enrilo Ugalde <71367882+Jruuuu@users.noreply.github.com> Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde Co-authored-by: isaiahsaucedo Co-authored-by: Calvin Co-authored-by: Jeremy Croteau Co-authored-by: (Jeffrey) Aaron Willis <98484567+Aaron-Willis@users.noreply.github.com> Co-authored-by: Konstantin Shevtsov <166068160+KonstantinShevtsov@users.noreply.github.com> Co-authored-by: TuckerRose Co-authored-by: Chris-Martine <135330019+Chris-Martine@users.noreply.github.com> Co-authored-by: kshiflett88 Co-authored-by: root Co-authored-by: nicorithner-bah <148365039+nicorithner-bah@users.noreply.github.com> Co-authored-by: Chris-Martine Co-authored-by: Matt Ray Co-authored-by: Nico Rithner Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> Co-authored-by: Jonathan Tsang --------- Co-authored-by: Will Love Co-authored-by: TuckerRose Co-authored-by: Enrilo Ugalde <71367882+Jruuuu@users.noreply.github.com> Co-authored-by: Jonathan Tsang <98970951+jtsangVA@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Enrilo Ugalde Co-authored-by: isaiahsaucedo Co-authored-by: Calvin Co-authored-by: Craig Reese Co-authored-by: Jeremy Croteau Co-authored-by: (Jeffrey) Aaron Willis <98484567+Aaron-Willis@users.noreply.github.com> Co-authored-by: Konstantin Shevtsov <166068160+KonstantinShevtsov@users.noreply.github.com> Co-authored-by: TuckerRose Co-authored-by: Chris-Martine <135330019+Chris-Martine@users.noreply.github.com> Co-authored-by: kshiflett88 Co-authored-by: root Co-authored-by: nicorithner-bah <148365039+nicorithner-bah@users.noreply.github.com> Co-authored-by: Chris-Martine Co-authored-by: Matt Ray Co-authored-by: Nico Rithner Co-authored-by: Matt Ray <108031363+mattray-gov@users.noreply.github.com> Co-authored-by: Jonathan Tsang Co-authored-by: Matthew Thornton <99351305+ThorntonMatthew@users.noreply.github.com> Co-authored-by: Matthew Thornton Co-authored-by: AdamShawBAH Co-authored-by: Marc Steele <71673522+msteele96@users.noreply.github.com> Co-authored-by: Ariana Konhilas Co-authored-by: breedbah Co-authored-by: mchbidwell <122634362+mchbidwell@users.noreply.github.com> Co-authored-by: Jeff Marks Co-authored-by: Jeff Marks <106996298+jefftmarks@users.noreply.github.com> Co-authored-by: minhazur9 <65432922+minhazur9@users.noreply.github.com> Co-authored-by: Ariana Konhilas <109693628+konhilas-ariana@users.noreply.github.com> Co-authored-by: Adam Ducker Co-authored-by: 631862 Co-authored-by: Minhazur Rahaman Co-authored-by: breedbah <123968373+breedbah@users.noreply.github.com> Co-authored-by: Jeff Marks Co-authored-by: Alex Ferencz VA <156860944+aferencz1987VA@users.noreply.github.com> Co-authored-by: Alexandra Ferencz Co-authored-by: Ron Wabukenda <130374706+ronwabVa@users.noreply.github.com> Co-authored-by: Ronald Wabukenda Co-authored-by: mikefinneran <110622959+mikefinneran@users.noreply.github.com> Co-authored-by: laurenyj <44596134+laurenyj@users.noreply.github.com> Co-authored-by: Brian Bommarito Co-authored-by: Andre Pollard Co-authored-by: Davy Wentworth Co-authored-by: andrecolinone <146746795+andrecolinone@users.noreply.github.com> Co-authored-by: Anusha Palliyil Co-authored-by: Raymond Hughes <131811099+raymond-hughes@users.noreply.github.com> Co-authored-by: Craig Reese <109101548+craigrva@users.noreply.github.com> Co-authored-by: seanrpa <155660052+seanrpa@users.noreply.github.com> Co-authored-by: Robert Travis Pierce Co-authored-by: Robert Travis Pierce Co-authored-by: Sean Craig <110493538+seancva@users.noreply.github.com> Co-authored-by: Tyler Broyles <109369527+TylerBroyles@users.noreply.github.com> Co-authored-by: jonathanh-va <111081469+jonathanh-va@users.noreply.github.com> Co-authored-by: Jonathan Hoang Co-authored-by: Brandon Lee Dorner Co-authored-by: Clay Sheppard Co-authored-by: almorbah <149511814+almorbah@users.noreply.github.com> Co-authored-by: Sean Craig Co-authored-by: Prajwal Amatya <122557351+pamatyatake2@users.noreply.github.com> Co-authored-by: = Co-authored-by: Prajwal Amatya Co-authored-by: zborgesva <145717155+zborgesva@users.noreply.github.com> Co-authored-by: Zackary Borges-Rowe Co-authored-by: Daniel Mage --- .reek.yml | 18 + Gemfile | 3 + Gemfile.lock | 15 +- .../hearings/hearing_day_controller.rb | 10 +- .../transcription_files_controller.rb | 25 + app/controllers/hearings_controller.rb | 1 + .../organizations/users_controller.rb | 9 + .../create_non_virtual_conference_job.rb | 27 + .../download_transcription_file_job.rb | 261 + .../fetch_webex_recordings_details_job.rb | 75 + .../fetch_webex_recordings_list_job.rb | 54 + .../fetch_webex_room_meeting_details_job.rb | 56 + .../hearings/fetch_webex_rooms_list_job.rb | 54 + .../refresh_webex_access_token_job.rb | 42 + .../send_transcription_issues_email.rb | 10 + .../virtual_hearings/conference_client.rb | 44 + app/jobs/virtual_hearings/conference_job.rb | 4 +- .../virtual_hearings/create_conference_job.rb | 78 +- .../delete_conference_link_job.rb | 29 + .../delete_conferences_job.rb | 43 +- app/jobs/virtual_hearings/pexip_client.rb | 13 - app/mailers/hearing_mailer.rb | 2 +- .../transcription_file_issues_mailer.rb | 101 + app/models/concerns/conferenceable_concern.rb | 51 + app/models/concerns/hearing_concern.rb | 48 + app/models/concerns/webex_concern.rb | 57 + app/models/hearing.rb | 6 + app/models/hearing_day.rb | 25 +- app/models/hearings/conference_link.rb | 95 +- .../forms/base_hearing_update_form.rb | 14 +- app/models/hearings/pexip_conference_link.rb | 74 + app/models/hearings/transcription_file.rb | 82 + app/models/hearings/virtual_hearing.rb | 49 +- app/models/hearings/webex_conference_link.rb | 27 + app/models/legacy_hearing.rb | 6 + app/models/meeting_type.rb | 20 + app/models/organizations_user.rb | 9 + app/models/request_issue.rb | 2 +- app/models/request_issues_update_event.rb | 10 +- .../administered_user_serializer.rb | 1 + .../tasks/assign_hearing_disposition_task.rb | 2 +- .../hearing_postponement_request_mail_task.rb | 3 +- app/models/tasks/schedule_hearing_task.rb | 1 + app/models/user.rb | 1 + .../hearings/conference_link_serializer.rb | 3 + .../hearings/hearing_day_serializer.rb | 3 +- .../hearings/hearing_serializer.rb | 14 + .../hearings/legacy_hearing_serializer.rb | 14 + .../hearings/transcription_file_serializer.rb | 13 + .../hearings/virtual_hearing_serializer.rb | 2 + app/services/external_api/pexip_service.rb | 25 +- .../external_api/pexip_service/response.rb | 6 + app/services/external_api/webex_service.rb | 162 + .../access_token_refresh_response.rb | 11 + .../webex_service/create_response.rb | 19 + .../webex_service/delete_response.rb | 3 + .../recording_details_response.rb | 19 + .../webex_service/recordings_list_response.rb | 20 + .../external_api/webex_service/response.rb | 70 + .../webex_service/room_details_response.rb | 7 + .../webex_service/rooms_list_response.rb | 18 + app/services/hearing_datetime_service.rb | 3 + app/services/hearing_time_service.rb | 3 + ...{link_service.rb => pexip_link_service.rb} | 2 +- .../notification.html.erb | 2 +- .../sections/_how_to_join.html.erb | 2 +- .../transcription_file_issues_mailer.html.erb | 16 + app/views/queue/index.html.erb | 3 + .../issue_notification.html.erb | 41 + app/workflows/transcription_file_upload.rb | 45 + app/workflows/transcription_transformer.rb | 201 + client/COPY.json | 6 +- client/app/components/CopyTextButton.jsx | 41 +- client/app/components/Table.jsx | 4 +- .../components/icons/FilterNoOutlineIcon.jsx | 1 + client/app/constants/AppConstants.js | 3 +- .../components/VirtualHearingLink.jsx | 20 +- .../components/dailyDocket/DailyDocket.jsx | 4 - .../DailyDocketGuestLinkSection.jsx | 71 - .../components/dailyDocket/DailyDocketRow.jsx | 24 +- .../dailyDocket/DailyDocketRowInputs.jsx | 31 +- .../components/details/DetailsForm.jsx | 7 +- .../components/details/HearingLinks.jsx | 142 +- .../details/TranscriptionDetailsInputs.jsx | 4 +- .../details/TranscriptionFilesTable.jsx | 94 + .../details/TranscriptionFormSection.jsx | 63 +- .../details/VirtualHearingFields.jsx | 24 +- .../components/scheduleHearing/TimeSlot.jsx | 13 +- client/app/hearings/constants.js | 8 - client/app/hearings/utils.js | 5 + client/app/queue/OrganizationUsers.jsx | 134 +- client/app/queue/QueueApp.jsx | 15 +- .../queue/SelectConferenceTypeRadioField.jsx | 62 + client/app/queue/uiReducer/uiActions.js | 7 + client/app/queue/uiReducer/uiConstants.js | 1 + client/app/queue/uiReducer/uiReducer.js | 4 + client/app/reader/DecisionReviewer.jsx | 9 +- client/app/reader/SideBarCategories.jsx | 3 +- client/app/reader/reducers.js | 2 +- client/app/reader/selectors.js | 2 +- client/app/reader/utils.js | 3 +- client/app/readerprototype/DocumentViewer.jsx | 155 +- .../components/Comments/Layer.jsx | 45 +- .../components/Comments/List.jsx | 2 +- .../app/readerprototype/components/Page.jsx | 103 +- .../components/PdfDocument.jsx | 122 +- .../components/ReaderFooter.jsx | 106 +- .../components/ReaderSearchBar.jsx | 5 +- .../components/ReaderSidebar.jsx | 7 +- .../components/ReaderToolbar.jsx | 27 +- .../readerprototype/components/TextLayer.jsx | 13 +- client/app/readerprototype/selectors/index.js | 2 + .../app/readerprototype/util/coordinates.js | 73 + .../app/readerprototype/util/documentUtil.js | 41 +- .../readerprototype/util/readerConstants.js | 6 + client/app/styles/_commons.scss | 2 +- client/app/styles/_team_management.scss | 39 +- client/app/styles/hearings/_hearings.scss | 55 + client/app/styles/reader/_reviewer.scss | 1 + .../REGIONAL_OFFICE_FACILITY_ADDRESS.json | 18 - .../REGIONAL_OFFICE_INFORMATION.json | 2 +- .../TRANSCRIPTION_FILE_STATUSES.json | 14 + client/package.json | 2 + .../app/hearings/components/Details.test.js | 178 +- .../components/ScheduleVeteran.test.js | 911 +- .../AppellantSection.test.js.snap | 208 +- .../__snapshots__/Fields.test.js.snap | 786 +- .../RepresentativeSection.test.js.snap | 212 +- .../__snapshots__/Timezone.test.js.snap | 974 +- .../__snapshots__/Details.test.js.snap | 75052 +-- .../EmailConfirmationModal.test.js.snap | 98 + .../HearingConversion.test.js.snap | 822 +- .../ScheduleVeteran.test.js.snap | 510288 +-------------- .../ScheduleVeteranForm.test.js.snap | 1130 +- .../DailyDocketGuestLinkSection.test.js | 46 - .../dailyDocket/DailyDocketRow.test.js | 16 +- .../dailyDocket/StaticVirtualHearing.test.js | 18 + .../DailyDocketGuestLinkSection.test.js.snap | 309 - .../__snapshots__/DailyDocketRow.test.js.snap | 270 +- .../StaticVirtualHearing.test.js.snap | 10 + .../components/details/HearingLinks.test.js | 156 +- .../details/VirtualHearingFields.test.js | 69 +- .../__snapshots__/DetailsForm.test.js.snap | 3019 +- .../__snapshots__/HearingLinks.test.js.snap | 5073 +- .../VirtualHearingFields.test.js.snap | 4613 +- .../ReadOnlyHearingTimeWithZone.test.js | 30 +- .../ReadOnlyHearingTimeWithZone.test.js.snap | 252 +- .../SelectConferenceTypeRadioField.test.js | 80 + .../NotificationsView.test.js.snap | 3 +- ...electConferenceTypeRadioField.test.js.snap | 55 + .../OrganizationUsers.test.js | 122 + .../readerprototype/DocumentViewer.test.js | 203 + .../readerprototype/components/Page.test.js | 35 + .../components/ReaderFooter.test.js | 431 + .../components/ReaderSidebar.test.js | 62 + .../__snapshots__/Page.test.js.snap | 19 + client/test/app/readerprototype/factories.js | 30 + .../readerprototype/util/coordinates.test.js | 122 + .../data/formattedCaseDistributionData.js | 2 +- client/test/data/hearings.js | 502 +- .../hearings/dailyDocket/dailyDocketProps.js | 9 +- .../dailyDocket/store/dailyDocketStore.js | 2 +- .../data/regionalOfficesJsonResponse.json | 4 +- client/test/data/stores/hearingsStore.js | 16 + client/test/data/user.js | 11 + client/test/data/virtualHearings.js | 9 +- client/yarn.lock | 25 + config/environments/development.rb | 2 +- config/environments/production.rb | 1 + config/environments/test.rb | 15 + config/initializers/aws.rb | 10 + .../create_transcription_files_folder.rb | 8 + config/initializers/credstash.rb | 7 + config/initializers/pexip.rb | 2 +- config/initializers/scheduled_jobs.rb | 3 + config/initializers/timezone.rb | 5 + config/initializers/webex.rb | 1 + config/routes.rb | 1 + ...0230726201514_add_meeting_type_to_users.rb | 9 + ...30_add_meeting_type_to_virtual_hearings.rb | 9 + ...50_add_meeting_type_to_conference_links.rb | 9 + ...3934_add_deleted_at_to_conference_links.rb | 8 + .../20230914210532_normalize_meeting_types.rb | 25 + ...4623_add_type_column_to_conference_link.rb | 9 + ...73746_convert_conference_ids_to_strings.rb | 15 + ...32_add_co_host_link_to_virtual_hearings.rb | 5 + ...240118165914_create_transcription_files.rb | 34 + ...29_add_co_host_link_to_conference_links.rb | 5 + ...id_and_hearing_type_to_conference_links.rb | 10 + ...226201012_add_index_to_conference_links.rb | 7 + db/schema.rb | 46 +- db/seeds/case_distribution_levers.rb | 2 +- docker-compose-m1.yml | 2 +- docker-compose.yml | 2 +- lib/caseflow/error.rb | 12 +- lib/fakes/webex_service.rb | 361 + lib/tasks/meeting_types.rake | 28 + .../transcription_files_controller_spec.rb | 29 + spec/controllers/hearings_controller_spec.rb | 4 +- spec/factories/conference_link.rb | 15 + spec/factories/hearing.rb | 27 + spec/factories/hearing_day.rb | 13 + spec/factories/legacy_hearing.rb | 31 +- spec/factories/meeting_type.rb | 12 + spec/factories/transcription_file.rb | 16 + spec/factories/virtual_hearing.rb | 11 +- spec/feature/hearings/hearing_details_spec.rb | 228 +- .../virtual_hearings/daily_docket_spec.rb | 7 +- spec/feature/reader/reader_spec.rb | 3 +- spec/initializers/timezone_spec.rb | 39 + .../create_non_virtual_conference_job_spec.rb | 43 + .../download_transcription_file_job_spec.rb | 376 + ...fetch_webex_recordings_details_job_spec.rb | 120 + .../fetch_webex_recordings_list_job_spec.rb | 87 + ...tch_webex_room_meeting_details_job_spec.rb | 81 + .../fetch_webex_rooms_list_job_spec.rb | 76 + .../refresh_webex_access_token_job_spec.rb | 41 + .../access_token_refresh_response_spec.rb | 34 + .../create_conference_job_spec.rb | 167 +- .../delete_conference_link_job_spec.rb | 22 + spec/mailers/hearing_mailer_spec.rb | 25 + ...ranscription_file_issues_mailer_preview.rb | 116 + .../transcription_file_issues_mailer_spec.rb | 144 + spec/models/hearing_day_spec.rb | 68 +- spec/models/hearing_spec.rb | 34 + spec/models/hearings/conference_link_spec.rb | 239 +- .../hearings/pexip_conference_link_spec.rb | 193 + .../hearings/transcription_file_spec.rb | 35 + spec/models/hearings/virtual_hearing_spec.rb | 259 +- .../hearings/webex_conference_link_spec.rb | 55 + spec/models/hearings_shared_examples.rb | 32 + spec/models/legacy_hearing_spec.rb | 9 + spec/models/meeting_type_spec.rb | 13 + spec/models/organizations_user_spec.rb | 16 + .../assign_hearing_disposition_task_spec.rb | 9 + .../tasks/schedule_hearing_task_spec.rb | 15 + spec/models/user_spec.rb | 16 + .../virtual_hearing_repository_spec.rb | 8 +- spec/requests/hearing_day_spec.rb | 5 - .../conference_link_serializer_spec.rb | 27 +- .../external_api/pexip_service_spec.rb | 15 +- .../external_api/webex_service_spec.rb | 241 + .../services/hearing_datetime_service_spec.rb | 57 + spec/services/hearing_time_service_spec.rb | 57 +- .../hearings/calendar_service_spec.rb | 60 +- .../stuck_virtual_hearings_checker_spec.rb | 64 +- ...ice_spec.rb => pexip_link_service_spec.rb} | 35 +- .../shared_context_conference_link.rb | 13 + .../transcription_transformer_spec.rb | 80 + 249 files changed, 65184 insertions(+), 549104 deletions(-) create mode 100644 app/controllers/hearings/transcription_files_controller.rb create mode 100644 app/jobs/hearings/create_non_virtual_conference_job.rb create mode 100644 app/jobs/hearings/download_transcription_file_job.rb create mode 100644 app/jobs/hearings/fetch_webex_recordings_details_job.rb create mode 100644 app/jobs/hearings/fetch_webex_recordings_list_job.rb create mode 100644 app/jobs/hearings/fetch_webex_room_meeting_details_job.rb create mode 100644 app/jobs/hearings/fetch_webex_rooms_list_job.rb create mode 100644 app/jobs/hearings/refresh_webex_access_token_job.rb create mode 100644 app/jobs/hearings/send_transcription_issues_email.rb create mode 100644 app/jobs/virtual_hearings/conference_client.rb create mode 100644 app/jobs/virtual_hearings/delete_conference_link_job.rb delete mode 100644 app/jobs/virtual_hearings/pexip_client.rb create mode 100644 app/mailers/transcription_file_issues_mailer.rb create mode 100644 app/models/concerns/conferenceable_concern.rb create mode 100644 app/models/concerns/webex_concern.rb create mode 100644 app/models/hearings/pexip_conference_link.rb create mode 100644 app/models/hearings/transcription_file.rb create mode 100644 app/models/hearings/webex_conference_link.rb create mode 100644 app/models/meeting_type.rb create mode 100644 app/serializers/hearings/transcription_file_serializer.rb create mode 100644 app/services/external_api/webex_service.rb create mode 100644 app/services/external_api/webex_service/access_token_refresh_response.rb create mode 100644 app/services/external_api/webex_service/create_response.rb create mode 100644 app/services/external_api/webex_service/delete_response.rb create mode 100644 app/services/external_api/webex_service/recording_details_response.rb create mode 100644 app/services/external_api/webex_service/recordings_list_response.rb create mode 100644 app/services/external_api/webex_service/response.rb create mode 100644 app/services/external_api/webex_service/room_details_response.rb create mode 100644 app/services/external_api/webex_service/rooms_list_response.rb rename app/services/virtual_hearings/{link_service.rb => pexip_link_service.rb} (97%) create mode 100644 app/views/layouts/transcription_file_issues_mailer.html.erb create mode 100644 app/views/transcription_file_issues_mailer/issue_notification.html.erb create mode 100644 app/workflows/transcription_file_upload.rb create mode 100644 app/workflows/transcription_transformer.rb delete mode 100644 client/app/hearings/components/dailyDocket/DailyDocketGuestLinkSection.jsx create mode 100644 client/app/hearings/components/details/TranscriptionFilesTable.jsx create mode 100644 client/app/queue/SelectConferenceTypeRadioField.jsx create mode 100644 client/app/readerprototype/util/coordinates.js create mode 100644 client/constants/TRANSCRIPTION_FILE_STATUSES.json delete mode 100644 client/test/app/hearings/components/dailyDocket/DailyDocketGuestLinkSection.test.js delete mode 100644 client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketGuestLinkSection.test.js.snap create mode 100644 client/test/app/queue/SelectConferenceTypeRadioField.test.js create mode 100644 client/test/app/queue/__snapshots__/SelectConferenceTypeRadioField.test.js.snap create mode 100644 client/test/app/queue/organizationUsers/OrganizationUsers.test.js create mode 100644 client/test/app/readerprototype/DocumentViewer.test.js create mode 100644 client/test/app/readerprototype/components/Page.test.js create mode 100644 client/test/app/readerprototype/components/ReaderFooter.test.js create mode 100644 client/test/app/readerprototype/components/ReaderSidebar.test.js create mode 100644 client/test/app/readerprototype/components/__snapshots__/Page.test.js.snap create mode 100644 client/test/app/readerprototype/factories.js create mode 100644 client/test/app/readerprototype/util/coordinates.test.js create mode 100644 config/initializers/aws.rb create mode 100644 config/initializers/create_transcription_files_folder.rb create mode 100644 config/initializers/credstash.rb create mode 100644 config/initializers/webex.rb create mode 100644 db/migrate/20230726201514_add_meeting_type_to_users.rb create mode 100644 db/migrate/20230726203030_add_meeting_type_to_virtual_hearings.rb create mode 100644 db/migrate/20230726203750_add_meeting_type_to_conference_links.rb create mode 100644 db/migrate/20230901183934_add_deleted_at_to_conference_links.rb create mode 100644 db/migrate/20230914210532_normalize_meeting_types.rb create mode 100644 db/migrate/20230924014623_add_type_column_to_conference_link.rb create mode 100644 db/migrate/20231107173746_convert_conference_ids_to_strings.rb create mode 100644 db/migrate/20240117030132_add_co_host_link_to_virtual_hearings.rb create mode 100644 db/migrate/20240118165914_create_transcription_files.rb create mode 100644 db/migrate/20240130191529_add_co_host_link_to_conference_links.rb create mode 100644 db/migrate/20240226140103_add_hearing_id_and_hearing_type_to_conference_links.rb create mode 100644 db/migrate/20240226201012_add_index_to_conference_links.rb create mode 100644 lib/fakes/webex_service.rb create mode 100644 lib/tasks/meeting_types.rake create mode 100644 spec/controllers/hearings/transcription_files_controller_spec.rb create mode 100644 spec/factories/meeting_type.rb create mode 100644 spec/factories/transcription_file.rb create mode 100644 spec/initializers/timezone_spec.rb create mode 100644 spec/jobs/hearings/create_non_virtual_conference_job_spec.rb create mode 100644 spec/jobs/hearings/download_transcription_file_job_spec.rb create mode 100644 spec/jobs/hearings/fetch_webex_recordings_details_job_spec.rb create mode 100644 spec/jobs/hearings/fetch_webex_recordings_list_job_spec.rb create mode 100644 spec/jobs/hearings/fetch_webex_room_meeting_details_job_spec.rb create mode 100644 spec/jobs/hearings/fetch_webex_rooms_list_job_spec.rb create mode 100644 spec/jobs/hearings/refresh_webex_access_token_job_spec.rb create mode 100644 spec/jobs/virtual_hearings/access_token_refresh_response_spec.rb create mode 100644 spec/jobs/virtual_hearings/delete_conference_link_job_spec.rb create mode 100644 spec/mailers/previews/transcription_file_issues_mailer_preview.rb create mode 100644 spec/mailers/transcription_file_issues_mailer_spec.rb create mode 100644 spec/models/hearings/pexip_conference_link_spec.rb create mode 100644 spec/models/hearings/transcription_file_spec.rb create mode 100644 spec/models/hearings/webex_conference_link_spec.rb create mode 100644 spec/models/hearings_shared_examples.rb create mode 100644 spec/models/meeting_type_spec.rb create mode 100644 spec/services/external_api/webex_service_spec.rb rename spec/services/virtual_hearings/{link_service_spec.rb => pexip_link_service_spec.rb} (78%) create mode 100644 spec/support/shared_context/models/hearings/shared_context_conference_link.rb create mode 100644 spec/workflows/transcription_transformer_spec.rb diff --git a/.reek.yml b/.reek.yml index cd12f9cc576..bb725320a37 100644 --- a/.reek.yml +++ b/.reek.yml @@ -55,6 +55,7 @@ detectors: exclude: - Address - Api::V3::DecisionReviews::ContestableIssueFinder#initialize + - ExternalApi::WebexService::RecordingsListResponse::Recording#initialize DataClump: exclude: - ForeignKeyPolymorphicAssociationJob @@ -135,6 +136,11 @@ detectors: - ExternalApi::PexipService#send_pexip_request - ControllerSchema#remove_unknown_keys - BusinessLineReporter#as_csv + - Hearings::DownloadTranscriptionFileJob#build_error_explanation + - Hearings::RefreshWebexAccessTokenJob#perform + - TranscriptionFile#update_status! + - ExternalApi::WebexService#create_conference + - TranscriptionTransformer InstanceVariableAssumption: exclude: - Appeal @@ -143,6 +149,7 @@ detectors: - Api::V3::DecisionReviews::HigherLevelReviewIntakeProcessor - ETL::Syncer - User + - Hearings::DownloadTranscriptionFileJob IrresponsibleModule: enabled: false LongParameterList: @@ -163,6 +170,8 @@ detectors: - LegacyDocket#distribute_priority_appeals - LegacyDocket#distribute_nonpriority_appeals - ExternalApi::MPIService#search_people_info + - Hearings::FetchWebexRecordingsDetailsJob#send_file + - ExternalApi::WebexService#initialize ManualDispatch: exclude: - Api::V3::DecisionReviews::IntakeError#potential_error_code @@ -180,6 +189,7 @@ detectors: max_allowed_nesting: 2 exclude: - AsyncableJobsReporter + - TranscriptionTransformer#create_transcription_pages NilCheck: enabled: false RepeatedConditional: @@ -199,6 +209,8 @@ detectors: - Veteran - LegacyDocket - Test::UsersController + - Fakes::WebexService + - Hearings::DownloadTranscriptionFileJob TooManyConstants: exclude: - Fakes::BGSServicePOA @@ -216,6 +228,8 @@ detectors: - Api::V3::DecisionReviews::IntakeError - ControllerSchema::Field - HearingEmailStatusMailer + - ExternalApi::WebexService + - TranscriptionFileIssuesMailer TooManyMethods: enabled: false TooManyStatements: @@ -255,10 +269,14 @@ detectors: - UpdatePOAConcern - VBMSCaseflowLogger#log - LegacyDocket + - ExternalApi::PexipService#not_found_response + - WebexConcern UnusedParameters: exclude: - Docket#distribute_appeals - HearingRequestDocket#distribute_appeals + - Fakes::WebexService#fetch_recording_details + - Fakes::WebexService#fetch_room_details ### Directory specific configuration diff --git a/Gemfile b/Gemfile index dfcd2f36636..ee62f560cca 100644 --- a/Gemfile +++ b/Gemfile @@ -74,6 +74,7 @@ gem "rack", "~> 2.2.6.2" gem "rails", "6.1.7.7" # Used to colorize output for rake tasks gem "rainbow" +gem "rcredstash", "~> 1.1.0" # React gem "react_on_rails", "11.3.0" gem "redis-mutex" @@ -83,6 +84,7 @@ gem "request_store" gem "roo", "~> 2.7" gem "rswag-api" gem "rswag-ui" +gem "rtf" gem "ruby_claim_evidence_api", git: "https://github.com/department-of-veterans-affairs/ruby_claim_evidence_api.git", ref: "fed623802afe7303f4b8b5fe27cff0e903699873" # Use SCSS for stylesheets gem "sass-rails", "~> 5.0" @@ -99,6 +101,7 @@ gem "tzinfo", "~> 2.0" # Use Uglifier as compressor for JavaScript assets gem "uglifier", ">= 1.3.0" gem "validates_email_format_of" +gem "webvtt-ruby" gem "ziptz" group :production, :staging, :ssh_forwarding, :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 62c505e6a11..e6db09febdc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1500,7 +1500,7 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) - d3-rails (7.0.0) + d3-rails (7.8.5) railties (>= 3.1) danger (6.2.2) claide (~> 1.0) @@ -1651,8 +1651,8 @@ GEM immigrant (0.3.6) activerecord (>= 3.0) jaro_winkler (1.5.6) - jmespath (1.3.1) - jquery-rails (4.5.1) + jmespath (1.6.2) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -1898,6 +1898,10 @@ GEM ffi (~> 1.0) rb-readline (0.5.5) rchardet (1.8.0) + rcredstash (1.1.0) + aws-sdk-dynamodb + aws-sdk-kms + thor react_on_rails (11.3.0) addressable connection_pool @@ -1974,6 +1978,7 @@ GEM rswag-ui (2.13.0) actionpack (>= 3.1, < 7.2) railties (>= 3.1, < 7.2) + rtf (0.3.3) rubocop (0.83.0) parallel (~> 1.10) parser (>= 2.7.0.1) @@ -2111,6 +2116,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + webvtt-ruby (0.4.0) xmldsig (0.3.2) nokogiri xmlenc (0.8.0) @@ -2211,6 +2217,7 @@ DEPENDENCIES rails-erd rainbow rb-readline + rcredstash (~> 1.1.0) react_on_rails (= 11.3.0) redis-mutex redis-namespace (~> 1.11.0) @@ -2225,6 +2232,7 @@ DEPENDENCIES rswag-api rswag-specs rswag-ui + rtf rubocop (= 0.83) rubocop-performance rubocop-rails @@ -2254,6 +2262,7 @@ DEPENDENCIES validates_email_format_of webdrivers webmock + webvtt-ruby ziptz BUNDLED WITH diff --git a/app/controllers/hearings/hearing_day_controller.rb b/app/controllers/hearings/hearing_day_controller.rb index 274e9f2387b..401f194051c 100644 --- a/app/controllers/hearings/hearing_day_controller.rb +++ b/app/controllers/hearings/hearing_day_controller.rb @@ -44,9 +44,9 @@ def show hearings: hearing_day.hearings_for_user(current_user).map { |hearing| hearing.quick_to_hash(current_user.id) } ) } - rescue VirtualHearings::LinkService::PINKeyMissingError, - VirtualHearings::LinkService::URLHostMissingError, - VirtualHearings::LinkService::URLPathMissingError => error + rescue VirtualHearings::PexipLinkService::PINKeyMissingError, + VirtualHearings::PexipLinkService::URLHostMissingError, + VirtualHearings::PexipLinkService::URLPathMissingError => error log_error(error) render json: { hearing_day: hearing_day.to_hash(include_conference_link: false).merge( @@ -89,7 +89,9 @@ def create def update hearing_day.update!(update_params) - render json: hearing_day.to_hash + render json: hearing_day.to_hash.merge( + conference_link: ::HearingDaySerializer.serialize_conference_link(hearing_day.conference_link) + ) end def destroy diff --git a/app/controllers/hearings/transcription_files_controller.rb b/app/controllers/hearings/transcription_files_controller.rb new file mode 100644 index 00000000000..9701580f50e --- /dev/null +++ b/app/controllers/hearings/transcription_files_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Hearings::TranscriptionFilesController < ApplicationController + include HearingsConcerns::VerifyAccess + + rescue_from ActiveRecord::RecordNotFound, with: :render_page_not_found + before_action :verify_access_to_hearings, only: [:download_transcription_file] + + # Downloads file and sends to user's local computer + def download_transcription_file + tmp_location = file.fetch_file_from_s3! + File.open(tmp_location, "r") { |stream| send_data stream.read, filename: file.file_name } + file.clean_up_tmp_location + end + + def render_page_not_found + redirect_to "/404" + end + + private + + def file + @file ||= TranscriptionFile.find(params[:file_id]) + end +end diff --git a/app/controllers/hearings_controller.rb b/app/controllers/hearings_controller.rb index 59095f24635..bfa4089daa3 100644 --- a/app/controllers/hearings_controller.rb +++ b/app/controllers/hearings_controller.rb @@ -82,6 +82,7 @@ def virtual_hearing_job_status alias_with_host: hearing.virtual_hearing&.formatted_alias_or_alias_with_host, guest_link: hearing.virtual_hearing&.guest_link, host_link: hearing.virtual_hearing&.host_link, + co_host_link: hearing.virtual_hearing&.co_host_hearing_link, guest_pin: hearing.virtual_hearing&.guest_pin, host_pin: hearing.virtual_hearing&.host_pin } diff --git a/app/controllers/organizations/users_controller.rb b/app/controllers/organizations/users_controller.rb index 6a125576a84..f5b7650bcf2 100644 --- a/app/controllers/organizations/users_controller.rb +++ b/app/controllers/organizations/users_controller.rb @@ -32,6 +32,7 @@ def update adjust_admin_rights end + update_user_conference_provider render json: { users: json_administered_users([user_to_modify]) }, status: :ok end @@ -67,6 +68,14 @@ def adjust_admin_rights end end + def update_user_conference_provider + new_conference_provider = params.dig(:attributes, :conference_provider) + + if organization["url"] == HearingsManagement.singleton.url && new_conference_provider + OrganizationsUser.update_user_conference_provider(user_to_modify, new_conference_provider) + end + end + def organization_url params[:organization_url] end diff --git a/app/jobs/hearings/create_non_virtual_conference_job.rb b/app/jobs/hearings/create_non_virtual_conference_job.rb new file mode 100644 index 00000000000..78b1ca678e3 --- /dev/null +++ b/app/jobs/hearings/create_non_virtual_conference_job.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# This job creates a Webex conference & link for a non virtual hearing + +class Hearings::CreateNonVirtualConferenceJob < CaseflowJob + # We are not using ensure_current_user_is_set because of some + # potential for rollbacks if the set user is not the system user + + queue_with_priority :high_priority + application_attr :hearing_schedule + + attr_reader :hearing + + # Retry if Webex returns an invalid response. + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + job.log_error(exception) + end + + def perform(hearing:) + RequestStore.store[:current_user] = User.system_user + WebexConferenceLink.find_or_create_by!( + hearing: hearing, + created_by: hearing.created_by, + updated_by: hearing.created_by + ) + end +end diff --git a/app/jobs/hearings/download_transcription_file_job.rb b/app/jobs/hearings/download_transcription_file_job.rb new file mode 100644 index 00000000000..8d04372928d --- /dev/null +++ b/app/jobs/hearings/download_transcription_file_job.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true + +require "open-uri" + +# Downloads transcription file from Webex using temporary download link and uploads to S3 +# - Download link passed to this job from FetchRecordingDetailsJob +# - File type either audio (mp3), video (mp4), or vtt (transcript) + +class Hearings::DownloadTranscriptionFileJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + + queue_with_priority :low_priority + application_attr :hearing_schedule + + attr_reader :file_name, :transcription_file + + class FileNameError < StandardError; end + class FileDownloadError < StandardError; end + class HearingAssociationError < StandardError; end + class NoDownloadLinkError < StandardError; end + + retry_on(FileDownloadError, wait: 5.minutes) do |job, exception| + details_hash = { + temporary_download_link: { link: job.arguments.first[:download_link] }, + error: { type: "download" }, + provider: "webex" + } + error_details = job.build_error_details(exception, details_hash) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + retry_on(TranscriptionFileUpload::FileUploadError, wait: :exponentially_longer) do |job, exception| + details_hash = { error: { type: "upload" }, provider: "S3" } + error_details = job.build_error_details(exception, details_hash) + job.transcription_file.clean_up_tmp_location + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + retry_on(TranscriptionTransformer::FileConversionError, wait: 10.seconds) do |job, exception| + job.transcription_file.clean_up_tmp_location + details_hash = { error: { type: "conversion" }, conversion_type: "rtf" } + error_details = job.build_error_details(exception, details_hash) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + discard_on(FileNameError) do |job, exception| + details_hash = { + error: { type: "download" }, + provider: "webex", + reason: "Unable to parse hearing information from file name: #{job.file_name}", + expected_file_name_format: "[docket_number]_[internal_id]_[hearing_type].[file_type]" + } + error_details = job.build_error_details(exception, details_hash) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + discard_on(NoDownloadLinkError) do |job, exception| + job.log_error(exception) + end + + # Purpose: Downloads audio (mp3), video (mp4), or transcript (vtt) file from Webex temporary download link and + # uploads the file to corresponding S3 location. If file is vtt, kicks off conversion of vtt to rtf + # and uploads rtf file to S3. + def perform(download_link:, file_name:) + fail NoDownloadLinkError if !download_link + + ensure_current_user_is_set + @file_name = file_name + @transcription_file ||= find_or_create_transcription_file + ensure_hearing_held + if should_download? + download_file_to_tmp!(download_link) + @transcription_file.upload_to_s3! + convert_to_rtf_and_upload_to_s3! if should_convert_and_upload? + end + @transcription_file.clean_up_tmp_location + end + + # Checks if file is a vtt and was not already converted + def should_convert_and_upload? + @transcription_file.file_type == "vtt" && @transcription_file.date_converted.nil? + end + + # Checks if the file either never started or failed to finish processing + def should_download? + file = @transcription_file + + (file.file_type == "vtt" && file.file_status != Constants.TRANSCRIPTION_FILE_STATUSES.conversion.success) || + (file.file_type != "vtt" && file.file_status != Constants.TRANSCRIPTION_FILE_STATUSES.upload.success) + end + + # Purpose: Builds hash of values to be listed in mail template + # + # Note: Public method to provide access during job retry + # + # Params: error - Instance of error + # details_hash - hash of attributes and values to be listed in mail template + # + # Returns: The hash for details on the error + def build_error_details(error, details_hash) + details_hash.merge( + docket_number: !file_name_error?(error) ? hearing.docket_number : nil, + appeal_id: !file_name_error?(error) ? hearing.appeal.external_id : nil, + error: details_hash[:error].merge( + explanation: build_error_explanation(details_hash) + ) + ) + end + + # Purpose: Logs error and captures exception + # + # Note: Public method to provide access during job retry + # + # Params: error - Error object + def log_download_error(error) + extra = { + application: self.class.name, + hearing_id: !file_name_error?(error) ? hearing.id : nil, + file_name: file_name, + job_id: job_id + } + log_error(error, extra: extra) + end + + private + + # Purpose: Downloads file from temporary download link provided by + # FetchRecordingDetailsJob. Update file status of transcription file depending on download success/failure. + # + # Params: download_link - string, URI for temporary download link + # file_name - string, to be parsed for hearing identifiers + # + # Returns: Updated @transcription_file + def download_file_to_tmp!(link) + transcription_file = @transcription_file + return if File.exist?(transcription_file.tmp_location) + + URI(link).open do |download| + IO.copy_stream(download, transcription_file.tmp_location) + end + transcription_file.update_status!(process: :retrieval, status: :success) + log_info("File #{file_name} successfully downloaded from Webex. Uploading to S3...") + rescue OpenURI::HTTPError => error + transcription_file.update_status!(process: :retrieval, status: :failure) + transcription_file.clean_up_tmp_location + raise FileDownloadError, "Webex temporary download link responded with error: #{error}" + end + + # Purpose: Hearing for which the Webex conference was held + # + # Note: Public method to provide access during job retry + # + # Returns: Hearing object + def hearing + @hearing ||= parse_hearing + end + + # Purpose: Parses hearing details from identifiers present in file name + # + # Returns: Hearing object or error if hearing not able to be parsed or found + def parse_hearing + identifiers = file_name.split(".").first + hearing_id = identifiers.split("_")[1] + hearing_type = identifiers.split("_").last.split("-").first + hearing_type.constantize.find(hearing_id) + rescue StandardError => error + raise FileNameError, "Encountered error #{error} when attempting to parse hearing from file name '#{file_name}'" + end + + # Purpose: Docket number associated with the hearing for which the transcription was created + # + # Returns: string or error + def docket_number + hearing.docket_number + end + + # Purpose: Finds existing transcription file record if job previously failed and retry initiated. Otherewise, creates + # new record. + # + # Params: file_name_arg - string, optional parameter with default value of file name attribute. Allows for method to + # be reused when converting vtt file to rtf. + # + # Returns: TranscriptionFile object + def find_or_create_transcription_file(file_name_arg = file_name) + TranscriptionFile.find_or_create_by( + file_name: file_name_arg, + hearing_id: hearing.id, + hearing_type: hearing.class.name, + docket_number: docket_number + ) do |file| + file.file_type = file_name_arg.split(".").last + file.created_by_id = RequestStore[:current_user].id + file.save! + end + rescue ActiveRecord::RecordInvalid => error + raise FileNameError, error + end + + # Purpose: Converts vtt to rtf, creates new record for converted transcription file, and uploads + # converted file to S3 + # + # Returns: integer value of 1 if tmp file deleted after successful upload + def convert_to_rtf_and_upload_to_s3! + log_info("Converting file #{file_name} to rtf...") + transcription_file = @transcription_file + file_paths = transcription_file.convert_to_rtf! + file_paths.each do |file_path| + output_file_name = file_path.split("/").last + output_file = find_or_create_transcription_file(output_file_name) + log_info("Successfully converted #{file_name} to rtf. Uploading #{output_file.file_type} to S3...") + output_file.upload_to_s3! + output_file.clean_up_tmp_location + end + end + + # Purpose: If disposition of associated hearing is not marked as held, sends email to VA Operations Team and + # continues with download job + def ensure_hearing_held + return if hearing.held? + + msg = "Download of Webex transcription files initiated for hearing (docket ##{docket_number}) successful, " \ + "but hearing's disposition not set to held." + Rails.logger.warn(HearingAssociationError.new(msg)) + # TO IMPLEMENT: SEND EMAIL TO VA OPS TEAM + end + + # Purpose: Logs message + # + # Params: message - string + def log_info(message) + Rails.logger.info(message) + end + + JOB_ACTIONS = { + download: { verb: "download", direction: "from" }, + upload: { verb: "upload", direction: "to" }, + conversion: { verb: "convert", direction: "to" } + }.freeze + + # Purpose: Builds error message to be printed in email notifications + # + # Params: details_hash - hash of attributes and values to be listed in mail template + # + # Returns: String message + def build_error_explanation(details_hash) + action = JOB_ACTIONS[details_hash[:error][:type].to_sym] + action_recipient = details_hash[:provider]&.titlecase || details_hash.delete(:conversion_type) + file_type = @transcription_file ? "#{@transcription_file.file_type} " : "" + + "#{action[:verb]} a #{file_type}file #{action[:direction]} #{action_recipient}" + end + + def file_name_error?(error) + error.is_a?(FileNameError) + end +end diff --git a/app/jobs/hearings/fetch_webex_recordings_details_job.rb b/app/jobs/hearings/fetch_webex_recordings_details_job.rb new file mode 100644 index 00000000000..d6bb8bc596c --- /dev/null +++ b/app/jobs/hearings/fetch_webex_recordings_details_job.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# This job will retrieve a list of webex hearing recording detail links +# and download the information from the links + +class Hearings::FetchWebexRecordingsDetailsJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearing_schedule + attr_reader :recording_id, :host_email, :meeting_title + + # rubocop:disable Layout/LineLength + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + recording_id = job.arguments&.first&.[](:recording_id) + host_email = job.arguments&.first&.[](:host_email) + meeting_title = job.arguments&.first&.[](:meeting_title) + query = "?hostEmail=#{host_email}" + error_details = { + error: { type: "retrieval", explanation: "retrieve recording details from Webex" }, + provider: "webex", + api_call: + "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}recordings/#{recording_id}#{query}", + response: { status: exception.code, message: exception.message }.to_json, + recording_id: recording_id, + host_email: host_email, + meeting_title: meeting_title + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + # rubocop:enable Layout/LineLength + + def perform(recording_id:, host_email:, meeting_title:) + ensure_current_user_is_set + data = fetch_recording_details(recording_id, host_email) + topic = data.topic + + mp4_link = data.mp4_link + send_file(topic, "mp4", mp4_link, meeting_title) + + vtt_link = data.vtt_link + send_file(topic, "vtt", vtt_link, meeting_title) + + mp3_link = data.mp3_link + send_file(topic, "mp3", mp3_link, meeting_title) + end + + def log_error(error) + extra = { + application: self.class.name, + job_id: job_id + } + super(error, extra: extra) + end + + private + + def fetch_recording_details(id, email) + query = { "hostEmail": email } + WebexService.new(recordings_config(query)).fetch_recording_details(id) + end + + def create_file_name(topic, extension, meeting_title) + counter = topic.split("-").last + "#{meeting_title}-#{counter}.#{extension}" + end + + def send_file(topic, extension, link, meeting_title) + file_name = create_file_name(topic, extension, meeting_title) + Hearings::DownloadTranscriptionFileJob.perform_later(download_link: link, file_name: file_name) + end +end diff --git a/app/jobs/hearings/fetch_webex_recordings_list_job.rb b/app/jobs/hearings/fetch_webex_recordings_list_job.rb new file mode 100644 index 00000000000..73fc4634278 --- /dev/null +++ b/app/jobs/hearings/fetch_webex_recordings_list_job.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# This job will retrieve a list of webex hearing recordings and details every hour + +class Hearings::FetchWebexRecordingsListJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearing_schedule + + attr_reader :meeting_id, :meeting_title + + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + max = 100 + id = job.arguments&.first&.[](:meeting_id) + meeting_title = job.arguments&.first&.[](:meeting_title) + query = "?max=#{max}&meetingId=#{id}" + error_details = { + error: { type: "retrieval", explanation: "retrieve a list of recordings from Webex" }, + provider: "webex", + api_call: + "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}admin/recordings/#{query}", + response: { status: exception.code, message: exception.message }.to_json, + meeting_id: id, + meeting_title: meeting_title + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + + def perform(meeting_id:, meeting_title:) + ensure_current_user_is_set + fetch_recordings_list(meeting_id).recordings.each do |recording| + Hearings::FetchWebexRecordingsDetailsJob.perform_later( + recording_id: recording.id, host_email: recording.host_email, meeting_title: meeting_title + ) + end + end + + def log_error(error) + super(error, extra: { application: self.class.name, job_id: job_id }) + end + + private + + def fetch_recordings_list(id) + max = 100 + meeting_id = id + query = { "max": max, "meetingId": meeting_id } + WebexService.new(recordings_config(query)).fetch_recordings_list + end +end diff --git a/app/jobs/hearings/fetch_webex_room_meeting_details_job.rb b/app/jobs/hearings/fetch_webex_room_meeting_details_job.rb new file mode 100644 index 00000000000..e5294811da2 --- /dev/null +++ b/app/jobs/hearings/fetch_webex_room_meeting_details_job.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# This job retrieves details about a specific meeting room from Webex using their API. +class Hearings::FetchWebexRoomMeetingDetailsJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearing_schedule + + attr_reader :room_id, :meeting_title + + class NoMeetingIdError < StandardError; end + + discard_on(NoMeetingIdError) do |job, exception| + job.log_error(exception) + end + + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + room_id = job.arguments&.first&.[](:room_id) + meeting_title = job.arguments&.first&.[](:meeting_title) + error_details = { + error: { type: "retrieval", explanation: "retrieve details of room from Webex" }, + provider: "webex", + api_call: + "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}rooms/#{room_id}/meetingInfo", + response: { status: exception.code, message: exception.message }.to_json, + room_id: room_id, + meeting_title: meeting_title + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + + def perform(room_id:, meeting_title:) + ensure_current_user_is_set + room_meeting_details = fetch_room_details(room_id) + fail NoMeetingIdError if room_meeting_details.meeting_id.nil? + + Hearings::FetchWebexRecordingsListJob.perform_later( + meeting_id: room_meeting_details.meeting_id, + meeting_title: meeting_title + ) + end + + private + + # This constructs the headers and calls on the webex endpoint + # to retreive the meeting details from the specified room using ID + # Params: id - The unique ID of the webex room + # Return: The response object created from the response from the API + def fetch_room_details(id) + WebexService.new(rooms_config).fetch_room_details(id) + end +end diff --git a/app/jobs/hearings/fetch_webex_rooms_list_job.rb b/app/jobs/hearings/fetch_webex_rooms_list_job.rb new file mode 100644 index 00000000000..f7253dd1ba6 --- /dev/null +++ b/app/jobs/hearings/fetch_webex_rooms_list_job.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# This job will retrieve a list of all the webex meeting rooms used by the VA to hold hearings + +class Hearings::FetchWebexRoomsListJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearings_schedule + + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + sort_by = "created" + max = 1000 + query = "?sortBy=#{sort_by}&max=#{max}" + error_details = { + error: { type: "retrieval", explanation: "retrieve a list of rooms from Webex" }, + provider: "webex", + api_call: "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}rooms#{query}", + response: { status: exception.code, message: exception.message }.to_json + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + + def perform + ensure_current_user_is_set + fetch_rooms_list.rooms.each do |room| + title = filter_title(room.title).first + next if title.blank? + + Hearings::FetchWebexRoomMeetingDetailsJob.perform_later(room_id: room.id, meeting_title: title) + end + end + + def log_error(error) + super(error, extra: { application: self.class.name, job_id: job_id }) + end + + private + + def filter_title(title) + title.scan(/\d*-*\d+_\d+_[A-Za-z]*Hearing/) + end + + def fetch_rooms_list + sort_by = "created" + max = 1000 + query = { "sortBy": sort_by, "max": max } + + WebexService.new(rooms_config(query)).fetch_rooms_list + end +end diff --git a/app/jobs/hearings/refresh_webex_access_token_job.rb b/app/jobs/hearings/refresh_webex_access_token_job.rb new file mode 100644 index 00000000000..c2721771675 --- /dev/null +++ b/app/jobs/hearings/refresh_webex_access_token_job.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# This file defines the RefreshWebexAccessTokenJob class, a job that refreshes the access token +# used for Webex API calls. This job is part of the VirtualHearings module. +# +# The job has the following key behaviors: +# 1. The perform method creates a new instance of WebexService and calls its +# refresh_access_token method to get a new access token from the Webex API. +# 2. If the response from the Webex API is successful, the new access and refresh tokens are +# stored in CredStash with the keys webex_#{Rails.deploy_env}_access_token and +# webex_#{Rails.deploy_env}_refresh_token respectively. +# 3. If an error occurs during the process, it is caught and logged using the log_error method. +# +# This job is queued with low priority, indicating that it does not need to be run immediately +# and can wait until the system is less busy. + +class Hearings::RefreshWebexAccessTokenJob < CaseflowJob + queue_with_priority :low_priority + + def perform + webex_service = WebexService.new( + host: ENV["WEBEX_HOST_MAIN"], + port: nil, + aud: nil, + apikey: nil, + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], + query: nil + ) + response = webex_service.refresh_access_token + + if response.success? + new_access_token = response.access_token + new_refresh_token = response.refresh_token + + CredStash.put("webex_#{Rails.deploy_env}_access_token", new_access_token) + CredStash.put("webex_#{Rails.deploy_env}_refresh_token", new_refresh_token) + end + rescue StandardError => error + log_error(error) + end +end diff --git a/app/jobs/hearings/send_transcription_issues_email.rb b/app/jobs/hearings/send_transcription_issues_email.rb new file mode 100644 index 00000000000..af1eb627dbe --- /dev/null +++ b/app/jobs/hearings/send_transcription_issues_email.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Hearings::SendTranscriptionIssuesEmail + def send_transcription_issues_email(error_details) + TranscriptionFileIssuesMailer.issue_notification(error_details).deliver_now! + rescue StandardError, Savon::Error, BGS::ShareError => error + # Savon::Error and BGS::ShareError are sometimes thrown when making requests to BGS endpoints + log_error(error) + end +end diff --git a/app/jobs/virtual_hearings/conference_client.rb b/app/jobs/virtual_hearings/conference_client.rb new file mode 100644 index 00000000000..ed1fc3a8941 --- /dev/null +++ b/app/jobs/virtual_hearings/conference_client.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module VirtualHearings::ConferenceClient + include WebexConcern + + def client(virtual_hearing) + @client ||= case virtual_hearing.conference_provider + when "pexip" then create_pexip_client + when "webex" then create_webex_client + when nil + virtual_hearing.set_default_meeting_type + + return create_pexip_client if virtual_hearing.conference_provider == "pexip" + + return create_webex_client if virtual_hearing.conference_provider == "webex" + + raise_not_found_error + else + raise_not_found_error + end + end + + private + + def raise_not_found_error + msg = "Conference Provider for the Virtual Hearing Not Found" + + fail Caseflow::Error::MeetingTypeNotFoundError, message: msg + end + + def create_webex_client + WebexService.new(instant_connect_config) + end + + def create_pexip_client + PexipService.new( + host: ENV["PEXIP_MANAGEMENT_NODE_HOST"], + port: ENV["PEXIP_MANAGEMENT_NODE_PORT"], + user_name: ENV["PEXIP_USERNAME"], + password: ENV["PEXIP_PASSWORD"], + client_host: ENV["PEXIP_CLIENT_HOST"] + ) + end +end diff --git a/app/jobs/virtual_hearings/conference_job.rb b/app/jobs/virtual_hearings/conference_job.rb index 4dfa3ad9e3c..a9389c88024 100644 --- a/app/jobs/virtual_hearings/conference_job.rb +++ b/app/jobs/virtual_hearings/conference_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -class VirtualHearings::ConferenceJob < ApplicationJob - include VirtualHearings::PexipClient +class VirtualHearings::ConferenceJob < CaseflowJob + include VirtualHearings::ConferenceClient private diff --git a/app/jobs/virtual_hearings/create_conference_job.rb b/app/jobs/virtual_hearings/create_conference_job.rb index 0c390164315..c5c6310a25d 100644 --- a/app/jobs/virtual_hearings/create_conference_job.rb +++ b/app/jobs/virtual_hearings/create_conference_job.rb @@ -50,7 +50,21 @@ class VirtualHearingLinkGenerationFailed < StandardError; end hearing_type: kwargs[:hearing_type] } - Raven.capture_exception(exception, extra: extra) + job.log_error(exception, extra: extra) + end + + # Retry if Webex returns an invalid response. + retry_on(Caseflow::Error::WebexApiError, attempts: 5, wait: :exponentially_longer) do |job, exception| + Rails.logger.error("#{job.class.name} (#{job.job_id}) failed with error: #{exception}") + + kwargs = job.arguments.first + extra = { + application: job.class.app_name.to_s, + hearing_id: kwargs[:hearing_id], + hearing_type: kwargs[:hearing_type] + } + + job.log_error(exception, extra: extra) end # Log the timezone of the job. This is primarily used for debugging context around times @@ -123,41 +137,45 @@ def log_virtual_hearing_state(virtual_hearing) Rails.logger.info("Establishment Updated At: (#{virtual_hearing.establishment.updated_at})") end - def create_conference_tags + def create_conference_metrics_tags custom_metric_info.merge(attrs: { hearing_id: virtual_hearing.hearing_id }) end def create_conference - if FeatureToggle.enabled?(:virtual_hearings_use_new_links, user: virtual_hearing.updated_by) - generate_links_and_pins - else - assign_virtual_hearing_alias_and_pins if should_initialize_alias_and_pins? + return generate_links_and_pins if virtual_hearing.conference_provider == "pexip" - Rails.logger.info( - "Trying to create conference for hearing (#{virtual_hearing.hearing_type} " \ - "[#{virtual_hearing.hearing_id}])..." - ) + create_webex_conference + end - pexip_response = create_pexip_conference + def create_webex_conference + Rails.logger.info( + "Trying to create Webex conference for hearing (#{virtual_hearing.hearing_type} " \ + "[#{virtual_hearing.hearing_id}])..." + ) - Rails.logger.info("Pexip response: #{pexip_response.inspect}") + create_webex_conference_response = create_new_conference - if pexip_response.error - error_display = pexip_error_display(pexip_response) + Rails.logger.info("Create Webex Conference Response: #{create_webex_conference_response.inspect}") - Rails.logger.error("CreateConferenceJob failed: #{error_display}") + conference_creation_error(create_webex_conference_response) if create_webex_conference_response.error - virtual_hearing.establishment.update_error!(error_display) + MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_metrics_tags) - MetricsService.increment_counter(metric_name: "created_conference.failed", **create_conference_tags) + virtual_hearing.update( + host_hearing_link: create_webex_conference_response.host_link, + co_host_hearing_link: create_webex_conference_response.co_host_link, + guest_hearing_link: create_webex_conference_response.guest_link + ) + end - fail pexip_response.error - end + def conference_creation_error(create_conference_response) + error_display = error_display(create_conference_response) - MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_tags) + MetricsService.increment_counter(metric_name: "created_conference.failed", **create_conference_metrics_tags) - virtual_hearing.update(conference_id: pexip_response.data[:conference_id]) - end + virtual_hearing.establishment.update_error!(error_display) + + fail create_conference_response.error end def send_emails(email_type) @@ -168,20 +186,16 @@ def send_emails(email_type) ).call rescue StandardError => error extra = { application: "hearings", email_type: email_type, virtual_hearing_id: virtual_hearing.id } - Raven.capture_exception(error, extra: extra) + log_error(error, extra: extra) end end - def pexip_error_display(response) + def error_display(response) "(#{response.error.code}) #{response.error.message}" end - def create_pexip_conference - client.create_conference( - host_pin: virtual_hearing.host_pin, - guest_pin: virtual_hearing.guest_pin, - name: virtual_hearing.alias - ) + def create_new_conference + client(virtual_hearing).create_conference(virtual_hearing) end def should_initialize_alias_and_pins? @@ -207,7 +221,7 @@ def generate_links_and_pins "[#{virtual_hearing.hearing_id}])..." ) begin - link_service = VirtualHearings::LinkService.new + link_service = VirtualHearings::PexipLinkService.new virtual_hearing.update!( host_hearing_link: link_service.host_link, guest_hearing_link: link_service.guest_link, @@ -216,7 +230,7 @@ def generate_links_and_pins alias_with_host: link_service.alias_with_host ) rescue StandardError => error - Raven.capture_exception(error: error) + log_error(error) raise VirtualHearingLinkGenerationFailed end end diff --git a/app/jobs/virtual_hearings/delete_conference_link_job.rb b/app/jobs/virtual_hearings/delete_conference_link_job.rb new file mode 100644 index 00000000000..10889d0ed97 --- /dev/null +++ b/app/jobs/virtual_hearings/delete_conference_link_job.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# The DeleteConferenceLinkJob is a job thats collects conference_links from past hearing days. +# It then iterates through that collection and adjusts attribute values for each link. +# Afterwards each link then has `.destroy` called on it to issue a [soft delete]. + +class VirtualHearings::DeleteConferenceLinkJob < CaseflowJob + queue_with_priority :low_priority + + def perform + begin + RequestStore[:current_user] = User.system_user + retreive_stale_conference_links.each(&:soft_removal_of_link) + rescue StandardError => error + log_error(error) + end + end + + private + + # Purpose: Queries the DB table of conference_links that are associated with a hearing_day that has already passed. + # + # Params: None + # + # Return: A collection of links for hearing days that have passed. + def retreive_stale_conference_links + ConferenceLink.joins(:hearing_day).where("scheduled_for < ?", Time.zone.today) + end +end diff --git a/app/jobs/virtual_hearings/delete_conferences_job.rb b/app/jobs/virtual_hearings/delete_conferences_job.rb index 9f713c64cf5..1a44843f2f8 100644 --- a/app/jobs/virtual_hearings/delete_conferences_job.rb +++ b/app/jobs/virtual_hearings/delete_conferences_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true ## -# Job that deletes the pexip conference resource if the hearing was held or +# Job that deletes the pexip/webex conference resource if the hearing was held or # if the hearing type is switched from virtual to original hearing type. # It also sends cancellation emails to hearing participants if latter is case. @@ -18,7 +18,7 @@ class EmailsFailedToSend < StandardError; end before_perform do Rails.logger.info( - "#{self.class.name} for deleting Pexip conferences and sending cancellation emails" + "#{self.class.name} for deleting Pexip or Webex conferences and sending cancellation emails" ) end @@ -47,14 +47,14 @@ def perform count_deleted_and_log(VirtualHearingRepository.ready_for_deletion) do |virtual_hearing| log_virtual_hearing_state(virtual_hearing) - Rails.logger.info("Deleting Pexip conference for hearing (#{virtual_hearing.hearing_id})") + Rails.logger.info("Deleting Pexip or Webex conference for hearing (#{virtual_hearing.hearing_id})") process_virtual_hearing(virtual_hearing) end log_failed_virtual_hearings if exception_list.present? - # raise DeleteConferencesJobFailure if EmailsFailedToSend and/or PexipApiErrors were raised + # raise DeleteConferencesJobFailure if EmailsFailedToSend and/or Pexip/Webex ApiErrors were raised fail DeleteConferencesJobFailure if exception_list.present? end @@ -66,9 +66,13 @@ def exception_list def log_failed_virtual_hearings vh_with_pexip_errors = exception_list[Caseflow::Error::PexipApiError] + vh_with_webex_errors = exception_list[Caseflow::Error::WebexApiError] if vh_with_pexip_errors - Rails.logger.info("Failed to delete conferences for the following hearings: " \ + Rails.logger.info("Failed to delete pexip conferences for the following hearings: " \ "#{vh_with_pexip_errors.map(&:hearing_id)}") + elsif vh_with_webex_errors + Rails.logger.info("Failed to delete webex conferences for the following hearings: " \ + "#{vh_with_webex_errors.map(&:hearing_id)}") end vh_with_email_errors = exception_list[EmailsFailedToSend] @@ -82,7 +86,8 @@ def log_virtual_hearing_state(virtual_hearing) super Rails.logger.info("Cancelled?: (#{virtual_hearing.cancelled?})") - Rails.logger.info("Pexip conference id: (#{virtual_hearing.conference_id?})") + Rails.logger.info("Conference id: (#{virtual_hearing.conference_id})") + Rails.logger.info("Meeting Type: (#{virtual_hearing.conference_provider})") end def send_cancellation_emails(virtual_hearing) @@ -138,16 +143,19 @@ def process_virtual_hearing(virtual_hearing) true end - # Returns whether or not the conference was deleted from Pexip + # Returns whether or not the conference was deleted from Pexip or Webex + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def delete_conference(virtual_hearing) - response = client.delete_conference(conference_id: virtual_hearing.conference_id) - Rails.logger.info("Pexip response: #{response}") + response = client(virtual_hearing).delete_conference(virtual_hearing) + Rails.logger.info("#{virtual_hearing.conference_provider.capitalize} response: #{response}") fail response.error unless response.success? true rescue Caseflow::Error::PexipNotFoundError Rails.logger.info("Conference for hearing (#{virtual_hearing.hearing_id}) was already deleted") + rescue Caseflow::Error::WebexNotFoundError + Rails.logger.info("Conference for hearing (#{virtual_hearing.hearing_id}) was already deleted") # Assume the conference was already deleted if it's no longer in Pexip. true @@ -167,6 +175,23 @@ def delete_conference(virtual_hearing) } ) + false + rescue Caseflow::Error::WebexApiError => error + Rails.logger.error("Failed to delete conference from Webex for hearing (#{virtual_hearing.hearing_id})" \ + " with error: (#{error.code}) #{error.message}") + + (exception_list[Caseflow::Error::WebexApiError] ||= []) << virtual_hearing + + capture_exception( + error: error, + extra: { + hearing_id: virtual_hearing.hearing_id, + virtual_hearing_id: virtual_hearing.id, + webex_conference_Id: virtual_hearing.conference_id + } + ) + false end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength end diff --git a/app/jobs/virtual_hearings/pexip_client.rb b/app/jobs/virtual_hearings/pexip_client.rb deleted file mode 100644 index 70e5023f662..00000000000 --- a/app/jobs/virtual_hearings/pexip_client.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module VirtualHearings::PexipClient - def client - @client ||= PexipService.new( - host: ENV["PEXIP_MANAGEMENT_NODE_HOST"], - port: ENV["PEXIP_MANAGEMENT_NODE_PORT"], - user_name: ENV["PEXIP_USERNAME"], - password: ENV["PEXIP_PASSWORD"], - client_host: ENV["PEXIP_CLIENT_HOST"] - ) - end -end diff --git a/app/mailers/hearing_mailer.rb b/app/mailers/hearing_mailer.rb index bec1385e9ee..73863c4ca65 100644 --- a/app/mailers/hearing_mailer.rb +++ b/app/mailers/hearing_mailer.rb @@ -165,7 +165,7 @@ def link end # Raise an error if the link contains the old virtual hearing link 2021-11-10 - if hearing_link.include?(BAD_VIRTUAL_LINK_TEXT) + if hearing_link.nil? || hearing_link.include?(BAD_VIRTUAL_LINK_TEXT) fail BadVirtualLinkError, virtual_hearing_id: virtual_hearing&.id end diff --git a/app/mailers/transcription_file_issues_mailer.rb b/app/mailers/transcription_file_issues_mailer.rb new file mode 100644 index 00000000000..324515b6646 --- /dev/null +++ b/app/mailers/transcription_file_issues_mailer.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +# rubocop:disable Rails/ApplicationMailer +## +# TranscriptionFileIssuesMailer: +# - Generate emails from the templates in app/views/transcription_file_issues +## +class TranscriptionFileIssuesMailer < ActionMailer::Base + default from: "Board of Veterans' Appeals " + layout "transcription_file_issues_mailer" + + # Purpose: Builds email from view in app/views/transcription_file_issues_mailer/issue_notification + # + # Params: details - Hash of key-value pairs required to populate email template: + # - error: { type: string, explanation: string } + # - type: to render subject in #build_subject + # - explanation: "Caseflow attempted to #{explanation} and received a fatal error." + # - provider: string, to build subject and closing statement in #build_outro + # - docket_number: string, optional, but if present renders in subject + # - appeal_id: string, optional, but if present renders Case Details link + # + # - Optionally, any additional key-value pairs are iterated over and included in body as bullets + # according to following formats: + # - key: value =>
  • key.to_s: value
  • + # - key: { link: value } =>
  • key.to_s
  • + # - key: { nested_key_1: value_1, nested_key_2: value_2 } => + #
  • key: + #
      + #
    • nested_key_1.to_s: value_1
    • + #
    • nested_key_2.to_us: value 2
    • + #
    + #
  • + # + def issue_notification(details) + @details = details + build_mailer_params + + mail(subject: build_subject, **mailer_config[:emails]) + end + + private + + def build_mailer_params + @provider = @details.delete(:provider) + @details[:case_details] = build_case_details_link(@details.delete(:appeal_id)) + @error_type = @details[:error][:type] + @explanation = @details.delete(:error)&.dig(:explanation) + @outro = build_outro + end + + def build_case_details_link(appeal_id) + return unless appeal_id + + { link: mailer_config[:base_url] + "/queue/appeals/#{appeal_id}" } + end + + def build_subject + provider = @provider ? " #{@provider.titlecase}" : "" + docket_number = @details[:docket_number] ? " #{@details[:docket_number].titlecase}" : "" + "File #{@error_type.titlecase} Error -" + provider + docket_number + end + + def mailer_config + case Rails.deploy_env + when :development, :test + { base_url: non_external_link, + emails: { to: "Caseflow@test.com" } } + when :uat + { base_url: "https://appeals.cf.uat.ds.va.gov", + emails: { to: "BID_Appeals_UAT@bah.com" } } + when :prodtest + { base_url: "https://appeals.cf.prodtest.ds.va.gov", + emails: { to: "VHACHABID_Appeals_ProdTest@va.gov" } } + when :preprod + { base_url: "https://appeals.cf.preprod.ds.va.gov" } + when :prod + { base_url: "https://appeals.cf.ds.va.gov", + emails: { to: "BVAHearingTeam@VA.gov", + cc: "OITAppealsHelpDesk@va.gov" } } + end + end + + # The link for the case details page when not in prod or uat + def non_external_link + # Rails.deploy_env returns :development for both development and demo envs, use ENV["DEPLOY_ENV"] + return "https://demo.appeals.va.gov" if ENV["DEPLOY_ENV"] == "demo" + + "localhost:3000" + end + + def build_outro + return "continued communication between Caseflow and #{@provider.titlecase}" if issue_with_conference_provider? + + "Caseflow has been supplied with the necessary files" + end + + def issue_with_conference_provider? + %w[webex].include?(@provider) + end +end +# rubocop:enable Rails/ApplicationMailer diff --git a/app/models/concerns/conferenceable_concern.rb b/app/models/concerns/conferenceable_concern.rb new file mode 100644 index 00000000000..0d37f1aec9d --- /dev/null +++ b/app/models/concerns/conferenceable_concern.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +## +# Any model that includes this concern will be able to be assigned a conference provider +# for use in creating virtual conference links. + +module ConferenceableConcern + extend ActiveSupport::Concern + + DEFAULT_SERVICE = ENV["DEFAULT_CONFERENCE_SERVICE"] || "pexip" + + included do + has_one :meeting_type, as: :conferenceable + + after_create :set_default_meeting_type + + delegate :conference_provider, to: :meeting_type, allow_nil: true + end + + # Determines which associated entity this new item should inherit its conference + # provider from. + # + # Virtual hearings will inherit their conference providers from the hearing they've + # been created for. + # + # Other items will inherit from the conference provider assigned to the user who is + # creating them. + # + # @return [String] the conference provider/service name to assign to the new object ("webex" or "pexip") + def determine_service_name + return hearing&.conference_provider if is_a? VirtualHearing + + try(:created_by).try(:conference_provider) + end + + # Creates an associated MeetingType record for the newly created object. + # Which conference provider will be configured within this record is determined + # by #determine_service_name + # + # @return [MeetingType] the new MeetingType object after it has been reloaded. + def set_default_meeting_type + unless meeting_type + MeetingType.create!( + service_name: determine_service_name || DEFAULT_SERVICE, + conferenceable: self + ) + + reload_meeting_type + end + end +end diff --git a/app/models/concerns/hearing_concern.rb b/app/models/concerns/hearing_concern.rb index 74d5331a1c9..59c6af1d4f0 100644 --- a/app/models/concerns/hearing_concern.rb +++ b/app/models/concerns/hearing_concern.rb @@ -5,6 +5,7 @@ ## module HearingConcern extend ActiveSupport::Concern + include RunAsyncable CLOSED_HEARING_DISPOSITIONS = [ Constants.HEARING_DISPOSITION_TYPES.postponed, @@ -102,4 +103,51 @@ def calculate_submission_window end_date end + + def subject_for_conference + "#{docket_number}_#{id}_#{self.class}" + end + + def nbf + scheduled_for.beginning_of_day.to_i + end + + def exp + scheduled_for.end_of_day.to_i + end + + # Returns the new 1:1 conference link object for legacy and ama hearings + # that are non virtual and have a webex meeting type + def non_virtual_conference_link + ConferenceLink.find_by(hearing: self) + end + + # Associate hearing with transcription files across multiple dockets and order accordingly + def transcription_files_by_docket_number + # Remove hyphen in case of counter at end of file name to allow for alphabetical sort + transcription_files.sort_by { |file| file.file_name.split("-").join }.group_by(&:docket_number).values + end + + # Group transcription files by docket number before mapping through nested array and serializing + def serialized_transcription_files + transcription_files_by_docket_number.map do |file_groups| + file_groups.map do |file| + TranscriptionFileSerializer.new(file).serializable_hash[:data][:attributes] + end + end + end + + def start_non_virtual_hearing_job? + disposition.nil? && conference_provider == "webex" && + virtual_hearing.nil? && ConferenceLink.find_by(hearing: self).nil? + end + + def start_non_virtual_hearing_job + perform_later_or_now(Hearings::CreateNonVirtualConferenceJob, hearing: self) + end + + # Complexity of create schedule hearing task was too large - had to break out + def maybe_create_non_virtual_conference + start_non_virtual_hearing_job if start_non_virtual_hearing_job? + end end diff --git a/app/models/concerns/webex_concern.rb b/app/models/concerns/webex_concern.rb new file mode 100644 index 00000000000..ad1fd700a1f --- /dev/null +++ b/app/models/concerns/webex_concern.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Shared Webex methods used for Webex related functions +module WebexConcern + extend ActiveSupport::Concern + + # Purpose: Set up the configuration for calling the recordings endpoint + # + # Params: query - additional details for how returned data should be displayed + # + # Return: Object with header information for the endpoin + def recordings_config(query) + { + host: ENV["WEBEX_HOST_MAIN"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: WebexService.access_token, + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], + query: query + } + end + + # Purpose: Set up the configuration for calling the rooms endpoint + # + # Params: query - additional details for how returned data should be displayed + # + # Return: Object with header information for the endpoint + def rooms_config(query = nil) + { + host: ENV["WEBEX_HOST_MAIN"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: ENV["WEBEX_BOTTOKEN"], + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], + query: query + } + end + + # Purpose: Set up the configuration for calling the instant connect endpoint + # + # Params: query - additional details for how returned data should be displayed + # + # Return: Object with header information for the endpoint + def instant_connect_config(query = nil) + { + host: ENV["WEBEX_HOST_IC"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: ENV["WEBEX_BOTTOKEN"], + domain: ENV["WEBEX_DOMAIN_IC"], + api_endpoint: ENV["WEBEX_API_IC"], + query: query + } + end +end diff --git a/app/models/hearing.rb b/app/models/hearing.rb index 6008a49d157..ae4cf65c02b 100644 --- a/app/models/hearing.rb +++ b/app/models/hearing.rb @@ -31,6 +31,7 @@ class Hearing < CaseflowRecord include UpdatedByUserConcern include HearingConcern include HasHearingEmailRecipientsConcern + include ConferenceableConcern # VA Notify Hooks prepend HearingScheduled @@ -49,6 +50,7 @@ class Hearing < CaseflowRecord has_many :hearing_issue_notes has_many :email_events, class_name: "SentHearingEmailEvent" has_many :email_recipients, class_name: "HearingEmailRecipient" + has_many :transcription_files, as: :hearing class HearingDayFull < StandardError; end @@ -187,6 +189,10 @@ def advance_on_docket_motion .first end + def daily_docket_conference_link + hearing_day.conference_link + end + # returns scheduled datetime object considering the timezones # @return [nil] if hearing_day is nil # @return [Time] in scheduled_in_timezone timezone - if scheduled_datetime and scheduled_in_timezone are present diff --git a/app/models/hearing_day.rb b/app/models/hearing_day.rb index 9d559d2695c..76df6df34b1 100644 --- a/app/models/hearing_day.rb +++ b/app/models/hearing_day.rb @@ -52,6 +52,7 @@ class HearingDayHasChildrenRecords < StandardError; end before_create :assign_created_by_user after_update :update_children_records after_create :generate_link_on_create + before_destroy :soft_link_removal # Validates if the judge id maps to an actual record. validates :judge, presence: true, if: -> { judge_id.present? } @@ -136,6 +137,7 @@ def hearings_for_user(current_user) caseflow_and_vacols_hearings end + # :reek:BooleanParameter def to_hash(include_conference_link = false) judge_names = HearingDayJudgeNameQuery.new([self]).call video_hearing_days_request_types = if VirtualHearing::VALID_REQUEST_TYPES.include? request_type @@ -215,13 +217,22 @@ def half_day? total_slots ? total_slots <= 5 : false end - # over write of the .conference_link method from belongs_to :conference_link to add logic to create of not there + def scheduled_date_passed? + scheduled_for < Date.current + end + + # over write of the .conference_link method from belongs_to :conference_link to add logic to create if not there def conference_link - @conference_link ||= find_or_create_conference_link! + @conference_link ||= scheduled_date_passed? ? nil : find_or_create_conference_link! end private + # called through the 'before_destroy' callback on the hearing_day object. + def soft_link_removal + ConferenceLink.where(hearing_day: self).find_each(&:soft_removal_of_link) + end + def assign_created_by_user self.created_by ||= RequestStore[:current_user] end @@ -233,7 +244,7 @@ def log_error(error) def generate_link_on_create begin - this.conference_link + conference_link rescue StandardError => error log_error(error) end @@ -279,13 +290,11 @@ def combine_time_and_date(time, timezone, date) formatted_datetime_string end - # Method to get the associated conference link record if exists and if not create new one + # Method to get the associated conference link records if they exist and if not create new ones def find_or_create_conference_link! - conference_link = ConferenceLink.find_by_hearing_day_id(id) - if conference_link.nil? - conference_link = ConferenceLink.create(hearing_day_id: id) + if FeatureToggle.enabled?(:pexip_conference_service) + PexipConferenceLink.find_or_create_by!(hearing_day: self, created_by: created_by) end - conference_link end class << self diff --git a/app/models/hearings/conference_link.rb b/app/models/hearings/conference_link.rb index c6e3ea56e4e..65724685fdf 100644 --- a/app/models/hearings/conference_link.rb +++ b/app/models/hearings/conference_link.rb @@ -4,84 +4,47 @@ class ConferenceLink < CaseflowRecord class NoAliasWithHostPresentError < StandardError; end class LinkMismatchError < StandardError; end + acts_as_paranoid + include UpdatedByUserConcern include CreatedByUserConcern + include ConferenceableConcern - after_create :generate_links_and_pins - - class << self - def client_host_or_default - ENV["VIRTUAL_HEARING_URL_HOST"] || "care.evn.va.gov" - end + belongs_to :hearing, polymorphic: true - def formatted_alias(alias_name) - "BVA#{alias_name}@#{client_host_or_default}" - end - - def base_url - "https://#{client_host_or_default}/bva-app/" - end - end - - alias_attribute :alias_name, :alias + after_create :generate_conference_information belongs_to :hearing_day + belongs_to :created_by, class_name: "User" - # Override the host pin - def host_pin - host_pin_long || self[:host_pin] - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def host_link - @full_host_link ||= "#{ConferenceLink.base_url}?join=1&media=&escalate=1&" \ - "conference=#{alias_with_host}&" \ - "pin=#{host_pin}&role=host" - end - # rubocop:enable Naming/MemoizedInstanceVariableName - - def guest_pin - return guest_pin_long if !guest_pin_long.nil? + alias_attribute :alias_name, :alias - link_service = VirtualHearings::LinkService.new - update!(guest_pin_long: link_service.guest_pin) - guest_pin_long + # Purpose: updates the conf_link and then soft_deletes them. + # + # Params: None + # + # Return: None + def soft_removal_of_link + update!(update_conf_links) + destroy end - def guest_link - return guest_hearing_link if !guest_hearing_link.to_s.empty? + private - if !alias_name.nil? - link_service = VirtualHearings::LinkService.new(alias_name) - update!(guest_hearing_link: link_service.guest_link) - elsif !alias_with_host.nil? - link_service = VirtualHearings::LinkService.new(alias_with_host.split("@")[0].split("A")[1]) - update!(guest_hearing_link: link_service.guest_link, alias: link_service.get_conference_id) - end - guest_hearing_link + # Purpose: Updates conference_link attributes when passed into the 'update!' method. + # + # Params: None + # + # Return: Hash that will update the conference_link + def update_conf_links + { + conference_deleted: true, + updated_by_id: RequestStore[:current_user] = User.system_user, + updated_at: Time.zone.now + } end - private - - def generate_links_and_pins - Rails.logger.info( - "Trying to create conference links for Hearing Day Id: #{hearing_day_id}." - ) - begin - link_service = VirtualHearings::LinkService.new - update!( - alias: link_service.get_conference_id, - host_link: link_service.host_link, - host_pin_long: link_service.host_pin, - alias_with_host: link_service.alias_with_host, - guest_hearing_link: link_service.guest_link, - guest_pin_long: link_service.guest_pin - ) - rescue VirtualHearings::LinkService::PINKeyMissingError, - VirtualHearings::LinkService::URLHostMissingError, - VirtualHearings::LinkService::URLPathMissingError => error - Raven.capture_exception(error: error) - raise error - end + def generate_conference_information + fail NotImplementedError end end diff --git a/app/models/hearings/forms/base_hearing_update_form.rb b/app/models/hearings/forms/base_hearing_update_form.rb index 259b4acf547..83887aa39ed 100644 --- a/app/models/hearings/forms/base_hearing_update_form.rb +++ b/app/models/hearings/forms/base_hearing_update_form.rb @@ -128,14 +128,24 @@ def start_async_job? end def start_async_job + # If converting hearing from virtual to non-virtual if start_async_job? && virtual_hearing_cancelled? perform_later_or_now(VirtualHearings::DeleteConferencesJob) + maybe_start_activate_non_virtual_job + # If converting hearing from non-virtual to virtual elsif start_async_job? - start_activate_job + start_activate_virtual_job end end - def start_activate_job + # If a Webex hearing, activate new Webex conference links when converting from virtual to non-virtual + def maybe_start_activate_non_virtual_job + return unless hearing.conference_provider == "webex" + + perform_later_or_now(Hearings::CreateNonVirtualConferenceJob, hearing: hearing) + end + + def start_activate_virtual_job hearing.virtual_hearing.establishment.submit_for_processing! job_args = { diff --git a/app/models/hearings/pexip_conference_link.rb b/app/models/hearings/pexip_conference_link.rb new file mode 100644 index 00000000000..47b4b6455d0 --- /dev/null +++ b/app/models/hearings/pexip_conference_link.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class PexipConferenceLink < ConferenceLink + class << self + def client_host_or_default + ENV["VIRTUAL_HEARING_URL_HOST"] || "care.evn.va.gov" + end + + def formatted_alias(alias_name) + "BVA#{alias_name}@#{client_host_or_default}" + end + + def base_url + "https://#{client_host_or_default}/bva-app/" + end + end + + # Override the host pin + def host_pin + host_pin_long || self[:host_pin] + end + + def host_link + "#{self.class.base_url}?join=1&media=&escalate=1&" \ + "conference=#{alias_with_host}&" \ + "pin=#{host_pin}&role=host" + end + + def guest_pin + return guest_pin_long if !guest_pin_long.nil? + + link_service = VirtualHearings::PexipLinkService.new + update!(guest_pin_long: link_service.guest_pin) + guest_pin_long + end + + def guest_link + return guest_hearing_link if !guest_hearing_link.to_s.empty? + + if !alias_name.nil? + link_service = VirtualHearings::PexipLinkService.new(alias_name) + update!(guest_hearing_link: link_service.guest_link) + elsif !alias_with_host.nil? + link_service = VirtualHearings::PexipLinkService.new(alias_with_host.split("@")[0].split("A")[1]) + update!(guest_hearing_link: link_service.guest_link, alias: link_service.get_conference_id) + end + guest_hearing_link + end + + private + + # :reek:FeatureEnvy + def generate_conference_information + Rails.logger.info( + "Trying to create a Pexip conference link for Hearing Day Id: #{hearing_day_id}." + ) + begin + link_service = VirtualHearings::PexipLinkService.new + update!( + alias: link_service.get_conference_id, + host_link: link_service.host_link, + host_pin_long: link_service.host_pin, + alias_with_host: link_service.alias_with_host, + guest_hearing_link: link_service.guest_link, + guest_pin_long: link_service.guest_pin + ) + rescue VirtualHearings::PexipLinkService::PINKeyMissingError, + VirtualHearings::PexipLinkService::URLHostMissingError, + VirtualHearings::PexipLinkService::URLPathMissingError => error + Raven.capture_exception(error: error) + raise error + end + end +end diff --git a/app/models/hearings/transcription_file.rb b/app/models/hearings/transcription_file.rb new file mode 100644 index 00000000000..6342e0f8fc6 --- /dev/null +++ b/app/models/hearings/transcription_file.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class TranscriptionFile < CaseflowRecord + belongs_to :hearing, polymorphic: true + + belongs_to :transcription + belongs_to :docket + + VALID_FILE_TYPES = %w[mp3 mp4 vtt rtf xls csv].freeze + + validates :file_type, inclusion: { in: VALID_FILE_TYPES, message: "'%s' is not valid" } + + # Purpose: Fetches file from S3 + # Return: The temporary save location of the file + def fetch_file_from_s3! + S3Service.fetch_file(aws_link, tmp_location) + tmp_location + end + + # Purpose: Uploads transcription file to its corresponding location in S3 + def upload_to_s3! + TranscriptionFileUpload.new(self).call + end + + # Purpose: Converts transcription file from vtt to rtf + # + # Returns: string, tmp location of rtf (or xls/csv file if error) + def convert_to_rtf! + return unless file_type == "vtt" + + hearing_info = { + judge: hearing.judge&.full_name, + appeal_id: hearing.appeal&.veteran_file_number, + date: hearing.scheduled_for + } + file_paths = TranscriptionTransformer.new(tmp_location, hearing_info).call + update_status!(process: :conversion, status: :success) + file_paths + rescue TranscriptionTransformer::FileConversionError => error + update_status!(process: :conversion, status: :failure) + raise error, error.message + end + + # Purpose: Maps file handling process with associated field to update + DATE_FIELDS = { + retrieval: :date_receipt_webex, + upload: :date_upload_aws, + conversion: :date_converted + }.freeze + + # Purpose: Updates statue of transcription file after completion of process. If process was success, updates + # associated date field on record. + # + # Params: process - symbol, used to map process with associated file status and date field + # status - symbol, either :success or :failure + # aws_link - string, optional argument of AWS S3 location + # + # Returns: Updated transcription file record + def update_status!(process:, status:, upload_link: nil) + params = { + file_status: Constants.TRANSCRIPTION_FILE_STATUSES.send(process).send(status), + updated_by_id: RequestStore[:current_user].id + } + params[:aws_link] = upload_link if upload_link + params[DATE_FIELDS[process]] = Time.zone.now if status == :success + update!(params) + end + + # Purpose: Location of temporary file in tmp/transcription_files/ folder + # + # Returns: string, folder path + def tmp_location + File.join(Rails.root, "tmp", "transcription_files", file_type, file_name) + end + + # Purpose: Removes temporary file (if it exists) from corresponding tmp folder + # + # Returns: integer value of 1 if file deleted, nil if file not found + def clean_up_tmp_location + File.delete(tmp_location) if File.exist?(tmp_location) + end +end diff --git a/app/models/hearings/virtual_hearing.rb b/app/models/hearings/virtual_hearing.rb index 378f3bdfd36..d58d2346284 100644 --- a/app/models/hearings/virtual_hearing.rb +++ b/app/models/hearings/virtual_hearing.rb @@ -37,6 +37,7 @@ class NoAliasWithHostPresentError < StandardError; end class LinkMismatchError < StandardError; end include UpdatedByUserConcern + include ConferenceableConcern class << self def client_host_or_default @@ -67,8 +68,7 @@ def base_url lambda { joins(:establishment) .where(" - conference_deleted = false AND - conference_id IS NOT NULL AND ( + conference_deleted = false AND ( request_cancelled = true OR virtual_hearing_establishments.processed_at IS NOT NULL )") @@ -165,26 +165,36 @@ def host_pin def guest_link return guest_hearing_link if guest_hearing_link.present? - "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ - "conference=#{formatted_alias_or_alias_with_host}&" \ - "pin=#{guest_pin}&role=guest" + if conference_provider == "pexip" + "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ + "conference=#{formatted_alias_or_alias_with_host}&" \ + "pin=#{guest_pin}&role=guest" + end + end + + def co_host_hearing_link + self[:co_host_hearing_link] end def host_link return host_hearing_link if host_hearing_link.present? - "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ - "conference=#{formatted_alias_or_alias_with_host}&" \ - "pin=#{host_pin}&role=host" + if conference_provider == "pexip" + "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ + "conference=#{formatted_alias_or_alias_with_host}&" \ + "pin=#{host_pin}&role=host" + end end def test_link(title) + return "https://instant-usgov.webex.com/mediatest" if conference_provider == "webex" + if use_vc_test_link? if ENV["VIRTUAL_HEARING_URL_HOST"].blank? - fail(VirtualHearings::LinkService::URLHostMissingError, message: COPY::URL_HOST_MISSING_ERROR_MESSAGE) + fail(VirtualHearings::PexipLinkService::URLHostMissingError, message: COPY::URL_HOST_MISSING_ERROR_MESSAGE) end if ENV["VIRTUAL_HEARING_URL_PATH"].blank? - fail(VirtualHearings::LinkService::URLPathMissingError, message: COPY::URL_PATH_MISSING_ERROR_MESSAGE) + fail(VirtualHearings::PexipLinkService::URLPathMissingError, message: COPY::URL_PATH_MISSING_ERROR_MESSAGE) end host_and_path = "#{ENV['VIRTUAL_HEARING_URL_HOST']}#{ENV['VIRTUAL_HEARING_URL_PATH']}" @@ -215,7 +225,7 @@ def pending? # Determines if the hearing conference has been created def active? # the conference has been created the virtual hearing is active - conference_id.present? || (guest_hearing_link.present? && host_hearing_link.present?) + guest_hearing_link.present? && host_hearing_link.present? end # Determines if the conference was deleted @@ -225,7 +235,7 @@ def active? # require us to delete the conference but not set `request_cancelled`. def closed? # the conference has been created the virtual hearing was deleted - conference_id.present? && conference_deleted? + conference_deleted? end # Determines the status of the Virtual Hearing based on the establishment @@ -264,7 +274,7 @@ def rebuild_and_save_links fail NoAliasWithHostPresentError if alias_with_host.blank? conference_id = alias_with_host[/BVA(\d+)@/, 1] - link_service = VirtualHearings::LinkService.new(conference_id) + link_service = VirtualHearings::PexipLinkService.new(conference_id) # confirm that we extracted the conference ID correctly, # and that the original link was generated with the link service @@ -277,6 +287,19 @@ def rebuild_and_save_links update!(host_hearing_link: link_service.host_link, guest_hearing_link: link_service.guest_link) end + # :reek:FeatureEnvy + def subject_for_conference + "#{hearing.docket_number}_#{hearing.id}_#{hearing.class}" + end + + def nbf + hearing.scheduled_for.beginning_of_day.to_i + end + + def exp + hearing.scheduled_for.end_of_day.to_i + end + private def assign_created_by_user diff --git a/app/models/hearings/webex_conference_link.rb b/app/models/hearings/webex_conference_link.rb new file mode 100644 index 00000000000..c4ab9c82447 --- /dev/null +++ b/app/models/hearings/webex_conference_link.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class WebexConferenceLink < ConferenceLink + include WebexConcern + + def guest_pin + nil + end + + def guest_link + guest_hearing_link + end + + private + + def generate_conference_information + meeting_type.update!(service_name: "webex") + + conference_response = WebexService.new(instant_connect_config).create_conference(hearing) + + update!( + host_link: conference_response.host_link, + co_host_link: conference_response.co_host_link, + guest_hearing_link: conference_response.guest_link + ) + end +end diff --git a/app/models/legacy_hearing.rb b/app/models/legacy_hearing.rb index ab137d2fa8b..ab86f4c0914 100644 --- a/app/models/legacy_hearing.rb +++ b/app/models/legacy_hearing.rb @@ -35,6 +35,7 @@ class LegacyHearing < CaseflowRecord include UpdatedByUserConcern include HearingConcern include HasHearingEmailRecipientsConcern + include ConferenceableConcern # VA Notify Hooks prepend HearingScheduled @@ -75,6 +76,7 @@ class LegacyHearing < CaseflowRecord has_one :hearing_location, as: :hearing has_many :email_events, class_name: "SentHearingEmailEvent", foreign_key: :hearing_id has_many :email_recipients, class_name: "HearingEmailRecipient", foreign_key: :hearing_id + has_many :transcription_files, as: :hearing alias_attribute :location, :hearing_location accepts_nested_attributes_for :hearing_location, reject_if: proc { |attributes| attributes.blank? } @@ -368,6 +370,10 @@ def vacols_hearing_exists? end end + def daily_docket_conference_link + hearing_day.conference_link + end + # The scheduled time for a legacy hearing after it have been retrieved from VACOLS and processed for time zone. # # @return [Time] a Time object in the calculated time zone and DST offset diff --git a/app/models/meeting_type.rb b/app/models/meeting_type.rb new file mode 100644 index 00000000000..be1518480c6 --- /dev/null +++ b/app/models/meeting_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +## +# A class to repreesnt a polymorphic join table that the allow for the use of the +# conferenceable association. +# +# Any model that includes a conferenceable assoication with end up with record in this table. +# +# The service_name pertains to which video conferencing service an entity is assigned to use. + +class MeetingType < CaseflowRecord + belongs_to :conferenceable, polymorphic: true + + enum service_name: { pexip: 0, webex: 1 } + + scope :pexip, -> { where(service_name: "pexip") } + scope :webex, -> { where(service_name: "webex") } + + alias_attribute :conference_provider, :service_name +end diff --git a/app/models/organizations_user.rb b/app/models/organizations_user.rb index 189d98de647..caf892a125a 100644 --- a/app/models/organizations_user.rb +++ b/app/models/organizations_user.rb @@ -28,6 +28,15 @@ def remove_admin_rights_from_user(user, organization) existing_record(user, organization)&.update!(admin: false) end + def update_user_conference_provider(user, new_service_name) + # This could be an upsert once we get to Rails 6 + if user.meeting_type + user.meeting_type.update!(service_name: new_service_name) + else + MeetingType.create!(service_name: new_service_name, conferenceable: user) + end + end + def remove_user_from_organization(user, organization) if user_is_judge_of_team?(user, organization) fail Caseflow::Error::ActionForbiddenError, message: COPY::JUDGE_TEAM_REMOVE_JUDGE_ERROR diff --git a/app/models/request_issue.rb b/app/models/request_issue.rb index 3f9b9f41dd2..11744387a14 100644 --- a/app/models/request_issue.rb +++ b/app/models/request_issue.rb @@ -449,7 +449,7 @@ def fetch_request_issues_updates def fetch_removed_by_user if removed? - relevant_update = request_issues_updates.find do |update| + relevant_update = request_issues_updates&.find do |update| update.removed_issues.any? { |issue| issue.id == id } end diff --git a/app/models/request_issues_update_event.rb b/app/models/request_issues_update_event.rb index 07cf49d989e..3656fae8ed2 100644 --- a/app/models/request_issues_update_event.rb +++ b/app/models/request_issues_update_event.rb @@ -196,7 +196,7 @@ def process_legacy_issues_for_ineligible_to_eligible!(request_issue, parser_issu end def reset_or_create_legacy_issue!(legacy_issue, request_issue) - if legacy_issue && optin?(@review) && request_issue.ineligible_reason.blank? + if legacy_issue && optin? && request_issue.ineligible_reason.blank? legacy_issue.legacy_issue_optin.update!( optin_processed_at: nil, rollback_processed_at: nil, @@ -206,7 +206,7 @@ def reset_or_create_legacy_issue!(legacy_issue, request_issue) legacy_issue = create_legacy_issue_backfill(request_issue) # LegacyIssueOptin - if optin?(@review) && request_issue.ineligible_reason.blank? + if optin? && request_issue.ineligible_reason.blank? create_legacy_optin_backfill(request_issue, legacy_issue) end end @@ -323,7 +323,7 @@ def create_request_issue_backfill legacy_issue = create_legacy_issue_backfill(ri) # LegacyIssueOptin - if optin?(@review) && ri.ineligible_reason.blank? + if optin? && ri.ineligible_reason.blank? create_legacy_optin_backfill(ri, legacy_issue) end end @@ -337,8 +337,8 @@ def vacols_ids_exist?(request_issue) request_issue.vacols_id.present? && request_issue.vacols_sequence_id.present? end - def optin?(decision_review) - decision_review.legacy_opt_in_approved? + def optin? + ActiveModel::Type::Boolean.new.cast(@parser.claim_review_legacy_opt_in_approved) end def create_legacy_issue_backfill(request_issue) diff --git a/app/models/serializers/work_queue/administered_user_serializer.rb b/app/models/serializers/work_queue/administered_user_serializer.rb index 61b86097292..dffbb0f75a4 100644 --- a/app/models/serializers/work_queue/administered_user_serializer.rb +++ b/app/models/serializers/work_queue/administered_user_serializer.rb @@ -11,4 +11,5 @@ class WorkQueue::AdministeredUserSerializer < WorkQueue::UserSerializer params[:organization].dvc&.eql?(object) end end + attribute :conference_provider end diff --git a/app/models/tasks/assign_hearing_disposition_task.rb b/app/models/tasks/assign_hearing_disposition_task.rb index a501e32e7d9..d0f218535ee 100644 --- a/app/models/tasks/assign_hearing_disposition_task.rb +++ b/app/models/tasks/assign_hearing_disposition_task.rb @@ -207,7 +207,7 @@ def reschedule( elsif email_recipients_attributes.present? create_or_update_email_recipients(new_hearing, email_recipients_attributes) end - + new_hearing.maybe_create_non_virtual_conference [new_hearing_task, self.class.create_assign_hearing_disposition_task!(appeal, new_hearing_task, new_hearing)] end end diff --git a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb index 1d898787a6c..4815fe61deb 100644 --- a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb +++ b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb @@ -142,9 +142,8 @@ def reschedule( disposition_task = AssignHearingDispositionTask .create_assign_hearing_disposition_task!(appeal, new_hearing_task, new_hearing) - + new_hearing.maybe_create_non_virtual_conference AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.hearing_scheduled) - [new_hearing_task, disposition_task] end end diff --git a/app/models/tasks/schedule_hearing_task.rb b/app/models/tasks/schedule_hearing_task.rb index da3962cba04..e8589018c98 100644 --- a/app/models/tasks/schedule_hearing_task.rb +++ b/app/models/tasks/schedule_hearing_task.rb @@ -202,6 +202,7 @@ def create_schedule_hearing_tasks(params) # Create and assign the hearing now that it has been scheduled created_tasks << AssignHearingDispositionTask.create_assign_hearing_disposition_task!(appeal, parent, hearing) + hearing.maybe_create_non_virtual_conference # The only other option is to cancel the schedule hearing task elsif params[:status] == Constants.TASK_STATUSES.cancelled # If we are cancelling the schedule hearing task, we need to withdraw the request diff --git a/app/models/user.rb b/app/models/user.rb index 43d9eab9662..d218be4b0fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,6 +2,7 @@ class User < CaseflowRecord # rubocop:disable Metrics/ClassLength include BgsService + include ConferenceableConcern include EventConcern has_many :dispatch_tasks, class_name: "Dispatch::Task" diff --git a/app/serializers/hearings/conference_link_serializer.rb b/app/serializers/hearings/conference_link_serializer.rb index 3797eae384d..5cc550e1a20 100644 --- a/app/serializers/hearings/conference_link_serializer.rb +++ b/app/serializers/hearings/conference_link_serializer.rb @@ -7,4 +7,7 @@ class ConferenceLinkSerializer attribute :alias, &:alias_with_host attribute :guest_pin, &:guest_pin attribute :guest_link, &:guest_link + attribute :co_host_link, &:co_host_link + attribute :type + attribute :conference_provider end diff --git a/app/serializers/hearings/hearing_day_serializer.rb b/app/serializers/hearings/hearing_day_serializer.rb index 905578e02fd..478b672f653 100644 --- a/app/serializers/hearings/hearing_day_serializer.rb +++ b/app/serializers/hearings/hearing_day_serializer.rb @@ -95,7 +95,8 @@ def self.serialize_collection(hearing_days) filled_slots_count_for_days: filled_slots_count_for_days, judge_names: judge_names } - ).serializable_hash[:data].map { |hearing_day| hearing_day[:attributes] } + ).serializable_hash[:data] + .pluck(:attributes) end def self.serialize_conference_link(conference_link) diff --git a/app/serializers/hearings/hearing_serializer.rb b/app/serializers/hearings/hearing_serializer.rb index d6e3f808694..5bb7dcd5652 100644 --- a/app/serializers/hearings/hearing_serializer.rb +++ b/app/serializers/hearings/hearing_serializer.rb @@ -4,6 +4,9 @@ class HearingSerializer include FastJsonapi::ObjectSerializer include HearingSerializerBase + attribute :daily_docket_conference_link do |hearing| + HearingDaySerializer.serialize_conference_link(hearing.daily_docket_conference_link) + end attribute :aod, &:aod? attribute :advance_on_docket_motion do |hearing| if hearing.aod? @@ -44,6 +47,7 @@ class HearingSerializer attribute :contested_claim do |hearing| hearing.appeal.contested_claim? end + attribute :conference_provider attribute :mst do |hearing| hearing.appeal.mst? end @@ -63,6 +67,11 @@ class HearingSerializer attribute :judge_id attribute :location attribute :military_service, if: for_full + attribute :non_virtual_conference_link do |object| + if !object.non_virtual_conference_link.nil? + ConferenceLinkSerializer.new(object.non_virtual_conference_link).serializable_hash[:data][:attributes] + end + end attribute :notes attribute :paper_case do false @@ -92,6 +101,11 @@ class HearingSerializer attribute :transcript_requested attribute :transcript_sent_date attribute :transcription + attribute :transcription_files, if: for_worksheet do |hearing| + if hearing.conference_provider == "webex" + hearing.serialized_transcription_files + end + end attribute :uuid attribute :veteran_age, if: for_full attribute :veteran_file_number diff --git a/app/serializers/hearings/legacy_hearing_serializer.rb b/app/serializers/hearings/legacy_hearing_serializer.rb index 8abb72a7d22..536abeb5579 100644 --- a/app/serializers/hearings/legacy_hearing_serializer.rb +++ b/app/serializers/hearings/legacy_hearing_serializer.rb @@ -37,7 +37,11 @@ class LegacyHearingSerializer attribute :contested_claim do |hearing| hearing.appeal.contested_claim end + attribute :conference_provider attribute :current_issue_count + attribute :daily_docket_conference_link do |hearing| + HearingDaySerializer.serialize_conference_link(hearing.daily_docket_conference_link) + end attribute :disposition attribute :disposition_editable attribute :docket_name @@ -58,6 +62,11 @@ class LegacyHearingSerializer attribute :judge_id attribute :location attribute :military_service, if: for_worksheet + attribute :non_virtual_conference_link do |object| + if !object.non_virtual_conference_link.nil? + ConferenceLinkSerializer.new(object.non_virtual_conference_link).serializable_hash[:data][:attributes] + end + end attribute :notes attribute :paper_case do |object| object.appeal.paper_case? @@ -84,6 +93,11 @@ class LegacyHearingSerializer attribute :submission_window_end, if: for_worksheet, &:calculate_submission_window attribute :summary attribute :transcript_requested + attribute :transcription_files, if: for_worksheet do |hearing| + if hearing.conference_provider == "webex" + hearing.serialized_transcription_files + end + end attribute :user_id attribute :vacols_id, if: for_worksheet attribute :vbms_id diff --git a/app/serializers/hearings/transcription_file_serializer.rb b/app/serializers/hearings/transcription_file_serializer.rb new file mode 100644 index 00000000000..2f13cd94492 --- /dev/null +++ b/app/serializers/hearings/transcription_file_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class TranscriptionFileSerializer + include FastJsonapi::ObjectSerializer + + attribute :id + attribute :docket_number + attribute :hearing_type + attribute :date_upload_aws + attribute :file_name + attribute :file_status + attribute :file_type +end diff --git a/app/serializers/hearings/virtual_hearing_serializer.rb b/app/serializers/hearings/virtual_hearing_serializer.rb index a3803336ccd..e4c55ba66f9 100644 --- a/app/serializers/hearings/virtual_hearing_serializer.rb +++ b/app/serializers/hearings/virtual_hearing_serializer.rb @@ -14,5 +14,7 @@ class VirtualHearingSerializer attribute :guest_pin attribute :host_link, &:host_link attribute :guest_link, &:guest_link + attribute :co_host_link, &:co_host_hearing_link attribute :job_completed, &:job_completed? + attribute :conference_provider end diff --git a/app/services/external_api/pexip_service.rb b/app/services/external_api/pexip_service.rb index 6cb26d61704..5f47d40d5fd 100644 --- a/app/services/external_api/pexip_service.rb +++ b/app/services/external_api/pexip_service.rb @@ -13,7 +13,12 @@ def initialize(host:, port: 443, user_name:, password:, client_host:) @client_host = client_host end - def create_conference(host_pin:, guest_pin:, name:) + # :reek:FeatureEnvy + def create_conference(virtual_hearing) + host_pin = virtual_hearing.host_pin + guest_pin = virtual_hearing.guest_pin + name = virtual_hearing.alias + body = { "aliases": [{ "alias": "BVA#{name}" }, { "alias": VirtualHearing.formatted_alias(name) }, { "alias": name }], "allow_guests": true, @@ -35,18 +40,28 @@ def create_conference(host_pin:, guest_pin:, name:) ExternalApi::PexipService::CreateResponse.new(resp) end - def delete_conference(conference_id:) - return if conference_id.nil? + def delete_conference(virtual_hearing) + if virtual_hearing.conference_id.nil? + return ExternalApi::PexipService::DeleteResponse.new(not_found_response) + end - delete_endpoint = "#{CONFERENCES_ENDPOINT}#{conference_id}/" + delete_endpoint = "#{CONFERENCES_ENDPOINT}#{virtual_hearing.conference_id}/" resp = send_pexip_request(delete_endpoint, :delete) - return if resp.nil? + return lack_of_connectivity_response if resp.nil? ExternalApi::PexipService::DeleteResponse.new(resp) end + def not_found_response + HTTPI::Response.new(404, {}, {}) + end + private + def lack_of_connectivity_response + HTTPI::Response.new(503, {}, {}) + end + attr_reader :host, :port, :user_name, :password, :client_host # :nocov: diff --git a/app/services/external_api/pexip_service/response.rb b/app/services/external_api/pexip_service/response.rb index bcb91028f96..bb987aecf97 100644 --- a/app/services/external_api/pexip_service/response.rb +++ b/app/services/external_api/pexip_service/response.rb @@ -21,6 +21,7 @@ def success? private # :nocov: + # rubocop:disable Metrics/CyclomaticComplexity def check_for_error return if success? @@ -34,10 +35,15 @@ def check_for_error Caseflow::Error::PexipNotFoundError.new(code: code, message: msg) when 405 Caseflow::Error::PexipMethodNotAllowedError.new(code: code, message: msg) + when 503 + Caseflow::Error::PexipServiceNotReachableError.new(code: code, message: "Pexip Service is currently not + available") else + Caseflow::Error::PexipApiError.new(code: code, message: msg) end end + # rubocop:enable Metrics/CyclomaticComplexity def error_message return "No error message from Pexip" if resp.raw_body.empty? diff --git a/app/services/external_api/webex_service.rb b/app/services/external_api/webex_service.rb new file mode 100644 index 00000000000..5d44dea3406 --- /dev/null +++ b/app/services/external_api/webex_service.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "json" + +# This file defines the ExternalApi::WebexService class, which is responsible for interacting +# with the Webex API. This service is used for creating and deleting conferences, refreshing +# access tokens, and fetching recording details. +# +# Key behaviors include: +# 1. The initialize method sets up the service with necessary parameters like host, port, aud, +# apikey, domain, api_endpoint, and query. +# 2. The create_conference method sends a POST request to the Webex API to create a new conference. +# 3. The delete_conference method sends a POST request to the Webex API to delete a conference. +# 4. The refresh_access_token method sends a POST request to the Webex API to refresh the access token. +# 5. The fetch_recordings_list method sends a GET request to the Webex API to fetch a list of recordings. +# 6. The fetch_recording_details method sends a GET request to the Webex API to fetch details of a recording. +# 7. The send_webex_request method is a private method used to send requests to the Webex API +# with the specified body and method. +# +# All requests to the Webex API are recorded using the MetricsService. +class ExternalApi::WebexService + # rubocop:disable Metrics/ParameterLists + def initialize(host:, port:, aud:, apikey:, domain:, api_endpoint:, query: nil) + @host = host + @port = port + @aud = aud + @apikey = apikey + @domain = domain + @api_endpoint = api_endpoint + @query = query + end + # rubocop:enable Metrics/ParameterLists + + def self.access_token + CredStash.get("webex_#{Rails.deploy_env}_access_token") + end + + def create_conference(conferenced_item) + body = { + "jwt": { + "sub": conferenced_item.subject_for_conference, + "nbf": conferenced_item.nbf, + "exp": conferenced_item.exp + }, + "aud": @aud, + "numHost": 2, + "provideShortUrls": true, + "verticalType": "gen" + } + method = "POST" + ExternalApi::WebexService::CreateResponse.new(send_webex_request(body, method)) + end + + def delete_conference(conferenced_item) + body = { + "jwt": { + "sub": conferenced_item.subject_for_conference, + "nbf": 0, + "exp": 0 + }, + "aud": @aud, + "numHost": 2, + "provideShortUrls": true, + "verticalType": "gen" + } + method = "POST" + ExternalApi::WebexService::DeleteResponse.new(send_webex_request(body, method)) + end + + # Purpose: Refreshing the access token to access the API + # Return: The response body + def refresh_access_token + url = URI::DEFAULT_PARSER.escape("https://#{@host}#{@domain}#{@api_endpoint}access_token") + + body = { + grant_type: "refresh_token", + client_id: ENV["WEBEX_CLIENT_ID"], + client_secret: ENV["WEBEX_CLIENT_SECRET"], + refresh_token: CredStash.get("webex_#{Rails.deploy_env}_refresh_token") + } + + headers = { + "Content-Type" => "application/x-www-form-urlencoded", + "Accept" => "application/json", + "Authorization" => CredStash.get("webex_#{Rails.deploy_env}_access_token") + } + + request = HTTPI::Request.new + request.url = url + request.body = URI.encode_www_form(body) + request.headers = headers + + response = HTTPI.post(request) + + ExternalApi::WebexService::AccessTokenRefreshResponse.new(response) + end + + def fetch_recordings_list + body = nil + method = "GET" + @api_endpoint += "admin/recordings" + ExternalApi::WebexService::RecordingsListResponse.new(send_webex_request(body, method)) + end + + def fetch_recording_details(recording_id) + body = nil + method = "GET" + @api_endpoint += "recordings/#{recording_id}" + ExternalApi::WebexService::RecordingDetailsResponse.new(send_webex_request(body, method)) + end + + def fetch_rooms_list + body = nil + method = "GET" + @api_endpoint += "rooms" + ExternalApi::WebexService::RoomsListResponse.new(send_webex_request(body, method)) + end + + def fetch_room_details(room_id) + body = nil + method = "GET" + @api_endpoint += "rooms/#{room_id}/meetingInfo" + ExternalApi::WebexService::RoomDetailsResponse.new(send_webex_request(body, method)) + end + + private + + # :nocov: + # rubocop:disable Metrics/MethodLength + def send_webex_request(body, method) + url = "https://#{@host}#{@domain}#{@api_endpoint}" + request = HTTPI::Request.new(url) + request.open_timeout = 300 + request.read_timeout = 300 + request.body = body.to_json unless body.nil? + request.query = @query + request.headers = { "Authorization": "Bearer #{@apikey}", "Content-Type": "application/json" } + + MetricsService.record( + "#{@host} #{method} request to #{url}", + service: :webex, + name: @api_endpoint + ) do + case method + when "POST" + response = HTTPI.post(request) + fail ExternalApi::WebexService::Response.new(response).error if response.error? + + response + when "GET" + response = HTTPI.get(request) + fail ExternalApi::WebexService::Response.new(response).error if response.error? + + response + else + fail NotImplementedError + end + end + end + # rubocop:enable Metrics/MethodLength + # :nocov: +end diff --git a/app/services/external_api/webex_service/access_token_refresh_response.rb b/app/services/external_api/webex_service/access_token_refresh_response.rb new file mode 100644 index 00000000000..f82c1ddcc26 --- /dev/null +++ b/app/services/external_api/webex_service/access_token_refresh_response.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::AccessTokenRefreshResponse < ExternalApi::WebexService::Response + def access_token + data["access_token"] + end + + def refresh_token + data["refresh_token"] + end +end diff --git a/app/services/external_api/webex_service/create_response.rb b/app/services/external_api/webex_service/create_response.rb new file mode 100644 index 00000000000..2e45b8152a5 --- /dev/null +++ b/app/services/external_api/webex_service/create_response.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::CreateResponse < ExternalApi::WebexService::Response + def base_url + data["baseUrl"] + end + + def host_link + "#{base_url}#{data.dig('host', 0, 'short')}" + end + + def co_host_link + "#{base_url}#{data.dig('host', 1, 'short')}" + end + + def guest_link + "#{base_url}#{data.dig('guest', 0, 'short')}" + end +end diff --git a/app/services/external_api/webex_service/delete_response.rb b/app/services/external_api/webex_service/delete_response.rb new file mode 100644 index 00000000000..5c8be1f2f1c --- /dev/null +++ b/app/services/external_api/webex_service/delete_response.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::DeleteResponse < ExternalApi::WebexService::Response; end diff --git a/app/services/external_api/webex_service/recording_details_response.rb b/app/services/external_api/webex_service/recording_details_response.rb new file mode 100644 index 00000000000..e8ab5afb393 --- /dev/null +++ b/app/services/external_api/webex_service/recording_details_response.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RecordingDetailsResponse < ExternalApi::WebexService::Response + def mp4_link + data["temporaryDirectDownloadLinks"]["recordingDownloadLink"] + end + + def vtt_link + data["temporaryDirectDownloadLinks"]["transcriptDownloadLink"] + end + + def mp3_link + data["temporaryDirectDownloadLinks"]["audioDownloadLink"] + end + + def topic + data["topic"] + end +end diff --git a/app/services/external_api/webex_service/recordings_list_response.rb b/app/services/external_api/webex_service/recordings_list_response.rb new file mode 100644 index 00000000000..709480fa488 --- /dev/null +++ b/app/services/external_api/webex_service/recordings_list_response.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RecordingsListResponse < ExternalApi::WebexService::Response + def recordings + return [] if data["items"].blank? + + data["items"].map { |item| Recording.new(item["id"], item["hostEmail"]) } + end + + class Recording + attr_reader :id, :host_email + + # rubocop:disable Naming/MethodParameterName, Naming/VariableName + def initialize(id, hostEmail) + @id = id + @host_email = hostEmail + end + # rubocop:enable Naming/MethodParameterName, Naming/VariableName + end +end diff --git a/app/services/external_api/webex_service/response.rb b/app/services/external_api/webex_service/response.rb new file mode 100644 index 00000000000..5bd77e23247 --- /dev/null +++ b/app/services/external_api/webex_service/response.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::Response + attr_reader :resp, :code + + def initialize(resp) + @resp = resp + @code = @resp.code + end + + def data + JSON.parse(resp.raw_body).with_indifferent_access + end + + def error + check_for_errors + end + + def success? + !resp.error? + end + + private + + def check_for_errors + return false if success? + + msg = error_message + case code + when 400 + Caseflow::Error::WebexBadRequestError.new(code: code, message: msg) + when 501 + Caseflow::Error::WebexApiError.new(code: code, message: msg) + when 404 + Caseflow::Error::WebexNotFoundError.new(code: code, message: msg) + when 405 + Caseflow::Error::WebexMethodNotAllowedError.new(code: code, message: msg) + else + Caseflow::Error::WebexApiError.new(code: code, message: msg) + end + end + + def error_message + return "No error message from Webex" if resp.raw_body.empty? + + begin + if !invalid_token + { + message: data.dig(:message), + descriptions: data.dig(:errors)&.pluck(:description)&.compact + } + end + data["message"] + rescue JSON::ParserError + "No error message from Webex" + end + end + + def invalid_token + if data["error_description"] == "The access token expired" + fail Caseflow::Error::WebexInvalidTokenError.new( + code: @code, + message: data["error"], + descriptions: data["error_description"] + ) + end + + nil + end +end diff --git a/app/services/external_api/webex_service/room_details_response.rb b/app/services/external_api/webex_service/room_details_response.rb new file mode 100644 index 00000000000..6211420efbf --- /dev/null +++ b/app/services/external_api/webex_service/room_details_response.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RoomDetailsResponse < ExternalApi::WebexService::Response + def meeting_id + data["meetingId"] + end +end diff --git a/app/services/external_api/webex_service/rooms_list_response.rb b/app/services/external_api/webex_service/rooms_list_response.rb new file mode 100644 index 00000000000..b88da04a133 --- /dev/null +++ b/app/services/external_api/webex_service/rooms_list_response.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RoomsListResponse < ExternalApi::WebexService::Response + def rooms + return [] if data["items"].blank? + + data["items"].map { |item| Room.new(item["id"], item["title"]) } + end + + class Room + attr_reader :id, :title + + def initialize(id, title) + @id = id + @title = title + end + end +end diff --git a/app/services/hearing_datetime_service.rb b/app/services/hearing_datetime_service.rb index 2ca9b015c5c..9bf02cae167 100644 --- a/app/services/hearing_datetime_service.rb +++ b/app/services/hearing_datetime_service.rb @@ -81,6 +81,9 @@ def central_office_time_string # @return [String] def scheduled_time_string tz = ActiveSupport::TimeZone::MAPPING.key(@hearing.scheduled_in_timezone) + tz ||= ActiveSupport::TimeZone::MAPPING.key( + TIMEZONE_ALIASES[@hearing.scheduled_in_timezone] + ) "#{local_time.strftime('%l:%M %p')} #{tz}".lstrip end diff --git a/app/services/hearing_time_service.rb b/app/services/hearing_time_service.rb index 775035c8173..fde96f44761 100644 --- a/app/services/hearing_time_service.rb +++ b/app/services/hearing_time_service.rb @@ -13,6 +13,9 @@ def time_to_string(time, hearing) datetime = time.to_datetime tz = ActiveSupport::TimeZone::MAPPING.key(hearing.regional_office_timezone) + tz ||= ActiveSupport::TimeZone::MAPPING.key( + TIMEZONE_ALIASES[hearing.regional_office_timezone] + ) "#{datetime.strftime('%l:%M %p')} #{tz}".lstrip end diff --git a/app/services/virtual_hearings/link_service.rb b/app/services/virtual_hearings/pexip_link_service.rb similarity index 97% rename from app/services/virtual_hearings/link_service.rb rename to app/services/virtual_hearings/pexip_link_service.rb index 21439be6865..4ecace901a3 100644 --- a/app/services/virtual_hearings/link_service.rb +++ b/app/services/virtual_hearings/pexip_link_service.rb @@ -5,7 +5,7 @@ ## # Service for generating new guest and host virtual hearings links ## -class VirtualHearings::LinkService +class VirtualHearings::PexipLinkService class PINKeyMissingError < StandardError; end class URLHostMissingError < StandardError; end class URLPathMissingError < StandardError; end diff --git a/app/views/hearing_email_status_mailer/notification.html.erb b/app/views/hearing_email_status_mailer/notification.html.erb index 5b2388c1a53..d71290485c8 100644 --- a/app/views/hearing_email_status_mailer/notification.html.erb +++ b/app/views/hearing_email_status_mailer/notification.html.erb @@ -4,4 +4,4 @@ <%= content_for :instructions do %>

    Please check the email address and <%= external_link hearing_details_url(@hearing), display_text: "update it on the Hearing Details page" %> if it contains a typo or you have an alternate address. If you update the email address and click Save at the bottom of the screen, email will be sent to the updated address.

    -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hearing_mailer/sections/_how_to_join.html.erb b/app/views/hearing_mailer/sections/_how_to_join.html.erb index 396a58ebd5f..f8fca697590 100644 --- a/app/views/hearing_mailer/sections/_how_to_join.html.erb +++ b/app/views/hearing_mailer/sections/_how_to_join.html.erb @@ -6,4 +6,4 @@ Click on the link below, or copy and paste the link into the address field of your web browser:
    <%= external_link @link %>

    -

    IMPORTANT: You must click on the green video button (icon) to enter the virtual hearing room.

    +

    IMPORTANT: You must click on the video button (icon) to enter the virtual hearing room.

    diff --git a/app/views/layouts/transcription_file_issues_mailer.html.erb b/app/views/layouts/transcription_file_issues_mailer.html.erb new file mode 100644 index 00000000000..c83fd898a0b --- /dev/null +++ b/app/views/layouts/transcription_file_issues_mailer.html.erb @@ -0,0 +1,16 @@ + + + + + + + + + <%= yield %> + <%= yield :intro %> + <%= yield :content %> + <%= yield :signature %> + + diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index 8832fea20d7..ef7a6762643 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -25,6 +25,7 @@ canEditCavcDashboards: current_user.can_edit_cavc_dashboards?, canViewCavcDashboards: current_user.can_view_cavc_dashboards?, userIsCobAdmin: ClerkOfTheBoard.singleton.admins.include?(current_user), + conferenceProvider: current_user.conference_provider, featureToggles: { collect_video_and_central_emails: FeatureToggle.enabled?(:collect_video_and_central_emails, user: current_user), enable_hearing_time_slots: FeatureToggle.enabled?(:enable_hearing_time_slots, user: current_user), @@ -60,6 +61,8 @@ cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), metricsBrowserError: FeatureToggle.enabled?(:metrics_browser_error, user: current_user), cc_vacatur_visibility: FeatureToggle.enabled?(:cc_vacatur_visibility, user: current_user), + conference_selection_visibility: (FeatureToggle.enabled?(:pexip_conference_service) && + FeatureToggle.enabled?(:webex_conference_service)), additional_remand_reasons: FeatureToggle.enabled?(:additional_remand_reasons, user: current_user), acd_cases_tied_to_judges_no_longer_with_board: FeatureToggle.enabled?(:acd_cases_tied_to_judges_no_longer_with_board, user: current_user), admin_case_distribution: FeatureToggle.enabled?(:admin_case_distribution, user: current_user), diff --git a/app/views/transcription_file_issues_mailer/issue_notification.html.erb b/app/views/transcription_file_issues_mailer/issue_notification.html.erb new file mode 100644 index 00000000000..886b42e03cd --- /dev/null +++ b/app/views/transcription_file_issues_mailer/issue_notification.html.erb @@ -0,0 +1,41 @@ +<%= content_for :intro do %> +

    + Attn: +

    +

    + Caseflow attempted to <%= @explanation %> and received a fatal error. +

    +<% end %> + +<%= content_for :content do %> +
      + <% @details.each do |key, value|%> +
    • + <% if value.respond_to?(:has_key?) %> + <% if value[:link] %> + ><%= key.to_s.titlecase %> + <% else %> + <%= key.to_s.titlecase %>: +
        + <% value.each do |k, v| %> +
      • <%= k.to_s.titlecase %>: <%= v %>
      • + <% end%> +
      + <% end %> + <% else %> + <%= key.to_s.titlecase %>: <%= value || "N/A" %> + <% end %> +
    • + <% end %> +
    +<% end %> + +<%= content_for :signature do %> +

    + Please investigate this issue further to ensure <%= @outro %>. Direct any questions to OITAppealsHelpDesk@va.gov. +

    +

    + Sincerely,
    + The Board of Veterans' Appeals +

    +<% end %> diff --git a/app/workflows/transcription_file_upload.rb b/app/workflows/transcription_file_upload.rb new file mode 100644 index 00000000000..19db1c44742 --- /dev/null +++ b/app/workflows/transcription_file_upload.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class TranscriptionFileUpload + attr_reader :file_name, :file_type + + S3_SUB_BUCKET = "vaec-appeals-caseflow" + + S3_SUB_FOLDERS = { + mp3: "transcript_audio", + mp4: "transcript_audio", + vtt: "transcript_raw", + rtf: "transcript_text", + xls: "transcript_text", + csv: "transcript_text" + }.freeze + + class FileUploadError < StandardError; end + + # Params: transcription_file - TranscriptionFile object + def initialize(transcription_file) + @transcription_file = transcription_file + @file_name = @transcription_file.file_name + @file_type = @transcription_file.file_type + @folder_name = (Rails.deploy_env == :prod) ? S3_SUB_BUCKET : "#{S3_SUB_BUCKET}-#{Rails.deploy_env}" + end + + # Purpose: Uploads transcription file to its corresponding location in S3 + def call + S3Service.store_file(s3_location, @transcription_file.tmp_location, :filepath) + @transcription_file.update_status!(process: :upload, status: :success, upload_link: s3_location) + Rails.logger.info("File #{file_name} successfully uploaded to S3 location: #{s3_location}") + rescue StandardError => error + @transcription_file.update_status!(process: :upload, status: :failure) + raise FileUploadError "Amazon S3 service responded with error: #{error}" + end + + private + + # Purpose: Location of uploaded file in s3 + # + # Returns: string, s3 filepath + def s3_location + @folder_name + "/" + S3_SUB_FOLDERS[file_type.to_sym] + "/" + file_name + end +end diff --git a/app/workflows/transcription_transformer.rb b/app/workflows/transcription_transformer.rb new file mode 100644 index 00000000000..999edaba214 --- /dev/null +++ b/app/workflows/transcription_transformer.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require "webvtt" +require "rtf" +require "csv" + +# Workflow for converting VTT transcription files to RTF +class TranscriptionTransformer + class FileConversionError < StandardError; end + + def initialize(vtt_path, hearing_info) + @vtt_path = vtt_path + @error_count = 0 + @hearing_info = hearing_info + @length = 0 + end + + def call + paths = [convert_to_rtf(@vtt_path)] + csv_path = @vtt_path.gsub("vtt", "csv") + if File.exist?(csv_path) + paths.push(csv_path) + elsif @error_count > 0 + error_hash = { + error_count: @error_count, + length: @length, + hearing_info: @hearing_info + } + paths.push(build_csv(csv_path, error_hash)) + end + paths + end + + private + + # Convert vtt file to rtf or csv if there is an error + # Params: path - the file path of the vtt file + # Returns the file path of the newly converted file + def convert_to_rtf(path) + rtf_path = path.gsub("vtt", "rtf") + return rtf_path if File.exist?(rtf_path) + + begin + converted_file = File.open(path, "r") { |io| io.read.encode("UTF-8", invalid: :replace, replace: "...") } + File.open(path, "w") { |file| file.write(converted_file) } + vtt = WebVTT.read(path) + @length = vtt.actual_total_length + doc = RTF::Document.new(RTF::Font.new(RTF::Font::ROMAN, "Times New Roman")) + doc.footer = RTF::FooterNode.new(doc, RTF::FooterNode::UNIVERSAL) + doc.style.left_margin = 1300 + doc.style.right_margin = 1300 + create_cover_page(doc) + doc.page_break + create_transcription_pages(vtt, doc) + raw_doc = create_footer_and_spacing(doc) + File.open(rtf_path, "w") { |file| file.write(raw_doc) } + rtf_path + rescue StandardError + raise FileConversionError + end + end + + # Create cover page + # Params: document - the document object + # Returns the document with the cover page + def create_cover_page(document) + border_width = 40 + document.table(2, 1, 9200) do |table| + table.cell_margin = 30 + header_row = table[0] + header_row.border_width = border_width + header_row.shading_colour = RTF::Colour.new(0, 0, 0) + table[1].border_width = border_width + header_row[0] << " Department of Veterans Affairs" + generate_cover_info(table[1][0]) + end + end + + # Create the text pages on the file + # Params: + # transcript - the original vtt file + # document - the document object + # Returns the document with the transcription pages + def create_transcription_pages(transcript, document) + styles = {} + styles["PS_CODE"] = RTF::ParagraphStyle.new + styles["CS_CODE"] = RTF::CharacterStyle.new + styles["PS_CODE"].line_spacing = -1 + styles["CS_CODE"].underline = true + format_transcript(transcript).each do |cue| + document.paragraph(styles["PS_CODE"]) do |paragraph_style| + paragraph_style.apply(styles["CS_CODE"]) do |char_style| + char_style << cue[:identifier].upcase + end + paragraph_style.paragraph << ": #{cue[:text]}" + paragraph_style.paragraph + end + end + end + + # Format the transcript by consolidating speakers who talk multiple times in a row + # Params: transcript - the original vtt file + # Returns the compressed transcript + def format_transcript(transcript) + compressed_transcript = [] + prev_id = " This is not anyones name." + prev_index = -1 + transcript.cues.each do |cue| + identifier = cue.identifier&.strip&.scan(/[a-zA-Z]+/)&.join(" ") || "" + name = (identifier == "") ? "Unknown" : identifier + if name.match?(/#{prev_id}/) + compressed_transcript[prev_index][:text] += " " + cue.text + else + original_text = cue.text + @error_count += original_text.scan("[...]").size + prev_id = name + compressed_transcript.push(identifier: name, text: original_text) + prev_index += 1 + end + end + + compressed_transcript + end + + # create the footer + # Params: document - the document object + # returns the document with the footer + def create_footer_and_spacing(document) + document.footer << "Insert Veteran's Last Name, First Name, MI, Claim No" + rtf_footer = + "\\footer\\pard\\" + (" " * 47) + "\\chpgn" + (" " * 18) + "Veteran's Last, First, Claim No\\par" + rtf_spacing = "sl120\\slmult1" + raw_rtf = document.to_rtf.sub(document.footer.to_rtf, "{#{rtf_footer}}") + raw_rtf.gsub("sl-1", rtf_spacing) + end + + # streamlines adding line breaks + # Params: + # row - the current row in the document + # count - amount of line breaks to add + def insert_line_breaks(row, count) + breaks = 0 + while breaks < count + row.line_break + breaks += 1 + end + end + + # Params: path - the path to save the csv + # details - hash that has details pertaining to the error + # Returns the created csv + def build_csv(path, details) + filename = path.split("/").last.sub(".csv", "") + header = %w[length appeal_id hearing_date judge issues filename] + length = details[:length] + count = details[:count] + hearing_info = details[:hearing_info] + length_string = "#{(length / 3600).floor}:#{(length / 60 % 60).floor}:#{(length % 60).floor}" + CSV.open(path, "w") do |writer| + writer << header + writer << [length_string, hearing_info[:appeal_id], hearing_info[:date]&.strftime("%m/%d/%Y"), + hearing_info[:judge]&.upcase, "#{count} inaudible", filename] + end + path + end + + # rubocop:disable Metrics/MethodLength + # Generates the template info for the cover page + # Params: row - the table row that the info will be occupying in the doc + # return the modified cover doc + def generate_cover_info(row) + insert_line_breaks(row, 1) + row << " TRANSCRIPT OF HEARING" + insert_line_breaks(row, 2) + row << " BEFORE" + insert_line_breaks(row, 2) + row << " BOARD OF VETERANS' APPEALS" + insert_line_breaks(row, 2) + row << " WASHINGTON, D.C. 20420" + insert_line_breaks(row, 4) + row << " Video Conference at Insert City, State" + insert_line_breaks(row, 4) + row << " IN THE APPEAL OF : Insert Veterans Last Name, First Name, MI" + insert_line_breaks(row, 1) + row << "  Insert Veteran's Claim No" + insert_line_breaks(row, 5) + row << " DATE : Insert Date" + insert_line_breaks(row, 5) + row << " REPRESENTED BY : Insert Name of Representative" + insert_line_breaks(row, 1) + row << " Insert Representative's Organization" + insert_line_breaks(row, 5) + row << " MEMBER OF BOARD : Insert Veterans Law Judge's name, Judge" + insert_line_breaks(row, 5) + row << " WITNESSES :  Insert Full Name of witness, Appellant" + insert_line_breaks(row, 1) + row << "     Insert Full Name of other witnesses, Witness" + insert_line_breaks(row, 5) + end + # rubocop:enable Metrics/MethodLength +end diff --git a/client/COPY.json b/client/COPY.json index 9c87e505b6e..f390031085c 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -785,6 +785,7 @@ "USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Add admin rights", "USER_MANAGEMENT_REMOVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Remove admin rights", "USER_MANAGEMENT_REMOVE_USER_FROM_ORG_BUTTON_TEXT": "Remove from team", + "USER_MANAGEMENT_SELECT_HEARINGS_CONFERENCE_TYPE": "Schedule hearings using:", "MEMBERSHIP_REQUEST_ACTION_SUCCESS_TITLE": "You successfully %s %s's request", "MEMBERSHIP_REQUEST_ACTION_SUCCESS_MESSAGE": "The user was %s regular member access to %s.", "VHA_MEMBERSHIP_REQUEST_AUTOMATIC_VHA_ACCESS_NOTE": "Note: If you are requesting specialized access and are not a member of the general VHA group, you will automatically be given access to the general VHA group if your request is approved.", @@ -1216,8 +1217,9 @@ "VLJ_VIRTUAL_HEARING_LINK_LABEL": "VLJ Link", "GUEST_VIRTUAL_HEARING_LINK_LABEL": "Guest Link", "REPRESENTATIVE_VIRTUAL_HEARING_LINK_LABEL": "Virtual Hearing Link", - "VLJ_VIRTUAL_HEARINGS_LINK_TEXT": "Start Virtual Hearing", - "GUEST_VIRTUAL_HEARINGS_LINK_TEXT": "Join Virtual Hearing", + "HC_VIRTUAL_HEARING_LINK_LABEL": "Hearing Coordinator Link", + "VLJ_VIRTUAL_HEARINGS_LINK_TEXT": "Start Hearing", + "GUEST_VIRTUAL_HEARINGS_LINK_TEXT": "Join Hearing", "PIN_KEY_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid PIN key", "URL_HOST_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid URL host", "URL_PATH_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid URL path", diff --git a/client/app/components/CopyTextButton.jsx b/client/app/components/CopyTextButton.jsx index 77e49f0c3da..85b687ec6d2 100644 --- a/client/app/components/CopyTextButton.jsx +++ b/client/app/components/CopyTextButton.jsx @@ -14,27 +14,32 @@ export const clipboardButtonStyling = (defaults) => padding: '0.75rem', // Offset the additional padding so when this component appears in an unordered list of items its baseline matches. margin: '-0.75rem 0', - overflowWrap: 'break-word' + overflowWrap: 'break-word', }); export default class CopyTextButton extends React.PureComponent { render = () => { const { text, textToCopy, label, styling, ariaLabel } = this.props; - const buttonStyles = isEmpty(styling) ? - { - borderColor: COLORS.GREY_LIGHT, - borderWidth: '1px', + + const buttonStyles = isEmpty(styling) ? { + borderColor: COLORS.GREY_LIGHT, + borderWidth: '1px', + color: COLORS.GREY_DARK, + ':hover': { + backgroundColor: 'transparent', color: COLORS.GREY_DARK, - ':hover': { - backgroundColor: 'transparent', - color: COLORS.GREY_DARK, - borderColor: COLORS.PRIMARY, - borderBottomWidth: '1px' - }, - '& > svg path': { fill: COLORS.GREY_LIGHT }, - '&:hover > svg path': { fill: COLORS.PRIMARY } - } : - styling; + borderColor: COLORS.PRIMARY, + borderBottomWidth: '1px', + }, + ':disabled': { + backgroundColor: COLORS.GREY_BACKGROUND, + borderColor: COLORS.GREY_LIGHT, + color: COLORS.GREY_LIGHT, + borderBottomWidth: '1px', + }, + '& > svg path': { fill: COLORS.GREY_LIGHT }, + '&:hover > svg path': { fill: COLORS.PRIMARY }, + } : styling; return ( @@ -43,6 +48,7 @@ export default class CopyTextButton extends React.PureComponent { type="submit" className="cf-apppeal-id" aria-label={ariaLabel || `Copy ${label} ${text}`} + disabled={textToCopy === null} {...clipboardButtonStyling(buttonStyles)} > {text}  @@ -57,7 +63,7 @@ export default class CopyTextButton extends React.PureComponent { CopyTextButton.defaultProps = { styling: {}, label: '', - textToCopy: null + textToCopy: null, }; CopyTextButton.propTypes = { @@ -77,5 +83,6 @@ CopyTextButton.propTypes = { * If ariaLabel not set, populates the aria-label as `Copy ${label} ${text}` */ label: PropTypes.string, - styling: PropTypes.object + styling: PropTypes.object, + disabled: PropTypes.bool, }; diff --git a/client/app/components/Table.jsx b/client/app/components/Table.jsx index 6bad532a713..7e0c3ea200e 100644 --- a/client/app/components/Table.jsx +++ b/client/app/components/Table.jsx @@ -312,7 +312,7 @@ BodyRows.propTypes = { tbodyRef: PropTypes.func, id: PropTypes.string, getKeyForRow: PropTypes.func, - bodyStyling: PropTypes.array + bodyStyling: PropTypes.object }; FooterRow.propTypes = { @@ -342,5 +342,5 @@ Table.propTypes = { sortAscending: PropTypes.bool }), bodyClassName: PropTypes.string, - bodyStyling: PropTypes.array + bodyStyling: PropTypes.object }; diff --git a/client/app/components/icons/FilterNoOutlineIcon.jsx b/client/app/components/icons/FilterNoOutlineIcon.jsx index f73598c72a0..eb44be73cf0 100644 --- a/client/app/components/icons/FilterNoOutlineIcon.jsx +++ b/client/app/components/icons/FilterNoOutlineIcon.jsx @@ -6,6 +6,7 @@ export const FilterNoOutlineIcon = (props) => { const { color, size, className } = props; return + filtered indicator Connect to Recording System} + onClick={this.conferenceLinkOnClick} > Connect to Recording System } + {hearing?.isVirtual !== true && hearing?.scheduledForIsPast && userJudgeOrCoordinator(user, hearing) &&
    + + Host Link: N/A +
    } + {
    + {StringUtil.capitalizeFirst(hearing?.conferenceProvider || 'Pexip')} hearing +
    } (
    - Edit Hearing Details + Hearing Details and links
    @@ -350,18 +351,22 @@ PreppedCheckbox.propTypes = { export const StaticVirtualHearing = ({ hearing, user }) => (
    - + { hearing?.scheduledForIsPast ? ( + {virtualHearingScheduledDatePassedLabelFull(virtualHearingRoleForUser(user, hearing))} + ) : ( + + )} {hearing?.virtualHearing?.status === 'pending' && (
    {COPY.VIRTUAL_HEARING_SCHEDULING_IN_PROGRESS} diff --git a/client/app/hearings/components/details/DetailsForm.jsx b/client/app/hearings/components/details/DetailsForm.jsx index c528201c72b..075e28c20ac 100644 --- a/client/app/hearings/components/details/DetailsForm.jsx +++ b/client/app/hearings/components/details/DetailsForm.jsx @@ -117,12 +117,14 @@ const DetailsForm = (props) => { initialRepresentativeTz={initialHearing?.representativeTz} /> - {!isLegacy && ( + {/* Don't render Transcription Details section if Legacy Hearing AND Pexip conference provider*/} + {!(isLegacy && hearing.conferenceProvider === 'pexip') && ( )} @@ -140,7 +142,8 @@ DetailsForm.propTypes = { wasVirtual: PropTypes.bool, isVirtual: PropTypes.bool, scheduledTimeString: PropTypes.string, - readableRequestType: PropTypes.string + readableRequestType: PropTypes.string, + conferenceProvider: PropTypes.string }), initialHearing: PropTypes.shape({ virtualHearing: PropTypes.object diff --git a/client/app/hearings/components/details/HearingLinks.jsx b/client/app/hearings/components/details/HearingLinks.jsx index 4e7d1edd6b9..10f34bc059e 100644 --- a/client/app/hearings/components/details/HearingLinks.jsx +++ b/client/app/hearings/components/details/HearingLinks.jsx @@ -2,7 +2,6 @@ import { css } from 'glamor'; import PropTypes from 'prop-types'; import React from 'react'; -import { COLORS } from '../../../constants/AppConstants'; import { VIRTUAL_HEARING_HOST, virtualHearingRoleForUser } from '../../utils'; import { rowThirds, @@ -20,16 +19,13 @@ export const VirtualHearingLinkDetails = ({ role, link, hearing, - wasVirtual, isVirtual, user, label, virtualHearing }) => ( - {hearing?.scheduledForIsPast || wasVirtual ? ( - Expired - ) : ( + {link ? ( + ) : ( + N/A )} -
    - Conference Room: - {`${aliasWithHost}`} -
    -
    - PIN: - {pin} -
    - {!hearing?.scheduledForIsPast && !wasVirtual && ( - + {hearing.conferenceProvider === 'pexip' ? ( + <> +
    + Conference Room: + {aliasWithHost || 'N/A'} +
    +
    + PIN: + {pin || 'N/A'} +
    + + ) : ( +
    + {link} +
    )} +
    ); @@ -70,13 +78,18 @@ VirtualHearingLinkDetails.propTypes = { }; export const LinkContainer = ( - { link, linkText, user, hearing, isVirtual, wasVirtual, virtualHearing, role, label } -) => ( -
    - {label}: - {!virtualHearing || virtualHearing?.status === 'pending' ? ( - {COPY.VIRTUAL_HEARING_SCHEDULING_IN_PROGRESS} - ) : ( + { link, linkText, user, hearing, isVirtual, wasVirtual, virtualHearing, role, label, links } +) => { + // The pin used depends on the role and link used depends on virtual or not + const getPin = () => { + const isPexipHearingCoordinator = (hearing.conferenceProvider === 'pexip' && role === 'HC'); + + return (role === 'VLJ' || isPexipHearingCoordinator) ? links?.hostPin : links?.guestPin; + }; + + return ( +
    + {label}: - )} -
    -); +
    + ); +}; LinkContainer.propTypes = { hearing: PropTypes.object, @@ -102,41 +115,75 @@ LinkContainer.propTypes = { role: PropTypes.string, user: PropTypes.object, virtualHearing: PropTypes.object, - wasVirtual: PropTypes.bool + wasVirtual: PropTypes.bool, + links: PropTypes.object }; -export const HearingLinks = ({ hearing, virtualHearing, isVirtual, wasVirtual, user }) => { - if (!isVirtual && !wasVirtual) { - return null; - } - +export const HearingLinks = ({ hearing, virtualHearing, isVirtual, wasVirtual, user, isCancelled }) => { + const { + scheduledForIsPast, + conferenceProvider, + dailyDocketConferenceLink, + nonVirtualConferenceLink + } = hearing; const showHostLink = virtualHearingRoleForUser(user, hearing) === VIRTUAL_HEARING_HOST; + const getLinks = () => { + if (scheduledForIsPast || isCancelled) { + return null; + } else if (isVirtual) { + return virtualHearing; + } else if (conferenceProvider === 'pexip') { + return dailyDocketConferenceLink; + } else if (conferenceProvider === 'webex') { + return nonVirtualConferenceLink; + } + }; + + const links = getLinks(); + return (
    - {showHostLink && } + {showHostLink && ( + <> + + + + )} -
    ); }; @@ -146,5 +193,6 @@ HearingLinks.propTypes = { hearing: PropTypes.object, isVirtual: PropTypes.bool, wasVirtual: PropTypes.bool, - virtualHearing: PropTypes.object + virtualHearing: PropTypes.object, + isCancelled: PropTypes.bool }; diff --git a/client/app/hearings/components/details/TranscriptionDetailsInputs.jsx b/client/app/hearings/components/details/TranscriptionDetailsInputs.jsx index 442c98ac90b..1b84e71ceeb 100644 --- a/client/app/hearings/components/details/TranscriptionDetailsInputs.jsx +++ b/client/app/hearings/components/details/TranscriptionDetailsInputs.jsx @@ -38,8 +38,8 @@ const TranscriptionDetailsInputs = ({ transcription, update, readOnly }) => ( value: 'Jamison Professional Services' }, { - label: 'The Ravens Group, Inc.', - value: 'The Ravens Group, Inc.' + label: 'Vet Reporting', + value: 'Vet Reporting' } ]} onChange={(option) => update({ transcriber: (option || {}).value })} diff --git a/client/app/hearings/components/details/TranscriptionFilesTable.jsx b/client/app/hearings/components/details/TranscriptionFilesTable.jsx new file mode 100644 index 00000000000..2757b61ecd5 --- /dev/null +++ b/client/app/hearings/components/details/TranscriptionFilesTable.jsx @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { useState, useEffect } from 'react'; +import _ from 'lodash'; +import moment from 'moment-timezone'; + +import Link from '../../../components/Link'; +import Table from '../../../components/Table'; +import { genericRow } from './style'; +import DocketTypeBadge from '../../../components/DocketTypeBadge'; +import { COLORS } from '../../../constants/AppConstants'; +import { DownloadIcon } from '../../../components/icons/DownloadIcon'; + +const transcriptionFileColumns = [ + { + align: 'left', + valueFunction: (rowObject) => rowObject.docketName && ( + + + {rowObject.docketNumber} + + ), + header: 'Docket(s)' + }, + { + align: 'left', + valueName: 'dateUploadAws', + valueFunction: (rowObject) => moment(rowObject.dateUploadAws).format('MM/DD/YYYY'), + header: 'Uploaded', + }, + { + align: 'left', + valueFunction: (rowObject) => ( + + {rowObject.fileName} + + + ), + header: 'File Link' + }, + { + align: 'left', + valueName: 'fileStatus', + header: 'Status' + } +]; + +const rowClassNames = (rowObject) => `${rowObject.isEvenGroup ? 'even' : 'odd'}-row-group`; + +const TranscriptionFilesTable = ({ hearing }) => { + const [rows, setRows] = useState([]); + + // Format table to group files by docket number and style accordingly + const buildRowsFromFileGroups = () => { + // Flatten nested objects into nested arrays + const fileGroups = _.values(hearing.transcriptionFiles).map((rec) => _.values(rec)); + + return fileGroups.map((fileGroup, groupIndex) => { + return fileGroup.map((file, fileIndex) => { + return { + ...file, + isEvenGroup: groupIndex % 2 === 0, + docketName: fileIndex === 0 ? file.hearingType : null, + docketNumber: fileIndex === 0 ? file.docketNumber : null + }; + }); + }).flat(); + }; + + useEffect(() => { + setRows(buildRowsFromFileGroups()); + }, []); + + return ( +
    + index} + rowObjects={rows} + rowClassNames={rowClassNames} + /> + + ); +}; + +TranscriptionFilesTable.propTypes = { + hearing: PropTypes.shape({ + transcriptionFiles: PropTypes.object, + docketName: PropTypes.string, + docketNumber: PropTypes.string + }) +}; + +export default TranscriptionFilesTable; diff --git a/client/app/hearings/components/details/TranscriptionFormSection.jsx b/client/app/hearings/components/details/TranscriptionFormSection.jsx index 1e3169dfcb2..81e73f794b3 100644 --- a/client/app/hearings/components/details/TranscriptionFormSection.jsx +++ b/client/app/hearings/components/details/TranscriptionFormSection.jsx @@ -5,33 +5,51 @@ import { ContentSection } from '../../../components/ContentSection'; import TranscriptionDetailsInputs from './TranscriptionDetailsInputs'; import TranscriptionProblemInputs from './TranscriptionProblemInputs'; import TranscriptionRequestInputs from './TranscriptionRequestInputs'; +import TranscriptionFilesTable from './TranscriptionFilesTable'; +import { genericRow } from './style'; export const TranscriptionFormSection = ( - { hearing, transcription, readOnly, update } + { hearing, transcription, readOnly, update, isLegacy } ) => ( - update('transcription', values)} - readOnly={readOnly} - /> -
    + {/* If Legacy Hearing and conference provider Webex, only render Transcription Files table */} + {!isLegacy && ( + <> + update('transcription', values)} + readOnly={readOnly} + /> +
    -

    Transcription Problem

    - update('transcription', values)} - readOnly={readOnly} - /> -
    +

    Transcription Problem

    + update('transcription', values)} + readOnly={readOnly} + /> +
    -

    Transcription Request

    - update('hearing', values)} - readOnly={readOnly} - /> +

    Transcription Request

    + update('hearing', values)} + readOnly={readOnly} + /> + {hearing.conferenceProvider === 'webex' &&
    } + + )} + + {/* If conference provider not Webex, do not render Transcriptoin Files table */} + {hearing.conferenceProvider === 'webex' && ( + <> +

    Transcription Files

    + + + )} ); @@ -39,5 +57,6 @@ TranscriptionFormSection.propTypes = { update: PropTypes.func, hearing: PropTypes.object, readOnly: PropTypes.bool, - transcription: PropTypes.object + transcription: PropTypes.object, + isLegacy: PropTypes.bool }; diff --git a/client/app/hearings/components/details/VirtualHearingFields.jsx b/client/app/hearings/components/details/VirtualHearingFields.jsx index 08c807de10d..c3e82d32322 100644 --- a/client/app/hearings/components/details/VirtualHearingFields.jsx +++ b/client/app/hearings/components/details/VirtualHearingFields.jsx @@ -1,27 +1,36 @@ import PropTypes from 'prop-types'; import React, { useContext } from 'react'; +import { css } from 'glamor'; import { ContentSection } from '../../../components/ContentSection'; import { HearingLinks } from './HearingLinks'; import { HearingsUserContext } from '../../contexts/HearingsUserContext'; +import StringUtil from '../../../util/StringUtil'; export const VirtualHearingFields = ({ hearing, virtualHearing }) => { - if (!hearing?.isVirtual && !hearing?.wasVirtual) { - return null; - } - const user = useContext(HearingsUserContext); + const checkCancelled = () => { + const disposition = ['postponed', 'cancelled', 'scheduled_in_error']; + + return disposition.includes(hearing?.disposition); + }; + return ( +
    + {StringUtil.capitalizeFirst(hearing?.conferenceProvider || 'Pexip')} Hearing +
    ); @@ -35,7 +44,8 @@ VirtualHearingFields.propTypes = { appellantIsNotVeteran: PropTypes.bool, scheduledForIsPast: PropTypes.bool, wasVirtual: PropTypes.bool, - isVirtual: PropTypes.bool + isVirtual: PropTypes.bool, + conferenceProvider: PropTypes.string }), initialHearing: PropTypes.shape({ virtualHearing: PropTypes.object @@ -44,7 +54,7 @@ VirtualHearingFields.propTypes = { virtualHearing: PropTypes.shape({ appellantEmail: PropTypes.string, representativeEmail: PropTypes.string, - jobCompleted: PropTypes.bool + jobCompleted: PropTypes.bool, }), errors: PropTypes.shape({ appellantEmail: PropTypes.string, diff --git a/client/app/hearings/components/scheduleHearing/TimeSlot.jsx b/client/app/hearings/components/scheduleHearing/TimeSlot.jsx index eb5d51d7d4e..add0040e72f 100644 --- a/client/app/hearings/components/scheduleHearing/TimeSlot.jsx +++ b/client/app/hearings/components/scheduleHearing/TimeSlot.jsx @@ -3,7 +3,12 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; // Local Dependencies -import { setTimeSlots, TIMEZONES_WITH_LUNCHBREAK, regionalOfficeDetails } from '../../utils'; +import { + setTimeSlots, + TIMEZONES_WITH_LUNCHBREAK, + regionalOfficeDetails, + getFriendlyZoneName +} from '../../utils'; import { TimeSlotButton } from './TimeSlotButton'; import Button from '../../../components/Button'; import SmallLoader from '../../../components/SmallLoader'; @@ -59,8 +64,12 @@ export const TimeSlot = ({ const handleChange = (time, custom = false) => { setSelected(time); setIsCustomTime(custom); + + // Handles aliased timezones, like America/Boise => Mountain Time. + const friendlyZone = getFriendlyZoneName(roTimezone); // Time zone name is expected in tasks#update. Similar to HearingTime component - const tzName = Object.keys(TIMEZONES).find((key) => TIMEZONES[key] === roTimezone) || 'Eastern Time (US & Canada)'; + const tzName = Object.keys(TIMEZONES).find((key) => + TIMEZONES[key] === friendlyZone) || 'Eastern Time (US & Canada)'; onChange('scheduledTimeString', `${time.tz(roTimezone).format('h:mm A')} ${tzName}`); }; diff --git a/client/app/hearings/constants.js b/client/app/hearings/constants.js index c4407d2267b..c430e7a7c49 100644 --- a/client/app/hearings/constants.js +++ b/client/app/hearings/constants.js @@ -91,14 +91,6 @@ export const ACTIONS = { HANDLE_CONFERENCE_LINK_ERROR: 'HANDLE_CONFERENCE_LINK_ERROR' }; -// Labels for guest link -export const GUEST_LINK_LABELS = { - COPY_GUEST_LINK: 'Copy Guest Link', - GUEST_LINK_SECTION_LABEL: 'Guest links for non-virtual hearings', - GUEST_CONFERENCE_ROOM: 'Conference Room', - GUEST_PIN: 'PIN', -} - export const SPREADSHEET_TYPES = { RoSchedulePeriod: { value: 'RoSchedulePeriod', diff --git a/client/app/hearings/utils.js b/client/app/hearings/utils.js index b4a38677e5a..72994842d10 100644 --- a/client/app/hearings/utils.js +++ b/client/app/hearings/utils.js @@ -219,6 +219,11 @@ export const virtualHearingLinkLabelFull = (role) => COPY.VLJ_VIRTUAL_HEARING_LINK_LABEL_FULL : COPY.REPRESENTATIVE_VIRTUAL_HEARING_LINK_LABEL; +export const virtualHearingScheduledDatePassedLabelFull = (role) => +role === VIRTUAL_HEARING_HOST ? +`${COPY.VLJ_VIRTUAL_HEARING_LINK_LABEL_FULL}: N/A` : +`${COPY.REPRESENTATIVE_VIRTUAL_HEARING_PASSED_LABEL}: N/A`; + export const pollVirtualHearingData = (hearingId, onSuccess) => ( // Did not specify retryCount so if api call fails, it'll stop polling. // If need to retry on failure, pass in retryCount diff --git a/client/app/queue/OrganizationUsers.jsx b/client/app/queue/OrganizationUsers.jsx index 886850ccba6..de900bf9009 100644 --- a/client/app/queue/OrganizationUsers.jsx +++ b/client/app/queue/OrganizationUsers.jsx @@ -1,8 +1,9 @@ +/* eslint-disable max-lines */ /* eslint-disable no-nested-ternary */ /* eslint-disable max-len */ + import React from 'react'; import PropTypes from 'prop-types'; -import { css } from 'glamor'; import { sprintf } from 'sprintf-js'; import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; @@ -16,33 +17,7 @@ import { LOGO_COLORS } from '../constants/AppConstants'; import COPY from '../../COPY'; import LoadingDataDisplay from '../components/LoadingDataDisplay'; import MembershipRequestTable from './MembershipRequestTable'; - -const userStyle = css({ - margin: '.5rem 0 .5rem', - padding: '.5rem 0 .5rem', - listStyle: 'none' -}); -const topUserStyle = css({ - borderTop: '.1rem solid gray', - margin: '.5rem 0 .5rem', - padding: '1rem 0 .5rem', - listStyle: 'none' -}); -const topUserBorder = css({ - borderBottom: '.1rem solid gray', -}); -const buttonStyle = css({ - paddingRight: '1rem', - display: 'inline-block' -}); -const buttonContainerStyle = css({ - borderBottom: '1rem solid gray', - borderWidth: '1px', - padding: '.5rem 0 2rem', -}); -const listStyle = css({ - listStyle: 'none' -}); +import SelectConferenceTypeRadioField from './SelectConferenceTypeRadioField'; export default class OrganizationUsers extends React.PureComponent { constructor(props) { @@ -226,7 +201,7 @@ export default class OrganizationUsers extends React.PureComponent { } adminButton = (user, admin) => -
    removeUserButton = (user) => -

    {COPY.USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL}

    -
      +
        { (judgeTeam || dvcTeam) ? '' :
      • {COPY.USER_MANAGEMENT_ADMIN_RIGHTS_HEADING}{COPY.USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION}
      • }
      • {COPY.USER_MANAGEMENT_REMOVE_USER_HEADING}{ judgeTeam ? COPY.USER_MANAGEMENT_JUDGE_TEAM_REMOVE_USER_DESCRIPTION : @@ -334,7 +336,7 @@ getFilteredUsers = () => {
    { listOfUsers.length > 0 ? ( -
      {listOfUsers}
    +
      {listOfUsers}
    ) : ( <>

    No results found

    @@ -342,7 +344,6 @@ getFilteredUsers = () => { ) } -
    ; } @@ -414,5 +415,6 @@ getFilteredUsers = () => { } OrganizationUsers.propTypes = { - organization: PropTypes.string + organization: PropTypes.string, + conferenceSelectionVisibility: PropTypes.bool }; diff --git a/client/app/queue/QueueApp.jsx b/client/app/queue/QueueApp.jsx index a0c0a7c447d..06d50aa6530 100644 --- a/client/app/queue/QueueApp.jsx +++ b/client/app/queue/QueueApp.jsx @@ -17,6 +17,7 @@ import { setCanEditCavcDashboards, setCanViewCavcDashboards, setFeatureToggles, + setMeetingType, setUserId, setUserRole, setUserCssId, @@ -115,6 +116,7 @@ class QueueApp extends React.PureComponent { this.props.setCanEditAod(this.props.canEditAod); this.props.setCanEditNodDate(this.props.userCanViewEditNodDate); this.props.setUserIsCobAdmin(this.props.userIsCobAdmin); + this.props.setMeetingType(this.props.conferenceProvider); this.props.setCanEditCavcRemands(this.props.canEditCavcRemands); this.props.setCanEditCavcDashboards(this.props.canEditCavcDashboards); this.props.setCanViewCavcDashboards(this.props.canViewCavcDashboards); @@ -602,7 +604,9 @@ class QueueApp extends React.PureComponent { }; routedOrganizationUsers = (props) => ( - + ); routedTeamManagement = (props) => ; @@ -948,7 +952,8 @@ class QueueApp extends React.PureComponent { render={this.routedAssignToUser} /> @@ -1161,7 +1166,8 @@ class QueueApp extends React.PureComponent { /> ({ @@ -1496,6 +1504,7 @@ const mapDispatchToProps = (dispatch) => setCanEditAod, setCanEditNodDate, setUserIsCobAdmin, + setMeetingType, setCanEditCavcRemands, setCanEditCavcDashboards, setCanViewCavcDashboards, diff --git a/client/app/queue/SelectConferenceTypeRadioField.jsx b/client/app/queue/SelectConferenceTypeRadioField.jsx new file mode 100644 index 00000000000..52c3417e2a5 --- /dev/null +++ b/client/app/queue/SelectConferenceTypeRadioField.jsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import ApiUtil from '../util/ApiUtil'; + +import RadioField from '../components/RadioField'; +import COPY from '../../COPY'; + +const radioOptions = [ + { displayText: 'Pexip', value: 'pexip' }, + { displayText: 'Webex', value: 'webex' }, +]; + +const SelectConferenceTypeRadioField = ({ + name, + conferenceProvider, + organization, + user, +}) => { + const [value, setValue] = useState(conferenceProvider); + + const modifyConferenceType = (newConferenceProvider) => { + const payload = { + data: { + ...user, + attributes: { + ...user.attributes, + conference_provider: newConferenceProvider, + }, + }, + }; + + ApiUtil.patch(`/organizations/${organization}/users/${user.id}`, payload); + }; + + return ( + <> + + setValue(newValue) || modifyConferenceType(newValue) + } + vertical + /> + + ); +}; + +SelectConferenceTypeRadioField.propTypes = { + name: PropTypes.string, + onClick: PropTypes.func, + conferenceProvider: PropTypes.string, + organization: PropTypes.string, + user: PropTypes.shape({ + id: PropTypes.string, + attributes: PropTypes.object, + }), +}; + +export default SelectConferenceTypeRadioField; diff --git a/client/app/queue/uiReducer/uiActions.js b/client/app/queue/uiReducer/uiActions.js index b479848b394..47fbe37eb02 100644 --- a/client/app/queue/uiReducer/uiActions.js +++ b/client/app/queue/uiReducer/uiActions.js @@ -48,6 +48,13 @@ export const setUserIsCobAdmin = (userIsCobAdmin) => ({ } }); +export const setMeetingType = (conferenceProvider) => ({ + type: ACTIONS.SET_CONFERENCE_PROVIDER, + payload: { + conferenceProvider + } +}); + export const setCanViewOvertimeStatus = (canViewOvertimeStatus) => ({ type: ACTIONS.SET_CAN_VIEW_OVERTIME_STATUS, payload: { diff --git a/client/app/queue/uiReducer/uiConstants.js b/client/app/queue/uiReducer/uiConstants.js index 15723d7f431..d8dab0638ab 100644 --- a/client/app/queue/uiReducer/uiConstants.js +++ b/client/app/queue/uiReducer/uiConstants.js @@ -8,6 +8,7 @@ export const ACTIONS = { SET_FEEDBACK_URL: 'SET_FEEDBACK_URL', SET_TARGET_USER: 'SET_TARGET_USER', + SET_CONFERENCE_PROVIDER: 'SET_CONFERENCE_PROVIDER', HIGHLIGHT_INVALID_FORM_ITEMS: 'HIGHLIGHT_INVALID_FORM_ITEMS', RESET_ERROR_MESSAGES: 'RESET_ERROR_MESSAGES', diff --git a/client/app/queue/uiReducer/uiReducer.js b/client/app/queue/uiReducer/uiReducer.js index 96a66077b1c..94dc63eaf7e 100644 --- a/client/app/queue/uiReducer/uiReducer.js +++ b/client/app/queue/uiReducer/uiReducer.js @@ -100,6 +100,10 @@ const workQueueUiReducer = (state = initialState, action = {}) => { return update(state, { userIsCobAdmin: { $set: action.payload.userIsCobAdmin } }); + case ACTIONS.SET_CONFERENCE_PROVIDER: + return update(state, { + conferenceProvider: { $set: action.payload.conferenceProvider } + }); case ACTIONS.SET_CAN_EDIT_CAVC_REMANDS: return update(state, { canEditCavcRemands: { $set: action.payload.canEditCavcRemands } diff --git a/client/app/reader/DecisionReviewer.jsx b/client/app/reader/DecisionReviewer.jsx index d1f7478f9a8..310dd2d8156 100644 --- a/client/app/reader/DecisionReviewer.jsx +++ b/client/app/reader/DecisionReviewer.jsx @@ -33,7 +33,8 @@ export class DecisionReviewer extends React.PureComponent { super(props); this.state = { - isCommentLabelSelected: false + isCommentLabelSelected: false, + zoomLevel: 100 }; this.routedPdfListView.displayName = 'RoutedPdfListView'; @@ -83,6 +84,10 @@ export class DecisionReviewer extends React.PureComponent { } }; + updateZoomLevel = (newZoomLevel) => { + this.setState({ zoomLevel: newZoomLevel }); + }; + routedPdfListView = (props) => { const { vacolsId } = props.match.params; @@ -142,6 +147,8 @@ export class DecisionReviewer extends React.PureComponent { allDocuments={_.values(this.props.storeDocuments)} showPdf={this.showPdf(props.history, vacolsId)} documentPathBase={`/${vacolsId}/documents`} + zoomLevel={this.state.zoomLevel} + onZoomChange={this.updateZoomLevel} {...props} /> diff --git a/client/app/reader/SideBarCategories.jsx b/client/app/reader/SideBarCategories.jsx index 351ae14d778..049a430f0e1 100644 --- a/client/app/reader/SideBarCategories.jsx +++ b/client/app/reader/SideBarCategories.jsx @@ -42,8 +42,7 @@ class SideBarCategories extends PureComponent { const categoryToggleStates = _.mapValues( Constants.documentCategories, - (val, key) => - documents[doc.id][categoryFieldNameOfCategoryName(key)] + (val, key) => documents[doc.id][categoryFieldNameOfCategoryName(key)] ); return
    diff --git a/client/app/reader/reducers.js b/client/app/reader/reducers.js index 1e9d9c32132..b3e7d49e47a 100644 --- a/client/app/reader/reducers.js +++ b/client/app/reader/reducers.js @@ -9,7 +9,7 @@ import pdfViewerReducer from './PdfViewer/PdfViewerReducer'; import documentsReducer from './Documents/DocumentsReducer'; import annotationLayerReducer from './AnnotationLayer/AnnotationLayerReducer'; -const rootReducer = combineReducers({ +export const rootReducer = combineReducers({ caseSelect: caseSelectReducer, pdf: pdfReducer, searchActionReducer, diff --git a/client/app/reader/selectors.js b/client/app/reader/selectors.js index 44461d32011..79ba0a923bb 100644 --- a/client/app/reader/selectors.js +++ b/client/app/reader/selectors.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import { keyBy, memoize, reject, sum, uniqBy, map, mapValues, filter, size, values, some } from 'lodash'; -const getFilteredDocIds = (state) => state.documentList.filteredDocIds; +export const getFilteredDocIds = (state) => state.documentList.filteredDocIds; const getAllDocs = (state) => state.documents; export const getFilteredDocuments = createSelector( diff --git a/client/app/reader/utils.js b/client/app/reader/utils.js index 9d07ca53c2a..374adb74d2c 100644 --- a/client/app/reader/utils.js +++ b/client/app/reader/utils.js @@ -48,7 +48,7 @@ export const rotateCoordinates = ({ x, y }, container, rotation) => { case 180: rotatedCoords = { x: container.width - x, y: container.height - y }; break; - case 27: + case 270: rotatedCoords = { x: container.height - y, y: x }; break; default: @@ -80,6 +80,7 @@ export const getPageCoordinatesOfMouseEventPrototype = (event, container, scale, rotation ); }; + /** * immutability-helper takes two arguments: an object and a spec for how to change it: * diff --git a/client/app/readerprototype/DocumentViewer.jsx b/client/app/readerprototype/DocumentViewer.jsx index 27d7c031f52..c2a5c8b00fc 100644 --- a/client/app/readerprototype/DocumentViewer.jsx +++ b/client/app/readerprototype/DocumentViewer.jsx @@ -1,35 +1,33 @@ import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; - +import { Helmet } from 'react-helmet'; import PdfDocument from './components/PdfDocument'; import ReaderFooter from './components/ReaderFooter'; import ReaderSearchBar from './components/ReaderSearchBar'; import ReaderSidebar from './components/ReaderSidebar'; import ReaderToolbar from './components/ReaderToolbar'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { CATEGORIES } from '../reader/analytics'; import { stopPlacingAnnotation } from '../reader/AnnotationLayer/AnnotationActions'; import DeleteModal from './components/Comments/DeleteModal'; import ShareModal from './components/Comments/ShareModal'; -import { getNextDocId, getPrevDocId, getRotationDeg, selectedDoc, selectedDocIndex } from './util/documentUtil'; -import { ROTATION_DEGREES } from './util/readerConstants'; - -const ZOOM_LEVEL_MIN = 20; -const ZOOM_LEVEL_MAX = 300; -const ZOOM_INCREMENT = 20; +import { getRotationDeg } from './util/documentUtil'; +import { ROTATION_DEGREES, ZOOM_INCREMENT, ZOOM_LEVEL_MAX, ZOOM_LEVEL_MIN } from './util/readerConstants'; +import { showSideBarSelector } from './selectors'; +import { togglePdfSidebar } from '../reader/PdfViewer/PdfViewerActions'; const DocumentViewer = (props) => { const [currentPage, setCurrentPage] = useState(1); const [numPages, setNumPages] = useState(null); const [rotateDeg, setRotateDeg] = useState('0deg'); const [showSearchBar, setShowSearchBar] = useState(false); - const [showSideBar, setShowSideBar] = useState(true); - const [zoomLevel, setZoomLevel] = useState(100); - const [disabled, setDisabled] = useState(true); + const [isDocumentLoadError, setIsDocumentLoadError] = useState(false); + const showSideBar = useSelector(showSideBarSelector); const dispatch = useDispatch(); const currentDocumentId = Number(props.match.params.docId); + const doc = props.allDocuments.find((x) => x.id === currentDocumentId); useEffect(() => { setShowSearchBar(false); @@ -41,17 +39,21 @@ const DocumentViewer = (props) => { event.preventDefault(); setShowSearchBar(false); } + const metaKey = navigator.appVersion.includes('Win') ? 'ctrlKey' : 'metaKey'; - if (event.metaKey && event.code === 'KeyF') { + if (event[metaKey] && event.code === 'KeyF') { event.preventDefault(); setShowSearchBar(true); } if (event.altKey && event.code === 'Backspace') { window.analyticsEvent(CATEGORIES.VIEW_DOCUMENT_PAGE, 'back-to-claims-folder'); - dispatch(stopPlacingAnnotation('from-back-to-documents')); props.history.push(props.documentPathBase); } + + if (event.altKey && event.code === 'KeyM' && !event.shiftKey) { + dispatch(togglePdfSidebar()); + } }; window.addEventListener('keydown', keyHandler); @@ -59,8 +61,6 @@ const DocumentViewer = (props) => { return () => window.removeEventListener('keydown', keyHandler); }, []); - const doc = selectedDoc(props); - const getPageNumFromScrollTop = (event) => { const { clientHeight, scrollTop, scrollHeight } = event.target; const pageHeightEstimate = @@ -76,61 +76,84 @@ const DocumentViewer = (props) => { } }; - document.body.style.overflow = 'hidden'; + const handleZoomIn = () => { + const newZoomLevel = props.zoomLevel + ZOOM_INCREMENT; + + props.onZoomChange(newZoomLevel); + }; + + const handleZoomOut = () => { + const newZoomLevel = props.zoomLevel - ZOOM_INCREMENT; + + props.onZoomChange(newZoomLevel); + }; + + useEffect(() => { + document.body.style.overflow = 'hidden'; + + return () => document.body.style.overflow = 'auto'; + }, [window.location.pathname]); + + useEffect(() => { + dispatch(stopPlacingAnnotation('navigation')); + }, [doc.id, dispatch]); return ( -
    -
    - setZoomLevel(100)} - rotateDocument={() => setRotateDeg(getRotationDeg(rotateDeg))} - setZoomInLevel={() => setZoomLevel(zoomLevel + ZOOM_INCREMENT)} - setZoomOutLevel={() => setZoomLevel(zoomLevel - ZOOM_INCREMENT)} - showClaimsFolderNavigation={props.allDocuments.length > 1} - showSearchBar={showSearchBar} - toggleSearchBar={setShowSearchBar} - showSideBar={showSideBar} - toggleSideBar={() => setShowSideBar(true)} - zoomLevel={zoomLevel} - /> - {showSearchBar && } -
    - + + {`${(doc?.type) || ''} | Document Viewer | Caseflow Reader`} + +
    +
    + props.onZoomChange(100)} + rotateDocument={() => setRotateDeg(getRotationDeg(rotateDeg))} + setZoomInLevel={handleZoomIn} + setZoomOutLevel={handleZoomOut} + showClaimsFolderNavigation={props.allDocuments.length > 1} + showSearchBar={showSearchBar} + toggleSearchBar={setShowSearchBar} + showSideBar={showSideBar} + toggleSideBar={() => dispatch(togglePdfSidebar())} + zoomLevel={props.zoomLevel} + /> + {showSearchBar && } +
    + +
    + setCurrentPage()} + showPdf={props.showPdf} />
    - setCurrentPage()} - selectedDocIndex={selectedDocIndex(props)} - showNextDocument={props.showPdf(getNextDocId(props))} - showPreviousDocument={props.showPdf(getPrevDocId(props))} - disablePreviousNext={disabled} - /> + {showSideBar && ( + dispatch(togglePdfSidebar())} + vacolsId={props.match.params.vacolsId} + /> + )} + +
    - {showSideBar && ( - setShowSideBar(false)} - vacolsId={props.match.params.vacolsId} - /> - )} - - -
    + ); }; @@ -141,7 +164,9 @@ DocumentViewer.propTypes = { fetchAppealDetails: PropTypes.func, history: PropTypes.any, showPdf: PropTypes.func, - match: PropTypes.object + match: PropTypes.object, + zoomLevel: PropTypes.number, + onZoomChange: PropTypes.func }; export default DocumentViewer; diff --git a/client/app/readerprototype/components/Comments/Layer.jsx b/client/app/readerprototype/components/Comments/Layer.jsx index 0914de0ef96..d17345e601b 100644 --- a/client/app/readerprototype/components/Comments/Layer.jsx +++ b/client/app/readerprototype/components/Comments/Layer.jsx @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import noop from 'lodash/noop'; import PropTypes from 'prop-types'; import React, { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -13,6 +13,7 @@ import { import { handleSelectCommentIcon } from '../../../reader/PdfViewer/PdfViewerActions'; import { getPageCoordinatesOfMouseEventPrototype } from '../../../reader/utils'; import { annotationPlacement, annotationsForDocumentId, annotationsForDocumentIdAndPageId } from '../../selectors'; +import { centerOfPage, iconKeypressOffset } from '../../util/coordinates'; import Icon from './Icon'; // This comment provides the framework for positioning, moving and selecting comment icons. @@ -27,7 +28,7 @@ import Icon from './Icon'; // The second is after they've clicked but haven't saved it yet (ie, they still need to type in their comment) // The third is the display of the comments already saved that were fetched from the db. const Layer = (props) => { - const { zoomLevel, pageNumber, documentId, rotation, children, dimensions } = props; + const { zoomLevel, pageNumber, documentId, rotation, children, dimensions, isCurrentPage } = props; const scale = zoomLevel / 100; const rotationDegrees = Number(rotation.replace('deg', '')); @@ -55,8 +56,37 @@ const Layer = (props) => { } } - if (event.altKey && event.code === 'KeyC') { + if (isPlacingAnnotation && placingAnnotationIconPageCoords?.pageIndex === pageNumber && [ + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight' + ].includes(event.key) + ) { + event.preventDefault(); + + dispatch( + showPlaceAnnotationIcon( + pageNumber, + iconKeypressOffset( + placingAnnotationIconPageCoords, + event.key, + rotation + ) + ) + ); + } + + if (event.altKey && event.code === 'KeyC' && isCurrentPage) { dispatch(startPlacingAnnotation(INTERACTION_TYPES.KEYBOARD_SHORTCUT)); + + const boundingRect = layerRef.current?.getBoundingClientRect(); + + if (boundingRect) { + const coords = centerOfPage(boundingRect, rotationDegrees); + + dispatch(showPlaceAnnotationIcon(pageNumber, coords)); + } } if (event.altKey && event.code === 'Enter') { @@ -78,7 +108,7 @@ const Layer = (props) => { window.addEventListener('keydown', keyHandler); return () => window.removeEventListener('keydown', keyHandler); - }, [isPlacingAnnotation, placingAnnotationIconPageCoords]); + }, [isPlacingAnnotation, placingAnnotationIconPageCoords, rotation, isCurrentPage]); const onPageDragOver = (event) => { event.preventDefault(); @@ -199,7 +229,7 @@ const Layer = (props) => { pageIndex: placingAnnotationIconPageCoords.pageIndex, }} comment={{}} - onClick={_.noop} + onClick={noop} /> )} {!isPlacingAnnotation && placedButUnsavedAnnotation && placedButUnsavedAnnotation.page === pageNumber && ( @@ -208,7 +238,7 @@ const Layer = (props) => { comment={placedButUnsavedAnnotation} rotation={-rotationDegrees} position={{ x: placedButUnsavedAnnotation.x * scale, y: placedButUnsavedAnnotation.y * scale }} - onClick={_.noop} + onClick={noop} /> )} {annotations.map((annotation) => ( @@ -218,7 +248,7 @@ const Layer = (props) => { comment={annotation} rotation={-rotationDegrees} position={{ x: annotation.x * scale, y: annotation.y * scale }} - onClick={annotation.isPlacingAnnotation ? _.noop : () => onIconClick(annotation)} + onClick={annotation.isPlacingAnnotation ? noop : () => onIconClick(annotation)} /> ))}
    @@ -235,6 +265,7 @@ Layer.propTypes = { rotation: PropTypes.string, children: PropTypes.element, dimensions: PropTypes.object, + isCurrentPage: PropTypes.bool }; export default Layer; diff --git a/client/app/readerprototype/components/Comments/List.jsx b/client/app/readerprototype/components/Comments/List.jsx index f2ce872fae3..85eb66995ef 100644 --- a/client/app/readerprototype/components/Comments/List.jsx +++ b/client/app/readerprototype/components/Comments/List.jsx @@ -48,7 +48,7 @@ const List = (props) => { }; List.propTypes = { - annotations: PropTypes.object, + annotations: PropTypes.array, onSelect: PropTypes.func, selectedAnnotationId: PropTypes.number, }; diff --git a/client/app/readerprototype/components/Page.jsx b/client/app/readerprototype/components/Page.jsx index db728164e79..ee0a655b161 100644 --- a/client/app/readerprototype/components/Page.jsx +++ b/client/app/readerprototype/components/Page.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import React, { useEffect, useRef } from 'react'; -import { ROTATION_DEGREES } from '../util/readerConstants'; +import React, { memo, useEffect, useRef, useState } from 'react'; import usePageVisibility from '../hooks/usePageVisibility'; +import { ROTATION_DEGREES } from '../util/readerConstants'; // This Page component is expected to be used within a flexbox container. Flex doesn't notice when children are // transformed (scaled and rotated). Where * is the flex container: @@ -28,12 +28,16 @@ import usePageVisibility from '../hooks/usePageVisibility'; // When rotating, we swap height and width of the container. // The child is still centered in the container, so we must offset it put it back to the // top / center of the container. -const Page = ({ page, rotation = ROTATION_DEGREES.ZERO, renderItem, scale }) => { +const Page = memo(({ page, rotation = ROTATION_DEGREES.ZERO, renderItem, scale, setRenderingMetrics }) => { const canvasRef = useRef(null); const isVisible = usePageVisibility(canvasRef); const wrapperRef = useRef(null); - const scaleFraction = scale / 100; + const renderTimeout = useRef(null); + const [previousScale, setPreviousScale] = useState(scale); + const [hasRendered, setHasRendered] = useState(false); + const reportedStatsRef = useRef(false); + const scaleFraction = scale / 100; const viewport = page.getViewport({ scale: scaleFraction }); const scaledHeight = viewport.height; const scaledWidth = viewport.width; @@ -63,14 +67,88 @@ const Page = ({ page, rotation = ROTATION_DEGREES.ZERO, renderItem, scale }) => contentVisibility: 'auto', }; + const render = () => { + if (canvasRef.current && isVisible && !hasRendered) { + const task = page.render({ canvasContext: canvasRef.current.getContext('2d', { alpha: false }), viewport }); + + task.promise.then(() => { + if (scale === previousScale) { + setHasRendered(true); + } else { + // if the scale has changed while this was processing, render it again + clearTimeout(renderTimeout.current); + renderTimeout.current = setTimeout(render, 0); + + } + }).catch(() => { + clearTimeout(renderTimeout.current); + renderTimeout.current = setTimeout(render, 0); + }); + } + }; + + // render immediately when the canvas ref is ready + useEffect(() => { + clearTimeout(renderTimeout.current); + renderTimeout.current = setTimeout(render, 0); + }, [canvasRef.current]); + + // render when the page becomes visible. only do it the first time at this zoom level + // so that scrolling doesn't trigger rerenders useEffect(() => { - if (canvasRef.current && isVisible) { - page.render({ canvasContext: canvasRef.current?.getContext('2d', { alpha: false }), viewport }). - promise.catch(() => { - // this catch is necessary to prevent the error: Cannot use the same canvas during multiple render operations - }); + if (isVisible) { + clearTimeout(renderTimeout.current); + renderTimeout.current = setTimeout(render, 500); } - }, [canvasRef.current, viewport, isVisible]); + + }, [isVisible]); + + // render when hasRendered has been reset to false. if the page isn't visible, the render + // function ignores the render request + useEffect(() => { + if (hasRendered) { + const pageStats = page?._stats; + + if (pageStats && Array.isArray(pageStats.times)) { + + const renderingTimes = pageStats.times.find((time) => time.name === 'Rendering'); + + if (!reportedStatsRef.current && renderingTimes) { + setRenderingMetrics(renderingTimes.end - renderingTimes.start); + reportedStatsRef.current = true; + } + } + } else { + clearTimeout(renderTimeout.current); + renderTimeout.current = setTimeout(render, 0); + } + }, [hasRendered]); + + // as we zoom in and out, we need to re-render + useEffect(() => { + clearTimeout(renderTimeout.current); + renderTimeout.current = setTimeout(render, 1000); + }, [scale, previousScale]); + + // clean up the timeout if we navigate away + useEffect(() => { + return () => { + clearTimeout(renderTimeout.current); + }; + }, []); + + // previousScale keeps track of the previous value of scale. if scale changes, that will trigger a component + // rerender. before that happens, we'd normally try to finish the current component render. + // since we are about to rerender the whole component anyway, short-circuit the current one to improve + // performance. + if (previousScale !== scale) { + setPreviousScale(scale); + setHasRendered(false); + + return; + } + + const hideCanvas = !hasRendered || scale !== previousScale; return (
    > })}
    ); -}; +}); Page.propTypes = { page: PropTypes.object, rotation: PropTypes.string, renderItem: PropTypes.func, scale: PropTypes.number, + setRenderingMetrics: PropTypes.func }; export default Page; diff --git a/client/app/readerprototype/components/PdfDocument.jsx b/client/app/readerprototype/components/PdfDocument.jsx index d0b2ec2f902..fe81cb945ef 100644 --- a/client/app/readerprototype/components/PdfDocument.jsx +++ b/client/app/readerprototype/components/PdfDocument.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import Layer from './Comments/Layer'; import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist'; @@ -9,39 +9,116 @@ import ApiUtil from '../../util/ApiUtil'; import Page from './Page'; import TextLayer from './TextLayer'; import DocumentLoadError from './DocumentLoadError'; +import { useDispatch } from 'react-redux'; +import { selectCurrentPdf } from 'app/reader/Documents/DocumentsActions'; +import { storeMetrics } from '../../util/Metrics'; -const PdfDocument = ({ doc, rotateDeg, setNumPages, zoomLevel, onLoad }) => { - const [isDocumentLoadError, setIsDocumentLoadError] = useState(false); +const PdfDocument = ({ + currentPage, + doc, + isDocumentLoadError, + rotateDeg, + setIsDocumentLoadError, + setNumPages, + zoomLevel }) => { const [pdfDoc, setPdfDoc] = useState(null); const [pdfPages, setPdfPages] = useState([]); + const dispatch = useDispatch(); + const pdfMetrics = useRef({ renderedPageCount: 0, renderedTimeTotal: 0 }); + const [allPagesRendered, setAllPagesRendered] = useState(false); + const [metricsLogged, setMetricsLogged] = useState(false); + const metricsLoggedRef = useRef(metricsLogged); const containerStyle = { width: '100%', height: '100%', overflow: 'auto', paddingTop: '10px', + paddingLeft: '6px', + paddingRight: '6px', alignContent: 'start', justifyContent: 'center', - gap: '5rem', + gap: '8rem', + }; + + const handleRenderingMetrics = (renderingTime) => { + if (renderingTime) { + pdfMetrics.current.renderedTimeTotal += renderingTime; + pdfMetrics.current.renderedPageCount += 1; + if (pdfMetrics.current.renderedPageCount === pdfPages.length && pdfPages.length > 0) { + setAllPagesRendered(true); + } + } + }; + + const getFirstPageOverallTime = () => { + if (pdfPages && pdfPages.length > 0) { + const firstPageStats = pdfPages[0]?._stats; + + if (firstPageStats && Array.isArray(firstPageStats.times)) { + + const overallTime = firstPageStats.times.find((time) => time.name === 'Overall'); + + if (overallTime) { + return overallTime.end - overallTime.start; + } + } + } + + return 0; + }; + + const logMetrics = () => { + const calculatedAverage = Math.round( + pdfMetrics.current.renderedPageCount > 0 ? + pdfMetrics.current.renderedTimeTotal / pdfMetrics.current.renderedPageCount : 0 + ); + + storeMetrics( + doc.id, + { + document_request_time: pdfMetrics.current.getEndTime - pdfMetrics.current.getStartTime, + number_of_pages_rendered: pdfMetrics.current.renderedPageCount, + rendering_time_for_allPages: pdfMetrics.current.renderedTimeTotal, + average_rendering_time_per_page: calculatedAverage, + first_page_overall_time: getFirstPageOverallTime(), + }, + { + message: 'Reader Prototype times in milliseconds', + type: 'performance', + product: 'reader prototype', + start: null, + end: null, + duration: null, + }, + null + ); + + setMetricsLogged(true); }; useEffect(() => { const getDocData = async () => { + pdfMetrics.current.renderedPageCount = 0; + pdfMetrics.current.renderedTimeTotal = 0; setPdfDoc(null); setPdfPages([]); - onLoad(true); + setAllPagesRendered(false); + setMetricsLogged(false); const requestOptions = { cache: true, withCredentials: true, timeout: true, responseType: 'arraybuffer', }; + + pdfMetrics.current.getStartTime = new Date().getTime(); const byteArr = await ApiUtil.get(doc.content_url, requestOptions).then((response) => { return response.body; }); - onLoad(false); - const docProxy = await getDocument({ data: byteArr }).promise; + pdfMetrics.current.getEndTime = new Date().getTime(); + const docProxy = await getDocument({ data: byteArr, pdfBug: true, verbosity: 0 }).promise; if (docProxy) { setPdfDoc(docProxy); @@ -73,6 +150,29 @@ const PdfDocument = ({ doc, rotateDeg, setNumPages, zoomLevel, onLoad }) => { getPdfData(); }, [pdfDoc]); + useEffect(() => { + dispatch(selectCurrentPdf(doc.id)); + }, [doc.id]); + + useEffect(() => { + if (allPagesRendered && !metricsLogged) { + logMetrics(); + } + }, [allPagesRendered, metricsLogged]); + + useEffect(() => { + return () => { + if (!metricsLoggedRef.current) { + + logMetrics(); + } + }; + }, [doc.id]); + + useEffect(() => { + metricsLoggedRef.current = metricsLogged; + }, [metricsLogged]); + return (
    {isDocumentLoadError && } @@ -83,10 +183,12 @@ const PdfDocument = ({ doc, rotateDeg, setNumPages, zoomLevel, onLoad }) => { rotation={rotateDeg} key={`doc-${doc.id}-page-${index}`} renderItem={(childProps) => ( - + )} + setRenderingMetrics={handleRenderingMetrics} /> ))}
    @@ -94,16 +196,18 @@ const PdfDocument = ({ doc, rotateDeg, setNumPages, zoomLevel, onLoad }) => { }; PdfDocument.propTypes = { + currentPage: PropTypes.number, doc: PropTypes.shape({ content_url: PropTypes.string, filename: PropTypes.string, id: PropTypes.number, type: PropTypes.string, }), + isDocumentLoadError: PropTypes.bool, rotateDeg: PropTypes.string, + setIsDocumentLoadError: PropTypes.func, setNumPages: PropTypes.func, zoomLevel: PropTypes.number, - onLoad: PropTypes.func, }; export default PdfDocument; diff --git a/client/app/readerprototype/components/ReaderFooter.jsx b/client/app/readerprototype/components/ReaderFooter.jsx index cd796658131..7b5fe3e5ff3 100644 --- a/client/app/readerprototype/components/ReaderFooter.jsx +++ b/client/app/readerprototype/components/ReaderFooter.jsx @@ -1,23 +1,25 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; import Button from '../../components/Button'; import TextField from '../../components/TextField'; +import { FilterNoOutlineIcon } from '../../components/icons/FilterNoOutlineIcon'; import { PageArrowLeftIcon } from '../../components/icons/PageArrowLeftIcon'; import { PageArrowRightIcon } from '../../components/icons/PageArrowRightIcon'; +import { docListIsFiltered, getFilteredDocIds } from '../../reader/selectors'; +import { annotationPlacement } from '../selectors'; const ReaderFooter = ({ currentPage, - docCount, - nextDocId, + docId, + isDocumentLoadError, numPages, - prevDocId, setCurrentPage, - selectedDocIndex, - showNextDocument, - showPreviousDocument, - disablePreviousNext, + showPdf, }) => { + const { isPlacingAnnotation } = useSelector(annotationPlacement); + const isValidInputPageNumber = (pageNumber) => { if (!isNaN(pageNumber) && pageNumber % 1 === 0) { @@ -47,16 +49,65 @@ const ReaderFooter = ({ } }; + const isDocListFiltered = useSelector((state) => docListIsFiltered(state)); + + const filteredDocIds = useSelector(getFilteredDocIds); + const currentDocIndex = filteredDocIds.indexOf(docId); + const getPrevDocId = () => filteredDocIds?.[currentDocIndex - 1]; + const getNextDocId = () => { + return filteredDocIds?.[currentDocIndex + 1]; + }; + + useEffect(() => { + const keyHandler = (event) => { + if (event.key === 'ArrowLeft' && !isPlacingAnnotation) { + showPdf(getPrevDocId())(); + } + if (event.key === 'ArrowRight' && !isPlacingAnnotation) { + showPdf(getNextDocId())(); + } + }; + + window.addEventListener('keydown', keyHandler); + + return () => window.removeEventListener('keydown', keyHandler); + }, [currentDocIndex, isPlacingAnnotation]); + + const footerCenterContent = () => { + let content = Loading document...; + + if (numPages) { + content = ( + +
    + +
    + of {numPages} +
    + ); + } + + return content; + }; + return (