diff --git a/.cfignore b/.cfignore index 12c195dbad..77578421b6 100644 --- a/.cfignore +++ b/.cfignore @@ -6,3 +6,4 @@ src/ terraform/ hses.zip temp/ +.tmp/ diff --git a/.circleci/config.yml b/.circleci/config.yml index e530b925e1..d7e019a7a0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -160,7 +160,7 @@ parameters: default: "main" type: string sandbox_git_branch: # change to feature branch to test deployment - default: "cm-370-downloading-files-does-not-work" + default: "js-338-383-frontend-tweaks" type: string prod_new_relic_app_id: default: "877570491" diff --git a/.gitignore b/.gitignore index 4c9795285d..a63a9aa49b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ node_modules .DS_Store .env **/*secrets.auto.tfvars + +# Temp files +.tmp diff --git a/R14ActivityReportsTest.csv b/R14ActivityReportsTest.csv new file mode 100644 index 0000000000..111380c120 --- /dev/null +++ b/R14ActivityReportsTest.csv @@ -0,0 +1,284 @@ +ReportID,Grantee Name,CDI Grantee Name (if applicable),Multi-Grantee Activities,Program Type(s),Non-Grantee Activity,Source of Request,Reason/s,T -TA,Topics,Other topics,Grantee Participants,Non-Grantee Participants,Number of Participants,Start Date,End Date,Duration,Other Specialists,Target Populations,Resources Used,Non-OHS Resources,GOAL 1,Grantee's learning level GOAL 1 (Optional),Objective 1.1,Objective 1.1 status,Objective 1.2,Objective 1.2 status,GOAL 2,Grantee's learning level GOAL 2 (Optional),Objective 2.1,Objective 2.1 status,Objective 2.2,Objective 2.2 status,TTA Provided and Grantee Progress Made,Grantee Follow Up Tasks & Objectives,Specialist Follow Up Tasks & Objectives,Format,Additional notes for this activity,,Manager,Manager approval,Created,Created By,Override Created By,Modified,Modified By +R14-AR-000435,"Action, Inc. | 14CH010848",,,,,Grantee,Need: Professional Development,Both,"Coaching / Teaching / Instructional Support | ECS, FES",,Manager / Coordinator / Specialist / Case Manager,,,2020-11-06,2020-11-06,2.0,,Preschool,,,"Grantee will have a minimum of 5% of staff receive mentoring and coaching (PG#1, Obj #4).",,Completed: Train the Education Manager on an overview of the TLC’s due to the cancelled even in May.,,,,,,,,,,"Pre TA: Working on PD plans and has a Creative Curriculum Training coming up. + +Trained and provided TA for the education manager on Together Learning and Collaborating (TCL’s) with the following discussions: +• Reviewed the content of the TLC group sessions 3 and 4. +• Discussed only having one group this year of lead teachers and might start with Fostering Connections with families since some children are distance learning. +• Discussed the topic may be worked on for multiple TLC sessions. +• Program may make the Plan form a pdf fillable or create it via forms. + +Progress Toward Outcomes: +Trained Education Manager on part of the TLC facilitator training. Discussed logistics of TLC implementation. +Post TA: +• The whole rest of the TLC cyclical process and using the In-Service suites as part of the process. +Post TA: +• Setting up and doing the TLC’s.","Grantee: +• Implement one or more TLC sessions with lead teachers – by end of January 2021","Post TA: +• After program implements them, meet to give feedback and strategies + +ECS: +• Send Plan form• Mail a thing",Virtual,,,manager1@test.region14,Approved,2020-11-09 08:20:57,creator@first.row,,2020-12-21 07:47:05,creator@first.row +R14-AR-000442,S College | 14CH011664,,,,,Regional Office,"Monitoring | Area of Concern +Monitoring | Deficiency",Technical Assistance,"Behavioral / Mental Health | HS, FES +Corrective Actions | GS +Quality Improvement / QIP | GS",,"Coach / Trainer +Manager / Coordinator / Specialist / Case Manager",,,2020-11-05,2020-11-05,2.0,,"Infant/Toddlers +Preschool",,,"Grantee will finalize additions to Technical Service Plan, defining T/TA outcomes, plan steps, strategies and identify dates of service.",,"Continued support for Social/Emotional Plan implementation- In Progress +Revise/refine coaching needs assessments- In Progress +Support staff’s reflective practices- in Progress",,,,,,,,,,"Anticipated Outcome: Grantee will finalize additions to Technical Service Plan, defining T/TA outcomes, plan steps, strategies and identify dates of service. + +Grantee Progress: Grantee continues to provide in-person services through partner sites and virtual services in managed sites. Pyramid Model implementation continues as program will schedule an Administrator training for Center Coordinators and continues to review process and forms. + +T/TA Provided: ECS opened with a question of the day activity to begin conversation. ECS then led grantee into review of T/TA priorities within ECS scope of work that further supports the current DEF/ANC’s. Meeting participants identified program’s strengths as community collaborations, SIU as grantee, connections among families, children and staff. Growth opportunities were identified as enhancing systems to support program and ensuring systems are evident to all staff. Supporting collaborations across all sectors and building understanding that program pieces connect toward a common goal. Strengthening reflective practices among staff. + +The group reviewed current coaching needs assessment designed to identify needs and reflect practices within providing services during COVID 19. Instructional Services/Coach asked ECS about thoughts on using the reflective needs assessments with all staff and then through individual discussions determine any other potential area of focus and completion of second-level assessment. ECS agreed it could be helpful in many capacities including overall wellness support. ECS encouraged grantee to consider revision to scoring criteria to allow for a greater range between; I am comfortable and always use and I struggle and rarely use. ECS suggested “I struggle” may not be entirely accurate. It could be staff are unsure or just rarely use the practice but aren’t at level of “struggling” and possibly staff may shy away from declaring “I Struggle”. +Grantee shared the successes of “Ask the Coach” sessions with teaching staff. Plans are in place to begin similar idea with Center Coordinators (CC). Rather than the teacher Munch and Learn, coaches will hold a Coffee Chat with CC’s to follow up on Munch & Learn sessions, allow time for administrator’s training for Pyramid Model and encourage use of reflective practices. + +Participant’s requested continued support with program’s implementation of social/emotional system working with on-going check-in’s, making revisions, and providing feedback as requested. + +Progress toward Outcome: ECS and Instructional Services/Coach (ISC) outlined program strengths and growth opportunities, discussed desired T/TA outcomes, aligned to corrective action plan and identified strategies and dates throughout the program year. T/TA activities will include continued support for Social/Emotional Plan implementation and support to revise/refine coaching needs assessments that support staff’s reflective practices. + +Post TA: +1. Aligning Professional Development arrow chart, strategies to gauge teacher’s reflective practices and individual needs, reminder to review Head Start Heals material. +2. Utilize information in planning, implementation and monitoring of coaching and social/emotional development of children and staff.","Next Steps for Grantee: +November 13, 2020- revise education staff needs assessments per discussion. + distribute needs assessments to education staff. +November 30, 2020- review Head Start Heals materials for integration to services.","Next Steps for Specialist: +November 11, 2020- send participants the arrow-PD chart.",Virtual,,,manager1@test.region14,Approved,2020-11-09 10:39:08,creator@second.row,,2020-12-21 07:47:05,creator@second.row +R14-AR-000770,"Program, Inc. | 14CH014598",,,,,Grantee,Need: Program Planning,Technical Assistance,"CLASS / Learning Environments / Classroom Management | ECS +Environmental Health and Safety | HS",,"Manager / Coordinator / Specialist / Case Manager +Teacher / Infant-Toddler Caregiver",,,2020-12-18,2020-12-18,2.0,,Preschool,"https://eclkc.ohs.acf.hhs.gov/publication/guiding-questions-active-supervision-safety +https://eclkc.ohs.acf.hhs.gov/safety-practices/article/keep-children-safe-using-active-supervision +https://eclkc.ohs.acf.hhs.gov/safety-practices/article/active-supervision +https://eclkc.ohs.acf.hhs.gov/safety-practices/article/tips-keeping-children-safe-developmental-guide-preschoolers +https://eclkc.ohs.acf.hhs.gov/publication/home-safety",,Grantee will use safe practices program wide to develop a culture of safety.,,Completed: ECS provided TTA around the 6 steps to Active Supervision and facilitated discussion with participants to increase their understanding of the importance of a program-wide approach that prioritizes children’s safety.,,,,,,,,,,"ECS provided TA on Active Supervision which included: Teachers, Teacher Assistants, and Management staff. Topics addressed were: +-6 Active Supervision Strategies +-Active Supervision Action Planning +-Steps to Implement Safe and Secure Environments + +ECS opened with an interactive group activity. ECS showed an awareness video and allowed time for group sharing. ECS facilitate discussion and the following examples to Identify 6 steps to Active Supervision: + +• Actively Supervise with Zoning +• Keep Environments Safe +• Make Playgrounds Safe +• Transport Children Safely +• Model Safe Behaviors +• Teach Families about Safety +• Know Your Children and Families + +ECS facilitated an overview of the 6 strategies for Active Supervision and led participants virtually to engage and share ideas through Padlet posing a question for each strategy and having participants engage in an interactive small group discussion in breakout rooms. Participants listed strategies to take back and use for future planning. Participants viewed Active Supervision videos and discussed in breakout rooms and shared out in large groups. ECS facilitated a large group discussion on keeping environments safe in the home and classrooms and participants shared strategies and procedures they have done to prevent injuries. ECS wrapped up the training session by sharing tips of teaching families about safety and behaviors to model for safety because children learn by watching adults. + +Post TA: +*15 Minute In-service Suites +*Scanning and counting in the classroom +*Transition ideas utilizing cue cards",Provide ongoing training and professional development opportunities to staff by utilizing the resources shared by ECS from the 6 strategies and provide follow up with staff. Grantee will ensure staff understand and have reviewed all policies and procedures of safety practices.,Follow-up with the Director during ongoing TTA visits and send data collected from training to Director,Virtual,,,manager2@test.region14,Approved,2020-12-18 11:32:54,creator@third.row,,2020-12-21 07:47:05,admin@test.gov +R14-AR-001005,The Public Schools | 14CH014747,,,Head Start (ages 3-5),,Grantee,New Director or Management,Technical Assistance,Other,Orientation/Onboarding,"Manager / Coordinator / Specialist +Program Director (HS/EHS)",,8.0,2021-01-28,2021-01-28,2.5,,Preschool,"TTA ECLKC Resources: +https://eclkc.ohs.acf.hhs.gov/human-resources/article/ensuring-new-employees-success-best-practices-employee-onboarding",,"Anticipated Outcomes: +GS Goal: The Grantee will strengthen its Human Resource system.",1 - Introductory,"TTA Objective: +Assess current practices, identifying future methods, and formalizing plans to support the program goals and Head Start (HS) regulations.",In progress,,,,,,,,,"Pre-TA: The leadership staff from Quincy Public School District 172 provided the following updates related to the program since the last TTA virtual meeting on November 24, 2020: +Operations: +- the head start program is conducting in-person services with 13 children learning remotely +- one classroom is quarantined due to a positive COVID test +- the leadership team is following the state and county guidelines keeping everyone safe with modified operations, as needed +- the school district officials are communicating with staff and staff with parents on conditions and guidance +- All school staff can receive COVID vaccines if they choose to +- Head Start Director (HSD) is starting to write the continuation grant. TA Specialist recommended the HSD to contact the Program Specialist (PS) for direction to questions about grant writing +- HSD informed Grantee Specialist (GS) they have a new PS +Funded enrollment (299): +- 271 children receiving in-person services (119 full-day/83AM/69PM) +- 13 children receiving remote services +Previous TTA topic update (Human Resources): +- The HSD noted that as a team they have not been able to complete the HR Audit as the next steps. GS encouraged the team to complete the next steps when possible. + +TTA Provided: Grantee Specialist (GS) welcomed the grantee with an introduction asking all participants “how they are doing/feeling”, a review of the agenda, and updates from the grantee. The leadership team introduced themselves and gave a description based on their day and/or the holiday. GS engaged the grantee in a group ice breaker activity of “would you rather questions” using the reaction features in Zoom. GS reviewed PMFO’s staff onboarding example, orientation vs. onboarding, and onboarding planning tool, and “Ensuring Success” article in a PowerPoint format. GS explained the purpose and effects of an Orientation/Onboarding process and engaged the grantee team in a discussion on their process. The grantee stated that several factors highlighted in their process are viewed as a strength and a challenge. + +As a result, the grantee identified the following as strengths of the program: +- school district completes the orientation process through the Board of Education office +- team environment/approach +- established mentor teacher/mentee teacher process +- developed professional learning community for teachers with a focus on Conscious Discipline + +As a result of TTA, the grantee identified challenges: +- complex framework/school district (SD) (HS & SD conformity) +- pandemic occurrences +- familiarity with HS resources on orientation/onboarding processes +- utilizing a coordinated approach within the planning process +- SD & HS having two different process for Orientation/Onboarding for staff due to ISBE regulations + +The grantee was split into breakout groups to continue developing their orientation and onboarding plan for the program. GS spent time in both groups participating in the discussion. Through feedback, the team described that they: +- plan to utilize HS resources in building their orientation and onboarding plan +- recognize the importance of a coordinated approach when making a shift/change in program operations + +GS recommended/suggested the following: +- Develop a written plan/outline of the process +- utilize “Onboarding: An Ongoing Process” ECLKC tool to guide the process/assist in building a plan +- continue to develop the program calendar include the planning process +- Utilize ECLKC resources for HS planning +- utilize HR audit to self-evaluate and assist leadership staff in recognizing their HR strengths, identify areas for improvement and ensure compliance with federal and state laws","Grantee will: (How will you apply what you have learned?) +- begin to document their continuous improvement (CIP) on the planning form +- review TSR and provide edits, as needed (January 28, 2021, TSR) +- continue to discuss HR as it relates to their orientation and onboarding process +- complete HR Audit +- Continue to build an Orientation and Onboarding Plan","The TTA Specialist will: (What more do you need from TTA Specialists?) +",Virtual,,,manager3@test.region14,Approved,2021-01-29 11:44:28,creator@third.row,,2021-02-03 02:13:36,creator@third.row +R14-AR-000892,Board of Trustees of | 14CH041588,,,Head Start (ages 3-5),,Grantee,Ongoing Quality Improvement,Technical Assistance,Other,Staff Wellness,Manager / Coordinator / Specialist,,4.0,2021-01-14,2021-01-14,2.0,,Preschool,Trauma Briefs 1-5,Sesame St- Elmo & COVID www.sesamestreet.org,"Program will foster the growth and development of its staff through individualized, job-embedded professional development, mental health supports, and daily classroom support.",2 - Intermediate,ECS will provide TA to support program's efforts with resources related to school readiness and building staff resilience and wellness.,In progress,,,,,,,,,"T/TA Provided: ECS facilitated TA to provide further resources sharing, Sesame Street and Elmo COVID-19 information, Self-Care checklist, and introduced the concept of Compassion Fatigue and its effect on staff and remaining well. ECS provided Professional Quality of Life Scale allowing participants to complete and a de-brief where they expressed rating just as expected to thinking scores would be higher on burnout section to “shocked”. ECS guided them to the criteria on each section and elements of burnout vs. secondary trauma stress sharing the ProQOL helper card. ECS asked if they thought the scale could be used with staff. Participants did think it could be used and shared examples of staff challenges, asked for strategies to support. ECS recommended including the mental health consultant as possible, providing affirmation and validation of feelings as well as support to identify any resources. ECS spotlighted resource from NC Pyramid Model Innovations Self-Care for Teacher and HS NCPFCE Trauma Briefs to generate supports for staff and families. ECS offered data on staff wellness, retention, and engaged staff in discussion of characteristics that effect stress levels and job satisfaction. Grantee is in process of self-assessment and plan to review current practices in development of a formalized Wellness plan for PY 21-22 and requested one-page, parent-friendly resources on COVID supports to include on newsletters and/or distribution to parents. + +Progress toward Outcome: Grantee participated in exercise, gained awareness of resources, and strategized on possible uses of resources. Health/Nutrition Coordinator reported feeling adequately prepared to incorporate ideas and complete draft staff wellness plan for PY 21-22 based on ECS TA and resource sharing. Final Check scheduled for May 18, 2021 to ensure no further needs are necessary. + +Post TA: +1. ProQOL scale, information on change. +2. Will add to training sessions possibly include in orientation/on-boarding process. +3. N/A","Next Steps for Grantee: +draft staff wellness action plan.","Next Steps for Specialist: +January- Send resources to grantee. +As identified- send short, parent-friendly resources on COVID support, well-being",Virtual,"Grantee Progress: Program has initiated and planned for several staff wellness activities such as weekly staff highlight, etc.",,manager1@test.region14,Approved,2021-01-15 14:46:25,creator@fourth.row,,2021-01-17 06:40:49,creator@fourth.row +R14-AR-001002,"High Five, Inc. | 14CH010143",,,Head Start (ages 3-5),,Grantee,Ongoing Quality Improvement,Training,Other,Reflective Supervision,"Coach +Family Service Worker / Case Manager +Manager / Coordinator / Specialist +Program Director (HS/EHS) +Teacher / Infant-Toddler Caregiver",,30.0,2021-01-25,2021-01-25,1.5,,Preschool,,,Enhance reflective practice.,2 - Intermediate,Grantee will increase understanding of the components of Reflective Supervision.,Completed,,,,,,,,,"Began with an opening activity around setting the intention for the year. +Reviewed Reflective Supervision practices through video examples +and discussion. Key points included: +-The definition of reflective supervision +-Reflective supervision as a parallel process +-Active listening components: Stop, Look, Listen, Respond +Grantee participated in an active listening activity through the use of breakout rooms. +-Reflect back strategies: +---Paraphrase +---Use of Open-ended Questions +---Hypothesizing +---Reframing & Restating +---Scaffolding -Key +Components of Reflective Supervision: Reflection, Collaboration and +Regularity Facilitated conversation around the Grantee's use of reflective practice. Grantee sees implementation as reflective practice through coaching. Shared that +implementation is a process. Staff shared that they are comfortable going to managers for support. The Grantee is reflective without having the regular scheduled time. + +Post TTA: +1. BETH strategy, Active listening reminder, video and practice. +2. Listen with intention",The Grantee will reflect on strategies and determine next steps,The ECS will conduct a virtual visit for Technical Service Plan development.,"Virtual +In Person +Telephone",,,manager1@test.region14,Approved,2021-02-01 16:55:13,creator@row.seven,,2021-02-02 12:22:38,creator@row.seven +R14-AR-001162,Big District | 14CH014444,,,"Early Head Start (ages 0-3) +Head Start (ages 3-5)",,Grantee,Planning/Coordination (also TTA Plan Agreement),Technical Assistance,Other,Grantee T/TA Planning,"Manager / Coordinator / Specialist +Program Director (HS/EHS) +Program Support / Administrative Assistant",,4.0,2021-02-23,2021-02-23,3.0,"specialist1@test.gov,","Infant/Toddlers +Preschool",,,Reflect on the program’s strengths and areas to strengthen and set priorities for next program year.,,Develop a TTA plan for the grantee with action steps and timelines for the next program year.,Completed,,,,,,,,,"TTA Provided: +ECS and leadership team, including Head Start Director (HSD), participated in a brainstorming session to review strengths and areas to strengthen in the next program year. The discussion included the following: +*ECS shared the intention and purpose of developing a TTA Plan in coordination and the connection to the grantee’s annual planning time. +*HSD and leadership team gave an update of the grantee, including: +>COVID updates – Children coming in 4-days a week. Staff are in offices full-time. Home Visitors are still remote, with office time 2 days a week. All home visits continue to be virtual. The first dose of vaccines completed for staff that signed up with 70 staff out of 200 receiving the first dose. +>Two changes to org. chart including the addition of two Coordinators to assist in supporting direct service staff, a Family Service Coordinator to supervise Family Service staff, the manager oversees all of PFCE & ERSEA. And a Home-Based Coordinator to directly supervise home visitors. These transitions are still occurring. ECS encouraged the HSD to discuss these changes with assigned GS to consider these positions as considering communication and implementing this change with staff. In addition, as part of QI (quality improvement) funds, a parent educator will be hired to focus on meeting the needs of families – this position is currently posted. +>Grantee changes – two key positions are being absorbed within other people’s job duties due to grantee eliminating positions in IT and finance. The program was not ready for this change and did not have time to plan effectively. +>Current enrollment: EHS – 97%; HS = 60% -lost families due to access to technology resources and those that were choosing not to stay enrolled. +>HS – 3 childcare partnerships (44 children). EHS – 1 home visitor partner (24). +*Team reviewed the TTA Plan as developed in the fall during the Initial Engagement visit. Successes were identified and topics to carry over to this program year were identified. +*Team completed an analysis of strengths and areas to strengthen in the areas of school readiness (SR); parent, family, and community engagement (PFCE); and career development. +-Strengths: +>Developed a COVID plan to ensure the health and safety of staff and children. +>Used technology to our advantage, including providing technology to families as needed. +>Increased attendance at virtual socialization. +>Staff worked together to implement services when not fully staffed. +>Working hard to provide services to families and children. +*Team reflected and de-briefed around the areas to strengthen and voted on the areas they considered a priority. +>Continuous improvement strategies for School Readiness. Is there a more effective way to work. Ensuring all areas are being worked on, including reviewing and revising transition systems between EHS and HS and HS to kindergarten. (Carried over from last TTA plan) In addition, increasing staff knowledge and awareness in effective teaching practices in the area of math and language/literacy. +>Continuous improvement strategies for PFCE, including continuing work from last TTA plan, as well as exploration of the Relationship-Based Competencies and strengthening staff understanding of positive parent engagement. +>Connecting PFCE and School Readiness (Carried over from last TTA plan) +>Implement a system of coaching and mentoring throughout the program (Carried over from last TTA plan). +>Review and revise, as needed, systems around active supervision including data collection and aggregation; active supervision on the playgroup and transportation; and supporting families in understanding the safety of their children. +>Review and revise coordinated approach around professional development, disabilities, dual language learners, and data. + +Progress Toward Outcomes: +Participants contributed to the conversations regarding annual planning and developed the next steps.","Grantee will: +*See TTA Plan","ECS will: +*See TTA Plan",Virtual,,,manager4@test.region14,Approved,2021-02-24 10:46:04,creator@row.seven,,2021-02-24 10:56:01,creator@row.seven +R14-AR-000279,Healthy | 14CH010545,,,,,Regional Office,Need: Program Planning,Technical Assistance,Other,TSP Development,Manager / Coordinator / Specialist / Case Manager,,,2020-10-16,2020-10-16,2.0,,"Infant/Toddlers +Preschool",,,"Grantee will finalize Technical Service Plan, defining T/TA outcomes, plan steps, strategies and identify dates of service.",,TSP development - In progress,,,,,,,,,,"Anticipated Outcome: Grantee will finalize Technical Service Plan, defining T/TA outcomes, plan steps, strategies and identify dates of service. + +Grantee Progress: Grantee will close largest site due to COVID positive cases at site. Several counties in area are on state’s warning list due to increase in cases. Program is enrolled at 67%, PBC coach/specialist who recently resigned is returning to program position. + +T/TA Provided: ECS facilitated visit with HSD further planning for T/TA desired services. HSD identified strengths as having many experienced staff, community collaborations including newer one with University of Illinois Extension to share space in HS building providing a variety of events including a cooking classes for parents, Kindergarten transition committees, and having Southern Seven Health Department as the grantee. Growth opportunities identified as finalizing coaching system, increasing parent engagement and using current documents, identifying gaps to combine into a coordinated approach format. Program has worked toward a formalized coaching approach however having 3 different coaches in the position throughout the process has provided challenges. ECS reminded grantee of the PBC Program Leader’s Guide provided during implementation academy as a tool to support planning. ECS and identified implementation team will review current plan alongside other training and PD activities to finalize the formal coaching system including evaluation of services and incorporate into a Training and Professional Development Coordinated Approach. FA 1 identified the program’s transition committee as a strength however grantee wants to review practices for consistency among sites and review school readiness goals in conjunction with alignment to new child assessment DRDP. HSD shared the decline in overall parent engagement but concern with lack of/low attendance at parenting curriculum sessions. ECS suggested a review of the curriculum thru lens of parent interests expressed on program surveys, grantee desires a look at the use to fidelity and possibilities for increasing parent engagement throughout the program. + +Progress toward Outcome: ECS and HSD outlined program strengths and growth opportunities, discussed desired T/TA outcomes, aligned to program goals identified strategies and dates of service areas for T/TA, all Coordinated Approaches, Transition, health-related to SR and school readiness goals, parenting curriculum. + +Post TA: +1. Gained more information on Coordinated Approaches (CA). +2. Use in format development of CA","Next Steps for Grantee: +- Email ECS child Outcomes PY 19-20 report and Coaching plan.","Next Steps for Specialist: +October- Email team PBC Program Leader’s Guide.",Virtual,,,manager9@test.region14,Approved,2020-10-19 07:23:40,creator@fourth.row,Person Overrider,2020-12-21 07:47:05,creator@fourth.row +R14-AR-000976,"ACRONYM, Co. | 14CH010140",,,"Early Head Start (ages 0-3) +Head Start (ages 3-5)",,Grantee,Ongoing Quality Improvement,Technical Assistance,Transition Practices,,Manager / Coordinator / Specialist,,1.0,2021-01-25,2021-01-25,2.0,,"Infant/Toddlers +Preschool",https://eclkc.ohs.acf.hhs.gov/sites/default/files/pdf/supporting-transitions-brief-two.pdf,,We will provide responsive practices that support all children’s positive growth and development (PG#1).,2 - Intermediate,Incorporate new strategies to create smooth transitions.,In progress,,,,,,,,,"Pre TA: No big changes + +Brainstormed ideas for a smooth transition for children and families from EHS to HS, and HS to the school with the following conversations: +• Reviewed the Supporting Transitions: Early Educations Partnering with Parents document discussing strategies to support EHS to HS transitions breaking it down into these sections: Parent-Team Member, Child-Team Member, Setting-Setting. +• Some ideas brainstormed for Parent-Team Member section include: Staff sharing stories of what they like best about working with the family and child and what they are going to miss; asking parents what will be challenging for their child, what gets their child excited, how does their child manage stressful situations, what soothes their child; what are their hopes and expectations for themselves and their child in the new program; what are their fears/challenges moving into the new setting; transportation fears/issues worked in advance, and many more. +• Some ideas brainstormed for the Child-Team Member section include: making a countdown calendar to help the child understand the journey of moving from the current setting to the new one; help arrange for the child and family to visit the new program; Make a book with the child’s favorite foods, activities, a me book for the child to give to the new teacher, and many more. +• Some ideas brainstormed for the Setting-Setting section include: pre-meeting between teachers to learn about their background and cultural background in regard to the child – this one may be for the parent and the new setting too; and learn routines about the new setting. +• Referred to the HSPPS on Transitions looking for the timeline of when to start Transitions and found that EHS is 6 months prior to the Transition; however, HS did not state a timeline. Recommended the program review the HSPPS deeper to see if a timeline is listed for HS in that section or a different section. +• Discussed transitioning children to another setting if they no longer qualify for Head Start. Asked how early a program can determine if they qualify for Head Start and staff will talk to their ERSEA person to find out the answer. +• Another idea brainstormed were to add a question of the month to the home visits for EHS HB for home visitors to ask. +• Briefly discussed a few HS to Kindergarten transition ideas. Determined a need to create a timeline next. ECS will send the resources discussed today along with the Kindergarten Transition resources. ECS referred to the Transition box the program received. Program will review the resources prior to the next meeting. + +Progress Toward Outcomes: +Reviewed transition resources discussing ideas for EHS to HS transitions and some ideas for HS to School transitions. +Post TA: +• Kids transitioning into Kindergarten may not have a timeline of 6 months to start the Transition process. +Post TA: +• Review the information ECS will send +• Find ideas to send to teachers going to Kindergarten +• Have a new transition guide for children and families to ensure a smooth transition.","• Talk to Manager to discuss how blah blah +• Review transition resources identifying key ideas","Post TA: +• Help develop timeline +• Review anything program sends in advance of next meeting + +ECS: +• Send Transition resources supporting multiple places +• Send Transitions information found in the HS Act +• Plan for upcoming meeting on Transitions",Both,,,manager10@test.region,Approved,2021-01-26 12:28:28,creator@row.ten,,2021-01-26 12:33:04,creator@row.ten +R14-AR-001132,"Healthy | 14CH010545 +Another | 14CH010114 +This Is One | 14CH1414140",,,Head Start (ages 3-5),,Grantee,Ongoing Quality Improvement,Training,"CLASS: Classroom Management +CLASS: Emotional Support +CLASS: Instructional Support",,"Center Director / Site Director +Coach +Manager / Coordinator / Specialist +Teacher / Infant-Toddler Caregiver",,10.0,2021-02-16,2021-02-18,16.0,"specialist1@test.gov, specialist2@test.gov, s3@test.gov",Preschool,,Teachstone Provided Materials,"Participants will demonstrate knowledge of the CLASS dimensions, indicators, and behavior markers; practice notetaking and coding process; and prepare for CLASS certification",,"reviewed all domains, dimensions, indicators, and behavior makers",Completed,provided knowledge about and practiced note taking and presented and practiced the CLASS scoring process,Completed,,,,,,,"TA Provided: + +Trainers shared the three-day virtual training agenda, including the following: + +Provided the CLASS introduction, including the structure, research, and video (the Power of Interactions) + +Introduced the CLASS Framework and the Manual + +Provided the face pages, indicators, behavior markers, and supporting pages of the 10 dimensions, including exemplar videos + +Facilitated activities through polls, breakout rooms, and small/large group discussions for the dimensions and each domain, with a focus on key points and the experience of using the CLASS lens + +Presented the recommendations for Notetaking and demonstrating examples of ineffective and effective notes + +Focus of video one was for notetaking and assigning a Low, Mid, High to each indicator; used a poll to reflect on participants’ understanding. Polls provided trainers with additional information to share with participants + +Introduced Page 17 and the scoring guidelines. Also sharing the Power Words Rarely/Minimally, Sometimes, and Often/Consistently + +Video 2 focused on notetaking and assigning Low, Mid, High to indicators and Video 3 and 4 focused on notetaking, assigning Low, Mid, High, confirming with paragraphs, and using page 17 to assign scores to each indicator. With both videos, used breakout rooms to have individual discussions about the videos, master code justifications, challenges, and Q&A. + +Trainers shared information about completing live observations, reviewing the focus and systematic approach of CLASS Observations, as well as resources available on the Teachstone website. + +Trainers reviewed the reliability testing process and highlighted the preparing to certify and sample reliability results webpage examples + +Trainers provided live demonstration of accessing the testing site on Teachstone + +Trainers asked participants to share their score sheet data, asking for the number of times they were reliable during tests 2, 3, and 4 for each dimension through polling. Most videos demonstrated participants as 80% or more reliable on videos 3-5. + +Trainers asked participants to share individual needs of support through chat discussions. + +The training ended with a reminder that the purpose of CLASS is an important part of a systematic approach to achieving growth in teacher practice and, ultimately, making a positive impact on child outcomes + +An evaluation link was provided to all participants to complete, then log off when finished.","Participants were encouraged to complete the certification process within 2 weeks of the training, but will participate in the certification process prior to April 16, 2021","Trainers will: + +Complete the training process in Teachstone. + +Support participants until the testing is complete (through April 16th). + +Share information with assigned ECS (as test results are available).",Virtual,,,manager11@test.region,Approved,2021-02-22 06:37:35,creatror@creator.biz,,2021-02-22 06:52:19,admin@test.gov diff --git a/cucumber/features/activityReport.feature b/cucumber/features/activityReport.feature index 4999a35692..a2d70ddab9 100644 --- a/cucumber/features/activityReport.feature +++ b/cucumber/features/activityReport.feature @@ -2,6 +2,6 @@ Feature: TTA Smarthub Activity Report Scenario: Report can be filled out Given I am logged in And I am on the landing page - Then I see "New activity report for Region 14" message + Then I see "Activity report for Region 1" message When I select "Non-Grantee" Then I see "QRIS System" as an option in the "Non-grantee name(s)" multiselect diff --git a/frontend/src/App.js b/frontend/src/App.js index 0623dcb99d..78f99c2d7d 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -18,6 +18,7 @@ import NotFound from './pages/NotFound'; import Home from './pages/Home'; import Landing from './pages/Landing'; import ActivityReport from './pages/ActivityReport'; +import LegacyReport from './pages/LegacyReport'; import isAdmin from './permissions'; import 'react-dates/initialize'; import 'react-dates/lib/css/_datepicker.css'; @@ -77,6 +78,14 @@ function App() { logoutUser={logout} /> + ( + + )} + /> )} /> - )} /> - ( @@ -130,7 +137,7 @@ function App() {
-
+
{!authenticated && (authError === 403 ? diff --git a/frontend/src/components/FileUploader.js b/frontend/src/components/FileUploader.js index 228a651f9a..de813f447c 100644 --- a/frontend/src/components/FileUploader.js +++ b/frontend/src/components/FileUploader.js @@ -199,7 +199,7 @@ const FileTable = ({ onFileRemoved, files }) => { )} @@ -38,14 +52,7 @@ const ResourceSelector = ({ name, ariaName }) => {
) : ( - diff --git a/frontend/src/pages/ActivityReport/index.js b/frontend/src/pages/ActivityReport/index.js index 50366d011b..011dc5de15 100644 --- a/frontend/src/pages/ActivityReport/index.js +++ b/frontend/src/pages/ActivityReport/index.js @@ -95,6 +95,12 @@ function ActivityReport({ return array.map((value) => ({ value })); }; + const convertReportToFormData = (fetchedReport) => { + const ECLKCResourcesUsed = unflattenResourcesUsed(fetchedReport.ECLKCResourcesUsed); + const nonECLKCResourcesUsed = unflattenResourcesUsed(fetchedReport.nonECLKCResourcesUsed); + return { ...fetchedReport, ECLKCResourcesUsed, nonECLKCResourcesUsed }; + }; + useDeepCompareEffect(() => { const fetch = async () => { let report; @@ -103,9 +109,7 @@ function ActivityReport({ updateLoading(true); if (activityReportId !== 'new') { const fetchedReport = await getReport(activityReportId); - const ECLKCResourcesUsed = unflattenResourcesUsed(fetchedReport.ECLKCResourcesUsed); - const nonECLKCResourcesUsed = unflattenResourcesUsed(fetchedReport.nonECLKCResourcesUsed); - report = { ...fetchedReport, ECLKCResourcesUsed, nonECLKCResourcesUsed }; + report = convertReportToFormData(fetchedReport); } else { report = { ...defaultValues, @@ -213,18 +217,21 @@ function ActivityReport({ }; const onFormSubmit = async (data) => { - const report = await submitReport(reportId.current, data); + const fetchedReport = await submitReport(reportId.current, data); + const report = convertReportToFormData(fetchedReport); updateFormData(report); updateEditable(false); }; const onReview = async (data) => { - const report = await reviewReport(reportId.current, data); + const fetchedReport = await reviewReport(reportId.current, data); + const report = convertReportToFormData(fetchedReport); updateFormData(report); }; const onResetToDraft = async () => { - const report = await resetToDraft(reportId.current); + const fetchedReport = await resetToDraft(reportId.current); + const report = convertReportToFormData(fetchedReport); updateFormData(report); updateEditable(true); }; @@ -236,7 +243,11 @@ function ActivityReport({ -

New activity report for Region 14

+

+ Activity report for Region + {' '} + {formData.regionId} +

{formData.status && ( diff --git a/frontend/src/pages/Landing/index.css b/frontend/src/pages/Landing/index.css index ab3fe1f8f4..5a5160aee2 100644 --- a/frontend/src/pages/Landing/index.css +++ b/frontend/src/pages/Landing/index.css @@ -13,21 +13,20 @@ .pagination li { display: inline-block; - margin-right: 7px; } .pagination li a { color: #3C4146; text-align: center; -margin-top: -5px; text-decoration: none; +margin: 9px; } .pagination li.active { background-image: url(../../images/blue-circle.png); background-repeat: no-repeat; background-position: center center; - padding: 10px; + padding: 10px 0px 10px 0px; } .landing .disabled { @@ -35,8 +34,6 @@ text-decoration: none; } .pagination li.active a { - padding: 3.5px; - margin-left: 1.3px; font-weight: bold; color: white; outline: none; @@ -87,7 +84,7 @@ div.smart-hub--total-count { } h1.landing { - font-size: 45px; + font-size: 40px; font-family: 'Merriweather', serif; font-weight: 700; white-space: nowrap; @@ -95,19 +92,18 @@ h1.landing { } .landing .usa-table { - line-height: 2; + line-height: 2.5; } .landing .usa-table caption { font-size: 21px; font-weight: 900; - padding: 14px 0px 17px 20px; + padding: 8px 0px 17px 20px; margin-bottom: -0.25rem; margin-top: 1px; } .landing .usa-table thead th { - font-size: 13px; white-space: nowrap; background-color: white; border-top: solid 1.5px; @@ -134,6 +130,11 @@ h1.landing { white-space: nowrap; background-color: transparent; border-style: none; + color: black; +} + +.smart-hub--blue a { + color: #3C72AE; } .landing .usa-button { @@ -180,7 +181,11 @@ h1.landing { } .smart-hub--status- { - background: #f8f8f8; + display: none; +} + +.smart-hub--status-null { + display: none; } .smart-hub--table-tag-status { @@ -194,7 +199,7 @@ h1.landing { overflow: hidden; text-overflow: ellipsis; display: inline-block; - width: 200px; + width: 180px; vertical-align: middle; } @@ -215,6 +220,7 @@ h1.landing { .smart-hub--new-report-btn { line-height: 0.5; min-width: 220px; + background-color: #0166ab; } .smart-hub--create-new-report { diff --git a/frontend/src/pages/Landing/index.js b/frontend/src/pages/Landing/index.js index 83ea48e8be..3cf4e16f9b 100644 --- a/frontend/src/pages/Landing/index.js +++ b/frontend/src/pages/Landing/index.js @@ -50,6 +50,7 @@ function renderReports(reports, history) { collaborators, lastSaved, status, + legacyId, } = report; const authorName = author ? author.fullName : ''; @@ -104,11 +105,13 @@ function renderReports(reports, history) { ]; const contextMenuLabel = `Edit activity report ${displayId}`; + const linkTarget = legacyId ? `/activity-reports/legacy/${legacyId}` : `/activity-reports/${id}`; + return ( - + {displayId} @@ -326,6 +329,7 @@ function Landing() { {showAlert && message && ( {error && ( - - {error} - + + {error} + )} - + { + // eslint-disable-next-line react/prop-types + const { id } = report; + const url = `/api/activity-reports/legacy/${id}`; + const history = createMemoryHistory(); + + if (fail) { + fetchMock.get(url, 500); + } else { + fetchMock.get(url, report); + } + + return ( + + + + ); +}; + +const report = { + id: '1', + imported: { + granteeName: 'first\nsecond\nlast', + }, +}; + +describe('LegacyReport', () => { + afterEach(() => fetchMock.restore()); + + it('handles failure to fetch the report', async () => { + render(); + const alert = await screen.findByTestId('alert'); + expect(alert).toHaveTextContent('Unable to load activity report'); + }); + + it('displays the report', async () => { + render(); + const first = await screen.findByText('first'); + const second = await screen.findByText('second'); + const last = await screen.findByText('last'); + + expect(first).toBeVisible(); + expect(second).toBeVisible(); + expect(last).toBeVisible(); + }); +}); diff --git a/frontend/src/pages/LegacyReport/index.js b/frontend/src/pages/LegacyReport/index.js new file mode 100644 index 0000000000..f521e542e0 --- /dev/null +++ b/frontend/src/pages/LegacyReport/index.js @@ -0,0 +1,104 @@ +import React, { useEffect, useState } from 'react'; +import ReactRouterPropTypes from 'react-router-prop-types'; +import { Helmet } from 'react-helmet'; +import { Alert, Table } from '@trussworks/react-uswds'; +import { map } from 'lodash'; + +import Container from '../../components/Container'; +import { legacyReportById } from '../../fetchers/activityReports'; +import reportColumns from './reportColumns'; + +function LegacyReport({ match }) { + const { params: { legacyId } } = match; + const [legacyReport, updateLegacyReport] = useState(); + const [loading, updateLoading] = useState(true); + const [error, updateError] = useState(false); + + useEffect(() => { + const fetchReport = async () => { + try { + const report = await legacyReportById(legacyId); + updateLegacyReport(report); + updateError(false); + } catch (e) { + updateError('Unable to load activity report'); + } finally { + updateLoading(false); + } + }; + fetchReport(); + }, [legacyId]); + + if (loading) { + return ( +
+ loading... +
+ ); + } + + if (error) { + return ( + + {error} + + ); + } + + const { imported } = legacyReport; + const entries = map(reportColumns, (display, field) => { + const value = imported[field]; + return { + display, + field, + value, + }; + }); + + const tableEntries = entries.filter((item) => item.value).map(({ field, display, value }) => ( + + + {display} + + + {value.split('\n').map((string) =>
{string}
)} + + + )); + + return ( + <> + + Legacy Report + + +

+ Legacy report + {' '} + {legacyId} +

+ + + + + + + + + {tableEntries} + +
+ Field + + Value +
+
+ + ); +} + +LegacyReport.propTypes = { + match: ReactRouterPropTypes.match.isRequired, +}; + +export default LegacyReport; diff --git a/frontend/src/pages/LegacyReport/reportColumns.js b/frontend/src/pages/LegacyReport/reportColumns.js new file mode 100644 index 0000000000..eaeaadd2e3 --- /dev/null +++ b/frontend/src/pages/LegacyReport/reportColumns.js @@ -0,0 +1,49 @@ +const reportFields = { + granteeName: 'Grantee name(s)', + programType: 'Program type(s)', + cdiGranteeName: 'CDI grantee(s)', + multiGranteeActivities: 'Multi-grantee activities', + nonGranteeActivity: 'Non-grantee activity', + sourceOfRequest: 'Who requested the TTA?', + reasons: 'Reason(s)', + startDate: 'Start Date', + endDate: 'End Date', + duration: 'Duration in hours', + otherSpecialists: 'Other Specialist(s) involved', + format: 'How was TTA provided?', + tTa: 'Was this Training or Technical Assistance', + topics: 'Topic(s) covered', + otherTopics: 'Other topics covered', + granteeParticipants: 'Grantee participant(s)', + nonGranteeParticipants: 'Non-grantee participant(s)', + participants: 'Participants', + numberOfParticipants: 'Number of participants', + targetPopulations: 'Target populations addressed', + resourcesUsed: 'OHS / ECLKC resources', + nonOhsResources: 'Non-ECLKC resources', + contextForThisActivity: 'Context for this activity', + goal1: 'Goal 1', + granteesLearningLevelGoal1: "Grantee's learning level Goal 1", + objective11: '1st objective for Goal 1', + objective11Status: '1st Objective for Goal 1 status', + objective12: '2nd objective for Goal 1', + objective12Status: '2nd objective for Goal 1 status', + goal2: 'Goal 2', + granteesLearningLevelGoal2: "Grantee's learning level Goal 2", + objective21: '1rst objective for Goal 2', + objective21Status: '1rst objective for Goal 2 status', + objective22: '2nd objective for Goal 2', + objective22Status: '2nd objective for Goal 2 status', + ttaProvidedAndGranteeProgressMade: 'TTA Provided', + specialistFollowUpTasksObjectives: 'Specialist follow-up', + granteeFollowUpTasksObjectives: 'Grantee follow-up', + additionalNotesForThisActivity: 'Additional notes for this activity', + manager: 'Manager', + managerApproval: 'Manager approval', + created: 'Time Created', + createdBy: 'Created By', + modified: 'Time Modified', + modifiedBy: 'Modified By', +}; + +export default reportFields; diff --git a/package.json b/package.json index 9f1d9de621..e3aa280f9f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "cucumber:ci": "cross-env TTA_SMART_HUB_URI=http://localhost:3000 yarn cucumber", "db:bootstrap:admin:local": "./node_modules/.bin/babel-node ./src/tools/bootstrapAdminCLI.js", "db:bootstrap:admin": "node ./build/server/tools/bootstrapAdminCLI.js", + "db:validation": "node ./build/server/tools/dataValidationCLI.js", "db:migrate": "node_modules/.bin/sequelize db:migrate", "db:migrate:ci": "cross-env POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub node_modules/.bin/sequelize db:migrate", "db:migrate:prod": "node_modules/.bin/sequelize db:migrate --options-path .production.sequelizerc", @@ -59,6 +60,8 @@ "docker:db:migrate:undo": "docker-compose run --rm backend node_modules/.bin/sequelize db:migrate:undo", "docker:db:seed": "docker-compose run --rm backend yarn db:seed", "docker:db:seed:undo": "docker-compose run --rm backend yarn db:seed:undo", + "import:reports:local": "./node_modules/.bin/babel-node ./src/tools/importSSActivityReports.js", + "import:reports": "node ./build/server/tools/importSSActivityReports.js", "import:goals:local": "./node_modules/.bin/babel-node ./src/tools/importTTAPlanGoals.js", "import:goals": "node ./build/server/tools/importTTAPlanGoals.js", "import:hses:local": "./node_modules/.bin/babel-node ./src/tools/importGrantGranteesCLI.js --skipdownload", diff --git a/src/migrations/20210309214315-imported-data.js b/src/migrations/20210309214315-imported-data.js new file mode 100644 index 0000000000..a176dbc5e5 --- /dev/null +++ b/src/migrations/20210309214315-imported-data.js @@ -0,0 +1,22 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + /** + * Add altering commands here. + */ + await queryInterface.addColumn( + 'ActivityReports', + 'imported', + { + type: Sequelize.JSONB, + comment: 'Storage for raw values from smartsheet CSV imports', + }, + ); + }, + + down: async (queryInterface) => { + /** + * Add reverting commands here. + */ + await queryInterface.removeColumn('ActivityReports', 'imported'); + }, +}; diff --git a/src/migrations/20210310173436-activityreport-allow-null-user-ids.js b/src/migrations/20210310173436-activityreport-allow-null-user-ids.js new file mode 100644 index 0000000000..0edcee114a --- /dev/null +++ b/src/migrations/20210310173436-activityreport-allow-null-user-ids.js @@ -0,0 +1,20 @@ +module.exports = { +/** + * Drop 'NOT NULL' requirement, so we can import data not associable with users (yet) + */ + up: (queryInterface, Sequelize) => queryInterface.sequelize.transaction((transaction) => { + const nullIntegerColumn = { type: Sequelize.DataTypes.INTEGER, allowNull: true }; + return Promise.all([ + queryInterface.changeColumn('ActivityReports', 'userId', nullIntegerColumn, { transaction }), + queryInterface.changeColumn('ActivityReports', 'lastUpdatedById', nullIntegerColumn, { transaction }), + ]); + }), + + down: (queryInterface, Sequelize) => queryInterface.sequelize.transaction((transaction) => { + const notNullIntegerColumn = { type: Sequelize.DataTypes.INTEGER, allowNull: false }; + return Promise.all([ + queryInterface.changeColumn('ActivityReports', 'userId', notNullIntegerColumn, { transaction }), + queryInterface.changeColumn('ActivityReports', 'lastUpdatedById', notNullIntegerColumn, { transaction }), + ]); + }), +}; diff --git a/src/migrations/20210311211736-legacyid-unique.js b/src/migrations/20210311211736-legacyid-unique.js new file mode 100644 index 0000000000..2ceab9121f --- /dev/null +++ b/src/migrations/20210311211736-legacyid-unique.js @@ -0,0 +1,13 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + /** + * Make legacyId unique, so we can bulkCreate but not duplicate rows + * + */ + await queryInterface.changeColumn('ActivityReports', 'legacyId', { type: Sequelize.STRING, unique: true }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('ActivityReports', 'legacyId', { type: Sequelize.STRING }); + }, +}; diff --git a/src/migrations/20210315181828-goal-timeframe-text.js b/src/migrations/20210315181828-goal-timeframe-text.js new file mode 100644 index 0000000000..7eb8938838 --- /dev/null +++ b/src/migrations/20210315181828-goal-timeframe-text.js @@ -0,0 +1,9 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('Goals', 'timeframe', { type: Sequelize.TEXT }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn('Goals', 'timeframe', { type: Sequelize.STRING }); + }, +}; diff --git a/src/models/activityReport.js b/src/models/activityReport.js index a88ddc2c22..e44ebbaf35 100644 --- a/src/models/activityReport.js +++ b/src/models/activityReport.js @@ -41,21 +41,26 @@ export default (sequelize, DataTypes) => { } ActivityReport.init({ displayId: { - type: DataTypes.VIRTUAL, + type: DataTypes.VIRTUAL(DataTypes.STRING, ['legacyId', 'regionId', 'id']), get() { - if (this.legacyId) return this.legacyId; - return `R${this.regionId.toString().padStart(2, '0')}-AR-${this.id}`; + const { legacyId, regionId } = this; + if (legacyId) return legacyId.toString(); + const regionPrefix = !regionId ? '???' : `R${this.regionId.toString().padStart(2, '0')}`; + return `${regionPrefix}-AR-${this.id}`; }, }, legacyId: { comment: 'Legacy identifier taken from smartsheet ReportID. Some ids adjusted to match their region.', type: DataTypes.STRING, + unique: true, }, userId: { type: DataTypes.INTEGER, + allowNull: true, }, lastUpdatedById: { type: DataTypes.INTEGER, + allowNull: true, }, approvingManagerId: { type: DataTypes.INTEGER, @@ -175,6 +180,10 @@ export default (sequelize, DataTypes) => { return moment(this.updatedAt).format('MM/DD/YYYY'); }, }, + imported: { + type: DataTypes.JSONB, + comment: 'Storage for raw values from smartsheet CSV imports', + }, sortedTopics: { type: DataTypes.VIRTUAL, get() { diff --git a/src/policies/activityReport.js b/src/policies/activityReport.js index 1b482c1dc7..b0f19d81d4 100644 --- a/src/policies/activityReport.js +++ b/src/policies/activityReport.js @@ -35,6 +35,10 @@ export default class ActivityReport { && this.activityReport.status === REPORT_STATUSES.SUBMITTED; } + canViewLegacy() { + return this.canReadInRegion(); + } + canGet() { const { status } = this.activityReport; const canReadUnapproved = this.isAuthor() || this.isCollaborator() || this.isApprovingManager(); diff --git a/src/policies/activityReport.test.js b/src/policies/activityReport.test.js index 32a7324eb1..d5707fa269 100644 --- a/src/policies/activityReport.test.js +++ b/src/policies/activityReport.test.js @@ -46,6 +46,7 @@ const author = user(true, false, 1); const collaborator = user(true, false, 2); const manager = user(true, false, 3); const otherUser = user(false, true, 4); +const canNotReadRegion = user(false, false, 5); describe('Activity Report policies', () => { describe('canReview', () => { @@ -142,6 +143,20 @@ describe('Activity Report policies', () => { }); }); + describe('canViewLegacy', () => { + it('is true if the user can view the region', () => { + const report = activityReport(author.id); + const policy = new ActivityReport(author, report); + expect(policy.canViewLegacy()).toBeTruthy(); + }); + + it('is false if the user can not view the region', () => { + const report = activityReport(author.id); + const policy = new ActivityReport(canNotReadRegion, report); + expect(policy.canViewLegacy()).toBeFalsy(); + }); + }); + describe('canGet', () => { describe('for unapproved reports', () => { it('is true for the author', () => { diff --git a/src/routes/activityReports/handlers.js b/src/routes/activityReports/handlers.js index 0ef9f06228..8cc848cfbf 100644 --- a/src/routes/activityReports/handlers.js +++ b/src/routes/activityReports/handlers.js @@ -10,6 +10,7 @@ import { activityReports, setStatus, activityReportAlerts, + activityReportByLegacyId, } from '../../services/activityReports'; import { goalsForGrants } from '../../services/goals'; import { userById, usersWithPermissions } from '../../services/users'; @@ -23,6 +24,27 @@ const logContext = { namespace, }; +export async function getLegacyReport(req, res) { + try { + const { legacyReportId } = req.params; + const report = await activityReportByLegacyId(legacyReportId); + if (!report) { + res.sendStatus(404); + return; + } + const user = await userById(req.session.userId); + const authorization = new ActivityReport(user, report); + + if (!authorization.canViewLegacy()) { + res.sendStatus(403); + return; + } + res.json(report); + } catch (error) { + handleErrors(req, res, error, logContext); + } +} + /** * Gets all goals for any number of grants for use in an activity report * diff --git a/src/routes/activityReports/handlers.test.js b/src/routes/activityReports/handlers.test.js index cf74885e5f..b39125de54 100644 --- a/src/routes/activityReports/handlers.test.js +++ b/src/routes/activityReports/handlers.test.js @@ -9,6 +9,7 @@ import { resetToDraft, getReports, getReportAlerts, + getLegacyReport, } from './handlers'; import { activityReportById, @@ -18,6 +19,7 @@ import { setStatus, activityReports, activityReportAlerts, + activityReportByLegacyId, } from '../../services/activityReports'; import { userById, usersWithPermissions } from '../../services/users'; import ActivityReport from '../../policies/activityReport'; @@ -31,6 +33,7 @@ jest.mock('../../services/activityReports', () => ({ setStatus: jest.fn(), activityReports: jest.fn(), activityReportAlerts: jest.fn(), + activityReportByLegacyId: jest.fn(), })); jest.mock('../../services/users', () => ({ @@ -66,6 +69,37 @@ describe('Activity Report handlers', () => { jest.clearAllMocks(); }); + describe('activityReportByLegacyId', () => { + const request = { + ...mockRequest, + params: { legacyReportId: 1 }, + }; + + it('returns a report', async () => { + ActivityReport.mockImplementationOnce(() => ({ + canViewLegacy: () => true, + })); + activityReportByLegacyId.mockResolvedValue({ id: 1 }); + await getLegacyReport(request, mockResponse); + expect(mockResponse.json).toHaveBeenCalledWith({ id: 1 }); + }); + + it('handles report not being found', async () => { + activityReportByLegacyId.mockResolvedValue(null); + await getLegacyReport(request, mockResponse); + expect(mockResponse.sendStatus).toHaveBeenCalledWith(404); + }); + + it('handles unauthorized', async () => { + ActivityReport.mockImplementationOnce(() => ({ + canViewLegacy: () => false, + })); + activityReportByLegacyId.mockResolvedValue({ region: 1 }); + await getLegacyReport(request, mockResponse); + expect(mockResponse.sendStatus).toHaveBeenCalledWith(403); + }); + }); + describe('reviewReport', () => { const request = { ...mockRequest, diff --git a/src/routes/activityReports/index.js b/src/routes/activityReports/index.js index 127384fb5b..f47aee2039 100644 --- a/src/routes/activityReports/index.js +++ b/src/routes/activityReports/index.js @@ -11,6 +11,7 @@ import { getGoals, reviewReport, resetToDraft, + getLegacyReport, } from './handlers'; const router = express.Router(); @@ -24,6 +25,7 @@ router.get('/approvers', getApprovers); router.get('/activity-recipients', getActivityRecipients); router.get('/goals', getGoals); router.get('/alerts', getReportAlerts); +router.get('/legacy/:legacyReportId', getLegacyReport); router.get('/:activityReportId', getReport); router.get('/', getReports); router.put('/:activityReportId', saveReport); diff --git a/src/seeders/20201211172017-grants.js b/src/seeders/20201211172017-grants.js index 35fc702c6a..53bd2785dc 100644 --- a/src/seeders/20201211172017-grants.js +++ b/src/seeders/20201211172017-grants.js @@ -4,72 +4,84 @@ const grants = [ number: '14CH1234', regionId: 14, granteeId: 1, + status: 'Active', }, { id: 2, number: '14CH10000', regionId: 14, granteeId: 2, + status: 'Active', }, { id: 3, number: '14CH00001', regionId: 14, granteeId: 3, + status: 'Active', }, { id: 4, number: '14CH00002', regionId: 14, granteeId: 4, + status: 'Active', }, { id: 5, number: '14CH00003', regionId: 14, granteeId: 4, + status: 'Active', }, { id: 6, number: '09CH011111', regionId: 9, granteeId: 5, + status: 'Active', }, { id: 7, number: '09CH022222', regionId: 9, granteeId: 6, + status: 'Active', }, { id: 8, number: '09CH033333', regionId: 9, granteeId: 7, + status: 'Active', }, { id: 9, number: '09HP044444', regionId: 9, granteeId: 8, + status: 'Active', }, { id: 10, number: '01HP044444', regionId: 1, granteeId: 9, + status: 'Active', }, { id: 11, number: '01HP022222', regionId: 1, granteeId: 10, + status: 'Inactive', }, { id: 12, number: '09HP01111', regionId: 1, granteeId: 11, + status: 'Active', }, ]; diff --git a/src/services/accessValidation.test.js b/src/services/accessValidation.test.js index 53431b0657..1a898f2e8f 100644 --- a/src/services/accessValidation.test.js +++ b/src/services/accessValidation.test.js @@ -12,12 +12,7 @@ const { SITE_ACCESS, ADMIN, READ_REPORTS, READ_WRITE_REPORTS, } = SCOPES; -jest.mock('../logger', () => ({ - auditLogger: { - error: jest.fn(), - info: jest.fn(), - }, -})); +jest.mock('../logger'); const mockUser = { id: 47, diff --git a/src/services/activityReports.js b/src/services/activityReports.js index bf36c36e57..c1b6774e0f 100644 --- a/src/services/activityReports.js +++ b/src/services/activityReports.js @@ -170,9 +170,17 @@ export async function review(report, status, managerNotes) { return updatedReport; } +export function activityReportByLegacyId(legacyId) { + return ActivityReport.findOne({ + where: { + legacyId, + }, + }); +} + export function activityReportById(activityReportId) { return ActivityReport.findOne({ - attributes: { exclude: ['legacyId'] }, + attributes: { exclude: ['imported', 'legacyId'] }, where: { id: { [Op.eq]: activityReportId, @@ -294,6 +302,7 @@ export function activityReports(readRegions, { 'regionId', 'updatedAt', 'sortedTopics', + 'legacyId', sequelize.literal( '(SELECT name as authorName FROM "Users" WHERE "Users"."id" = "ActivityReport"."userId")', ), @@ -479,18 +488,22 @@ export async function createOrUpdate(newActivityReport, report) { author, granteeNextSteps, specialistNextSteps, + ECLKCResourcesUsed, + nonECLKCResourcesUsed, ...allFields } = newActivityReport; - const ECLKCResourcesUsed = allFields.ECLKCResourcesUsed - ? allFields.ECLKCResourcesUsed.map((item) => item.value) - : []; + const resources = {}; + + if (ECLKCResourcesUsed) { + resources.ECLKCResourcesUsed = ECLKCResourcesUsed.map((item) => item.value); + } - const nonECLKCResourcesUsed = allFields.nonECLKCResourcesUsed - ? allFields.nonECLKCResourcesUsed.map((item) => item.value) - : []; + if (nonECLKCResourcesUsed) { + resources.nonECLKCResourcesUsed = nonECLKCResourcesUsed.map((item) => item.value); + } - const updatedFields = { ...allFields, ECLKCResourcesUsed, nonECLKCResourcesUsed }; + const updatedFields = { ...allFields, ...resources }; await sequelize.transaction(async (transaction) => { if (report) { savedReport = await update(updatedFields, report, transaction); diff --git a/src/services/activityReports.test.js b/src/services/activityReports.test.js index faf0a722aa..df789c8a60 100644 --- a/src/services/activityReports.test.js +++ b/src/services/activityReports.test.js @@ -8,6 +8,7 @@ import { review, activityReports, activityReportAlerts, + activityReportByLegacyId, } from './activityReports'; import { copyGoalsToGrants } from './goals'; import { REPORT_STATUSES } from '../constants'; @@ -290,6 +291,14 @@ describe('Activity Reports DB service', () => { }); }); + describe('activityReportByLegacyId', () => { + it('returns the report with the legacyId', async () => { + const report = await ActivityReport.create({ ...reportObject, legacyId: 'legacy' }); + const found = await activityReportByLegacyId('legacy'); + expect(found.id).toBe(report.id); + }); + }); + describe('activityReportById', () => { it('retrieves an activity report', async () => { const report = await ActivityReport.create(reportObject); diff --git a/src/tools/bootstrapAdmin.test.js b/src/tools/bootstrapAdmin.test.js index b09cd83e13..337cfe48ac 100644 --- a/src/tools/bootstrapAdmin.test.js +++ b/src/tools/bootstrapAdmin.test.js @@ -2,8 +2,8 @@ import bootstrapAdmin, { ADMIN_USERNAME } from './bootstrapAdmin'; import db, { User, Permission } from '../models'; describe('Bootstrap the first Admin user', () => { - afterAll(() => { - db.sequelize.close(); + afterAll(async () => { + await db.sequelize.close(); }); describe('when user already exists', () => { diff --git a/src/tools/dataValidation.js b/src/tools/dataValidation.js new file mode 100644 index 0000000000..6c447cf520 --- /dev/null +++ b/src/tools/dataValidation.js @@ -0,0 +1,54 @@ +import { QueryTypes } from 'sequelize'; +import { sequelize } from '../models'; +import { auditLogger } from '../logger'; + +const runSelectQuery = (query) => ( + sequelize.query(query, { type: QueryTypes.SELECT }) +); + +const countAndLastUpdated = async (tableName) => { + const updatedAtQuery = `SELECT "updatedAt" FROM "${tableName}" ORDER BY "updatedAt" DESC LIMIT 1`; + const [results] = await runSelectQuery(updatedAtQuery); + let updatedAt = ''; + if (results) { updatedAt = results.updatedAt; } + const countQuery = `SELECT count(*) FROM "${tableName}"`; + const [{ count }] = await runSelectQuery(countQuery); + return { + updatedAt, + count, + }; +}; + +const dataValidation = async () => { + let query; + let results; + const tableNames = [ + 'ActivityReports', + 'Files', + 'Goals', + 'Objectives', + 'NextSteps', + 'Grantees', + 'Grants', + 'Users', + ]; + const tableChecks = tableNames.map(async (table) => { + const { updatedAt, count } = await countAndLastUpdated(table); + auditLogger.info(`${table} has ${count} records, last updated at: ${updatedAt}`); + }); + await Promise.allSettled(tableChecks); + + query = 'SELECT "regionId", "status", count(*) FROM "Grants" GROUP BY "regionId", "status" ORDER BY "regionId", "status"'; + results = await runSelectQuery(query); + auditLogger.info(`Grants data counts: ${JSON.stringify(results, null, 2)}`); + + query = 'SELECT "regionId", "status", count(*) FROM "ActivityReports" GROUP BY "regionId", "status" ORDER BY "regionId", "status"'; + results = await runSelectQuery(query); + auditLogger.info(`ActivityReports data counts: ${JSON.stringify(results, null, 2)}`); +}; + +export { + runSelectQuery, + countAndLastUpdated, +}; +export default dataValidation; diff --git a/src/tools/dataValidation.test.js b/src/tools/dataValidation.test.js new file mode 100644 index 0000000000..df76f5a12f --- /dev/null +++ b/src/tools/dataValidation.test.js @@ -0,0 +1,45 @@ +import dataValidation, { countAndLastUpdated, runSelectQuery } from './dataValidation'; +import { sequelize } from '../models'; +import { auditLogger } from '../logger'; +import { DECIMAL_BASE } from '../constants'; + +jest.mock('../logger'); + +describe('dataValidation', () => { + afterAll(async () => { + await sequelize.close(); + }); + + describe('run basic query', () => { + it('should return the data in an object', async () => { + const query = 'SELECT "regionId", "status", count(*) FROM "Grants" GROUP BY "regionId", "status" ORDER BY "regionId", "status"'; + const [ + { regionId: firstRowRegion, status: firstRowStatus, count: firstRowCount }, + { regionId: secondRowRegion, status: secondRowStatus, count: secondRowCount }, + ] = await runSelectQuery(query); + + expect(firstRowRegion).toBe(1); + expect(firstRowStatus).toBe('Active'); + expect(firstRowCount).toBe('2'); + expect(secondRowRegion).toBe(1); + expect(secondRowStatus).toBe('Inactive'); + expect(secondRowCount).toBe('1'); + }); + }); + + describe('run count and last updated', () => { + it('should return the count and last updated value for the given table', async () => { + const { + updatedAt, + count, + } = await countAndLastUpdated('Grants'); + expect(parseInt(count, DECIMAL_BASE)).toBeGreaterThan(0); + expect(updatedAt).not.toBe(''); + }); + }); + + it('should log results to the auditLogger', async () => { + await dataValidation(); + expect(auditLogger.info).toHaveBeenCalledTimes(10); + }); +}); diff --git a/src/tools/dataValidationCLI.js b/src/tools/dataValidationCLI.js new file mode 100644 index 0000000000..78befeac26 --- /dev/null +++ b/src/tools/dataValidationCLI.js @@ -0,0 +1,14 @@ +import dataValidation from './dataValidation'; +import { auditLogger } from '../logger'; + +/** + * dataValidationCLI runs basic queries against the DB to verify that the db state + * is as we expect following import or restore operations. + * + * To run: `cf run-task tta-smarthub-prod --command "yarn db:validation"` + */ + +dataValidation().catch((e) => { + auditLogger.error(e); + return process.exit(1); +}); diff --git a/src/tools/importActivityReports.js b/src/tools/importActivityReports.js new file mode 100644 index 0000000000..7efadff326 --- /dev/null +++ b/src/tools/importActivityReports.js @@ -0,0 +1,301 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-loop-func */ +import parse from 'csv-parse/lib/sync'; +import moment from 'moment'; +import { logger } from '../logger'; +import { downloadFile } from '../lib/s3'; +import { + ActivityReport, + ActivityRecipient, + Grant, +} from '../models'; +import { REPORT_STATUSES } from '../constants'; + +/* +## Import Notes + +- replace R14 with the proper region (R14 was a test region) + +## SS Field Notes + +- reportId +- granteeName // activityRecipients +- cdiGranteeName // Empty/Unused? +- multiGranteeActivities +- programType +- nonGranteeActivity +- sourceOfRequest +- reasons +- tTa +- topics +- otherTopics +- granteeParticipants +- nonGranteeParticipants +- numberOfParticipants +- startDate +- endDate +- duration +- otherSpecialists // 'collaborators'? +- targetPopulations +- resourcesUsed +- contextForThisActivity +- nonOhsResources +- goal1 +- granteesLearningLevelGoal1 +- objective11 +- objective11Status +- objective12 +- objective12Status +- goal2 +- granteesLearningLevelGoal2 +- objective21 +- objective21Status +- objective22 +- objective22Status +- ttaProvidedAndGranteeProgressMade +- granteeFollowUpTasksObjectives +- specialistFollowUpTasksObjectives +- format +- additionalNotesForThisActivity +- manager +- managerApproval +- created +- createdBy +- overrideCreatedBy +- modified +- modifiedBy + +## Relational Fields + +### Fields that would map to Users +- createdBy: 'author' +- modifiedBy: 'lastUpdatedBy' +- manager: 'approvingManager' +- null: 'collaborators' // NOTE: "Other specialists"? +// NOTE: "Grantee Name", but maybe also "Non-Grantee Activity" +- granteeName: 'activityRecipients' + +### Other relational fields +- null: 'regionId' // NOTE: Take number from sheet name. R14 should be remapped +- null: 'attachments' // FIXME: How to get attachments from smarthub? +- 'specialistFollowUpTasksObjectives': 'specialistNextSteps' +- 'granteeFollowUpTasksObjectives': 'granteeNextSteps' +- 'goal1': 'goals' +- 'goal2': 'goals' + */ + +const columnCleanupRE = /(\s?\(.*\)|:|\.|\/|&|')+/g; +const decimalRE = /^\d+(\.\d*)?$/; +const invalidRegionRE = /R14/; +const grantNumRE = /\|\s+(?[0-9A-Z]+)(\n|$)/g; +const mdyDateRE = /^\d{1,2}\/\d{1,2}\/(\d{2}|\d{4})$/; +const mdyFormat = 'MM/DD/YYYY'; + +async function readCsv(file) { + const { Body: csv } = await downloadFile(file); + return parse(csv, { skipEmptyLines: true, columns: true }); +} + +// helper for mapping to camelCase +const hyphensToSpaces = (x) => (x === '-' ? ' ' : x); + +// Map to camelCase. will not respect acronyms, other unusual capitalization +function mapToCamelCase(char, index, word) { + if (index === 0) return char.toLowerCase(); + if (char === ' ') return ''; + const prevChar = word[index - 1]; + if (prevChar === ' ') return char.toUpperCase(); + return char.toLowerCase(); +} + +// Headers need to be uniform across sheets if we are to import +// NOTE: Doing this once per file would be an enchancement +function normalizeKey(k) { + let value = k.trim(); + // Manual fix for reportID + if (value.toLowerCase() === 'reportid') return 'reportId'; + // Remove parentheticals, other non-alphanumeric chars + value = value.replace(columnCleanupRE, '').trim(); + // Replace hyphens with spaces, then map to camelCase + value = value.split('') + .map(hyphensToSpaces) + .map(mapToCamelCase) + .join(''); + + return value; +} + +function getValue(data, key) { + if (({}).hasOwnProperty.call(data, key)) { + return data[key]; + } + return null; +} + +function normalizeData(row) { + const data = Object.create(null); + Object.entries(row).forEach(([key, value]) => { + const lookupKey = normalizeKey(key); + data[lookupKey] = value; + }); + return data; +} + +function coerceDuration(value) { + if (!value) return null; + const match = value.trim().match(decimalRE); + if (match) { + return match[0].trim(); + } + return null; +} + +function coerceToArray(value) { + if (!value) return []; + return value.split('\n').filter((x) => x); +} + +function coerceStatus(value) { + if (!value) return null; + const status = value.toLowerCase() + .trim() + .replace(/\s/g, '_'); + const statusMatch = Object.values(REPORT_STATUSES).includes(status); + if (statusMatch) { + return status; + } + return null; +} + +function coerceDate(value) { + if (!value) return null; + let fmt; + if (mdyDateRE.test(value.trim())) { + fmt = mdyFormat; + } + return moment(value, fmt); +} + +function coerceInt(value) { + if (!value) return null; + if (decimalRE.test(value)) { + return parseInt(value, 10); + } + return null; +} + +function coerceReportId(value, region) { + if (!value) return null; + // R14 is a test region, and shouldn't be used in actual reportIds + return value.replace(invalidRegionRE, `R${region}`); +} + +function parseGrantNumbers(value) { + const matchIter = value.matchAll(grantNumRE); + const results = []; + for (const m of matchIter) { + const { groups: { grantNumber } } = m; + if (grantNumber) { + results.push(grantNumber); + } + } + return results; +} + +export default async function importActivityReports(fileKey, region) { + let csvFile; + try { + csvFile = await readCsv(fileKey); + } catch (e) { + logger.error(`key: '${fileKey}'`); + logger.error(e); + process.exit(1); + } + + const regionId = region; + + // const activityReportRecords = []; + for await (const row of csvFile) { + const data = normalizeData(row); + + // logger.log(Object.keys(data)); + + const legacyId = coerceReportId(getValue(data, 'reportId'), regionId); + // Ignore rows with no reportid + if (legacyId) { + const granteeActivity = getValue(data, 'granteeActivity'); + const activityRecipientType = granteeActivity ? 'grantee' : 'nonGrantee'; + + // Coerce values into appropriate data type + const status = coerceStatus(getValue(data, 'managerApproval')); + const duration = coerceDuration(getValue(data, 'duration')); + const numberOfParticipants = coerceInt(getValue(data, 'numberOfParticipants')); + + const programTypes = coerceToArray(getValue(data, 'programType')); // FIXME: Check this key + const targetPopulations = coerceToArray(getValue(data, 'targetPopulations')); + const reason = coerceToArray(getValue(data, 'reasons')); + const participants = coerceToArray(getValue(data, 'granteeParticipants')) + .concat(coerceToArray(getValue(data, 'nonGranteeParticipants'))); + const topics = coerceToArray(getValue(data, 'topics')); + const ttaType = coerceToArray(getValue(data, 'tTa')); + + const startDate = coerceDate(getValue(data, 'startDate')); + const endDate = coerceDate(getValue(data, 'endDate')); + + const arRecord = { + imported: data, // Store all the data in `imported` for later reuse + legacyId, + regionId, + deliveryMethod: getValue(data, 'format'), // FIXME: Check records like 'R01-AR-000135' + ECLKCResourcesUsed: coerceToArray(getValue(data, 'resourcesUsed')), + nonECLKCResourcesUsed: coerceToArray(getValue(data, 'nonOhsResources')), + duration, // Decimal + startDate, + endDate, + activityRecipientType, + requester: getValue(data, 'sourceOfRequest'), // 'Grantee' or 'Regional Office' + programTypes, // Array of strings + targetPopulations, // Array of strings + reason, // Array of strings + numberOfParticipants, // Integer + participants, // Array of strings + topics, // Array of strings + context: getValue(data, 'contextForThisActivity'), + // TODO: Are 'managerNotes' the smartsheet comments (which are a separate sheet in Excel) + // managerNotes: ??? + additionalNotes: getValue(data, 'additionalNotesForThisActivity'), + status, // Enum restriction: REPORT_STATUSES + ttaType, // Array of strings + createdAt: getValue(data, 'created'), // DATE + updatedAt: getValue(data, 'modified'), // DATE + }; + // Ideally this would be an upsert, but sequelize v5 upsert doesn't return the instance?!? + try { + // Imported ARs won't pass `checkRequiredForSubmission`, + // because `approvingManagerId`, `requester`, etc. may be null + // so we build, then save without validating; + const [ar, built] = await ActivityReport.findOrBuild( + { where: { legacyId }, defaults: arRecord }, + ); + if (built) { + await ar.save({ validate: false }); + } + + // ActivityRecipients: connect Grants to ActivityReports + const grantNumbers = parseGrantNumbers(getValue(data, 'granteeName')); + for await (const n of grantNumbers) { + const grant = await Grant.findOne({ where: { number: n } }); + if (grant) { + ActivityRecipient.findOrCreate( + { where: { activityReportId: ar.id, grantId: grant.id } }, + ); + } + } + } catch (e) { + logger.error(e); + } + } else { + logger.warn('ActivityReport with no reportId, skipping'); + } + } +} diff --git a/src/tools/importActivityReports.test.js b/src/tools/importActivityReports.test.js new file mode 100644 index 0000000000..4f0605eacc --- /dev/null +++ b/src/tools/importActivityReports.test.js @@ -0,0 +1,46 @@ +import { readFileSync } from 'fs'; +import importActivityReports from './importActivityReports'; +import { downloadFile } from '../lib/s3'; +import db, { + ActivityReport, + ActivityRecipient, +} from '../models'; + +jest.mock('../lib/s3'); + +describe('Import Activity Reports', () => { + beforeEach(() => { + downloadFile.mockReset(); + }); + afterAll(async () => { + await ActivityReport.destroy({ where: {} }); + await ActivityRecipient.destroy({ where: {} }); + await db.sequelize.close(); + }); + it('should import ActivityReports table', async () => { + const fileName = 'R14ActivityReportsTest.csv'; + + downloadFile.mockResolvedValue({ Body: readFileSync(fileName) }); + + await ActivityReport.destroy({ where: {} }); + const reportsBefore = await ActivityReport.findAll(); + + expect(reportsBefore.length).toBe(0); + await importActivityReports(fileName, 14); + + const records = await ActivityReport.findAll({ + attributes: ['id', 'legacyId', 'requester'], + }); + + expect(records).toBeDefined(); + expect(records.length).toBe(10); + + expect(records).toContainEqual( + expect.objectContaining({ id: expect.anything(), legacyId: 'R14-AR-000279', requester: 'Regional Office' }), + ); + + expect(records).toContainEqual( + expect.objectContaining({ id: expect.anything(), legacyId: 'R14-AR-001132', requester: 'Grantee' }), + ); + }); +}); diff --git a/src/tools/importPlanGoals.js b/src/tools/importPlanGoals.js index f912f257de..9af97ebf07 100644 --- a/src/tools/importPlanGoals.js +++ b/src/tools/importPlanGoals.js @@ -41,6 +41,19 @@ async function prePopulateRoles() { updateOnDuplicate: ['updatedAt'], }); } + +const grantNumRE = /\s(?[0-9]{2}[A-Z]{2}[0-9]+)(?:[,\s]|$)/g; +const parseGrantNumbers = (value) => { + const matchIter = value.matchAll(grantNumRE); + const results = []; + for (const { groups: { grantNumber } } of matchIter) { + if (grantNumber) { + results.push(grantNumber); + } + } + return results; +}; + /** * Processes data from .csv inserting the data during the processing as well as * creating data arrays for associations and then inserting them to the database @@ -66,32 +79,35 @@ export default async function importGoals(fileKey, region) { const cleanRoleTopics = []; const cleanGrantGoals = []; const cleanTopicGoals = []; - const currentGoals = []; await prePopulateRoles(); for await (const el of grantees) { - let currentGranteeId; - let grants; let currentGrants = []; - let currentGoalName; - let currentGoal = {}; + const currentGoals = []; + let currentGoalName = ''; + let currentGoalNum = 0; for await (const key of Object.keys(el)) { if (key && (key.trim().startsWith('Grantee (distinct') || key.trim().startsWith('Grantee Name'))) { - grants = el[key] ? el[key].split('|')[1].trim() : 'Unknown Grant'; - currentGrants = grants.split(','); + currentGrants = parseGrantNumbers(el[key]); } else if (key && key.startsWith('Goal')) { const goalColumn = key.split(' '); let column; if (goalColumn.length === 2) { // Column name is "Goal X" representing goal's name currentGoalName = el[key].trim(); + if (currentGoalName.match(/(no goals?|none)( identified)? at this time\.?/i)) { + currentGoalName = ''; + } if (currentGoalName !== '') { // Ignore empty goals - currentGoal = { name: currentGoalName }; // change to dbGoal - const goalNum = goalColumn[1]; - currentGoals[goalNum] = { ...currentGoals[goalNum], ...currentGoal }; + // eslint-disable-next-line prefer-destructuring + currentGoalNum = goalColumn[1]; + currentGoals[currentGoalNum] = { + ...currentGoals[currentGoalNum], + name: currentGoalName, + }; } - } else { + } else if (currentGoalName !== '') { // column will be either "topics", "timeframe" or "status" column = goalColumn[2].toLowerCase(); if (column === 'topics') { @@ -124,16 +140,17 @@ export default async function importGoals(fileKey, region) { } // Add topic to junction with goal cleanTopicGoals.push( - { topicId, goalName: currentGoal.name }, + { topicId, goalName: currentGoalName }, ); // we don't have goal's id at this point yet } } - } else // it's either "timeframe" or "status" - // both "timeframe" and "status" column names will be reused as goal's object keys - if (currentGoalName !== '') { - currentGoal[column] = el[key].trim(); - const goalNum = goalColumn[1].slice(0, 1); // represents a goal number from 1 to 5 - currentGoals[goalNum] = { ...currentGoals[goalNum], ...currentGoal }; + } else { + // it's either "timeframe" or "status" + // both "timeframe" and "status" column names will be reused as goal's object keys + currentGoals[currentGoalNum] = { + ...currentGoals[currentGoalNum], + [column]: el[key].trim(), + }; } } } @@ -142,12 +159,14 @@ export default async function importGoals(fileKey, region) { // after each row let goalId; let grantId; + let currentGranteeId; for await (const goal of currentGoals) { if (goal) { // ignore the dummy element at index 0 - const [dbGoal] = await Goal.findOrCreate( - { where: { ...goal, isFromSmartsheetTtaPlan: true } }, - ); + const [dbGoal] = await Goal.findOrCreate({ + where: { name: goal.name, isFromSmartsheetTtaPlan: true }, + defaults: goal, + }); goalId = dbGoal.id; // add goal id to cleanTopicGoals cleanTopicGoals.forEach((tp) => { diff --git a/src/tools/importSSActivityReports.js b/src/tools/importSSActivityReports.js new file mode 100644 index 0000000000..3c51ba1110 --- /dev/null +++ b/src/tools/importSSActivityReports.js @@ -0,0 +1,29 @@ +import {} from 'dotenv/config'; +import { option } from 'yargs'; +import importActivityReports from './importActivityReports'; +import { logger } from '../logger'; + +const { argv } = option('file', { + alias: 'f', + description: 'Input .csv file', + type: 'string', +}).option('region', { + description: 'grant\'s region', + type: 'number', +}) + .help() + .alias('help', 'h'); + +const { file, region } = argv; + +if (!file) { + logger.error('File not provided to importSSActivityReports'); + process.exit(1); +} + +if (!region) { + logger.error('Region not provided to importSSActivityReports'); + process.exit(1); +} + +importActivityReports(file, region);