diff --git a/epictrack-api/migrations/data/002-work_types.json b/epictrack-api/migrations/data/002-work_types.json index 9be909fb0..d0e37dc09 100644 --- a/epictrack-api/migrations/data/002-work_types.json +++ b/epictrack-api/migrations/data/002-work_types.json @@ -1,698 +1,698 @@ -{ - "phases": [ - { - "name": "Project Notification", - "work_type_id": 1, - "ea_act_id": 3, - "sort_order": 1, - "legislated": true, - "duration": 60, - "color": "#ffffff", - "id": "project_notification" - }, - { - "name": "Post: EAC Extension", - "work_type_id": 11, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 120, - "color": "#a6bb2e", - "id": "post_eac_extension" - }, - { - "name": "Early Engagement", - "work_type_id": 5, - "ea_act_id": 3, - "sort_order": 1, - "legislated": true, - "duration": 90, - "color": "#54858d", - "id": "early_engagement" - }, - { - "name": "Proponent Time: Early Engagement", - "work_type_id": 5, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 365, - "color": "#CCFFFF", - "id": "proponent_time_early_engagement" - }, - { - "name": "Readiness Decision", - "work_type_id": 5, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 60, - "color": "#da6d65", - "id": "readiness_decision" - }, - { - "name": "Post: EAC/Order Transfer", - "work_type_id": 13, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 60, - "color": "#a6bb2e", - "id": "post_eac_order_transfer" - }, - { - "name": "Post: Simple Amendment", - "work_type_id": 7, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 60, - "color": "#a6bb2e", - "id": "post_simple_amendment" - }, - { - "name": "Post: Typical Amendment", - "work_type_id": 8, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 180, - "color": "#a6bb2e", - "id": "post_typical_amendment" - }, - { - "name": "Post: Complex Amendment", - "work_type_id": 9, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 365, - "color": "#a6bb2e", - "id": "post_complex_amendment" - } - ], - "milestones": [ - { - "name": "Project Notification Submission", - "phase_id": "project_notification", - "milestone_type_id": 16, - "sort_order": 1, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "project_notification", - "milestone_type_id": 21, - "sort_order": 2, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "project_notification", - "milestone_type_id": 11, - "sort_order": 3, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "PCP", - "phase_id": "project_notification", - "milestone_type_id": 11, - "sort_order": 4, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "project_notification", - "milestone_type_id": 22, - "sort_order": 5, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "project_notification", - "milestone_type_id": 23, - "sort_order": 6, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Close", - "phase_id": "project_notification", - "milestone_type_id": 18, - "sort_order": 7, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Open House", - "phase_id": "project_notification", - "milestone_type_id": 6, - "sort_order": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Virtual Open House", - "phase_id": "project_notification", - "milestone_type_id": 17, - "sort_order": 9, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Report & Referral", - "phase_id": "project_notification", - "milestone_type_id": 13, - "sort_order": 10, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Notification Decision", - "phase_id": "project_notification", - "milestone_type_id": 13, - "sort_order": 11, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id":"project_notification_notification_decision", - "auto": true - }, - { - "name": "Other", - "phase_id": "project_notification", - "milestone_type_id": 8, - "sort_order": 32767, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - - - { - "name": "Extension Request", - "phase_id": "post_eac_extension", - "milestone_type_id": 14, - "sort_order": 12, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "post_eac_extension", - "milestone_type_id": 21, - "sort_order": 13, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "post_eac_extension", - "milestone_type_id": 11, - "sort_order": 14, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "PCP", - "phase_id": "post_eac_extension", - "milestone_type_id": 11, - "sort_order": 15, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "post_eac_extension", - "milestone_type_id": 22, - "sort_order": 16, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "post_eac_extension", - "milestone_type_id": 23, - "sort_order": 17, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Close", - "phase_id": "post_eac_extension", - "milestone_type_id": 18, - "sort_order": 18, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Open House", - "phase_id": "post_eac_extension", - "milestone_type_id": 6, - "sort_order": 19, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Virtual Open House", - "phase_id": "post_eac_extension", - "milestone_type_id": 17, - "sort_order": 20, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Report & Referral", - "phase_id": "post_eac_extension", - "milestone_type_id": 13, - "sort_order": 21, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Extension Decision", - "phase_id": "post_eac_extension", - "milestone_type_id": 13, - "sort_order": 22, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id": "post_eac_extension_extension_decision", - "auto": true - }, - { - "name": "Other", - "phase_id": "post_eac_extension", - "milestone_type_id": 8, - "sort_order": 32767, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - - - { - "name": "Submission of IPD & Engagement Plan", - "phase_id": "early_engagement", - "milestone_type_id": 16, - "sort_order": 23, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "early_engagement", - "milestone_type_id": 21, - "sort_order": 24, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "early_engagement", - "milestone_type_id": 11, - "sort_order": 25, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "PCP", - "phase_id": "early_engagement", - "milestone_type_id": 11, - "sort_order": 26, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "early_engagement", - "milestone_type_id": 22, - "sort_order": 27, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "early_engagement", - "milestone_type_id": 23, - "sort_order": 28, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Close", - "phase_id": "early_engagement", - "milestone_type_id": 18, - "sort_order": 29, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Open House", - "phase_id": "early_engagement", - "milestone_type_id": 6, - "sort_order": 30, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Virtual Open House", - "phase_id": "early_engagement", - "milestone_type_id": 17, - "sort_order": 31, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Project Withdrawn", - "phase_id": "early_engagement", - "milestone_type_id": 1, - "sort_order": 32, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id": "early_engagement_project_withdrawn", - "auto": false - }, - { - "name": "Other", - "phase_id": "early_engagement", - "milestone_type_id": 8, - "sort_order": 32767, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Summary of Engagement", - "phase_id": "early_engagement", - "milestone_type_id": 13, - "sort_order": 33, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Start of Proponent Time: Early Engagement", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 12, - "sort_order": 34, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Project Termination (s.39)", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 1, - "sort_order": 35, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id": "proponent_time_early_engagement_project_termination", - "auto": true - }, - { - "name": "Other", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 8, - "sort_order": 32767, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "End of Proponent Time: Early Engagement", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 12, - "sort_order": 36, - "start_at": 365, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Accept Detailed Project Description", - "phase_id": "readiness_decision", - "milestone_type_id": 16, - "sort_order": 37, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "DPD Accepted Announcement & Tweet", - "phase_id": "readiness_decision", - "milestone_type_id": 5, - "sort_order": 38, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "Referred to Minister for Decision", - "phase_id": "readiness_decision", - "milestone_type_id": 13, - "sort_order": 39, - "start_at": 30, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Referred to Minister Announcement & Tweet", - "phase_id": "readiness_decision", - "milestone_type_id": 5, - "sort_order": 40, - "start_at": 30, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "Minister's Decision Announcement & Tweet", - "phase_id": "readiness_decision", - "milestone_type_id": 5, - "sort_order": 41, - "start_at": 60, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "Other", - "phase_id": "readiness_decision", - "milestone_type_id": 8, - "sort_order": 32767, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Ministers Decision", - "phase_id": "readiness_decision", - "milestone_type_id": 4, - "sort_order": 42, - "start_at": 60, - "duration": 0, - "kind": "EVENT", - "id": "readiness_decision_ministers_decision", - "auto": true - } - - - , - { - "name": "Transfer Request", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 14, - "sort_order": 43, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Report & Referral", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 13, - "sort_order": 44, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Other", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 8, - "sort_order": 32767, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Transfer Decision", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 1, - "sort_order": 45, - "start_at": 60, - "duration": 0, - "kind": "EVENT", - "id": "post_eac_order_transfer_transfer_decision", - "auto": true - } - - ], - "outcomes": [ - { - "name": "No Further Review Required", - "milestone_id": "project_notification_notification_decision", - "sort_order": 1, - "terminates_work": false - }, - { - "name": "More Information Needed", - "milestone_id": "project_notification_notification_decision", - "sort_order": 2, - "terminates_work": false - }, - { - "name": "Project Referred to Minister", - "milestone_id": "project_notification_notification_decision", - "sort_order": 3, - "terminates_work": false - }, - { - "name": "EAC Extension Granted", - "milestone_id": "post_eac_extension_extension_decision", - "sort_order": 4, - "terminates_work": false - }, - { - "name": "EAC Extension Refused", - "milestone_id": "post_eac_extension_extension_decision", - "sort_order": 5, - "terminates_work": false - }, - { - "name": "Project Withdrawn", - "milestone_id": "early_engagement_project_withdrawn", - "sort_order": 6, - "terminates_work": true - }, - { - "name": "Project Termination", - "milestone_id": "proponent_time_early_engagement_project_termination", - "sort_order": 6, - "terminates_work": true - }, - - { - "name": "Revised DPD Ordered", - "milestone_id": "readiness_decision_ministers_decision", - "sort_order": 7, - "terminates_work": false - }, - { - "name": "Exemption Order Granted", - "milestone_id": "readiness_decision_ministers_decision", - "sort_order": 7, - "terminates_work": false - }, - { - "name": "Assessment Terminated", - "milestone_id": "readiness_decision_ministers_decision", - "sort_order": 8, - "terminates_work": true - }, - - - { - "name": "EAC/Order Transfer Granted", - "milestone_id": "post_eac_order_transfer_transfer_decision", - "sort_order": 9, - "terminates_work": false - }, - { - "name": "EAC/Order Transfer Refused", - "milestone_id": "post_eac_order_transfer_transfer_decision", - "sort_order": 10, - "terminates_work": false - } - ] - } +{ + "phases": [ + { + "name": "Project Notification", + "work_type_id": 1, + "ea_act_id": 3, + "sort_order": 1, + "legislated": true, + "duration": 60, + "color": "#ffffff", + "id": "project_notification" + }, + { + "name": "Post: EAC Extension", + "work_type_id": 11, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 120, + "color": "#a6bb2e", + "id": "post_eac_extension" + }, + { + "name": "Early Engagement", + "work_type_id": 5, + "ea_act_id": 3, + "sort_order": 1, + "legislated": true, + "duration": 90, + "color": "#54858d", + "id": "early_engagement" + }, + { + "name": "Proponent Time: Early Engagement", + "work_type_id": 5, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 365, + "color": "#CCFFFF", + "id": "proponent_time_early_engagement" + }, + { + "name": "Readiness Decision", + "work_type_id": 5, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 60, + "color": "#da6d65", + "id": "readiness_decision" + }, + { + "name": "Post: EAC/Order Transfer", + "work_type_id": 13, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 60, + "color": "#a6bb2e", + "id": "post_eac_order_transfer" + }, + { + "name": "Post: Simple Amendment", + "work_type_id": 7, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 60, + "color": "#a6bb2e", + "id": "post_simple_amendment" + }, + { + "name": "Post: Typical Amendment", + "work_type_id": 8, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 180, + "color": "#a6bb2e", + "id": "post_typical_amendment" + }, + { + "name": "Post: Complex Amendment", + "work_type_id": 9, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 365, + "color": "#a6bb2e", + "id": "post_complex_amendment" + } + ], + "milestones": [ + { + "name": "Project Notification Submission", + "phase_id": "project_notification", + "milestone_type_id": 16, + "sort_order": 1, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "project_notification", + "milestone_type_id": 21, + "sort_order": 2, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "project_notification", + "milestone_type_id": 11, + "sort_order": 3, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "PCP", + "phase_id": "project_notification", + "milestone_type_id": 11, + "sort_order": 4, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "project_notification", + "milestone_type_id": 22, + "sort_order": 5, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "project_notification", + "milestone_type_id": 23, + "sort_order": 6, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Close", + "phase_id": "project_notification", + "milestone_type_id": 18, + "sort_order": 7, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Open House", + "phase_id": "project_notification", + "milestone_type_id": 6, + "sort_order": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Virtual Open House", + "phase_id": "project_notification", + "milestone_type_id": 17, + "sort_order": 9, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Report & Referral", + "phase_id": "project_notification", + "milestone_type_id": 13, + "sort_order": 10, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Notification Decision", + "phase_id": "project_notification", + "milestone_type_id": 13, + "sort_order": 11, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id":"project_notification_notification_decision", + "auto": true + }, + { + "name": "Other", + "phase_id": "project_notification", + "milestone_type_id": 8, + "sort_order": 32767, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + + + { + "name": "Extension Request", + "phase_id": "post_eac_extension", + "milestone_type_id": 14, + "sort_order": 12, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "post_eac_extension", + "milestone_type_id": 21, + "sort_order": 13, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "post_eac_extension", + "milestone_type_id": 11, + "sort_order": 14, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "PCP", + "phase_id": "post_eac_extension", + "milestone_type_id": 11, + "sort_order": 15, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "post_eac_extension", + "milestone_type_id": 22, + "sort_order": 16, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "post_eac_extension", + "milestone_type_id": 23, + "sort_order": 17, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Close", + "phase_id": "post_eac_extension", + "milestone_type_id": 18, + "sort_order": 18, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Open House", + "phase_id": "post_eac_extension", + "milestone_type_id": 6, + "sort_order": 19, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Virtual Open House", + "phase_id": "post_eac_extension", + "milestone_type_id": 17, + "sort_order": 20, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Report & Referral", + "phase_id": "post_eac_extension", + "milestone_type_id": 13, + "sort_order": 21, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Extension Decision", + "phase_id": "post_eac_extension", + "milestone_type_id": 13, + "sort_order": 22, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id": "post_eac_extension_extension_decision", + "auto": true + }, + { + "name": "Other", + "phase_id": "post_eac_extension", + "milestone_type_id": 8, + "sort_order": 32767, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + + + { + "name": "Submission of IPD & Engagement Plan", + "phase_id": "early_engagement", + "milestone_type_id": 16, + "sort_order": 23, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "early_engagement", + "milestone_type_id": 21, + "sort_order": 24, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "early_engagement", + "milestone_type_id": 11, + "sort_order": 25, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "PCP", + "phase_id": "early_engagement", + "milestone_type_id": 11, + "sort_order": 26, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "early_engagement", + "milestone_type_id": 22, + "sort_order": 27, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "early_engagement", + "milestone_type_id": 23, + "sort_order": 28, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Close", + "phase_id": "early_engagement", + "milestone_type_id": 18, + "sort_order": 29, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Open House", + "phase_id": "early_engagement", + "milestone_type_id": 6, + "sort_order": 30, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Virtual Open House", + "phase_id": "early_engagement", + "milestone_type_id": 17, + "sort_order": 31, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Project Withdrawn", + "phase_id": "early_engagement", + "milestone_type_id": 1, + "sort_order": 32, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id": "early_engagement_project_withdrawn", + "auto": false + }, + { + "name": "Other", + "phase_id": "early_engagement", + "milestone_type_id": 8, + "sort_order": 32767, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Summary of Engagement", + "phase_id": "early_engagement", + "milestone_type_id": 13, + "sort_order": 33, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Start of Proponent Time: Early Engagement", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 12, + "sort_order": 34, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Project Termination (s.39)", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 1, + "sort_order": 35, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id": "proponent_time_early_engagement_project_termination", + "auto": true + }, + { + "name": "Other", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 8, + "sort_order": 32767, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "End of Proponent Time: Early Engagement", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 12, + "sort_order": 36, + "start_at": 365, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Accept Detailed Project Description", + "phase_id": "readiness_decision", + "milestone_type_id": 16, + "sort_order": 37, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "DPD Accepted Announcement & Tweet", + "phase_id": "readiness_decision", + "milestone_type_id": 5, + "sort_order": 38, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "Referred to Minister for Decision", + "phase_id": "readiness_decision", + "milestone_type_id": 13, + "sort_order": 39, + "start_at": 30, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Referred to Minister Announcement & Tweet", + "phase_id": "readiness_decision", + "milestone_type_id": 5, + "sort_order": 40, + "start_at": 30, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "Minister's Decision Announcement & Tweet", + "phase_id": "readiness_decision", + "milestone_type_id": 5, + "sort_order": 41, + "start_at": 60, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "Other", + "phase_id": "readiness_decision", + "milestone_type_id": 8, + "sort_order": 32767, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Ministers Decision", + "phase_id": "readiness_decision", + "milestone_type_id": 4, + "sort_order": 42, + "start_at": 60, + "duration": 0, + "kind": "EVENT", + "id": "readiness_decision_ministers_decision", + "auto": true + } + + + , + { + "name": "Transfer Request", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 14, + "sort_order": 43, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Report & Referral", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 13, + "sort_order": 44, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Other", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 8, + "sort_order": 32767, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Transfer Decision", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 1, + "sort_order": 45, + "start_at": 60, + "duration": 0, + "kind": "EVENT", + "id": "post_eac_order_transfer_transfer_decision", + "auto": true + } + + ], + "outcomes": [ + { + "name": "No Further Review Required", + "milestone_id": "project_notification_notification_decision", + "sort_order": 1, + "terminates_work": false + }, + { + "name": "More Information Needed", + "milestone_id": "project_notification_notification_decision", + "sort_order": 2, + "terminates_work": false + }, + { + "name": "Project Referred to Minister", + "milestone_id": "project_notification_notification_decision", + "sort_order": 3, + "terminates_work": false + }, + { + "name": "EAC Extension Granted", + "milestone_id": "post_eac_extension_extension_decision", + "sort_order": 4, + "terminates_work": false + }, + { + "name": "EAC Extension Refused", + "milestone_id": "post_eac_extension_extension_decision", + "sort_order": 5, + "terminates_work": false + }, + { + "name": "Project Withdrawn", + "milestone_id": "early_engagement_project_withdrawn", + "sort_order": 6, + "terminates_work": true + }, + { + "name": "Project Termination", + "milestone_id": "proponent_time_early_engagement_project_termination", + "sort_order": 6, + "terminates_work": true + }, + + { + "name": "Revised DPD Ordered", + "milestone_id": "readiness_decision_ministers_decision", + "sort_order": 7, + "terminates_work": false + }, + { + "name": "Exemption Order Granted", + "milestone_id": "readiness_decision_ministers_decision", + "sort_order": 7, + "terminates_work": false + }, + { + "name": "Assessment Terminated", + "milestone_id": "readiness_decision_ministers_decision", + "sort_order": 8, + "terminates_work": true + }, + + + { + "name": "EAC/Order Transfer Granted", + "milestone_id": "post_eac_order_transfer_transfer_decision", + "sort_order": 9, + "terminates_work": false + }, + { + "name": "EAC/Order Transfer Refused", + "milestone_id": "post_eac_order_transfer_transfer_decision", + "sort_order": 10, + "terminates_work": false + } + ] + } \ No newline at end of file diff --git a/epictrack-api/migrations/data/002-work_types_update.json b/epictrack-api/migrations/data/002-work_types_update.json index a0b018213..9adc4d87a 100644 --- a/epictrack-api/migrations/data/002-work_types_update.json +++ b/epictrack-api/migrations/data/002-work_types_update.json @@ -1,708 +1,708 @@ -{ - "phases": [ - { - "name": "Project Notification", - "work_type_id": 1, - "ea_act_id": 3, - "sort_order": 1, - "legislated": true, - "duration": 60, - "color": "#ffffff", - "id": "project_notification" - }, - { - "name": "Post: EAC Extension", - "work_type_id": 11, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 120, - "color": "#a6bb2e", - "id": "post_eac_extension" - }, - { - "name": "Early Engagement", - "work_type_id": 5, - "ea_act_id": 3, - "sort_order": 1, - "legislated": true, - "duration": 90, - "color": "#54858d", - "id": "early_engagement" - }, - { - "name": "Proponent Time: Early Engagement", - "work_type_id": 5, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 365, - "color": "#CCFFFF", - "id": "proponent_time_early_engagement" - }, - { - "name": "Readiness Decision", - "work_type_id": 5, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 60, - "color": "#da6d65", - "id": "readiness_decision" - }, - { - "name": "Post: EAC/Order Transfer", - "work_type_id": 13, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 60, - "color": "#a6bb2e", - "id": "post_eac_order_transfer" - }, - { - "name": "Post: Simple Amendment", - "work_type_id": 7, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 60, - "color": "#a6bb2e", - "id": "post_simple_amendment" - }, - { - "name": "Post: Typical Amendment", - "work_type_id": 8, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 180, - "color": "#a6bb2e", - "id": "post_typical_amendment" - }, - { - "name": "Post: Complex Amendment", - "work_type_id": 9, - "ea_act_id": 3, - "sort_order": 1, - "legislated": false, - "duration": 365, - "color": "#a6bb2e", - "id": "post_complex_amendment" - } - ], - "milestones": [ - { - "name": "Project Notification Submission", - "phase_id": "project_notification", - "milestone_type_id": 16, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "project_notification", - "milestone_type_id": 21, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "project_notification", - "milestone_type_id": 11, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "PCP", - "phase_id": "project_notification", - "milestone_type_id": 11, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "project_notification", - "milestone_type_id": 22, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "project_notification", - "milestone_type_id": 23, - "start_at": 42, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Close", - "phase_id": "project_notification", - "milestone_type_id": 18, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Open House", - "phase_id": "project_notification", - "milestone_type_id": 6, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Virtual Open House", - "phase_id": "project_notification", - "milestone_type_id": 17, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Report & Referral", - "phase_id": "project_notification", - "milestone_type_id": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Notification Decision", - "phase_id": "project_notification", - "milestone_type_id": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id":"project_notification_notification_decision", - "auto": true - }, - { - "name": "Other", - "phase_id": "project_notification", - "milestone_type_id": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - - - { - "name": "Extension Request", - "phase_id": "post_eac_extension", - "milestone_type_id": 14, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "post_eac_extension", - "milestone_type_id": 21, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "post_eac_extension", - "milestone_type_id": 11, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "PCP", - "phase_id": "post_eac_extension", - "milestone_type_id": 11, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "post_eac_extension", - "milestone_type_id": 22, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "post_eac_extension", - "milestone_type_id": 23, - "start_at": 42, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Close", - "phase_id": "post_eac_extension", - "milestone_type_id": 18, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Open House", - "phase_id": "post_eac_extension", - "milestone_type_id": 6, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Virtual Open House", - "phase_id": "post_eac_extension", - "milestone_type_id": 17, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Report & Referral", - "phase_id": "post_eac_extension", - "milestone_type_id": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Extension Decision", - "phase_id": "post_eac_extension", - "milestone_type_id": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id": "post_eac_extension_extension_decision", - "auto": true - }, - { - "name": "Other", - "phase_id": "post_eac_extension", - "milestone_type_id": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - - - { - "name": "Submission of IPD & Engagement Plan", - "phase_id": "early_engagement", - "milestone_type_id": 16, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "early_engagement", - "milestone_type_id": 21, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "early_engagement", - "milestone_type_id": 11, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "PCP", - "phase_id": "early_engagement", - "milestone_type_id": 11, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "early_engagement", - "milestone_type_id": 22, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "early_engagement", - "milestone_type_id": 23, - "start_at": 42, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Close", - "phase_id": "early_engagement", - "milestone_type_id": 18, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Open House", - "phase_id": "early_engagement", - "milestone_type_id": 6, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Virtual Open House", - "phase_id": "early_engagement", - "milestone_type_id": 17, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Project Withdrawn", - "phase_id": "early_engagement", - "milestone_type_id": 1, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id": "early_engagement_project_withdrawn", - "auto": false - }, - { - "name": "Other", - "phase_id": "early_engagement", - "milestone_type_id": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Summary of Engagement", - "phase_id": "early_engagement", - "milestone_type_id": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Start of Proponent Time: Early Engagement", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 12, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Project Termination (s.39)", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 1, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "id": "proponent_time_early_engagement_project_termination", - "auto": false - }, - { - "name": "Other", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "End of Proponent Time: Early Engagement", - "phase_id": "proponent_time_early_engagement", - "milestone_type_id": 12, - "start_at": 365, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Accept Detailed Project Description", - "phase_id": "readiness_decision", - "milestone_type_id": 16, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "DPD Accepted Announcement & Tweet", - "phase_id": "readiness_decision", - "milestone_type_id": 5, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "Referred to Minister for Decision", - "phase_id": "readiness_decision", - "milestone_type_id": 13, - "start_at": 30, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Referred to Minister Announcement & Tweet", - "phase_id": "readiness_decision", - "milestone_type_id": 5, - "start_at": 30, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "Minister's Decision Announcement & Tweet", - "phase_id": "readiness_decision", - "milestone_type_id": 5, - "start_at": 60, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "Other", - "phase_id": "readiness_decision", - "milestone_type_id": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Ministers Decision", - "phase_id": "readiness_decision", - "milestone_type_id": 4, - "start_at": 60, - "duration": 0, - "kind": "EVENT", - "id": "readiness_decision_ministers_decision", - "auto": true - } - - - , - { - "name": "Transfer Request", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 14, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Report & Referral", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": true - }, - { - "name": "Other", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 8, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": false - }, - { - "name": "Transfer Decision", - "phase_id": "post_eac_order_transfer", - "milestone_type_id": 1, - "start_at": 60, - "duration": 0, - "kind": "EVENT", - "id": "post_eac_order_transfer_transfer_decision", - "auto": true - }, - - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "post_typical_amendment", - "milestone_type_id": 21, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "post_typical_amendment", - "milestone_type_id": 11, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "post_typical_amendment", - "milestone_type_id": 22, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "post_typical_amendment", - "milestone_type_id": 23, - "start_at": 42, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - - { - "name": "PCP Coming Announcement & Tweet", - "phase_id": "post_complex_amendment", - "milestone_type_id": 21, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP", - "phase_id": "post_complex_amendment", - "milestone_type_id": 11, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Open Announcement & Tweet", - "phase_id": "post_complex_amendment", - "milestone_type_id": 22, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - }, - { - "name": "PCP Closing Announcement & Tweet", - "phase_id": "post_complex_amendment", - "milestone_type_id": 23, - "start_at": 42, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": true - } - - ], - "outcomes": [ - { - "name": "No Further Review Required", - "milestone_id": "project_notification_notification_decision", - "terminates_work": false - }, - { - "name": "More Information Needed", - "milestone_id": "project_notification_notification_decision", - "terminates_work": false - }, - { - "name": "Project Referred to Minister", - "milestone_id": "project_notification_notification_decision", - "terminates_work": false - }, - { - "name": "EAC Extension Granted", - "milestone_id": "post_eac_extension_extension_decision", - "terminates_work": false - }, - { - "name": "EAC Extension Refused", - "milestone_id": "post_eac_extension_extension_decision", - "terminates_work": false - }, - { - "name": "Project Withdrawn", - "milestone_id": "early_engagement_project_withdrawn", - "terminates_work": true - }, - { - "name": "Project Termination", - "milestone_id": "proponent_time_early_engagement_project_termination", - "terminates_work": true - }, - - { - "name": "Revised DPD Ordered", - "milestone_id": "readiness_decision_ministers_decision", - "terminates_work": false - }, - { - "name": "Exemption Order Granted", - "milestone_id": "readiness_decision_ministers_decision", - "terminates_work": false - }, - { - "name": "Assessment Terminated", - "milestone_id": "readiness_decision_ministers_decision", - "terminates_work": true - }, - - - { - "name": "EAC/Order Transfer Granted", - "milestone_id": "post_eac_order_transfer_transfer_decision", - "terminates_work": false - }, - { - "name": "EAC/Order Transfer Refused", - "milestone_id": "post_eac_order_transfer_transfer_decision", - "terminates_work": false - } - ] -} +{ + "phases": [ + { + "name": "Project Notification", + "work_type_id": 1, + "ea_act_id": 3, + "sort_order": 1, + "legislated": true, + "duration": 60, + "color": "#ffffff", + "id": "project_notification" + }, + { + "name": "Post: EAC Extension", + "work_type_id": 11, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 120, + "color": "#a6bb2e", + "id": "post_eac_extension" + }, + { + "name": "Early Engagement", + "work_type_id": 5, + "ea_act_id": 3, + "sort_order": 1, + "legislated": true, + "duration": 90, + "color": "#54858d", + "id": "early_engagement" + }, + { + "name": "Proponent Time: Early Engagement", + "work_type_id": 5, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 365, + "color": "#CCFFFF", + "id": "proponent_time_early_engagement" + }, + { + "name": "Readiness Decision", + "work_type_id": 5, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 60, + "color": "#da6d65", + "id": "readiness_decision" + }, + { + "name": "Post: EAC/Order Transfer", + "work_type_id": 13, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 60, + "color": "#a6bb2e", + "id": "post_eac_order_transfer" + }, + { + "name": "Post: Simple Amendment", + "work_type_id": 7, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 60, + "color": "#a6bb2e", + "id": "post_simple_amendment" + }, + { + "name": "Post: Typical Amendment", + "work_type_id": 8, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 180, + "color": "#a6bb2e", + "id": "post_typical_amendment" + }, + { + "name": "Post: Complex Amendment", + "work_type_id": 9, + "ea_act_id": 3, + "sort_order": 1, + "legislated": false, + "duration": 365, + "color": "#a6bb2e", + "id": "post_complex_amendment" + } + ], + "milestones": [ + { + "name": "Project Notification Submission", + "phase_id": "project_notification", + "milestone_type_id": 16, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "project_notification", + "milestone_type_id": 21, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "project_notification", + "milestone_type_id": 11, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "PCP", + "phase_id": "project_notification", + "milestone_type_id": 11, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "project_notification", + "milestone_type_id": 22, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "project_notification", + "milestone_type_id": 23, + "start_at": 42, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Close", + "phase_id": "project_notification", + "milestone_type_id": 18, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Open House", + "phase_id": "project_notification", + "milestone_type_id": 6, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Virtual Open House", + "phase_id": "project_notification", + "milestone_type_id": 17, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Report & Referral", + "phase_id": "project_notification", + "milestone_type_id": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Notification Decision", + "phase_id": "project_notification", + "milestone_type_id": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id":"project_notification_notification_decision", + "auto": true + }, + { + "name": "Other", + "phase_id": "project_notification", + "milestone_type_id": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + + + { + "name": "Extension Request", + "phase_id": "post_eac_extension", + "milestone_type_id": 14, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "post_eac_extension", + "milestone_type_id": 21, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "post_eac_extension", + "milestone_type_id": 11, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "PCP", + "phase_id": "post_eac_extension", + "milestone_type_id": 11, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "post_eac_extension", + "milestone_type_id": 22, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "post_eac_extension", + "milestone_type_id": 23, + "start_at": 42, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Close", + "phase_id": "post_eac_extension", + "milestone_type_id": 18, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Open House", + "phase_id": "post_eac_extension", + "milestone_type_id": 6, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Virtual Open House", + "phase_id": "post_eac_extension", + "milestone_type_id": 17, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Report & Referral", + "phase_id": "post_eac_extension", + "milestone_type_id": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Extension Decision", + "phase_id": "post_eac_extension", + "milestone_type_id": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id": "post_eac_extension_extension_decision", + "auto": true + }, + { + "name": "Other", + "phase_id": "post_eac_extension", + "milestone_type_id": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + + + { + "name": "Submission of IPD & Engagement Plan", + "phase_id": "early_engagement", + "milestone_type_id": 16, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "early_engagement", + "milestone_type_id": 21, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "early_engagement", + "milestone_type_id": 11, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "PCP", + "phase_id": "early_engagement", + "milestone_type_id": 11, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "early_engagement", + "milestone_type_id": 22, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "early_engagement", + "milestone_type_id": 23, + "start_at": 42, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Close", + "phase_id": "early_engagement", + "milestone_type_id": 18, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Open House", + "phase_id": "early_engagement", + "milestone_type_id": 6, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Virtual Open House", + "phase_id": "early_engagement", + "milestone_type_id": 17, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Project Withdrawn", + "phase_id": "early_engagement", + "milestone_type_id": 1, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id": "early_engagement_project_withdrawn", + "auto": false + }, + { + "name": "Other", + "phase_id": "early_engagement", + "milestone_type_id": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Summary of Engagement", + "phase_id": "early_engagement", + "milestone_type_id": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Start of Proponent Time: Early Engagement", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 12, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Project Termination (s.39)", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 1, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "id": "proponent_time_early_engagement_project_termination", + "auto": false + }, + { + "name": "Other", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "End of Proponent Time: Early Engagement", + "phase_id": "proponent_time_early_engagement", + "milestone_type_id": 12, + "start_at": 365, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Accept Detailed Project Description", + "phase_id": "readiness_decision", + "milestone_type_id": 16, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "DPD Accepted Announcement & Tweet", + "phase_id": "readiness_decision", + "milestone_type_id": 5, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "Referred to Minister for Decision", + "phase_id": "readiness_decision", + "milestone_type_id": 13, + "start_at": 30, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Referred to Minister Announcement & Tweet", + "phase_id": "readiness_decision", + "milestone_type_id": 5, + "start_at": 30, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "Minister's Decision Announcement & Tweet", + "phase_id": "readiness_decision", + "milestone_type_id": 5, + "start_at": 60, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "Other", + "phase_id": "readiness_decision", + "milestone_type_id": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Ministers Decision", + "phase_id": "readiness_decision", + "milestone_type_id": 4, + "start_at": 60, + "duration": 0, + "kind": "EVENT", + "id": "readiness_decision_ministers_decision", + "auto": true + } + + + , + { + "name": "Transfer Request", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 14, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Report & Referral", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": true + }, + { + "name": "Other", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 8, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": false + }, + { + "name": "Transfer Decision", + "phase_id": "post_eac_order_transfer", + "milestone_type_id": 1, + "start_at": 60, + "duration": 0, + "kind": "EVENT", + "id": "post_eac_order_transfer_transfer_decision", + "auto": true + }, + + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "post_typical_amendment", + "milestone_type_id": 21, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "post_typical_amendment", + "milestone_type_id": 11, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "post_typical_amendment", + "milestone_type_id": 22, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "post_typical_amendment", + "milestone_type_id": 23, + "start_at": 42, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + + { + "name": "PCP Coming Announcement & Tweet", + "phase_id": "post_complex_amendment", + "milestone_type_id": 21, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP", + "phase_id": "post_complex_amendment", + "milestone_type_id": 11, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Open Announcement & Tweet", + "phase_id": "post_complex_amendment", + "milestone_type_id": 22, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + }, + { + "name": "PCP Closing Announcement & Tweet", + "phase_id": "post_complex_amendment", + "milestone_type_id": 23, + "start_at": 42, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": true + } + + ], + "outcomes": [ + { + "name": "No Further Review Required", + "milestone_id": "project_notification_notification_decision", + "terminates_work": false + }, + { + "name": "More Information Needed", + "milestone_id": "project_notification_notification_decision", + "terminates_work": false + }, + { + "name": "Project Referred to Minister", + "milestone_id": "project_notification_notification_decision", + "terminates_work": false + }, + { + "name": "EAC Extension Granted", + "milestone_id": "post_eac_extension_extension_decision", + "terminates_work": false + }, + { + "name": "EAC Extension Refused", + "milestone_id": "post_eac_extension_extension_decision", + "terminates_work": false + }, + { + "name": "Project Withdrawn", + "milestone_id": "early_engagement_project_withdrawn", + "terminates_work": true + }, + { + "name": "Project Termination", + "milestone_id": "proponent_time_early_engagement_project_termination", + "terminates_work": true + }, + + { + "name": "Revised DPD Ordered", + "milestone_id": "readiness_decision_ministers_decision", + "terminates_work": false + }, + { + "name": "Exemption Order Granted", + "milestone_id": "readiness_decision_ministers_decision", + "terminates_work": false + }, + { + "name": "Assessment Terminated", + "milestone_id": "readiness_decision_ministers_decision", + "terminates_work": true + }, + + + { + "name": "EAC/Order Transfer Granted", + "milestone_id": "post_eac_order_transfer_transfer_decision", + "terminates_work": false + }, + { + "name": "EAC/Order Transfer Refused", + "milestone_id": "post_eac_order_transfer_transfer_decision", + "terminates_work": false + } + ] +} diff --git a/epictrack-api/migrations/versions/09a79ca8e8fd_add_none_ministry.py b/epictrack-api/migrations/versions/09a79ca8e8fd_add_none_ministry.py index 6267f90c7..8a10db9d3 100644 --- a/epictrack-api/migrations/versions/09a79ca8e8fd_add_none_ministry.py +++ b/epictrack-api/migrations/versions/09a79ca8e8fd_add_none_ministry.py @@ -1,54 +1,54 @@ -"""Add Not applicable ministry in ministries table - -Revision ID: 09a79ca8e8fd -Revises: 65cb91595d6a -Create Date: 2024-01-11 13:19:49.744798 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy import text - -# revision identifiers, used by Alembic. -revision = '09a79ca8e8fd' -down_revision = '65cb91595d6a' -branch_labels = None -depends_on = None -na_ministry_name = "Not Applicable" - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - - # check if not applicable ministry exists - result = op.get_bind().execute(text(f"select * from ministries where name='{na_ministry_name}'")).fetchone() - - if not result: - ministries = sa.table( - 'ministries', - sa.Column('name', sa.String), - sa.column('abbreviation', sa.String), - sa.Column('is_active', sa.Boolean), - sa.Column('is_deleted', sa.Boolean), - sa.Column('sort_order', sa.Integer), - ) - - op.execute( - ministries.insert().values( - name=na_ministry_name, - abbreviation='NA', - is_active=True, - is_deleted=False, - sort_order=-1, - ) - ) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - - op.execute(f"delete from ministries where name='{na_ministry_name}'") - - # ### end Alembic commands ### +"""Add Not applicable ministry in ministries table + +Revision ID: 09a79ca8e8fd +Revises: 65cb91595d6a +Create Date: 2024-01-11 13:19:49.744798 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text + +# revision identifiers, used by Alembic. +revision = '09a79ca8e8fd' +down_revision = '65cb91595d6a' +branch_labels = None +depends_on = None +na_ministry_name = "Not Applicable" + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # check if not applicable ministry exists + result = op.get_bind().execute(text(f"select * from ministries where name='{na_ministry_name}'")).fetchone() + + if not result: + ministries = sa.table( + 'ministries', + sa.Column('name', sa.String), + sa.column('abbreviation', sa.String), + sa.Column('is_active', sa.Boolean), + sa.Column('is_deleted', sa.Boolean), + sa.Column('sort_order', sa.Integer), + ) + + op.execute( + ministries.insert().values( + name=na_ministry_name, + abbreviation='NA', + is_active=True, + is_deleted=False, + sort_order=-1, + ) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + op.execute(f"delete from ministries where name='{na_ministry_name}'") + + # ### end Alembic commands ### diff --git a/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py b/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py index 023d4880b..523b3c5ed 100644 --- a/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py +++ b/epictrack-api/migrations/versions/88b0bbb53b72_unique_abbreviation.py @@ -1,30 +1,30 @@ -"""Add unique constraint to projects abbreviation - -Revision ID: 88b0bbb53b72 -Revises: 03791c319e2b -Create Date: 2023-12-05 11:34:09.723702 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '88b0bbb53b72' -down_revision = '03791c319e2b' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('projects', schema=None) as batch_op: - batch_op.create_unique_constraint('uq_projects_abbreviation', ['abbreviation']) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('projects', schema=None) as batch_op: - batch_op.drop_constraint('uq_projects_abbreviation', type_='unique') - # ### end Alembic commands ### +"""Add unique constraint to projects abbreviation + +Revision ID: 88b0bbb53b72 +Revises: 03791c319e2b +Create Date: 2023-12-05 11:34:09.723702 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '88b0bbb53b72' +down_revision = '03791c319e2b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('projects', schema=None) as batch_op: + batch_op.create_unique_constraint('uq_projects_abbreviation', ['abbreviation']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('projects', schema=None) as batch_op: + batch_op.drop_constraint('uq_projects_abbreviation', type_='unique') + # ### end Alembic commands ### diff --git a/epictrack-api/migrations/versions/94d5aec71e37_staff_id_foreign_key_correction.py b/epictrack-api/migrations/versions/94d5aec71e37_staff_id_foreign_key_correction.py index f794d6e9a..ac787d73e 100644 --- a/epictrack-api/migrations/versions/94d5aec71e37_staff_id_foreign_key_correction.py +++ b/epictrack-api/migrations/versions/94d5aec71e37_staff_id_foreign_key_correction.py @@ -1,30 +1,30 @@ -"""staff_id foreign key correction - -Revision ID: 94d5aec71e37 -Revises: bc5314531502 -Create Date: 2022-06-10 16:25:20.942697 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '94d5aec71e37' -down_revision = 'bc5314531502' -branch_labels = None -depends_on = None - - -def upgrade(): +"""staff_id foreign key correction + +Revision ID: 94d5aec71e37 +Revises: bc5314531502 +Create Date: 2022-06-10 16:25:20.942697 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '94d5aec71e37' +down_revision = 'bc5314531502' +branch_labels = None +depends_on = None + + +def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_constraint('engagements_staff_id_fkey', 'engagements', type_='foreignkey') op.create_foreign_key(None, 'engagements', 'staffs', ['staff_id'], ['id']) - # ### end Alembic commands ### - - -def downgrade(): + # ### end Alembic commands ### + + +def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_constraint(None, 'engagements', type_='foreignkey') op.create_foreign_key('engagements_staff_id_fkey', 'engagements', 'staff_work_roles', ['staff_id'], ['id']) - # ### end Alembic commands ### + # ### end Alembic commands ### diff --git a/epictrack-api/migrations/versions/a85a39851cab_staff_id_removed_from_engagement.py b/epictrack-api/migrations/versions/a85a39851cab_staff_id_removed_from_engagement.py index eb60f8390..0c57a759e 100644 --- a/epictrack-api/migrations/versions/a85a39851cab_staff_id_removed_from_engagement.py +++ b/epictrack-api/migrations/versions/a85a39851cab_staff_id_removed_from_engagement.py @@ -1,30 +1,30 @@ -"""staff_id removed from engagement - -Revision ID: a85a39851cab -Revises: 99dd99e4dbbc -Create Date: 2022-07-21 21:01:30.719175 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'a85a39851cab' -down_revision = '99dd99e4dbbc' -branch_labels = None -depends_on = None - - -def upgrade(): +"""staff_id removed from engagement + +Revision ID: a85a39851cab +Revises: 99dd99e4dbbc +Create Date: 2022-07-21 21:01:30.719175 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a85a39851cab' +down_revision = '99dd99e4dbbc' +branch_labels = None +depends_on = None + + +def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_constraint('engagements_staff_id_fkey', 'engagements', type_='foreignkey') op.drop_column('engagements', 'staff_id') - # ### end Alembic commands ### - - -def downgrade(): + # ### end Alembic commands ### + + +def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column('engagements', sa.Column('staff_id', sa.INTEGER(), autoincrement=False, nullable=True)) op.create_foreign_key('engagements_staff_id_fkey', 'engagements', 'staffs', ['staff_id'], ['id']) - # ### end Alembic commands ### + # ### end Alembic commands ### diff --git a/epictrack-api/migrations/versions/c91a2375e5b1_projects_phase_codes_milestones.py b/epictrack-api/migrations/versions/c91a2375e5b1_projects_phase_codes_milestones.py index 1f229c5de..de231bcb2 100644 --- a/epictrack-api/migrations/versions/c91a2375e5b1_projects_phase_codes_milestones.py +++ b/epictrack-api/migrations/versions/c91a2375e5b1_projects_phase_codes_milestones.py @@ -1,1605 +1,1605 @@ -"""projects, phase_codes, milestones - -Revision ID: c91a2375e5b1 -Revises: 411ca8b6d6d8 -Create Date: 2022-06-27 13:20:44.699199 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import column, table - - -# revision identifiers, used by Alembic. -revision = 'c91a2375e5b1' -down_revision = '411ca8b6d6d8' -branch_labels = None -depends_on = None - - -def upgrade(): - #phase codes - op.execute('TRUNCATE phase_codes RESTART IDENTITY CASCADE') - op.drop_column('phase_codes', 'start_event') - op.drop_column('phase_codes', 'end_event') - - milestone_type = table('milestone_types', - column('id',sa.Integer), - column('name',sa.String)) - op.bulk_insert(milestone_type, - [{ - 'name': 'PECP Close' - }] - ) - phase_code = table('phase_codes', - column('sort_order',sa.Integer), - column('work_type_id',sa.Integer), - column('ea_act_id',sa.Integer), - column('duration',sa.Integer), - column('legislated',sa.Boolean), - column('color',sa.String), - column('name',sa.String)) - op.bulk_insert( - phase_code, - [{ - 'sort_order': 1, - 'name': 'Early Engagement', - 'work_type_id': 6, - 'duration': 90, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#E1EBF3' - },{ - 'sort_order': 2, - 'name': 'Proponent Time: Early Engagement', - 'work_type_id': 6, - 'duration': 365, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#CCFFFF' - },{ - 'sort_order': 3, - 'name': 'Readiness Decision', - 'work_type_id': 6, - 'duration': 60, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#C3D7E8' - }, { - 'sort_order': 4, - 'name': 'Process Planning', - 'work_type_id': 6, - 'duration': 120, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#A6C3DD' - }, { - 'sort_order': 5, - 'name': 'Proponent Time: Process Planning', - 'work_type_id': 6, - 'duration': 1095, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#CCFFFF' - }, { - 'sort_order': 6, - 'name': 'Application Development & Review', - 'work_type_id': 6, - 'duration': 180, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#FAEADC' - }, { - 'sort_order': 7, - 'name': 'Proponent Time: Application Review', - 'work_type_id': 6, - 'duration': 365, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#CCFFFF' - }, { - 'sort_order': 8, - 'name': 'Effects Assessment/Recommentations', - 'work_type_id': 6, - 'duration': 150, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#F6D5B9' - }, { - 'sort_order': 10, - 'name': 'Referral/Decision', - 'work_type_id': 6, - 'duration': 30, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#F2C096' - }]) - - op.execute('TRUNCATE milestones RESTART IDENTITY CASCADE') - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('milestones', sa.Column('start_at', sa.Integer(), nullable=False)) - op.add_column('milestones', sa.Column('duration', sa.Integer(), nullable=False)) - op.add_column('milestones', sa.Column('kind', sa.String(), nullable=False)) - op.add_column('milestones', sa.Column('auto', sa.Boolean(), nullable=True)) - op.drop_column('milestones', 'is_end_event') - op.drop_column('milestones', 'is_start_event') - milestone = table('milestones', - column('name',sa.String), - column('phase_id',sa.Integer), - column('milestone_type_id',sa.Integer), - column('sort_order',sa.Integer), - column('start_at',sa.Integer), - column('duration',sa.Integer), - column('kind',sa.String), - column('auto',sa.Boolean)) - op.bulk_insert(milestone, - [ - { - "name": "Accept IPD & Engagement Plan", - "phase_id": 1, - "milestone_type_id": 16, - "sort_order": 1, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "IPD Accepted Announcement & Tweet", - "phase_id": 1, - "milestone_type_id": 5, - "sort_order": 2, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Coming Announcement & Tweet", - "phase_id": 1, - "milestone_type_id": 5, - "sort_order": 3, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP", - "phase_id": 1, - "milestone_type_id": 11, - "sort_order": 4, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Open Announcement & Tweet", - "phase_id": 1, - "milestone_type_id": 5, - "sort_order": 5, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Closing Announcement & Tweet", - "phase_id": 1, - "milestone_type_id": 5, - "sort_order": 6, - "start_at": 42, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Close", - "phase_id": 1, - "milestone_type_id": 18, - "sort_order": 7, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Format SoE", - "phase_id": 1, - "milestone_type_id": 15, - "sort_order": 8, - "start_at": 68, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP", - "phase_id": 1, - "milestone_type_id": 11, - "sort_order": 9, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Open House", - "phase_id": 1, - "milestone_type_id": 6, - "sort_order": 10, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Virtual Open House", - "phase_id": 1, - "milestone_type_id": 17, - "sort_order": 11, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Project Withdrawn", - "phase_id": 1, - "milestone_type_id": 1, - "sort_order": 12, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 1, - "milestone_type_id": 8, - "sort_order": 13, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Summary of Engagement Announcement & Tweet", - "phase_id": 1, - "milestone_type_id": 5, - "sort_order": 14, - "start_at": 90, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Summary of Engagement", - "phase_id": 1, - "milestone_type_id": 13, - "sort_order": 15, - "start_at": 90, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Start of Proponent Time: Early Engagement", - "phase_id": 2, - "milestone_type_id": 12, - "sort_order": 16, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Project Termination (s.39)", - "phase_id": 2, - "milestone_type_id": 1, - "sort_order": 17, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 2, - "milestone_type_id": 8, - "sort_order": 18, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "End of Proponent Time: Early Engagement", - "phase_id": 2, - "milestone_type_id": 12, - "sort_order": 19, - "start_at": 365, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Accept Detailed Project Description", - "phase_id": 3, - "milestone_type_id": 16, - "sort_order": 20, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "DPD Accepted Announcement & Tweet", - "phase_id": 3, - "milestone_type_id": 5, - "sort_order": 21, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Referred to Minister for Decision", - "phase_id": 3, - "milestone_type_id": 13, - "sort_order": 22, - "start_at": 30, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Referred to Minister Announcement & Tweet", - "phase_id": 3, - "milestone_type_id": 5, - "sort_order": 23, - "start_at": 30, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Other", - "phase_id": 3, - "milestone_type_id": 8, - "sort_order": 24, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Minister's Decision Announcement & Tweet", - "phase_id": 3, - "milestone_type_id": 5, - "sort_order": 25, - "start_at": 60, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Ministers Decision", - "phase_id": 3, - "milestone_type_id": 4, - "sort_order": 26, - "start_at": 60, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Start of the Process Planning Phase", - "phase_id": 4, - "milestone_type_id": 16, - "sort_order": 27, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "PECP Coming Announcement & Tweet", - "phase_id": 4, - "milestone_type_id": 5, - "sort_order": 28, - "start_at": 52, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP", - "phase_id": 4, - "milestone_type_id": 11, - "sort_order": 29, - "start_at": 60, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Open Announcement & Tweet", - "phase_id": 4, - "milestone_type_id": 5, - "sort_order": 20, - "start_at": 60, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Closing Announcement & Tweet", - "phase_id": 4, - "milestone_type_id": 5, - "sort_order": 31, - "start_at": 88, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Close", - "phase_id": 4, - "milestone_type_id": 18, - "sort_order": 32, - "start_at": 90, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "PECP", - "phase_id": 4, - "milestone_type_id": 11, - "sort_order": 33, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Open House", - "phase_id": 4, - "milestone_type_id": 6, - "sort_order": 34, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Virtual Open House", - "phase_id": 4, - "milestone_type_id": 17, - "sort_order": 35, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Project Withdrawn", - "phase_id": 4, - "milestone_type_id": 1, - "sort_order": 36, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 4, - "milestone_type_id": 8, - "sort_order": 37, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Process Order Announcement & Tweet", - "phase_id": 4, - "milestone_type_id": 5, - "sort_order": 38, - "start_at": 120, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Process Order Issued", - "phase_id": 4, - "milestone_type_id": 7, - "sort_order": 39, - "start_at": 120, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Start of Proponent Time: Process Planning", - "phase_id": 5, - "milestone_type_id": 12, - "sort_order": 40, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Project Termination (s.39)", - "phase_id": 5, - "milestone_type_id": 1, - "sort_order": 41, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Process Order Amended (s.19)", - "phase_id": 5, - "milestone_type_id": 7, - "sort_order": 42, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 5, - "milestone_type_id": 8, - "sort_order": 43, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "End of Proponent Time: Process Planning", - "phase_id": 5, - "milestone_type_id": 12, - "sort_order": 44, - "start_at": 1095, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Accept EAC Application", - "phase_id": 6, - "milestone_type_id": 16, - "sort_order": 45, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Application Accepted Announcement & Tweet", - "phase_id": 6, - "milestone_type_id": 5, - "sort_order": 46, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Coming Announcement & Tweet", - "phase_id": 6, - "milestone_type_id": 5, - "sort_order": 47, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP", - "phase_id": 6, - "milestone_type_id": 11, - "sort_order": 48, - "start_at": 14, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Open Announcement & Tweet", - "phase_id": 6, - "milestone_type_id": 5, - "sort_order": 49, - "start_at": 14, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Closing Announcement & Tweet", - "phase_id": 6, - "milestone_type_id": 5, - "sort_order": 50, - "start_at": 44, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Close", - "phase_id": 6, - "milestone_type_id": 18, - "sort_order": 51, - "start_at": 45, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "PECP", - "phase_id": 6, - "milestone_type_id": 11, - "sort_order": 52, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Open House", - "phase_id": 6, - "milestone_type_id": 6, - "sort_order": 53, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Virtual Open House", - "phase_id": 6, - "milestone_type_id": 17, - "sort_order": 54, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Process Order Amended (s.19)", - "phase_id": 6, - "milestone_type_id": 7, - "sort_order": 55, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Project Withdrawn", - "phase_id": 6, - "milestone_type_id": 1, - "sort_order": 56, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 6, - "milestone_type_id": 8, - "sort_order": 57, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "EAC Application Notice Announcement & Tweet", - "phase_id": 6, - "milestone_type_id": 5, - "sort_order": 58, - "start_at": 180, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "EAC Application Notice Posted", - "phase_id": 6, - "milestone_type_id": 13, - "sort_order": 59, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Start of Proponent Time: Application Review", - "phase_id": 7, - "milestone_type_id": 12, - "sort_order": 60, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Project Termination (s.39)", - "phase_id": 7, - "milestone_type_id": 1, - "sort_order": 61, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Process Order Amended (s.19)", - "phase_id": 7, - "milestone_type_id": 7, - "sort_order": 62, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 7, - "milestone_type_id": 8, - "sort_order": 63, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "End of Proponent Time: Application Review", - "phase_id": 7, - "milestone_type_id": 12, - "sort_order": 64, - "start_at": 365, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Accept Revised EAC Application", - "phase_id": 8, - "milestone_type_id": 16, - "sort_order": 65, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Revised Application Accepted Announcement & Tweet", - "phase_id": 8, - "milestone_type_id": 5, - "sort_order": 66, - "start_at": 0, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Draft Assessment Report Posted", - "phase_id": 8, - "milestone_type_id": 13, - "sort_order": 67, - "start_at": 90, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "PECP Coming Announcement & Tweet", - "phase_id": 8, - "milestone_type_id": 5, - "sort_order": 68, - "start_at": 97, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP", - "phase_id": 8, - "milestone_type_id": 11, - "sort_order": 69, - "start_at": 104, - "duration": 30, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Open Announcement & Tweet", - "phase_id": 8, - "milestone_type_id": 5, - "sort_order": 70, - "start_at": 104, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Closing Announcement & Tweet", - "phase_id": 8, - "milestone_type_id": 5, - "sort_order": 71, - "start_at": 132, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "PECP Close", - "phase_id": 8, - "milestone_type_id": 18, - "sort_order": 72, - "start_at": 134, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Process Order Amended (s.19)", - "phase_id": 8, - "milestone_type_id": 7, - "sort_order": 73, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Project Withdrawn", - "phase_id": 8, - "milestone_type_id": 1, - "sort_order": 74, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Other", - "phase_id": 8, - "milestone_type_id": 8, - "sort_order": 75, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Referral Announcement & Tweet", - "phase_id": 8, - "milestone_type_id": 5, - "sort_order": 76, - "start_at": 150, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Open House", - "phase_id": 8, - "milestone_type_id": 6, - "sort_order": 77, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Virtual Open House", - "phase_id": 8, - "milestone_type_id": 17, - "sort_order": 78, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": False - }, - { - "name": "Referral Package Sent to Ministers", - "phase_id": 8, - "milestone_type_id": 14, - "sort_order": 77, - "start_at": 150, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Refered to Ministers", - "phase_id": 9, - "milestone_type_id": 13, - "sort_order": 80, - "start_at": 0, - "duration": 0, - "kind": "EVENT", - "auto": True - }, - { - "name": "Meeting with Ministers", - "phase_id": 9, - "milestone_type_id": 3, - "sort_order": 81, - "start_at": 7, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "EAC Decision Announcement & Tweet", - "phase_id": 9, - "milestone_type_id": 5, - "sort_order": 82, - "start_at": 30, - "duration": 0, - "kind": "ENGAGEMENT", - "auto": True - }, - { - "name": "Minister's EAC Decision", - "phase_id": 9, - "milestone_type_id": 4, - "sort_order": 83, - "start_at": 30, - "duration": 0, - "kind": "EVENT", - "auto": True - } - ]) - - op.add_column('projects', sa.Column('abbreviation', sa.String(length=10), nullable=True)) - outcome = table('outcomes', - column('id',sa.Integer), - column('name',sa.String), - column('milestone_id',sa.Integer), - column('sort_order',sa.Integer), - column('terminates_work',sa.Boolean)) - op.bulk_insert(outcome, - [{ - 'name': 'Project Withdrawn', - 'milestone_id': 12, - 'sort_order': 1, - 'terminates_work': True - },{ - 'name': 'Project Termination', - 'milestone_id': 17, - 'sort_order': 2, - 'terminates_work': False - },{ - 'name': 'Revised DPD Ordered', - 'milestone_id': 26, - 'sort_order': 3, - 'terminates_work': False - },{ - 'name': 'Exemption Order Granted', - 'milestone_id': 26, - 'sort_order': 4, - 'terminates_work': False - },{ - 'name': 'Assessment Terminated', - 'milestone_id': 26, - 'sort_order': 5, - 'terminates_work': True - },{ - 'name': 'Project Enters the EA Process', - 'milestone_id': 26, - 'sort_order': 6, - 'terminates_work': False - },{ - 'name': 'Project Withdrawn', - 'milestone_id': 36, - 'sort_order': 7, - 'terminates_work': True - },{ - 'name': 'Project Termination', - 'milestone_id': 41, - 'sort_order': 8, - 'terminates_work': False - },{ - 'name': 'Project Withdrawn', - 'milestone_id': 56, - 'sort_order': 9, - 'terminates_work': True - },{ - 'name': 'Project Termination', - 'milestone_id': 61, - 'sort_order': 10, - 'terminates_work': False - },{ - 'name': 'Project Withdrawn', - 'milestone_id': 74, - 'sort_order': 11, - 'terminates_work': True - },{ - 'name': 'EAC Granted', - 'milestone_id': 83, - 'sort_order': 12, - 'terminates_work': False - },{ - 'name': 'EAC Refused', - 'milestone_id': 83, - 'sort_order': 13, - 'terminates_work': False - }]) - # ### end Alembic commands ### - - -def downgrade(): - op.add_column('phase_codes', sa.Column('end_event', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('phase_codes', sa.Column('start_event', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.execute('TRUNCATE phase_codes RESTART IDENTITY CASCADE') - op.execute('ALTER SEQUENCE phase_codes_id_seq RESTART WITH 11') - phase_code = table('phase_codes', - column('sort_order',sa.Integer), - column('work_type_id',sa.Integer), - column('ea_act_id',sa.Integer), - column('start_event',sa.String), - column('end_event',sa.String), - column('duration',sa.Integer), - column('legislated',sa.Boolean), - column('color',sa.String), - column('name',sa.String)) - op.bulk_insert( - phase_code, - [ - { - 'id': 11, - 'sort_order': 1, - 'name': 'Early Engagement', - 'work_type_id': 6, - 'start_event': 'Initial Project Description accepted', - 'end_event': 'Summary of Engagement Issued', - 'duration': 90, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#E1EBF3' - }, - { - 'id': 12, - 'sort_order': 2, - 'name': 'Proponent Time: Project Description', - 'work_type_id': 6, - 'start_event': 'Day after the posting of Summary of Engagment', - 'end_event': 'Day before the submission of DPD', - 'duration': 365, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#CCFFFF' - }, - { - 'id': 13, - 'sort_order': 3, - 'name': 'Readiness Decision', - 'work_type_id': 6, - 'start_event': 'Submission of DPD', - 'end_event': 'Ministers\' Decision', - 'duration': 60, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#C3D7E8' - }, { - 'id': 14, - 'sort_order': 4, - 'name': 'Process Planning', - 'work_type_id': 6, - 'start_event': 'Day after Ministers\' Decision', - 'end_event': 'Process Order Issued', - 'duration': 120, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#A6C3DD' - }, { - 'id': 15, - 'sort_order': 5, - 'name': 'Proponent Time: Application Development', - 'work_type_id': 6, - 'start_event': 'Day after Process Order issued', - 'end_event': 'Day before the acceptance of the Application', - 'duration': 1095, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#CCFFFF' - }, { - 'id': 16, - 'sort_order': 6, - 'name': 'Application Development & Review', - 'work_type_id': 6, - 'start_event': 'Application for an EAC accepted', - 'end_event': 'Notice Regarding Application', - 'duration': 180, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#FAEADC' - }, { - 'id': 17, - 'sort_order': 7, - 'name': 'Proponent Time: Revised Application', - 'work_type_id': 6, - 'start_event': 'Day after Notice', - 'end_event': 'Day before Revised Application', - 'duration': 365, - 'legislated': False, - 'ea_act_id': 3, - 'color': '#CCFFFF' - }, { - 'id': 18, - 'sort_order': 8, - 'name': 'Effects Assessment', - 'work_type_id': 6, - 'start_event': 'Revised Application Submitted', - 'end_event': 'Draft Assessment Report is issued', - 'duration': 110, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#F6D5B9' - }, { - 'id': 19, - 'sort_order': 9, - 'name': 'Recommendation', - 'work_type_id': 6, - 'start_event': 'PECP starts for Draft Assessment Report?', - 'end_event': 'Package sent to Ministers', - 'duration': 40, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#F2C096' - }, { - 'id': 20, - 'sort_order': 10, - 'name': 'Referral/Decision', - 'work_type_id': 6, - 'start_event': 'Referral Package Received', - 'end_event': 'Minister\'s Decision', - 'duration': 30, - 'legislated': True, - 'ea_act_id': 3, - 'color': '#F2C096' - } - ]) - # # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('projects', 'abbreviation') - - op.add_column('milestones', sa.Column('is_start_event', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.add_column('milestones', sa.Column('is_end_event', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.drop_column('milestones', 'auto') - op.drop_column('milestones', 'kind') - op.drop_column('milestones', 'duration') - op.drop_column('milestones', 'start_at') - milestone = table('milestones', - column('name',sa.String), - column('phase_id',sa.Integer), - column('milestone_type_id',sa.Integer), - column('sort_order',sa.Integer), - column('is_start_event',sa.Boolean), - column('is_end_event',sa.Boolean)) - op.bulk_insert(milestone, - [ - { - "name" : "Submission of IPD & Engagement Plan", - "phase_id" : 11, - "milestone_type_id" : 16, - "sort_order" : 1, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "PECP", - "phase_id" : 11, - "milestone_type_id" : 11, - "sort_order" : 2, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Open House", - "phase_id" : 11, - "milestone_type_id" : 6, - "sort_order" : 3, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Virtual Open House", - "phase_id" : 11, - "milestone_type_id" : 17, - "sort_order" : 4, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Summary of Engagement", - "phase_id" : 11, - "milestone_type_id" : 13, - "sort_order" : 5, - "is_start_event": False, - "is_end_event": True - }, - { - "name" : "Project Withdrawn", - "phase_id" : 11, - "milestone_type_id" : 1, - "sort_order" : 6, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 11, - "milestone_type_id" : 8, - "sort_order" : 7, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "EE: Proponent Time", - "phase_id" : 12, - "milestone_type_id" : 12, - "sort_order" : 8, - "is_start_event": True, - "is_end_event": True - }, - { - "name" : "Project Termination (s.39)", - "phase_id" : 12, - "milestone_type_id" : 1, - "sort_order" : 9, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 12, - "milestone_type_id" : 8, - "sort_order" : 10, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Submission of DPD", - "phase_id" : 13, - "milestone_type_id" : 16, - "sort_order" : 11, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "Report & Referral", - "phase_id" : 13, - "milestone_type_id" : 13, - "sort_order" : 12, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Readiness Decision", - "phase_id" : 13, - "milestone_type_id" : 4, - "sort_order" : 13, - "is_start_event": False, - "is_end_event": True - }, - { - "name" : "Other", - "phase_id" : 13, - "milestone_type_id" : 8, - "sort_order" : 14, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Draft Process Order Submission", - "phase_id" : 14, - "milestone_type_id" : 16, - "sort_order" : 15, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "PECP", - "phase_id" : 14, - "milestone_type_id" : 11, - "sort_order" : 16, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Open House", - "phase_id" : 14, - "milestone_type_id" : 6, - "sort_order" : 17, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Virtual Open House", - "phase_id" : 14, - "milestone_type_id" : 17, - "sort_order" : 18, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Process Order Issued", - "phase_id" : 14, - "milestone_type_id" : 7, - "sort_order" : 19, - "is_start_event": False, - "is_end_event": True - }, - { - "name" : "Project Withdrawn", - "phase_id" : 14, - "milestone_type_id" : 1, - "sort_order" : 20, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 14, - "milestone_type_id" : 8, - "sort_order" : 21, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "ADR: Proponent Time", - "phase_id" : 15, - "milestone_type_id" : 12, - "sort_order" : 22, - "is_start_event": True, - "is_end_event": True - }, - { - "name" : "Project Termination (s.39)", - "phase_id" : 15, - "milestone_type_id" : 1, - "sort_order" : 23, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Process Order Amended (s.19)", - "phase_id" : 15, - "milestone_type_id" : 7, - "sort_order" : 24, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 15, - "milestone_type_id" : 8, - "sort_order" : 25, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Draft Application Submission", - "phase_id" : 16, - "milestone_type_id" : 16, - "sort_order" : 26, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "PECP", - "phase_id" : 16, - "milestone_type_id" : 11, - "sort_order" : 27, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Open House", - "phase_id" : 16, - "milestone_type_id" : 6, - "sort_order" : 28, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Virtual Open House", - "phase_id" : 16, - "milestone_type_id" : 17, - "sort_order" : 29, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Process Order Amended (s.19)", - "phase_id" : 16, - "milestone_type_id" : 7, - "sort_order" : 30, - "is_start_event": False, - "is_end_event": True - }, - { - "name" : "Project Withdrawn", - "phase_id" : 16, - "milestone_type_id" : 1, - "sort_order" : 31, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 16, - "milestone_type_id" : 8, - "sort_order" : 32, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "ADR: Proponent Time", - "phase_id" : 17, - "milestone_type_id" : 12, - "sort_order" : 33, - "is_start_event": True, - "is_end_event": True - }, - { - "name" : "Project Termination (s.39)", - "phase_id" : 17, - "milestone_type_id" : 1, - "sort_order" : 34, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Process Order Amended (s.19)", - "phase_id" : 17, - "milestone_type_id" : 7, - "sort_order" : 35, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 17, - "milestone_type_id" : 8, - "sort_order" : 36, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Application Submission", - "phase_id" : 18, - "milestone_type_id" : 16, - "sort_order" : 37, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "Process Order Amended (s.19)", - "phase_id" : 18, - "milestone_type_id" : 7, - "sort_order" : 38, - "is_start_event": False, - "is_end_event": True - }, - { - "name" : "Project Withdrawn", - "phase_id" : 18, - "milestone_type_id" : 1, - "sort_order" : 39, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 18, - "milestone_type_id" : 8, - "sort_order" : 40, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Draft Assessment Report", - "phase_id" : 19, - "milestone_type_id" : 13, - "sort_order" : 41, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "PECP", - "phase_id" : 19, - "milestone_type_id" : 11, - "sort_order" : 42, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Open House", - "phase_id" : 19, - "milestone_type_id" : 6, - "sort_order" : 43, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Virtual Open House", - "phase_id" : 19, - "milestone_type_id" : 17, - "sort_order" : 44, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Process Order Amended (s.19)", - "phase_id" : 19, - "milestone_type_id" : 7, - "sort_order" : 45, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Project Withdrawn", - "phase_id" : 19, - "milestone_type_id" : 1, - "sort_order" : 46, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Other", - "phase_id" : 19, - "milestone_type_id" : 8, - "sort_order" : 47, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "Referral Package Received", - "phase_id" : 20, - "milestone_type_id" : 16, - "sort_order" : 48, - "is_start_event": True, - "is_end_event": False - }, - { - "name" : "Minister Meeting", - "phase_id" : 20, - "milestone_type_id" : 3, - "sort_order" : 49, - "is_start_event": False, - "is_end_event": False - }, - { - "name" : "EAC Decision", - "phase_id" : 20, - "milestone_type_id" : 4, - "sort_order" : 50, - "is_start_event": False, - "is_end_event": True - }, - { - "name" : "Other", - "phase_id" : 20, - "milestone_type_id" : 8, - "sort_order" : 51, - "is_start_event": False, - "is_end_event": False - } -]) - # ### end Alembic commands ### +"""projects, phase_codes, milestones + +Revision ID: c91a2375e5b1 +Revises: 411ca8b6d6d8 +Create Date: 2022-06-27 13:20:44.699199 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import column, table + + +# revision identifiers, used by Alembic. +revision = 'c91a2375e5b1' +down_revision = '411ca8b6d6d8' +branch_labels = None +depends_on = None + + +def upgrade(): + #phase codes + op.execute('TRUNCATE phase_codes RESTART IDENTITY CASCADE') + op.drop_column('phase_codes', 'start_event') + op.drop_column('phase_codes', 'end_event') + + milestone_type = table('milestone_types', + column('id',sa.Integer), + column('name',sa.String)) + op.bulk_insert(milestone_type, + [{ + 'name': 'PECP Close' + }] + ) + phase_code = table('phase_codes', + column('sort_order',sa.Integer), + column('work_type_id',sa.Integer), + column('ea_act_id',sa.Integer), + column('duration',sa.Integer), + column('legislated',sa.Boolean), + column('color',sa.String), + column('name',sa.String)) + op.bulk_insert( + phase_code, + [{ + 'sort_order': 1, + 'name': 'Early Engagement', + 'work_type_id': 6, + 'duration': 90, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#E1EBF3' + },{ + 'sort_order': 2, + 'name': 'Proponent Time: Early Engagement', + 'work_type_id': 6, + 'duration': 365, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#CCFFFF' + },{ + 'sort_order': 3, + 'name': 'Readiness Decision', + 'work_type_id': 6, + 'duration': 60, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#C3D7E8' + }, { + 'sort_order': 4, + 'name': 'Process Planning', + 'work_type_id': 6, + 'duration': 120, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#A6C3DD' + }, { + 'sort_order': 5, + 'name': 'Proponent Time: Process Planning', + 'work_type_id': 6, + 'duration': 1095, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#CCFFFF' + }, { + 'sort_order': 6, + 'name': 'Application Development & Review', + 'work_type_id': 6, + 'duration': 180, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#FAEADC' + }, { + 'sort_order': 7, + 'name': 'Proponent Time: Application Review', + 'work_type_id': 6, + 'duration': 365, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#CCFFFF' + }, { + 'sort_order': 8, + 'name': 'Effects Assessment/Recommentations', + 'work_type_id': 6, + 'duration': 150, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#F6D5B9' + }, { + 'sort_order': 10, + 'name': 'Referral/Decision', + 'work_type_id': 6, + 'duration': 30, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#F2C096' + }]) + + op.execute('TRUNCATE milestones RESTART IDENTITY CASCADE') + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('milestones', sa.Column('start_at', sa.Integer(), nullable=False)) + op.add_column('milestones', sa.Column('duration', sa.Integer(), nullable=False)) + op.add_column('milestones', sa.Column('kind', sa.String(), nullable=False)) + op.add_column('milestones', sa.Column('auto', sa.Boolean(), nullable=True)) + op.drop_column('milestones', 'is_end_event') + op.drop_column('milestones', 'is_start_event') + milestone = table('milestones', + column('name',sa.String), + column('phase_id',sa.Integer), + column('milestone_type_id',sa.Integer), + column('sort_order',sa.Integer), + column('start_at',sa.Integer), + column('duration',sa.Integer), + column('kind',sa.String), + column('auto',sa.Boolean)) + op.bulk_insert(milestone, + [ + { + "name": "Accept IPD & Engagement Plan", + "phase_id": 1, + "milestone_type_id": 16, + "sort_order": 1, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "IPD Accepted Announcement & Tweet", + "phase_id": 1, + "milestone_type_id": 5, + "sort_order": 2, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Coming Announcement & Tweet", + "phase_id": 1, + "milestone_type_id": 5, + "sort_order": 3, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP", + "phase_id": 1, + "milestone_type_id": 11, + "sort_order": 4, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Open Announcement & Tweet", + "phase_id": 1, + "milestone_type_id": 5, + "sort_order": 5, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Closing Announcement & Tweet", + "phase_id": 1, + "milestone_type_id": 5, + "sort_order": 6, + "start_at": 42, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Close", + "phase_id": 1, + "milestone_type_id": 18, + "sort_order": 7, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Format SoE", + "phase_id": 1, + "milestone_type_id": 15, + "sort_order": 8, + "start_at": 68, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP", + "phase_id": 1, + "milestone_type_id": 11, + "sort_order": 9, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Open House", + "phase_id": 1, + "milestone_type_id": 6, + "sort_order": 10, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Virtual Open House", + "phase_id": 1, + "milestone_type_id": 17, + "sort_order": 11, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Project Withdrawn", + "phase_id": 1, + "milestone_type_id": 1, + "sort_order": 12, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 1, + "milestone_type_id": 8, + "sort_order": 13, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Summary of Engagement Announcement & Tweet", + "phase_id": 1, + "milestone_type_id": 5, + "sort_order": 14, + "start_at": 90, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Summary of Engagement", + "phase_id": 1, + "milestone_type_id": 13, + "sort_order": 15, + "start_at": 90, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Start of Proponent Time: Early Engagement", + "phase_id": 2, + "milestone_type_id": 12, + "sort_order": 16, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Project Termination (s.39)", + "phase_id": 2, + "milestone_type_id": 1, + "sort_order": 17, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 2, + "milestone_type_id": 8, + "sort_order": 18, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "End of Proponent Time: Early Engagement", + "phase_id": 2, + "milestone_type_id": 12, + "sort_order": 19, + "start_at": 365, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Accept Detailed Project Description", + "phase_id": 3, + "milestone_type_id": 16, + "sort_order": 20, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "DPD Accepted Announcement & Tweet", + "phase_id": 3, + "milestone_type_id": 5, + "sort_order": 21, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Referred to Minister for Decision", + "phase_id": 3, + "milestone_type_id": 13, + "sort_order": 22, + "start_at": 30, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Referred to Minister Announcement & Tweet", + "phase_id": 3, + "milestone_type_id": 5, + "sort_order": 23, + "start_at": 30, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Other", + "phase_id": 3, + "milestone_type_id": 8, + "sort_order": 24, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Minister's Decision Announcement & Tweet", + "phase_id": 3, + "milestone_type_id": 5, + "sort_order": 25, + "start_at": 60, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Ministers Decision", + "phase_id": 3, + "milestone_type_id": 4, + "sort_order": 26, + "start_at": 60, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Start of the Process Planning Phase", + "phase_id": 4, + "milestone_type_id": 16, + "sort_order": 27, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "PECP Coming Announcement & Tweet", + "phase_id": 4, + "milestone_type_id": 5, + "sort_order": 28, + "start_at": 52, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP", + "phase_id": 4, + "milestone_type_id": 11, + "sort_order": 29, + "start_at": 60, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Open Announcement & Tweet", + "phase_id": 4, + "milestone_type_id": 5, + "sort_order": 20, + "start_at": 60, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Closing Announcement & Tweet", + "phase_id": 4, + "milestone_type_id": 5, + "sort_order": 31, + "start_at": 88, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Close", + "phase_id": 4, + "milestone_type_id": 18, + "sort_order": 32, + "start_at": 90, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "PECP", + "phase_id": 4, + "milestone_type_id": 11, + "sort_order": 33, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Open House", + "phase_id": 4, + "milestone_type_id": 6, + "sort_order": 34, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Virtual Open House", + "phase_id": 4, + "milestone_type_id": 17, + "sort_order": 35, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Project Withdrawn", + "phase_id": 4, + "milestone_type_id": 1, + "sort_order": 36, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 4, + "milestone_type_id": 8, + "sort_order": 37, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Process Order Announcement & Tweet", + "phase_id": 4, + "milestone_type_id": 5, + "sort_order": 38, + "start_at": 120, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Process Order Issued", + "phase_id": 4, + "milestone_type_id": 7, + "sort_order": 39, + "start_at": 120, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Start of Proponent Time: Process Planning", + "phase_id": 5, + "milestone_type_id": 12, + "sort_order": 40, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Project Termination (s.39)", + "phase_id": 5, + "milestone_type_id": 1, + "sort_order": 41, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Process Order Amended (s.19)", + "phase_id": 5, + "milestone_type_id": 7, + "sort_order": 42, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 5, + "milestone_type_id": 8, + "sort_order": 43, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "End of Proponent Time: Process Planning", + "phase_id": 5, + "milestone_type_id": 12, + "sort_order": 44, + "start_at": 1095, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Accept EAC Application", + "phase_id": 6, + "milestone_type_id": 16, + "sort_order": 45, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Application Accepted Announcement & Tweet", + "phase_id": 6, + "milestone_type_id": 5, + "sort_order": 46, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Coming Announcement & Tweet", + "phase_id": 6, + "milestone_type_id": 5, + "sort_order": 47, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP", + "phase_id": 6, + "milestone_type_id": 11, + "sort_order": 48, + "start_at": 14, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Open Announcement & Tweet", + "phase_id": 6, + "milestone_type_id": 5, + "sort_order": 49, + "start_at": 14, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Closing Announcement & Tweet", + "phase_id": 6, + "milestone_type_id": 5, + "sort_order": 50, + "start_at": 44, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Close", + "phase_id": 6, + "milestone_type_id": 18, + "sort_order": 51, + "start_at": 45, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "PECP", + "phase_id": 6, + "milestone_type_id": 11, + "sort_order": 52, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Open House", + "phase_id": 6, + "milestone_type_id": 6, + "sort_order": 53, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Virtual Open House", + "phase_id": 6, + "milestone_type_id": 17, + "sort_order": 54, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Process Order Amended (s.19)", + "phase_id": 6, + "milestone_type_id": 7, + "sort_order": 55, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Project Withdrawn", + "phase_id": 6, + "milestone_type_id": 1, + "sort_order": 56, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 6, + "milestone_type_id": 8, + "sort_order": 57, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "EAC Application Notice Announcement & Tweet", + "phase_id": 6, + "milestone_type_id": 5, + "sort_order": 58, + "start_at": 180, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "EAC Application Notice Posted", + "phase_id": 6, + "milestone_type_id": 13, + "sort_order": 59, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Start of Proponent Time: Application Review", + "phase_id": 7, + "milestone_type_id": 12, + "sort_order": 60, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Project Termination (s.39)", + "phase_id": 7, + "milestone_type_id": 1, + "sort_order": 61, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Process Order Amended (s.19)", + "phase_id": 7, + "milestone_type_id": 7, + "sort_order": 62, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 7, + "milestone_type_id": 8, + "sort_order": 63, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "End of Proponent Time: Application Review", + "phase_id": 7, + "milestone_type_id": 12, + "sort_order": 64, + "start_at": 365, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Accept Revised EAC Application", + "phase_id": 8, + "milestone_type_id": 16, + "sort_order": 65, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Revised Application Accepted Announcement & Tweet", + "phase_id": 8, + "milestone_type_id": 5, + "sort_order": 66, + "start_at": 0, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Draft Assessment Report Posted", + "phase_id": 8, + "milestone_type_id": 13, + "sort_order": 67, + "start_at": 90, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "PECP Coming Announcement & Tweet", + "phase_id": 8, + "milestone_type_id": 5, + "sort_order": 68, + "start_at": 97, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP", + "phase_id": 8, + "milestone_type_id": 11, + "sort_order": 69, + "start_at": 104, + "duration": 30, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Open Announcement & Tweet", + "phase_id": 8, + "milestone_type_id": 5, + "sort_order": 70, + "start_at": 104, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Closing Announcement & Tweet", + "phase_id": 8, + "milestone_type_id": 5, + "sort_order": 71, + "start_at": 132, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "PECP Close", + "phase_id": 8, + "milestone_type_id": 18, + "sort_order": 72, + "start_at": 134, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Process Order Amended (s.19)", + "phase_id": 8, + "milestone_type_id": 7, + "sort_order": 73, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Project Withdrawn", + "phase_id": 8, + "milestone_type_id": 1, + "sort_order": 74, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Other", + "phase_id": 8, + "milestone_type_id": 8, + "sort_order": 75, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Referral Announcement & Tweet", + "phase_id": 8, + "milestone_type_id": 5, + "sort_order": 76, + "start_at": 150, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Open House", + "phase_id": 8, + "milestone_type_id": 6, + "sort_order": 77, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Virtual Open House", + "phase_id": 8, + "milestone_type_id": 17, + "sort_order": 78, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": False + }, + { + "name": "Referral Package Sent to Ministers", + "phase_id": 8, + "milestone_type_id": 14, + "sort_order": 77, + "start_at": 150, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Refered to Ministers", + "phase_id": 9, + "milestone_type_id": 13, + "sort_order": 80, + "start_at": 0, + "duration": 0, + "kind": "EVENT", + "auto": True + }, + { + "name": "Meeting with Ministers", + "phase_id": 9, + "milestone_type_id": 3, + "sort_order": 81, + "start_at": 7, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "EAC Decision Announcement & Tweet", + "phase_id": 9, + "milestone_type_id": 5, + "sort_order": 82, + "start_at": 30, + "duration": 0, + "kind": "ENGAGEMENT", + "auto": True + }, + { + "name": "Minister's EAC Decision", + "phase_id": 9, + "milestone_type_id": 4, + "sort_order": 83, + "start_at": 30, + "duration": 0, + "kind": "EVENT", + "auto": True + } + ]) + + op.add_column('projects', sa.Column('abbreviation', sa.String(length=10), nullable=True)) + outcome = table('outcomes', + column('id',sa.Integer), + column('name',sa.String), + column('milestone_id',sa.Integer), + column('sort_order',sa.Integer), + column('terminates_work',sa.Boolean)) + op.bulk_insert(outcome, + [{ + 'name': 'Project Withdrawn', + 'milestone_id': 12, + 'sort_order': 1, + 'terminates_work': True + },{ + 'name': 'Project Termination', + 'milestone_id': 17, + 'sort_order': 2, + 'terminates_work': False + },{ + 'name': 'Revised DPD Ordered', + 'milestone_id': 26, + 'sort_order': 3, + 'terminates_work': False + },{ + 'name': 'Exemption Order Granted', + 'milestone_id': 26, + 'sort_order': 4, + 'terminates_work': False + },{ + 'name': 'Assessment Terminated', + 'milestone_id': 26, + 'sort_order': 5, + 'terminates_work': True + },{ + 'name': 'Project Enters the EA Process', + 'milestone_id': 26, + 'sort_order': 6, + 'terminates_work': False + },{ + 'name': 'Project Withdrawn', + 'milestone_id': 36, + 'sort_order': 7, + 'terminates_work': True + },{ + 'name': 'Project Termination', + 'milestone_id': 41, + 'sort_order': 8, + 'terminates_work': False + },{ + 'name': 'Project Withdrawn', + 'milestone_id': 56, + 'sort_order': 9, + 'terminates_work': True + },{ + 'name': 'Project Termination', + 'milestone_id': 61, + 'sort_order': 10, + 'terminates_work': False + },{ + 'name': 'Project Withdrawn', + 'milestone_id': 74, + 'sort_order': 11, + 'terminates_work': True + },{ + 'name': 'EAC Granted', + 'milestone_id': 83, + 'sort_order': 12, + 'terminates_work': False + },{ + 'name': 'EAC Refused', + 'milestone_id': 83, + 'sort_order': 13, + 'terminates_work': False + }]) + # ### end Alembic commands ### + + +def downgrade(): + op.add_column('phase_codes', sa.Column('end_event', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('phase_codes', sa.Column('start_event', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.execute('TRUNCATE phase_codes RESTART IDENTITY CASCADE') + op.execute('ALTER SEQUENCE phase_codes_id_seq RESTART WITH 11') + phase_code = table('phase_codes', + column('sort_order',sa.Integer), + column('work_type_id',sa.Integer), + column('ea_act_id',sa.Integer), + column('start_event',sa.String), + column('end_event',sa.String), + column('duration',sa.Integer), + column('legislated',sa.Boolean), + column('color',sa.String), + column('name',sa.String)) + op.bulk_insert( + phase_code, + [ + { + 'id': 11, + 'sort_order': 1, + 'name': 'Early Engagement', + 'work_type_id': 6, + 'start_event': 'Initial Project Description accepted', + 'end_event': 'Summary of Engagement Issued', + 'duration': 90, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#E1EBF3' + }, + { + 'id': 12, + 'sort_order': 2, + 'name': 'Proponent Time: Project Description', + 'work_type_id': 6, + 'start_event': 'Day after the posting of Summary of Engagment', + 'end_event': 'Day before the submission of DPD', + 'duration': 365, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#CCFFFF' + }, + { + 'id': 13, + 'sort_order': 3, + 'name': 'Readiness Decision', + 'work_type_id': 6, + 'start_event': 'Submission of DPD', + 'end_event': 'Ministers\' Decision', + 'duration': 60, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#C3D7E8' + }, { + 'id': 14, + 'sort_order': 4, + 'name': 'Process Planning', + 'work_type_id': 6, + 'start_event': 'Day after Ministers\' Decision', + 'end_event': 'Process Order Issued', + 'duration': 120, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#A6C3DD' + }, { + 'id': 15, + 'sort_order': 5, + 'name': 'Proponent Time: Application Development', + 'work_type_id': 6, + 'start_event': 'Day after Process Order issued', + 'end_event': 'Day before the acceptance of the Application', + 'duration': 1095, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#CCFFFF' + }, { + 'id': 16, + 'sort_order': 6, + 'name': 'Application Development & Review', + 'work_type_id': 6, + 'start_event': 'Application for an EAC accepted', + 'end_event': 'Notice Regarding Application', + 'duration': 180, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#FAEADC' + }, { + 'id': 17, + 'sort_order': 7, + 'name': 'Proponent Time: Revised Application', + 'work_type_id': 6, + 'start_event': 'Day after Notice', + 'end_event': 'Day before Revised Application', + 'duration': 365, + 'legislated': False, + 'ea_act_id': 3, + 'color': '#CCFFFF' + }, { + 'id': 18, + 'sort_order': 8, + 'name': 'Effects Assessment', + 'work_type_id': 6, + 'start_event': 'Revised Application Submitted', + 'end_event': 'Draft Assessment Report is issued', + 'duration': 110, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#F6D5B9' + }, { + 'id': 19, + 'sort_order': 9, + 'name': 'Recommendation', + 'work_type_id': 6, + 'start_event': 'PECP starts for Draft Assessment Report?', + 'end_event': 'Package sent to Ministers', + 'duration': 40, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#F2C096' + }, { + 'id': 20, + 'sort_order': 10, + 'name': 'Referral/Decision', + 'work_type_id': 6, + 'start_event': 'Referral Package Received', + 'end_event': 'Minister\'s Decision', + 'duration': 30, + 'legislated': True, + 'ea_act_id': 3, + 'color': '#F2C096' + } + ]) + # # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('projects', 'abbreviation') + + op.add_column('milestones', sa.Column('is_start_event', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('milestones', sa.Column('is_end_event', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.drop_column('milestones', 'auto') + op.drop_column('milestones', 'kind') + op.drop_column('milestones', 'duration') + op.drop_column('milestones', 'start_at') + milestone = table('milestones', + column('name',sa.String), + column('phase_id',sa.Integer), + column('milestone_type_id',sa.Integer), + column('sort_order',sa.Integer), + column('is_start_event',sa.Boolean), + column('is_end_event',sa.Boolean)) + op.bulk_insert(milestone, + [ + { + "name" : "Submission of IPD & Engagement Plan", + "phase_id" : 11, + "milestone_type_id" : 16, + "sort_order" : 1, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "PECP", + "phase_id" : 11, + "milestone_type_id" : 11, + "sort_order" : 2, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Open House", + "phase_id" : 11, + "milestone_type_id" : 6, + "sort_order" : 3, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Virtual Open House", + "phase_id" : 11, + "milestone_type_id" : 17, + "sort_order" : 4, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Summary of Engagement", + "phase_id" : 11, + "milestone_type_id" : 13, + "sort_order" : 5, + "is_start_event": False, + "is_end_event": True + }, + { + "name" : "Project Withdrawn", + "phase_id" : 11, + "milestone_type_id" : 1, + "sort_order" : 6, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 11, + "milestone_type_id" : 8, + "sort_order" : 7, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "EE: Proponent Time", + "phase_id" : 12, + "milestone_type_id" : 12, + "sort_order" : 8, + "is_start_event": True, + "is_end_event": True + }, + { + "name" : "Project Termination (s.39)", + "phase_id" : 12, + "milestone_type_id" : 1, + "sort_order" : 9, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 12, + "milestone_type_id" : 8, + "sort_order" : 10, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Submission of DPD", + "phase_id" : 13, + "milestone_type_id" : 16, + "sort_order" : 11, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "Report & Referral", + "phase_id" : 13, + "milestone_type_id" : 13, + "sort_order" : 12, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Readiness Decision", + "phase_id" : 13, + "milestone_type_id" : 4, + "sort_order" : 13, + "is_start_event": False, + "is_end_event": True + }, + { + "name" : "Other", + "phase_id" : 13, + "milestone_type_id" : 8, + "sort_order" : 14, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Draft Process Order Submission", + "phase_id" : 14, + "milestone_type_id" : 16, + "sort_order" : 15, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "PECP", + "phase_id" : 14, + "milestone_type_id" : 11, + "sort_order" : 16, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Open House", + "phase_id" : 14, + "milestone_type_id" : 6, + "sort_order" : 17, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Virtual Open House", + "phase_id" : 14, + "milestone_type_id" : 17, + "sort_order" : 18, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Process Order Issued", + "phase_id" : 14, + "milestone_type_id" : 7, + "sort_order" : 19, + "is_start_event": False, + "is_end_event": True + }, + { + "name" : "Project Withdrawn", + "phase_id" : 14, + "milestone_type_id" : 1, + "sort_order" : 20, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 14, + "milestone_type_id" : 8, + "sort_order" : 21, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "ADR: Proponent Time", + "phase_id" : 15, + "milestone_type_id" : 12, + "sort_order" : 22, + "is_start_event": True, + "is_end_event": True + }, + { + "name" : "Project Termination (s.39)", + "phase_id" : 15, + "milestone_type_id" : 1, + "sort_order" : 23, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Process Order Amended (s.19)", + "phase_id" : 15, + "milestone_type_id" : 7, + "sort_order" : 24, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 15, + "milestone_type_id" : 8, + "sort_order" : 25, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Draft Application Submission", + "phase_id" : 16, + "milestone_type_id" : 16, + "sort_order" : 26, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "PECP", + "phase_id" : 16, + "milestone_type_id" : 11, + "sort_order" : 27, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Open House", + "phase_id" : 16, + "milestone_type_id" : 6, + "sort_order" : 28, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Virtual Open House", + "phase_id" : 16, + "milestone_type_id" : 17, + "sort_order" : 29, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Process Order Amended (s.19)", + "phase_id" : 16, + "milestone_type_id" : 7, + "sort_order" : 30, + "is_start_event": False, + "is_end_event": True + }, + { + "name" : "Project Withdrawn", + "phase_id" : 16, + "milestone_type_id" : 1, + "sort_order" : 31, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 16, + "milestone_type_id" : 8, + "sort_order" : 32, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "ADR: Proponent Time", + "phase_id" : 17, + "milestone_type_id" : 12, + "sort_order" : 33, + "is_start_event": True, + "is_end_event": True + }, + { + "name" : "Project Termination (s.39)", + "phase_id" : 17, + "milestone_type_id" : 1, + "sort_order" : 34, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Process Order Amended (s.19)", + "phase_id" : 17, + "milestone_type_id" : 7, + "sort_order" : 35, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 17, + "milestone_type_id" : 8, + "sort_order" : 36, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Application Submission", + "phase_id" : 18, + "milestone_type_id" : 16, + "sort_order" : 37, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "Process Order Amended (s.19)", + "phase_id" : 18, + "milestone_type_id" : 7, + "sort_order" : 38, + "is_start_event": False, + "is_end_event": True + }, + { + "name" : "Project Withdrawn", + "phase_id" : 18, + "milestone_type_id" : 1, + "sort_order" : 39, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 18, + "milestone_type_id" : 8, + "sort_order" : 40, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Draft Assessment Report", + "phase_id" : 19, + "milestone_type_id" : 13, + "sort_order" : 41, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "PECP", + "phase_id" : 19, + "milestone_type_id" : 11, + "sort_order" : 42, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Open House", + "phase_id" : 19, + "milestone_type_id" : 6, + "sort_order" : 43, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Virtual Open House", + "phase_id" : 19, + "milestone_type_id" : 17, + "sort_order" : 44, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Process Order Amended (s.19)", + "phase_id" : 19, + "milestone_type_id" : 7, + "sort_order" : 45, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Project Withdrawn", + "phase_id" : 19, + "milestone_type_id" : 1, + "sort_order" : 46, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Other", + "phase_id" : 19, + "milestone_type_id" : 8, + "sort_order" : 47, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "Referral Package Received", + "phase_id" : 20, + "milestone_type_id" : 16, + "sort_order" : 48, + "is_start_event": True, + "is_end_event": False + }, + { + "name" : "Minister Meeting", + "phase_id" : 20, + "milestone_type_id" : 3, + "sort_order" : 49, + "is_start_event": False, + "is_end_event": False + }, + { + "name" : "EAC Decision", + "phase_id" : 20, + "milestone_type_id" : 4, + "sort_order" : 50, + "is_start_event": False, + "is_end_event": True + }, + { + "name" : "Other", + "phase_id" : 20, + "milestone_type_id" : 8, + "sort_order" : 51, + "is_start_event": False, + "is_end_event": False + } +]) + # ### end Alembic commands ### diff --git a/epictrack-api/migrations/versions/f2071dc275f4_add_milestone_reference_to_work_.py b/epictrack-api/migrations/versions/f2071dc275f4_add_milestone_reference_to_work_.py index d8be0c0d4..a97ed3c2f 100644 --- a/epictrack-api/migrations/versions/f2071dc275f4_add_milestone_reference_to_work_.py +++ b/epictrack-api/migrations/versions/f2071dc275f4_add_milestone_reference_to_work_.py @@ -1,30 +1,30 @@ -"""add milestone reference to work_engagements - -Revision ID: f2071dc275f4 -Revises: e81a9bcad31d -Create Date: 2022-07-18 19:19:56.601130 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'f2071dc275f4' -down_revision = 'e81a9bcad31d' -branch_labels = None -depends_on = None - - -def upgrade(): +"""add milestone reference to work_engagements + +Revision ID: f2071dc275f4 +Revises: e81a9bcad31d +Create Date: 2022-07-18 19:19:56.601130 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f2071dc275f4' +down_revision = 'e81a9bcad31d' +branch_labels = None +depends_on = None + + +def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column('work_engagements', sa.Column('milestone_id', sa.Integer(), nullable=True)) op.create_foreign_key(None, 'work_engagements', 'milestones', ['milestone_id'], ['id']) - # ### end Alembic commands ### - - -def downgrade(): + # ### end Alembic commands ### + + +def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_constraint(None, 'work_engagements', type_='foreignkey') op.drop_column('work_engagements', 'milestone_id') - # ### end Alembic commands ### + # ### end Alembic commands ### diff --git a/epictrack-api/requirements.txt b/epictrack-api/requirements.txt index 19d3d52bf..dc1549dfe 100644 --- a/epictrack-api/requirements.txt +++ b/epictrack-api/requirements.txt @@ -1,63 +1,64 @@ -Flask-Caching==2.1.0 -Flask-Migrate==4.0.5 -Flask-Moment==1.0.5 -Flask-SQLAlchemy==3.0.5 -Flask==2.2.5 -Inflector==3.1.0 -Jinja2==3.1.2 -Mako==1.3.0 -MarkupSafe==2.1.3 -SQLAlchemy-Utils==0.41.1 -SQLAlchemy==2.0.17 -Werkzeug==2.3.6 -XlsxWriter==3.0.6 -alembic==1.13.1 +alembic==1.13.3 aniso8601==9.0.1 -attrs==23.2.0 +attrs==24.2.0 cachelib==0.9.0 -certifi==2023.11.17 -cffi==1.16.0 -charset-normalizer==3.3.2 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.4.0 click==8.1.7 -cryptography==41.0.7 -dpath==2.1.6 -ecdsa==0.18.0 +contourpy==1.3.0 +cryptography==43.0.3 +cycler==0.12.1 +dpath==2.2.0 +ecdsa==0.19.0 et-xmlfile==1.1.0 +Flask==2.2.5 +Flask-Caching==2.3.0 flask-jwt-oidc==0.3.0 flask-marshmallow==0.15.0 +Flask-Migrate==4.0.7 +Flask-Moment==1.0.6 flask-restx==1.1.0 -greenlet==3.0.3 -gunicorn==21.2.0 -idna==3.6 -importlib-metadata==7.0.1 -importlib-resources==6.1.1 -itsdangerous==2.1.2 -jsonschema-specifications==2023.12.1 -jsonschema==4.20.0 -marshmallow-sqlalchemy==0.29.0 +Flask-SQLAlchemy==3.0.5 +fonttools==4.54.1 +greenlet==3.1.1 +gunicorn==23.0.0 +idna==3.10 +Inflector==3.1.1 +itsdangerous==2.2.0 +Jinja2==3.1.4 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +kiwisolver==1.4.7 +Mako==1.3.6 +MarkupSafe==3.0.2 marshmallow==3.17.0 -numpy==1.24.4 +marshmallow-sqlalchemy==0.29.0 +matplotlib==3.7.4 +natsort==8.4.0 +numpy==1.26.4 openpyxl==3.0.10 -packaging==23.2 +packaging==24.1 pandas==2.0.2 -pillow==10.2.0 -pkgutil_resolve_name==1.3.10 -psycopg2-binary==2.9.9 -pyasn1==0.5.1 -pycparser==2.21 +pillow==11.0.0 +psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pycparser==2.22 +pyparsing==3.2.0 python-dateutil==2.8.2 -python-dotenv==1.0.0 +python-dotenv==1.0.1 python-jose==3.3.0 -pytz==2023.3.post1 -referencing==0.32.1 +pytz==2024.2 +referencing==0.35.1 reportlab==3.6.12 -requests==2.31.0 -rpds-py==0.16.2 +requests==2.32.3 +rpds-py==0.20.0 rsa==4.9 six==1.16.0 -typing_extensions==4.9.0 -tzdata==2023.4 -urllib3==2.1.0 -zipp==3.17.0 -matplotlib==3.7.4 -natsort==8.4.0 +SQLAlchemy==2.0.17 +SQLAlchemy-Utils==0.41.2 +typing_extensions==4.12.2 +tzdata==2024.2 +urllib3==2.2.3 +Werkzeug==2.3.6 +XlsxWriter==3.0.6 diff --git a/epictrack-api/requirements/prod.txt b/epictrack-api/requirements/prod.txt index 45dd89b16..cfb9e9e75 100644 --- a/epictrack-api/requirements/prod.txt +++ b/epictrack-api/requirements/prod.txt @@ -1,29 +1,29 @@ -gunicorn -Flask<2.3 -Flask-Caching -Flask-Migrate -Flask-Moment -Flask-SQLAlchemy==3.0.5 -flask-restx==1.1.0 -flask-jwt-oidc -python-dotenv -psycopg2-binary -jsonschema -requests -attrs -dpath -Werkzeug==2.3.6 -cryptography -sqlalchemy_utils -sqlalchemy==2.0.17 -Inflector -XlsxWriter==3.0.6 -python-dateutil==2.8.2 -reportlab==3.6.12 -marshmallow==3.17.0 -flask-marshmallow==0.15.0 -marshmallow-sqlalchemy==0.29.0 -pandas==2.0.2 -openpyxl==3.0.10 -matplotlib==3.7.4 -natsort==8.4.0 +gunicorn +Flask<2.3 +Flask-Caching +Flask-Migrate +Flask-Moment +Flask-SQLAlchemy==3.0.5 +flask-restx==1.1.0 +flask-jwt-oidc +python-dotenv +psycopg2-binary +jsonschema +requests +attrs +dpath +Werkzeug==2.3.6 +cryptography +sqlalchemy_utils +sqlalchemy==2.0.17 +Inflector +XlsxWriter==3.0.6 +python-dateutil==2.8.2 +reportlab==3.6.12 +marshmallow==3.17.0 +flask-marshmallow==0.15.0 +marshmallow-sqlalchemy==0.29.0 +pandas==2.0.2 +openpyxl==3.0.10 +matplotlib==3.7.4 +natsort==8.4.0 diff --git a/epictrack-api/src/api/actions/add_event.py b/epictrack-api/src/api/actions/add_event.py index 1ecd6a129..c5f64da36 100644 --- a/epictrack-api/src/api/actions/add_event.py +++ b/epictrack-api/src/api/actions/add_event.py @@ -1,90 +1,90 @@ -"""Disable work start date action handler""" -from api.actions.base import ActionFactory -from api.models import Event, db -from api.models.event_configuration import EventConfiguration -from api.models.event_template import EventTemplateVisibilityEnum -from api.models.phase_code import PhaseCode -from api.models.work_phase import WorkPhase -from api.schemas.response.event_configuration_response import ( - EventConfigurationResponseSchema, -) -from .set_event_date import SetEventDate -from .common import find_event_date - - -# pylint: disable=import-outside-toplevel - - -class AddEvent(ActionFactory): - """Add a new event""" - - def run(self, source_event: Event, params) -> None: - """Adds a new event based on params""" - from api.services.event import EventService - - for param in params: - event_data, work_phase_id = self.get_additional_params(source_event, param) - # setting anticipated date one day later so that the new event falls below the source event - event_data.update( - { - "is_active": True, - "work_id": source_event.work_id, - "anticipated_date": find_event_date(source_event), - } - ) - new_event = EventService.create_event( - event_data, work_phase_id=work_phase_id, push_events=True, commit=False - ) - set_event_date: SetEventDate = SetEventDate() - # Setting the event date from here cz, otherwise the event won't get pushed - set_event_date.run(source_event, param) - source_event = new_event - - def get_additional_params(self, source_event: Event, params): - """Returns additional parameter""" - from api.services.work import WorkService - - work_phase = ( - db.session.query(WorkPhase) - .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) - .filter( - WorkPhase.work_id == source_event.work_id, - PhaseCode.name == params.get("phase_name"), - PhaseCode.work_type_id == params.get("work_type_id"), - PhaseCode.ea_act_id == params.get("ea_act_id"), - WorkPhase.is_active.is_(True), - PhaseCode.is_active.is_(True), - ) - .order_by(WorkPhase.sort_order.desc()) - .first() - ) - old_event_config = ( - db.session.query(EventConfiguration) - .filter( - EventConfiguration.work_phase_id == work_phase.id, - EventConfiguration.name == params.get("event_name"), - EventConfiguration.is_active.is_(True), - ) - .order_by(EventConfiguration.repeat_count.desc()) - .first() - ) - - event_configuration = EventConfigurationResponseSchema().dump(old_event_config) - event_configuration["start_at"] = params["start_at"] - event_configuration["visibility"] = EventTemplateVisibilityEnum.MANDATORY.value - event_configuration["repeat_count"] = old_event_config.repeat_count + 1 - del event_configuration["id"] - event_configuration = EventConfiguration(**event_configuration) - event_configuration.flush() - WorkService.copy_outcome_and_actions( - old_event_config.as_dict(recursive=False), - event_configuration, - from_template=False, - ) - event_data = { - "event_configuration_id": event_configuration.id, - "name": event_configuration.name, - "number_of_days": event_configuration.number_of_days, - "source_event_id": event_configuration.parent_id, - } - return event_data, work_phase.id +"""Disable work start date action handler""" +from api.actions.base import ActionFactory +from api.models import Event, db +from api.models.event_configuration import EventConfiguration +from api.models.event_template import EventTemplateVisibilityEnum +from api.models.phase_code import PhaseCode +from api.models.work_phase import WorkPhase +from api.schemas.response.event_configuration_response import ( + EventConfigurationResponseSchema, +) +from .set_event_date import SetEventDate +from .common import find_event_date + + +# pylint: disable=import-outside-toplevel + + +class AddEvent(ActionFactory): + """Add a new event""" + + def run(self, source_event: Event, params) -> None: + """Adds a new event based on params""" + from api.services.event import EventService + + for param in params: + event_data, work_phase_id = self.get_additional_params(source_event, param) + # setting anticipated date one day later so that the new event falls below the source event + event_data.update( + { + "is_active": True, + "work_id": source_event.work_id, + "anticipated_date": find_event_date(source_event), + } + ) + new_event = EventService.create_event( + event_data, work_phase_id=work_phase_id, push_events=True, commit=False + ) + set_event_date: SetEventDate = SetEventDate() + # Setting the event date from here cz, otherwise the event won't get pushed + set_event_date.run(source_event, param) + source_event = new_event + + def get_additional_params(self, source_event: Event, params): + """Returns additional parameter""" + from api.services.work import WorkService + + work_phase = ( + db.session.query(WorkPhase) + .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) + .filter( + WorkPhase.work_id == source_event.work_id, + PhaseCode.name == params.get("phase_name"), + PhaseCode.work_type_id == params.get("work_type_id"), + PhaseCode.ea_act_id == params.get("ea_act_id"), + WorkPhase.is_active.is_(True), + PhaseCode.is_active.is_(True), + ) + .order_by(WorkPhase.sort_order.desc()) + .first() + ) + old_event_config = ( + db.session.query(EventConfiguration) + .filter( + EventConfiguration.work_phase_id == work_phase.id, + EventConfiguration.name == params.get("event_name"), + EventConfiguration.is_active.is_(True), + ) + .order_by(EventConfiguration.repeat_count.desc()) + .first() + ) + + event_configuration = EventConfigurationResponseSchema().dump(old_event_config) + event_configuration["start_at"] = params["start_at"] + event_configuration["visibility"] = EventTemplateVisibilityEnum.MANDATORY.value + event_configuration["repeat_count"] = old_event_config.repeat_count + 1 + del event_configuration["id"] + event_configuration = EventConfiguration(**event_configuration) + event_configuration.flush() + WorkService.copy_outcome_and_actions( + old_event_config.as_dict(recursive=False), + event_configuration, + from_template=False, + ) + event_data = { + "event_configuration_id": event_configuration.id, + "name": event_configuration.name, + "number_of_days": event_configuration.number_of_days, + "source_event_id": event_configuration.parent_id, + } + return event_data, work_phase.id diff --git a/epictrack-api/src/api/actions/add_phase.py b/epictrack-api/src/api/actions/add_phase.py index 42c8cc74a..b576aaa07 100644 --- a/epictrack-api/src/api/actions/add_phase.py +++ b/epictrack-api/src/api/actions/add_phase.py @@ -1,212 +1,212 @@ -"""Disable work start date action handler""" - -from datetime import timedelta -from typing import List -from operator import attrgetter -from sqlalchemy import and_ - -from api.actions.base import ActionFactory -from api.models import Event, EventConfiguration, WorkPhase, Work, db -from api.models.phase_code import PhaseCode, PhaseVisibilityEnum -from api.models.event_template import ( - EventTemplateVisibilityEnum, - EventPositionEnum, -) -from api.models.event_category import PRIMARY_CATEGORIES -from api.services.event_template import EventTemplateService -from api.schemas import response as res - - -# pylint: disable= import-outside-toplevel, too-many-locals -class AddPhase(ActionFactory): - """Add a new phase""" - - def run(self, source_event: Event, params) -> None: - """Adds a new phase based on params""" - # Importing here to avoid circular imports - from api.services.work import WorkService - from api.services.work_phase import WorkPhaseService - - number_of_phases = len(params) - # Push the sort order of the existing work phases after the new ones - self.preset_sort_order(source_event, number_of_phases) - number_of_days_to_be_added = self._get_number_of_days_to_be_added(source_event) - phase_start_date = source_event.actual_date + timedelta( - days=number_of_days_to_be_added - ) - sort_order = source_event.event_configuration.work_phase.sort_order + 1 - total_number_of_days = 0 - for param in params: - work_phase_data = self.get_additional_params(source_event, param) - total_number_of_days = total_number_of_days + work_phase_data.get( - "number_of_days" - ) - end_date = phase_start_date + timedelta( - days=work_phase_data.get("number_of_days") - ) - work_phase_data.update( - { - "work_id": source_event.work.id, - "start_date": f"{phase_start_date}", - "end_date": end_date, - "sort_order": sort_order, - } - ) - event_templates_for_the_phase = EventTemplateService.find_by_phase_id( - work_phase_data.get("phase_id") - ) - event_templates_for_the_phase_json = res.EventTemplateResponseSchema( - many=True - ).dump(event_templates_for_the_phase) - work_phase = WorkService.create_events_by_template( - work_phase_data, event_templates_for_the_phase_json - ) - sort_order = sort_order + 1 - phase_start_date = end_date + timedelta(days=1) - # update the current work phase - current_work_phase = WorkPhaseService.find_current_work_phase( - source_event.work_id - ) - - work = Work.find_by_id(source_event.work_id) - work.current_work_phase_id = current_work_phase.id - work.update(work.as_dict(recursive=False), commit=False) - - if work_phase: - self.update_susequent_work_phases(work_phase) - - def _get_number_of_days_to_be_added(self, source_event) -> int: - """Returns the phase start date""" - if source_event.event_position == EventPositionEnum.INTERMEDIATE.value: - work_phase_id = source_event.event_configuration.work_phase.id - event_configurations = EventConfiguration.find_by_params( - { - "work_phase_id": work_phase_id, - "visibility": EventTemplateVisibilityEnum.MANDATORY.value, - } - ) - primary_categories = list(map(lambda x: x.value, PRIMARY_CATEGORIES)) - filtered_configurations = [ - template - for template in event_configurations - if template.event_category_id in primary_categories - and template.sort_order > source_event.event_configuration.sort_order - ] - last_mandatory_configuration = max( - filtered_configurations, key=attrgetter("sort_order") - ) - return ( - int(last_mandatory_configuration.start_at) - - int(source_event.event_configuration.start_at) - ) + 1 - return 1 - - def preset_sort_order(self, source_event, number_of_new_work_phases: int) -> None: - """Adjust the sort order of the existing work phases for the new ones""" - db.session.query(WorkPhase).filter( - WorkPhase.sort_order - > source_event.event_configuration.work_phase.sort_order, - WorkPhase.work_id == source_event.work_id, - ).update( - {WorkPhase.sort_order: WorkPhase.sort_order + number_of_new_work_phases} - ) - - def update_susequent_work_phases( - self, - latest_work_phase_added: WorkPhase, - ): - """Update subsequent work phases with the number of additional days added by the phases""" - from api.services.event import EventService - - # Find the next work phase after the new work phases - next_work_phase = ( - db.session.query(WorkPhase) - .filter( - WorkPhase.work_id == latest_work_phase_added.work_id, - WorkPhase.sort_order > latest_work_phase_added.sort_order, - WorkPhase.visibility == PhaseVisibilityEnum.REGULAR.value, - WorkPhase.is_active.is_(True), - ) - .order_by(WorkPhase.sort_order) - .first() - ) - - # Find the start event of the work phase - if next_work_phase: - start_event = ( - db.session.query(Event) - .join( - EventConfiguration, - and_( - Event.event_configuration_id == EventConfiguration.id, - EventConfiguration.is_active.is_(True), - EventConfiguration.visibility - == EventTemplateVisibilityEnum.MANDATORY.value, - EventConfiguration.event_position == EventPositionEnum.START, - ), - ) - .join( - WorkPhase, - and_( - EventConfiguration.work_phase_id == WorkPhase.id, - WorkPhase.id == next_work_phase.id, - ), - ) - .first() - ) - - # Update the start event anticipated date by the number of total days added by all the new phases - # This will push all the susequent work phases and all the events - start_event_dict = start_event.as_dict(recursive=False) - start_event_dict["anticipated_date"] = ( - latest_work_phase_added.end_date + timedelta(days=1) - ) - EventService.update_event( - start_event_dict, start_event.id, True, commit=False - ) - - def get_additional_params(self, source_event, params): - """Returns additional parameter""" - query_params = { - "name": params.get("phase_name"), - "work_type_id": params.get("work_type_id"), - "ea_act_id": params.get("ea_act_id"), - } - phase = ( - db.session.query(PhaseCode) - .filter_by(**query_params, is_active=True) - .first() - ) - - work_phase_data = { - "phase_id": phase.id, - "name": params.get("new_name"), - "legislated": params.get("legislated"), - "number_of_days": phase.number_of_days, - "visibility": PhaseVisibilityEnum.REGULAR, - } - return work_phase_data - - def get_configurations( - self, source_event: Event, params - ) -> List[EventConfiguration]: - """Find the latest event configurations per the given params""" - work_phase = ( - db.session.query(WorkPhase) - .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) - .filter( - WorkPhase.work_id == source_event.work_id, - WorkPhase.name == params.get("phase_name"), - PhaseCode.work_type_id == params.get("work_type_id"), - PhaseCode.ea_act_id == params.get("ea_act_id"), - WorkPhase.is_active.is_(True), - PhaseCode.is_active.is_(True), - ) - .order_by(WorkPhase.sort_order.desc()) - .first() - ) - - event_configurations = EventConfiguration.find_by_params( - {"work_phase_id": work_phase.id} - ) - return event_configurations +"""Disable work start date action handler""" + +from datetime import timedelta +from typing import List +from operator import attrgetter +from sqlalchemy import and_ + +from api.actions.base import ActionFactory +from api.models import Event, EventConfiguration, WorkPhase, Work, db +from api.models.phase_code import PhaseCode, PhaseVisibilityEnum +from api.models.event_template import ( + EventTemplateVisibilityEnum, + EventPositionEnum, +) +from api.models.event_category import PRIMARY_CATEGORIES +from api.services.event_template import EventTemplateService +from api.schemas import response as res + + +# pylint: disable= import-outside-toplevel, too-many-locals +class AddPhase(ActionFactory): + """Add a new phase""" + + def run(self, source_event: Event, params) -> None: + """Adds a new phase based on params""" + # Importing here to avoid circular imports + from api.services.work import WorkService + from api.services.work_phase import WorkPhaseService + + number_of_phases = len(params) + # Push the sort order of the existing work phases after the new ones + self.preset_sort_order(source_event, number_of_phases) + number_of_days_to_be_added = self._get_number_of_days_to_be_added(source_event) + phase_start_date = source_event.actual_date + timedelta( + days=number_of_days_to_be_added + ) + sort_order = source_event.event_configuration.work_phase.sort_order + 1 + total_number_of_days = 0 + for param in params: + work_phase_data = self.get_additional_params(source_event, param) + total_number_of_days = total_number_of_days + work_phase_data.get( + "number_of_days" + ) + end_date = phase_start_date + timedelta( + days=work_phase_data.get("number_of_days") + ) + work_phase_data.update( + { + "work_id": source_event.work.id, + "start_date": f"{phase_start_date}", + "end_date": end_date, + "sort_order": sort_order, + } + ) + event_templates_for_the_phase = EventTemplateService.find_by_phase_id( + work_phase_data.get("phase_id") + ) + event_templates_for_the_phase_json = res.EventTemplateResponseSchema( + many=True + ).dump(event_templates_for_the_phase) + work_phase = WorkService.create_events_by_template( + work_phase_data, event_templates_for_the_phase_json + ) + sort_order = sort_order + 1 + phase_start_date = end_date + timedelta(days=1) + # update the current work phase + current_work_phase = WorkPhaseService.find_current_work_phase( + source_event.work_id + ) + + work = Work.find_by_id(source_event.work_id) + work.current_work_phase_id = current_work_phase.id + work.update(work.as_dict(recursive=False), commit=False) + + if work_phase: + self.update_susequent_work_phases(work_phase) + + def _get_number_of_days_to_be_added(self, source_event) -> int: + """Returns the phase start date""" + if source_event.event_position == EventPositionEnum.INTERMEDIATE.value: + work_phase_id = source_event.event_configuration.work_phase.id + event_configurations = EventConfiguration.find_by_params( + { + "work_phase_id": work_phase_id, + "visibility": EventTemplateVisibilityEnum.MANDATORY.value, + } + ) + primary_categories = list(map(lambda x: x.value, PRIMARY_CATEGORIES)) + filtered_configurations = [ + template + for template in event_configurations + if template.event_category_id in primary_categories + and template.sort_order > source_event.event_configuration.sort_order + ] + last_mandatory_configuration = max( + filtered_configurations, key=attrgetter("sort_order") + ) + return ( + int(last_mandatory_configuration.start_at) + - int(source_event.event_configuration.start_at) + ) + 1 + return 1 + + def preset_sort_order(self, source_event, number_of_new_work_phases: int) -> None: + """Adjust the sort order of the existing work phases for the new ones""" + db.session.query(WorkPhase).filter( + WorkPhase.sort_order + > source_event.event_configuration.work_phase.sort_order, + WorkPhase.work_id == source_event.work_id, + ).update( + {WorkPhase.sort_order: WorkPhase.sort_order + number_of_new_work_phases} + ) + + def update_susequent_work_phases( + self, + latest_work_phase_added: WorkPhase, + ): + """Update subsequent work phases with the number of additional days added by the phases""" + from api.services.event import EventService + + # Find the next work phase after the new work phases + next_work_phase = ( + db.session.query(WorkPhase) + .filter( + WorkPhase.work_id == latest_work_phase_added.work_id, + WorkPhase.sort_order > latest_work_phase_added.sort_order, + WorkPhase.visibility == PhaseVisibilityEnum.REGULAR.value, + WorkPhase.is_active.is_(True), + ) + .order_by(WorkPhase.sort_order) + .first() + ) + + # Find the start event of the work phase + if next_work_phase: + start_event = ( + db.session.query(Event) + .join( + EventConfiguration, + and_( + Event.event_configuration_id == EventConfiguration.id, + EventConfiguration.is_active.is_(True), + EventConfiguration.visibility + == EventTemplateVisibilityEnum.MANDATORY.value, + EventConfiguration.event_position == EventPositionEnum.START, + ), + ) + .join( + WorkPhase, + and_( + EventConfiguration.work_phase_id == WorkPhase.id, + WorkPhase.id == next_work_phase.id, + ), + ) + .first() + ) + + # Update the start event anticipated date by the number of total days added by all the new phases + # This will push all the susequent work phases and all the events + start_event_dict = start_event.as_dict(recursive=False) + start_event_dict["anticipated_date"] = ( + latest_work_phase_added.end_date + timedelta(days=1) + ) + EventService.update_event( + start_event_dict, start_event.id, True, commit=False + ) + + def get_additional_params(self, source_event, params): + """Returns additional parameter""" + query_params = { + "name": params.get("phase_name"), + "work_type_id": params.get("work_type_id"), + "ea_act_id": params.get("ea_act_id"), + } + phase = ( + db.session.query(PhaseCode) + .filter_by(**query_params, is_active=True) + .first() + ) + + work_phase_data = { + "phase_id": phase.id, + "name": params.get("new_name"), + "legislated": params.get("legislated"), + "number_of_days": phase.number_of_days, + "visibility": PhaseVisibilityEnum.REGULAR, + } + return work_phase_data + + def get_configurations( + self, source_event: Event, params + ) -> List[EventConfiguration]: + """Find the latest event configurations per the given params""" + work_phase = ( + db.session.query(WorkPhase) + .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) + .filter( + WorkPhase.work_id == source_event.work_id, + WorkPhase.name == params.get("phase_name"), + PhaseCode.work_type_id == params.get("work_type_id"), + PhaseCode.ea_act_id == params.get("ea_act_id"), + WorkPhase.is_active.is_(True), + PhaseCode.is_active.is_(True), + ) + .order_by(WorkPhase.sort_order.desc()) + .first() + ) + + event_configurations = EventConfiguration.find_by_params( + {"work_phase_id": work_phase.id} + ) + return event_configurations diff --git a/epictrack-api/src/api/actions/change_phase_end_event.py b/epictrack-api/src/api/actions/change_phase_end_event.py index dc2bfb662..a1e879bba 100644 --- a/epictrack-api/src/api/actions/change_phase_end_event.py +++ b/epictrack-api/src/api/actions/change_phase_end_event.py @@ -1,160 +1,160 @@ -"""Change the phase end event to another one""" - -from datetime import timedelta -from api.actions.base import ActionFactory -from api.models import Event, EventConfiguration, PhaseCode, WorkPhase, db -from api.models.event_configuration import EventPositionEnum -from api.models.phase_code import PhaseVisibilityEnum -from api.models.work import WorkStateEnum, Work -from api.utils import util -from .set_events_status import SetEventsStatus -from .common import find_configuration, find_event_date - - -class ChangePhaseEndEvent(ActionFactory): - """Change the phase end event to another one""" - - def run(self, source_event: Event, params) -> None: # pylint: disable=too-many-locals - """Change the phase end event to another one""" - from api.services.event import (EventService) # pylint: disable=import-outside-toplevel - - new_end_event_configuration = find_configuration(source_event, params) - # find the work phase of the future end event as per the params - if source_event.event_configuration_id == new_end_event_configuration.id: - work_phase = source_event.event_configuration.work_phase - new_end_event = [source_event] - else: - work_phase = self.get_additional_params(source_event, params) - new_end_event = Event.find_by_params( - {"event_configuration_id": new_end_event_configuration.id} - ) - # Find the current end event configuration - if ( - source_event.event_configuration.event_position - == EventPositionEnum.END.value - ): - current_end_event_config = source_event.event_configuration - else: - current_end_event_config = ( - db.session.query(EventConfiguration) - .filter( - EventConfiguration.work_phase_id == work_phase.id, - EventConfiguration.event_position == EventPositionEnum.END.value, - EventConfiguration.is_active.is_(True), - EventConfiguration.is_deleted.is_(False), - ) - .first() - ) - - if new_end_event_configuration.id != current_end_event_config.id: - current_end_event_config.event_position = EventPositionEnum.INTERMEDIATE - current_end_event_config.update( - current_end_event_config.as_dict(recursive=False), commit=False - ) - - new_end_event = new_end_event[0] - # Make sure all the other events after the new end event date should be deactivated - set_event_status = SetEventsStatus() - set_event_status.run(new_end_event, {"all_future_events": False}) - if new_end_event_configuration.id != current_end_event_config.id: - new_end_event_configuration.event_position = EventPositionEnum.END.value - new_end_event_configuration.update( - new_end_event_configuration.as_dict(recursive=False), commit=False - ) - - # In case if the source event was already an end event and we are making a new event as end event, - # updating the actual would have already - # completed the phase and updated the work state. Since there is going to be a new end event - # we should revert those changes - work = new_end_event.event_configuration.work_phase.work - new_end_event_work_phase = new_end_event.event_configuration.work_phase - if new_end_event_work_phase.is_completed and not new_end_event.actual_date: - # reset the work_phase_id at work level to point back to the current phase - work.current_work_phase_id = new_end_event.event_configuration.work_phase_id - new_end_event_work_phase.is_completed = False - new_end_event_work_phase.update( - new_end_event_work_phase.as_dict(recursive=False), commit=False - ) - if ( - new_end_event_work_phase.work.work_state == WorkStateEnum.COMPLETED - and not new_end_event.actual_date - ): - new_end_event_work_phase.work.work_state = WorkStateEnum.IN_PROGRESS - new_end_event_work_phase.work.update( - new_end_event_work_phase.work.as_dict(recursive=False), commit=False - ) - # if the new end event has an actual set already, set it as the enddate of the phase - if new_end_event.actual_date: - new_end_event_work_phase.is_completed = True - new_end_event_work_phase.end_date = new_end_event.actual_date - new_end_event_work_phase.update( - new_end_event_work_phase.as_dict(recursive=False), commit=False - ) - - all_work_phases = WorkPhase.find_by_params( - { - "work_id": source_event.event_configuration.work_phase.work_id, - "visibility": PhaseVisibilityEnum.REGULAR.value, - } - ) - all_work_phases = sorted(all_work_phases, key=lambda x: x.sort_order) - current_work_phase_index = util.find_index_in_array( - all_work_phases, new_end_event.event_configuration.work_phase - ) - # update the current_work_phase_id in the work model - work = new_end_event.event_configuration.work_phase.work - if current_work_phase_index == len(all_work_phases) - 1: - work.work_state = WorkStateEnum.COMPLETED - elif new_end_event.actual_date: - work.current_work_phase_id = all_work_phases[ - current_work_phase_index + 1 - ].id - work.update(work.as_dict(recursive=False), commit=False) - - if len(all_work_phases) > current_work_phase_index + 1: - next_work_phase = all_work_phases[current_work_phase_index + 1] - work_phase_events = Event.find_milestone_events_by_work_phase( - next_work_phase.id - ) - next_work_phase_start_event = next( - iter( - [ - event - for event in work_phase_events - if event.event_configuration.event_position.value - == EventPositionEnum.START.value - ] - ), - None, - ) - next_work_phase_start_event.anticipated_date = ( - find_event_date(new_end_event) + timedelta(days=1) - ) - EventService.update_event( - next_work_phase_start_event.as_dict(recursive=False), - next_work_phase_start_event.id, - True, - commit=False, - ) - - def _update_work(self, work: Work): - """Update the current phase and state of the work based on the actual date""" - - def get_additional_params(self, source_event: Event, params): - """Returns additional parameter""" - work_phase = ( - db.session.query(WorkPhase) - .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) - .filter( - WorkPhase.work_id == source_event.work_id, - WorkPhase.name == params.get("phase_name"), - PhaseCode.work_type_id == params.get("work_type_id"), - PhaseCode.ea_act_id == params.get("ea_act_id"), - WorkPhase.visibility == PhaseVisibilityEnum.REGULAR.value, - WorkPhase.is_active.is_(True), - PhaseCode.is_active.is_(True), - ) - .order_by(WorkPhase.sort_order.desc()) - .first() - ) - return work_phase +"""Change the phase end event to another one""" + +from datetime import timedelta +from api.actions.base import ActionFactory +from api.models import Event, EventConfiguration, PhaseCode, WorkPhase, db +from api.models.event_configuration import EventPositionEnum +from api.models.phase_code import PhaseVisibilityEnum +from api.models.work import WorkStateEnum, Work +from api.utils import util +from .set_events_status import SetEventsStatus +from .common import find_configuration, find_event_date + + +class ChangePhaseEndEvent(ActionFactory): + """Change the phase end event to another one""" + + def run(self, source_event: Event, params) -> None: # pylint: disable=too-many-locals + """Change the phase end event to another one""" + from api.services.event import (EventService) # pylint: disable=import-outside-toplevel + + new_end_event_configuration = find_configuration(source_event, params) + # find the work phase of the future end event as per the params + if source_event.event_configuration_id == new_end_event_configuration.id: + work_phase = source_event.event_configuration.work_phase + new_end_event = [source_event] + else: + work_phase = self.get_additional_params(source_event, params) + new_end_event = Event.find_by_params( + {"event_configuration_id": new_end_event_configuration.id} + ) + # Find the current end event configuration + if ( + source_event.event_configuration.event_position + == EventPositionEnum.END.value + ): + current_end_event_config = source_event.event_configuration + else: + current_end_event_config = ( + db.session.query(EventConfiguration) + .filter( + EventConfiguration.work_phase_id == work_phase.id, + EventConfiguration.event_position == EventPositionEnum.END.value, + EventConfiguration.is_active.is_(True), + EventConfiguration.is_deleted.is_(False), + ) + .first() + ) + + if new_end_event_configuration.id != current_end_event_config.id: + current_end_event_config.event_position = EventPositionEnum.INTERMEDIATE + current_end_event_config.update( + current_end_event_config.as_dict(recursive=False), commit=False + ) + + new_end_event = new_end_event[0] + # Make sure all the other events after the new end event date should be deactivated + set_event_status = SetEventsStatus() + set_event_status.run(new_end_event, {"all_future_events": False}) + if new_end_event_configuration.id != current_end_event_config.id: + new_end_event_configuration.event_position = EventPositionEnum.END.value + new_end_event_configuration.update( + new_end_event_configuration.as_dict(recursive=False), commit=False + ) + + # In case if the source event was already an end event and we are making a new event as end event, + # updating the actual would have already + # completed the phase and updated the work state. Since there is going to be a new end event + # we should revert those changes + work = new_end_event.event_configuration.work_phase.work + new_end_event_work_phase = new_end_event.event_configuration.work_phase + if new_end_event_work_phase.is_completed and not new_end_event.actual_date: + # reset the work_phase_id at work level to point back to the current phase + work.current_work_phase_id = new_end_event.event_configuration.work_phase_id + new_end_event_work_phase.is_completed = False + new_end_event_work_phase.update( + new_end_event_work_phase.as_dict(recursive=False), commit=False + ) + if ( + new_end_event_work_phase.work.work_state == WorkStateEnum.COMPLETED + and not new_end_event.actual_date + ): + new_end_event_work_phase.work.work_state = WorkStateEnum.IN_PROGRESS + new_end_event_work_phase.work.update( + new_end_event_work_phase.work.as_dict(recursive=False), commit=False + ) + # if the new end event has an actual set already, set it as the enddate of the phase + if new_end_event.actual_date: + new_end_event_work_phase.is_completed = True + new_end_event_work_phase.end_date = new_end_event.actual_date + new_end_event_work_phase.update( + new_end_event_work_phase.as_dict(recursive=False), commit=False + ) + + all_work_phases = WorkPhase.find_by_params( + { + "work_id": source_event.event_configuration.work_phase.work_id, + "visibility": PhaseVisibilityEnum.REGULAR.value, + } + ) + all_work_phases = sorted(all_work_phases, key=lambda x: x.sort_order) + current_work_phase_index = util.find_index_in_array( + all_work_phases, new_end_event.event_configuration.work_phase + ) + # update the current_work_phase_id in the work model + work = new_end_event.event_configuration.work_phase.work + if current_work_phase_index == len(all_work_phases) - 1: + work.work_state = WorkStateEnum.COMPLETED + elif new_end_event.actual_date: + work.current_work_phase_id = all_work_phases[ + current_work_phase_index + 1 + ].id + work.update(work.as_dict(recursive=False), commit=False) + + if len(all_work_phases) > current_work_phase_index + 1: + next_work_phase = all_work_phases[current_work_phase_index + 1] + work_phase_events = Event.find_milestone_events_by_work_phase( + next_work_phase.id + ) + next_work_phase_start_event = next( + iter( + [ + event + for event in work_phase_events + if event.event_configuration.event_position.value + == EventPositionEnum.START.value + ] + ), + None, + ) + next_work_phase_start_event.anticipated_date = ( + find_event_date(new_end_event) + timedelta(days=1) + ) + EventService.update_event( + next_work_phase_start_event.as_dict(recursive=False), + next_work_phase_start_event.id, + True, + commit=False, + ) + + def _update_work(self, work: Work): + """Update the current phase and state of the work based on the actual date""" + + def get_additional_params(self, source_event: Event, params): + """Returns additional parameter""" + work_phase = ( + db.session.query(WorkPhase) + .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) + .filter( + WorkPhase.work_id == source_event.work_id, + WorkPhase.name == params.get("phase_name"), + PhaseCode.work_type_id == params.get("work_type_id"), + PhaseCode.ea_act_id == params.get("ea_act_id"), + WorkPhase.visibility == PhaseVisibilityEnum.REGULAR.value, + WorkPhase.is_active.is_(True), + PhaseCode.is_active.is_(True), + ) + .order_by(WorkPhase.sort_order.desc()) + .first() + ) + return work_phase diff --git a/epictrack-api/src/api/actions/common.py b/epictrack-api/src/api/actions/common.py index 4788883ec..0d6b54b3f 100644 --- a/epictrack-api/src/api/actions/common.py +++ b/epictrack-api/src/api/actions/common.py @@ -1,72 +1,72 @@ -"""Common methods for the actions""" -from datetime import datetime -from api.models import Event, EventConfiguration, WorkPhase, db -from api.models.phase_code import PhaseCode, PhaseVisibilityEnum -from api.models.work_calendar_event import WorkCalendarEvent -from api.models.calendar_event import CalendarEvent - - -def find_configuration(source_event: Event, params) -> int: - """Find the configuration""" - work_phase = ( - db.session.query(WorkPhase) - .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) - .filter( - WorkPhase.work_id == source_event.work_id, - WorkPhase.name == params.get("phase_name"), - PhaseCode.work_type_id == params.get("work_type_id"), - PhaseCode.ea_act_id == params.get("ea_act_id"), - WorkPhase.visibility == PhaseVisibilityEnum.REGULAR.value, - WorkPhase.is_active.is_(True), - PhaseCode.is_active.is_(True), - ) - .order_by(WorkPhase.sort_order.desc()) - .first() - ) - - event_configuration = ( - db.session.query(EventConfiguration) - .filter( - EventConfiguration.work_phase_id == work_phase.id, - EventConfiguration.name == params.get("event_name"), - EventConfiguration.is_active.is_(True), - ) - .order_by(EventConfiguration.repeat_count.desc()) - .first() - ) - return event_configuration - - -def find_event_date(source_event: Event) -> datetime: - """Returns actual date if the event has one else anticipated""" - return ( - source_event.actual_date - if source_event.actual_date - else source_event.anticipated_date - ) - - -def deactivate_calendar_events_by_configuration_ids(configuration_ids: [int]): - """Make the calendar events inactive based on the source event configuration ids""" - if len(configuration_ids) > 0: - events_result = ( - db.session.query(Event) - .filter(Event.event_configuration_id.in_(configuration_ids)) - .all() - ) - source_event_ids = list(map(lambda x: x.id, events_result)) - work_calendar_events = ( - db.session.query(WorkCalendarEvent) - .filter(WorkCalendarEvent.source_event_id.in_(source_event_ids)) - .all() - ) - work_calendar_event_ids = list(map(lambda x: x.id, work_calendar_events)) - calendar_event_ids = list( - map(lambda x: x.calendar_event_id, work_calendar_events) - ) - db.session.query(WorkCalendarEvent).filter( - WorkCalendarEvent.id.in_(work_calendar_event_ids) - ).update({WorkCalendarEvent.is_active: False}) - db.session.query(CalendarEvent).filter( - CalendarEvent.id.in_(calendar_event_ids) - ).update({CalendarEvent.is_active: False}) +"""Common methods for the actions""" +from datetime import datetime +from api.models import Event, EventConfiguration, WorkPhase, db +from api.models.phase_code import PhaseCode, PhaseVisibilityEnum +from api.models.work_calendar_event import WorkCalendarEvent +from api.models.calendar_event import CalendarEvent + + +def find_configuration(source_event: Event, params) -> int: + """Find the configuration""" + work_phase = ( + db.session.query(WorkPhase) + .join(PhaseCode, WorkPhase.phase_id == PhaseCode.id) + .filter( + WorkPhase.work_id == source_event.work_id, + WorkPhase.name == params.get("phase_name"), + PhaseCode.work_type_id == params.get("work_type_id"), + PhaseCode.ea_act_id == params.get("ea_act_id"), + WorkPhase.visibility == PhaseVisibilityEnum.REGULAR.value, + WorkPhase.is_active.is_(True), + PhaseCode.is_active.is_(True), + ) + .order_by(WorkPhase.sort_order.desc()) + .first() + ) + + event_configuration = ( + db.session.query(EventConfiguration) + .filter( + EventConfiguration.work_phase_id == work_phase.id, + EventConfiguration.name == params.get("event_name"), + EventConfiguration.is_active.is_(True), + ) + .order_by(EventConfiguration.repeat_count.desc()) + .first() + ) + return event_configuration + + +def find_event_date(source_event: Event) -> datetime: + """Returns actual date if the event has one else anticipated""" + return ( + source_event.actual_date + if source_event.actual_date + else source_event.anticipated_date + ) + + +def deactivate_calendar_events_by_configuration_ids(configuration_ids: [int]): + """Make the calendar events inactive based on the source event configuration ids""" + if len(configuration_ids) > 0: + events_result = ( + db.session.query(Event) + .filter(Event.event_configuration_id.in_(configuration_ids)) + .all() + ) + source_event_ids = list(map(lambda x: x.id, events_result)) + work_calendar_events = ( + db.session.query(WorkCalendarEvent) + .filter(WorkCalendarEvent.source_event_id.in_(source_event_ids)) + .all() + ) + work_calendar_event_ids = list(map(lambda x: x.id, work_calendar_events)) + calendar_event_ids = list( + map(lambda x: x.calendar_event_id, work_calendar_events) + ) + db.session.query(WorkCalendarEvent).filter( + WorkCalendarEvent.id.in_(work_calendar_event_ids) + ).update({WorkCalendarEvent.is_active: False}) + db.session.query(CalendarEvent).filter( + CalendarEvent.id.in_(calendar_event_ids) + ).update({CalendarEvent.is_active: False}) diff --git a/epictrack-api/src/api/actions/set_event_date.py b/epictrack-api/src/api/actions/set_event_date.py index 3178779bd..e95e83c96 100644 --- a/epictrack-api/src/api/actions/set_event_date.py +++ b/epictrack-api/src/api/actions/set_event_date.py @@ -1,33 +1,33 @@ -"""Disable work start date action handler""" - -from datetime import timedelta -from api.actions.base import ActionFactory -from api.models import db -from api.models.event import Event - -from .common import find_configuration, find_event_date - - -class SetEventDate(ActionFactory): # pylint: disable=too-few-public-methods - """Sets the event date""" - - def run(self, source_event: Event, params: dict) -> None: - """Performs the required operations""" - from api.services.event import EventService # pylint: disable=import-outside-toplevel - - number_of_days_to_be_added = params.get("start_at") - event_configuration = find_configuration(source_event, params) - event = ( - db.session.query(Event) - .filter( - Event.work_id == source_event.work_id, - Event.is_active.is_(True), - Event.event_configuration_id == event_configuration.id, - ) - .first() - ) - event_dict = event.as_dict(recursive=False) - event_dict["anticipated_date"] = find_event_date(source_event) + timedelta( - days=number_of_days_to_be_added - ) - EventService.update_event(event_dict, event.id, True, commit=False) +"""Disable work start date action handler""" + +from datetime import timedelta +from api.actions.base import ActionFactory +from api.models import db +from api.models.event import Event + +from .common import find_configuration, find_event_date + + +class SetEventDate(ActionFactory): # pylint: disable=too-few-public-methods + """Sets the event date""" + + def run(self, source_event: Event, params: dict) -> None: + """Performs the required operations""" + from api.services.event import EventService # pylint: disable=import-outside-toplevel + + number_of_days_to_be_added = params.get("start_at") + event_configuration = find_configuration(source_event, params) + event = ( + db.session.query(Event) + .filter( + Event.work_id == source_event.work_id, + Event.is_active.is_(True), + Event.event_configuration_id == event_configuration.id, + ) + .first() + ) + event_dict = event.as_dict(recursive=False) + event_dict["anticipated_date"] = find_event_date(source_event) + timedelta( + days=number_of_days_to_be_added + ) + EventService.update_event(event_dict, event.id, True, commit=False) diff --git a/epictrack-api/src/api/actions/set_federal_involvement.py b/epictrack-api/src/api/actions/set_federal_involvement.py index c49e4ef37..205f5b38d 100644 --- a/epictrack-api/src/api/actions/set_federal_involvement.py +++ b/epictrack-api/src/api/actions/set_federal_involvement.py @@ -1,15 +1,15 @@ -"""Set federal involvement""" -from api.actions.base import ActionFactory -from api.models import db -from api.models.event import Event -from api.models.work import Work - - -class SetFederalInvolvement(ActionFactory): - """Sets the federal involvement field to None""" - - def run(self, source_event: Event, params) -> None: - """Sets the federal involvement field to None""" - db.session.query(Work).filter(Work.id == source_event.work_id).update( - {Work.federal_involvement_id: params.get("federal_involvement_id")} - ) +"""Set federal involvement""" +from api.actions.base import ActionFactory +from api.models import db +from api.models.event import Event +from api.models.work import Work + + +class SetFederalInvolvement(ActionFactory): + """Sets the federal involvement field to None""" + + def run(self, source_event: Event, params) -> None: + """Sets the federal involvement field to None""" + db.session.query(Work).filter(Work.id == source_event.work_id).update( + {Work.federal_involvement_id: params.get("federal_involvement_id")} + ) diff --git a/epictrack-api/src/api/actions/set_project_state.py b/epictrack-api/src/api/actions/set_project_state.py index 7b0253a85..9542686e0 100644 --- a/epictrack-api/src/api/actions/set_project_state.py +++ b/epictrack-api/src/api/actions/set_project_state.py @@ -1,14 +1,14 @@ -"""Sets the state of the project""" -from api.actions.base import ActionFactory -from api.models.event import Event -from api.models.project import Project, ProjectStateEnum - - -class SetProjectState(ActionFactory): - """Sets the state of the project""" - - def run(self, source_event: Event, params) -> None: - """Sets the federal involvement field to None""" - project = Project.find_by_id(source_event.work.project_id) - project.project_state = ProjectStateEnum(params.get("project_state")) - project.update(project.as_dict(recursive=False), commit=False) +"""Sets the state of the project""" +from api.actions.base import ActionFactory +from api.models.event import Event +from api.models.project import Project, ProjectStateEnum + + +class SetProjectState(ActionFactory): + """Sets the state of the project""" + + def run(self, source_event: Event, params) -> None: + """Sets the federal involvement field to None""" + project = Project.find_by_id(source_event.work.project_id) + project.project_state = ProjectStateEnum(params.get("project_state")) + project.update(project.as_dict(recursive=False), commit=False) diff --git a/epictrack-api/src/api/actions/set_work_decision_maker.py b/epictrack-api/src/api/actions/set_work_decision_maker.py index 9b4d8ea15..e5298bcc6 100644 --- a/epictrack-api/src/api/actions/set_work_decision_maker.py +++ b/epictrack-api/src/api/actions/set_work_decision_maker.py @@ -1,22 +1,22 @@ -"""Disable work start date action handler""" - -from api.actions.base import ActionFactory -from api.models import Work -from api.exceptions import UnprocessableEntityError - - -# pylint: disable= import-outside-toplevel -class SetWorkDecisionMaker(ActionFactory): # pylint: disable=too-few-public-methods - """Sets the work decision maker""" - - def run(self, source_event, params: dict) -> None: - """Performs the required operations""" - from api.services.staff import StaffService - - staff = StaffService.find_by_position_id(params.get("position_id")).all() - if len(staff) == 0: - raise UnprocessableEntityError("Could not find staff with selected designation") - work = Work.find_by_id(source_event.work_id) - work.decision_by_id = staff[0].id - work.decision_maker_position_id = params.get("position_id") - work.update(work.as_dict(recursive=False), commit=False) +"""Disable work start date action handler""" + +from api.actions.base import ActionFactory +from api.models import Work +from api.exceptions import UnprocessableEntityError + + +# pylint: disable= import-outside-toplevel +class SetWorkDecisionMaker(ActionFactory): # pylint: disable=too-few-public-methods + """Sets the work decision maker""" + + def run(self, source_event, params: dict) -> None: + """Performs the required operations""" + from api.services.staff import StaffService + + staff = StaffService.find_by_position_id(params.get("position_id")).all() + if len(staff) == 0: + raise UnprocessableEntityError("Could not find staff with selected designation") + work = Work.find_by_id(source_event.work_id) + work.decision_by_id = staff[0].id + work.decision_maker_position_id = params.get("position_id") + work.update(work.as_dict(recursive=False), commit=False) diff --git a/epictrack-api/src/api/config.py b/epictrack-api/src/api/config.py index ea32a1ccd..e8de22b11 100644 --- a/epictrack-api/src/api/config.py +++ b/epictrack-api/src/api/config.py @@ -1,242 +1,242 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""All of the configuration for the service is captured here. - -All items are loaded, -or have Constants defined here that are loaded into the Flask configuration. -All modules and lookups get their configuration from the Flask config, -rather than reading environment variables directly or by accessing this configuration directly. -""" - -import os -import sys - -from dotenv import find_dotenv, load_dotenv -from api.utils import constants - - -# this will load all the envars from a .env file located in the project root (api) -load_dotenv(find_dotenv()) - -CONFIGURATION = { - 'development': 'api.config.DevConfig', - 'testing': 'api.config.TestConfig', - 'production': 'api.config.ProdConfig', - 'default': 'api.config.ProdConfig', - 'migration': 'api.config.MigrationConfig', -} - - -def get_named_config(config_name: str = 'production'): - """Return the configuration object based on the name. - - :raise: KeyError: if an unknown configuration is requested - """ - if config_name in ['production', 'staging', 'default']: - config = ProdConfig() - elif config_name == 'testing': - config = TestConfig() - elif config_name == 'development': - config = DevConfig() - elif config_name == 'migration': - config = MigrationConfig() - else: - raise KeyError(f"Unknown configuration '{config_name}'") - return config - - -def _get_config(config_key: str, **kwargs): - """Get the config from environment, and throw error if there are no default values and if the value is None.""" - if 'default' in kwargs: - value = os.getenv(config_key, kwargs.get('default')) - else: - value = os.getenv(config_key) - # assert value TODO Un-comment once we find a solution to run pre-hook without initializing app - return value - - -class _Config(): # pylint: disable=too-few-public-methods - """Base class configuration that should set reasonable defaults for all the other configurations.""" - - PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) - - SECRET_KEY = 'a secret' - - SQLALCHEMY_TRACK_MODIFICATIONS = False - - # added to skip the event logic for the purpose of entering historical data without - # much hurdles - SKIP_EVENT_LOGIC = _get_config('SKIP_EVENT_LOGIC', default=False) == 'True' - - ALEMBIC_INI = 'migrations/alembic.ini' - - # POSTGRESQL - DB_USER = _get_config('DATABASE_USERNAME') - DB_PASSWORD = _get_config('DATABASE_PASSWORD') - DB_NAME = _get_config('DATABASE_NAME') - DB_HOST = _get_config('DATABASE_HOST') - DB_PORT = _get_config('DATABASE_PORT', default='5432') - SQLALCHEMY_DATABASE_URI = f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' - SQLALCHEMY_ECHO = _get_config('SQLALCHEMY_ECHO', default='False').lower() == 'true' - - # JWT_OIDC Settings - JWT_OIDC_WELL_KNOWN_CONFIG = _get_config('JWT_OIDC_WELL_KNOWN_CONFIG') - JWT_OIDC_ALGORITHMS = _get_config('JWT_OIDC_ALGORITHMS') - JWT_OIDC_JWKS_URI = _get_config('JWT_OIDC_JWKS_URI', default=None) - JWT_OIDC_ISSUER = _get_config('JWT_OIDC_ISSUER') - JWT_OIDC_AUDIENCE = _get_config('JWT_OIDC_AUDIENCE') - JWT_OIDC_CLIENT_SECRET = _get_config('JWT_OIDC_CLIENT_SECRET', default=None) - JWT_OIDC_CACHING_ENABLED = _get_config('JWT_OIDC_CACHING_ENABLED', default=False) - JWT_OIDC_JWKS_CACHE_TIMEOUT = int(_get_config('JWT_OIDC_JWKS_CACHE_TIMEOUT', default=300)) - - KEYCLOAK_BASE_URL = _get_config('KEYCLOAK_BASE_URL') - KEYCLOAK_REALM_NAME = _get_config('KEYCLOAK_REALM_NAME') - KEYCLOAK_ADMIN_CLIENT = _get_config('KEYCLOAK_ADMIN_CLIENT') - KEYCLOAK_ADMIN_SECRET = _get_config('KEYCLOAK_ADMIN_SECRET') - CONNECT_TIMEOUT = _get_config('CONNECT_TIMEOUT', default=60) - - TESTING = False - DEBUG = True - - CACHE_TYPE = constants.CACHE_TYPE - CACHE_DEFAULT_TIMEOUT = constants.CACHE_DEFAULT_TIMEOUT - - MIN_WORK_START_DATE = _get_config('MIN_WORK_START_DATE', default='1995-06-30') - - -class DevConfig(_Config): # pylint: disable=too-few-public-methods - """Dev config.""" - - TESTING = False - DEBUG = True - - -class TestConfig(_Config): # pylint: disable=too-few-public-methods - """In support of testing only used by the py.test suite.""" - - DEBUG = True - TESTING = True - - # POSTGRESQL - DB_USER = _get_config('DATABASE_TEST_USERNAME', default='postgres') - DB_PASSWORD = _get_config('DATABASE_TEST_PASSWORD', default='postgres') - DB_NAME = _get_config('DATABASE_TEST_NAME', default='testdb') - DB_HOST = _get_config('DATABASE_TEST_HOST', default='localhost') - DB_PORT = _get_config('DATABASE_TEST_PORT', default='5432') - SQLALCHEMY_DATABASE_URI = _get_config( - 'DATABASE_TEST_URL', - default=f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' - ) - - JWT_OIDC_TEST_MODE = True - # JWT_OIDC_ISSUER = _get_config('JWT_OIDC_TEST_ISSUER') - JWT_OIDC_TEST_AUDIENCE = _get_config('JWT_OIDC_TEST_AUDIENCE') - JWT_OIDC_TEST_CLIENT_SECRET = _get_config('JWT_OIDC_TEST_CLIENT_SECRET') - JWT_OIDC_TEST_ISSUER = _get_config('JWT_OIDC_TEST_ISSUER') - JWT_OIDC_WELL_KNOWN_CONFIG = _get_config('JWT_OIDC_WELL_KNOWN_CONFIG') - JWT_OIDC_TEST_ALGORITHMS = _get_config('JWT_OIDC_TEST_ALGORITHMS') - JWT_OIDC_TEST_JWKS_URI = _get_config('JWT_OIDC_TEST_JWKS_URI', default=None) - - JWT_OIDC_TEST_KEYS = { - 'keys': [ - { - 'kid': 'epictrack', - 'kty': 'RSA', - 'alg': 'RS256', - 'use': 'sig', - 'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-' - 'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR', - 'e': 'AQAB' - } - ] - } - - JWT_OIDC_TEST_PRIVATE_KEY_JWKS = { - 'keys': [ - { - 'kid': 'forms-flow-ai', - 'kty': 'RSA', - 'alg': 'RS256', - 'use': 'sig', - 'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-' - 'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR', - 'e': 'AQAB', - 'd': 'C0G3QGI6OQ6tvbCNYGCqq043YI_8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhskURaDwk4-' - '8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh_' - 'xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0', - 'p': 'APXcusFMQNHjh6KVD_hOUIw87lvK13WkDEeeuqAydai9Ig9JKEAAfV94W6Aftka7tGgE7ulg1vo3eJoLWJ1zvKM', - 'q': 'AOjX3OnPJnk0ZFUQBwhduCweRi37I6DAdLTnhDvcPTrrNWuKPg9uGwHjzFCJgKd8KBaDQ0X1rZTZLTqi3peT43s', - 'dp': 'AN9kBoA5o6_Rl9zeqdsIdWFmv4DB5lEqlEnC7HlAP-3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhc', - 'dq': 'ANtbSY6njfpPploQsF9sU26U0s7MsuLljM1E8uml8bVJE1mNsiu9MgpUvg39jEu9BtM2tDD7Y51AAIEmIQex1nM', - 'qi': 'XLE5O360x-MhsdFXx8Vwz4304-MJg-oGSJXCK_ZWYOB_FGXFRTfebxCsSYi0YwJo-oNu96bvZCuMplzRI1liZw' - } - ] - } - - JWT_OIDC_TEST_PRIVATE_KEY_PEM = """-----BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDfn1nKQshOSj8xw44oC2klFWSNLmK3BnHONCJ1bZfq0EQ5gIfg -tlvB+Px8Ya+VS3OnK7Cdi4iU1fxO9ktN6c6TjmmmFevk8wIwqLthmCSF3r+3+h4e -ddj7hucMsXWv05QUrCPoL6YUUz7Cgpz7ra24rpAmK5z7lsV+f3BEvXkrUQIDAQAB -AoGAC0G3QGI6OQ6tvbCNYGCqq043YI/8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhs -kURaDwk4+8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh/ -xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0CQQD13LrBTEDR44ei -lQ/4TlCMPO5bytd1pAxHnrqgMnWovSIPSShAAH1feFugH7ZGu7RoBO7pYNb6N3ia -C1idc7yjAkEA6Nfc6c8meTRkVRAHCF24LB5GLfsjoMB0tOeEO9w9Ous1a4o+D24b -AePMUImAp3woFoNDRfWtlNktOqLel5PjewJBAN9kBoA5o6/Rl9zeqdsIdWFmv4DB -5lEqlEnC7HlAP+3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhcCQQDb -W0mOp436T6ZaELBfbFNulNLOzLLi5YzNRPLppfG1SRNZjbIrvTIKVL4N/YxLvQbT -NrQw+2OdQACBJiEHsdZzAkBcsTk7frTH4yGx0VfHxXDPjfTj4wmD6gZIlcIr9lZg -4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn ------END RSA PRIVATE KEY-----""" - CACHE_TYPE = constants.NULL_CACHE_TYPE - - KEYCLOAK_BASE_URL = _get_config('KEYCLOAK_BASE_URL') - KEYCLOAK_REALM_NAME = _get_config('KEYCLOAK_REALM_NAME') - KEYCLOAK_ADMIN_CLIENT = _get_config('KEYCLOAK_ADMIN_CLIENT') - KEYCLOAK_ADMIN_SECRET = _get_config('KEYCLOAK_ADMIN_SECRET') - - -class ProdConfig(_Config): # pylint: disable=too-few-public-methods - """Production environment configuration.""" - - SECRET_KEY = _get_config('SECRET_KEY', default=None) - - if not SECRET_KEY: - SECRET_KEY = os.urandom(24) - print('WARNING: SECRET_KEY being set as a one-shot', file=sys.stderr) - - TESTING = False - DEBUG = False - - -class MigrationConfig(_Config): # pylint: disable=too-few-public-methods - """Config for db migration.""" - - TESTING = False - DEBUG = True - - # POSTGRESQL - DB_USER = _get_config('DATABASE_USERNAME') - DB_PASSWORD = _get_config('DATABASE_PASSWORD') - DB_NAME = _get_config('DATABASE_NAME') - DB_HOST = _get_config('DATABASE_HOST') - DB_PORT = _get_config('DATABASE_PORT', default='5432') - KEYCLOAK_BASE_URL = _get_config('KEYCLOAK_BASE_URL') - KEYCLOAK_REALM_NAME = _get_config('KEYCLOAK_REALM_NAME') - KEYCLOAK_ADMIN_CLIENT = _get_config('KEYCLOAK_ADMIN_CLIENT') - KEYCLOAK_ADMIN_SECRET = _get_config('KEYCLOAK_ADMIN_SECRET') - SQLALCHEMY_DATABASE_URI = f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' - SQLALCHEMY_TRACK_MODIFICATIONS = False - CACHE_TYPE = constants.NULL_CACHE_TYPE - CACHE_DEFAULT_TIMEOUT = constants.CACHE_DEFAULT_TIMEOUT +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""All of the configuration for the service is captured here. + +All items are loaded, +or have Constants defined here that are loaded into the Flask configuration. +All modules and lookups get their configuration from the Flask config, +rather than reading environment variables directly or by accessing this configuration directly. +""" + +import os +import sys + +from dotenv import find_dotenv, load_dotenv +from api.utils import constants + + +# this will load all the envars from a .env file located in the project root (api) +load_dotenv(find_dotenv()) + +CONFIGURATION = { + 'development': 'api.config.DevConfig', + 'testing': 'api.config.TestConfig', + 'production': 'api.config.ProdConfig', + 'default': 'api.config.ProdConfig', + 'migration': 'api.config.MigrationConfig', +} + + +def get_named_config(config_name: str = 'production'): + """Return the configuration object based on the name. + + :raise: KeyError: if an unknown configuration is requested + """ + if config_name in ['production', 'staging', 'default']: + config = ProdConfig() + elif config_name == 'testing': + config = TestConfig() + elif config_name == 'development': + config = DevConfig() + elif config_name == 'migration': + config = MigrationConfig() + else: + raise KeyError(f"Unknown configuration '{config_name}'") + return config + + +def _get_config(config_key: str, **kwargs): + """Get the config from environment, and throw error if there are no default values and if the value is None.""" + if 'default' in kwargs: + value = os.getenv(config_key, kwargs.get('default')) + else: + value = os.getenv(config_key) + # assert value TODO Un-comment once we find a solution to run pre-hook without initializing app + return value + + +class _Config(): # pylint: disable=too-few-public-methods + """Base class configuration that should set reasonable defaults for all the other configurations.""" + + PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) + + SECRET_KEY = 'a secret' + + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # added to skip the event logic for the purpose of entering historical data without + # much hurdles + SKIP_EVENT_LOGIC = _get_config('SKIP_EVENT_LOGIC', default=False) == 'True' + + ALEMBIC_INI = 'migrations/alembic.ini' + + # POSTGRESQL + DB_USER = _get_config('DATABASE_USERNAME') + DB_PASSWORD = _get_config('DATABASE_PASSWORD') + DB_NAME = _get_config('DATABASE_NAME') + DB_HOST = _get_config('DATABASE_HOST') + DB_PORT = _get_config('DATABASE_PORT', default='5432') + SQLALCHEMY_DATABASE_URI = f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + SQLALCHEMY_ECHO = _get_config('SQLALCHEMY_ECHO', default='False').lower() == 'true' + + # JWT_OIDC Settings + JWT_OIDC_WELL_KNOWN_CONFIG = _get_config('JWT_OIDC_WELL_KNOWN_CONFIG') + JWT_OIDC_ALGORITHMS = _get_config('JWT_OIDC_ALGORITHMS') + JWT_OIDC_JWKS_URI = _get_config('JWT_OIDC_JWKS_URI', default=None) + JWT_OIDC_ISSUER = _get_config('JWT_OIDC_ISSUER') + JWT_OIDC_AUDIENCE = _get_config('JWT_OIDC_AUDIENCE') + JWT_OIDC_CLIENT_SECRET = _get_config('JWT_OIDC_CLIENT_SECRET', default=None) + JWT_OIDC_CACHING_ENABLED = _get_config('JWT_OIDC_CACHING_ENABLED', default=False) + JWT_OIDC_JWKS_CACHE_TIMEOUT = int(_get_config('JWT_OIDC_JWKS_CACHE_TIMEOUT', default=300)) + + KEYCLOAK_BASE_URL = _get_config('KEYCLOAK_BASE_URL') + KEYCLOAK_REALM_NAME = _get_config('KEYCLOAK_REALM_NAME') + KEYCLOAK_ADMIN_CLIENT = _get_config('KEYCLOAK_ADMIN_CLIENT') + KEYCLOAK_ADMIN_SECRET = _get_config('KEYCLOAK_ADMIN_SECRET') + CONNECT_TIMEOUT = _get_config('CONNECT_TIMEOUT', default=60) + + TESTING = False + DEBUG = True + + CACHE_TYPE = constants.CACHE_TYPE + CACHE_DEFAULT_TIMEOUT = constants.CACHE_DEFAULT_TIMEOUT + + MIN_WORK_START_DATE = _get_config('MIN_WORK_START_DATE', default='1995-06-30') + + +class DevConfig(_Config): # pylint: disable=too-few-public-methods + """Dev config.""" + + TESTING = False + DEBUG = True + + +class TestConfig(_Config): # pylint: disable=too-few-public-methods + """In support of testing only used by the py.test suite.""" + + DEBUG = True + TESTING = True + + # POSTGRESQL + DB_USER = _get_config('DATABASE_TEST_USERNAME', default='postgres') + DB_PASSWORD = _get_config('DATABASE_TEST_PASSWORD', default='postgres') + DB_NAME = _get_config('DATABASE_TEST_NAME', default='testdb') + DB_HOST = _get_config('DATABASE_TEST_HOST', default='localhost') + DB_PORT = _get_config('DATABASE_TEST_PORT', default='5432') + SQLALCHEMY_DATABASE_URI = _get_config( + 'DATABASE_TEST_URL', + default=f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + ) + + JWT_OIDC_TEST_MODE = True + # JWT_OIDC_ISSUER = _get_config('JWT_OIDC_TEST_ISSUER') + JWT_OIDC_TEST_AUDIENCE = _get_config('JWT_OIDC_TEST_AUDIENCE') + JWT_OIDC_TEST_CLIENT_SECRET = _get_config('JWT_OIDC_TEST_CLIENT_SECRET') + JWT_OIDC_TEST_ISSUER = _get_config('JWT_OIDC_TEST_ISSUER') + JWT_OIDC_WELL_KNOWN_CONFIG = _get_config('JWT_OIDC_WELL_KNOWN_CONFIG') + JWT_OIDC_TEST_ALGORITHMS = _get_config('JWT_OIDC_TEST_ALGORITHMS') + JWT_OIDC_TEST_JWKS_URI = _get_config('JWT_OIDC_TEST_JWKS_URI', default=None) + + JWT_OIDC_TEST_KEYS = { + 'keys': [ + { + 'kid': 'epictrack', + 'kty': 'RSA', + 'alg': 'RS256', + 'use': 'sig', + 'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-' + 'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR', + 'e': 'AQAB' + } + ] + } + + JWT_OIDC_TEST_PRIVATE_KEY_JWKS = { + 'keys': [ + { + 'kid': 'forms-flow-ai', + 'kty': 'RSA', + 'alg': 'RS256', + 'use': 'sig', + 'n': 'AN-fWcpCyE5KPzHDjigLaSUVZI0uYrcGcc40InVtl-rQRDmAh-C2W8H4_Hxhr5VLc6crsJ2LiJTV_E72S03pzpOOaaYV6-' + 'TzAjCou2GYJIXev7f6Hh512PuG5wyxda_TlBSsI-gvphRTPsKCnPutrbiukCYrnPuWxX5_cES9eStR', + 'e': 'AQAB', + 'd': 'C0G3QGI6OQ6tvbCNYGCqq043YI_8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhskURaDwk4-' + '8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh_' + 'xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0', + 'p': 'APXcusFMQNHjh6KVD_hOUIw87lvK13WkDEeeuqAydai9Ig9JKEAAfV94W6Aftka7tGgE7ulg1vo3eJoLWJ1zvKM', + 'q': 'AOjX3OnPJnk0ZFUQBwhduCweRi37I6DAdLTnhDvcPTrrNWuKPg9uGwHjzFCJgKd8KBaDQ0X1rZTZLTqi3peT43s', + 'dp': 'AN9kBoA5o6_Rl9zeqdsIdWFmv4DB5lEqlEnC7HlAP-3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhc', + 'dq': 'ANtbSY6njfpPploQsF9sU26U0s7MsuLljM1E8uml8bVJE1mNsiu9MgpUvg39jEu9BtM2tDD7Y51AAIEmIQex1nM', + 'qi': 'XLE5O360x-MhsdFXx8Vwz4304-MJg-oGSJXCK_ZWYOB_FGXFRTfebxCsSYi0YwJo-oNu96bvZCuMplzRI1liZw' + } + ] + } + + JWT_OIDC_TEST_PRIVATE_KEY_PEM = """-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDfn1nKQshOSj8xw44oC2klFWSNLmK3BnHONCJ1bZfq0EQ5gIfg +tlvB+Px8Ya+VS3OnK7Cdi4iU1fxO9ktN6c6TjmmmFevk8wIwqLthmCSF3r+3+h4e +ddj7hucMsXWv05QUrCPoL6YUUz7Cgpz7ra24rpAmK5z7lsV+f3BEvXkrUQIDAQAB +AoGAC0G3QGI6OQ6tvbCNYGCqq043YI/8MiBl7C5dqbGZmx1ewdJBhMNJPStuckhs +kURaDwk4+8VBW9SlvcfSJJrnZhgFMjOYSSsBtPGBIMIdM5eSKbenCCjO8Tg0BUh/ +xa3CHST1W4RQ5rFXadZ9AeNtaGcWj2acmXNO3DVETXAX3x0CQQD13LrBTEDR44ei +lQ/4TlCMPO5bytd1pAxHnrqgMnWovSIPSShAAH1feFugH7ZGu7RoBO7pYNb6N3ia +C1idc7yjAkEA6Nfc6c8meTRkVRAHCF24LB5GLfsjoMB0tOeEO9w9Ous1a4o+D24b +AePMUImAp3woFoNDRfWtlNktOqLel5PjewJBAN9kBoA5o6/Rl9zeqdsIdWFmv4DB +5lEqlEnC7HlAP+3oo3jWFO9KQqArQL1V8w2D4aCd0uJULiC9pCP7aTHvBhcCQQDb +W0mOp436T6ZaELBfbFNulNLOzLLi5YzNRPLppfG1SRNZjbIrvTIKVL4N/YxLvQbT +NrQw+2OdQACBJiEHsdZzAkBcsTk7frTH4yGx0VfHxXDPjfTj4wmD6gZIlcIr9lZg +4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn +-----END RSA PRIVATE KEY-----""" + CACHE_TYPE = constants.NULL_CACHE_TYPE + + KEYCLOAK_BASE_URL = _get_config('KEYCLOAK_BASE_URL') + KEYCLOAK_REALM_NAME = _get_config('KEYCLOAK_REALM_NAME') + KEYCLOAK_ADMIN_CLIENT = _get_config('KEYCLOAK_ADMIN_CLIENT') + KEYCLOAK_ADMIN_SECRET = _get_config('KEYCLOAK_ADMIN_SECRET') + + +class ProdConfig(_Config): # pylint: disable=too-few-public-methods + """Production environment configuration.""" + + SECRET_KEY = _get_config('SECRET_KEY', default=None) + + if not SECRET_KEY: + SECRET_KEY = os.urandom(24) + print('WARNING: SECRET_KEY being set as a one-shot', file=sys.stderr) + + TESTING = False + DEBUG = False + + +class MigrationConfig(_Config): # pylint: disable=too-few-public-methods + """Config for db migration.""" + + TESTING = False + DEBUG = True + + # POSTGRESQL + DB_USER = _get_config('DATABASE_USERNAME') + DB_PASSWORD = _get_config('DATABASE_PASSWORD') + DB_NAME = _get_config('DATABASE_NAME') + DB_HOST = _get_config('DATABASE_HOST') + DB_PORT = _get_config('DATABASE_PORT', default='5432') + KEYCLOAK_BASE_URL = _get_config('KEYCLOAK_BASE_URL') + KEYCLOAK_REALM_NAME = _get_config('KEYCLOAK_REALM_NAME') + KEYCLOAK_ADMIN_CLIENT = _get_config('KEYCLOAK_ADMIN_CLIENT') + KEYCLOAK_ADMIN_SECRET = _get_config('KEYCLOAK_ADMIN_SECRET') + SQLALCHEMY_DATABASE_URI = f'postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{int(DB_PORT)}/{DB_NAME}' + SQLALCHEMY_TRACK_MODIFICATIONS = False + CACHE_TYPE = constants.NULL_CACHE_TYPE + CACHE_DEFAULT_TIMEOUT = constants.CACHE_DEFAULT_TIMEOUT diff --git a/epictrack-api/src/api/models/act_section.py b/epictrack-api/src/api/models/act_section.py index 2ebb1646a..9175d80fa 100644 --- a/epictrack-api/src/api/models/act_section.py +++ b/epictrack-api/src/api/models/act_section.py @@ -1,33 +1,33 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to act sections.""" - -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship - -from .code_table import CodeTableVersioned -from .db import db - - -class ActSection(db.Model, CodeTableVersioned): - """Model class for ActSection.""" - - __tablename__ = 'act_sections' - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String()) - ea_act_id = Column(ForeignKey('ea_acts.id'), nullable=False) - sort_order = Column(Integer, nullable=False) - - ea_act = relationship('EAAct', foreign_keys=[ea_act_id], lazy='select') +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to act sections.""" + +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .code_table import CodeTableVersioned +from .db import db + + +class ActSection(db.Model, CodeTableVersioned): + """Model class for ActSection.""" + + __tablename__ = 'act_sections' + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String()) + ea_act_id = Column(ForeignKey('ea_acts.id'), nullable=False) + sort_order = Column(Integer, nullable=False) + + ea_act = relationship('EAAct', foreign_keys=[ea_act_id], lazy='select') diff --git a/epictrack-api/src/api/models/action.py b/epictrack-api/src/api/models/action.py index dea24626a..2242a5293 100644 --- a/epictrack-api/src/api/models/action.py +++ b/epictrack-api/src/api/models/action.py @@ -1,48 +1,48 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Event Types.""" -import enum - -import sqlalchemy as sa - -from .code_table import CodeTableVersioned -from .db import db - - -class ActionEnum(enum.Enum): - """Action enum""" - - SET_EVENT_DATE = 1 - ADD_EVENT = 2 - SET_PHASES_STATUS = 3 - SET_EVENTS_STATUS = 4 - SET_WORK_STATE = 5 - SET_PROJECT_STATUS = 6 - LOCK_WORK_START_DATE = 7 - SET_WORK_DECISION_MAKER = 8 - ADD_PHASE = 9 - CREATE_WORK = 10 - CHANGE_PHASE_END_EVENT = 11 - SET_FEDERAL_INVOLVEMENT = 12 - SET_PROJECT_STATE = 13 - - -class Action(db.Model, CodeTableVersioned): - """Model class for Actions.""" - - __tablename__ = 'actions' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - description = sa.Column(sa.String) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Event Types.""" +import enum + +import sqlalchemy as sa + +from .code_table import CodeTableVersioned +from .db import db + + +class ActionEnum(enum.Enum): + """Action enum""" + + SET_EVENT_DATE = 1 + ADD_EVENT = 2 + SET_PHASES_STATUS = 3 + SET_EVENTS_STATUS = 4 + SET_WORK_STATE = 5 + SET_PROJECT_STATUS = 6 + LOCK_WORK_START_DATE = 7 + SET_WORK_DECISION_MAKER = 8 + ADD_PHASE = 9 + CREATE_WORK = 10 + CHANGE_PHASE_END_EVENT = 11 + SET_FEDERAL_INVOLVEMENT = 12 + SET_PROJECT_STATE = 13 + + +class Action(db.Model, CodeTableVersioned): + """Model class for Actions.""" + + __tablename__ = 'actions' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + description = sa.Column(sa.String) diff --git a/epictrack-api/src/api/models/action_configuration.py b/epictrack-api/src/api/models/action_configuration.py index 05e7d8cbc..521e066bb 100644 --- a/epictrack-api/src/api/models/action_configuration.py +++ b/epictrack-api/src/api/models/action_configuration.py @@ -1,48 +1,48 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Actions.""" -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned, db - - -class ActionConfiguration(BaseModelVersioned): - """Model class for Outcome.""" - - __tablename__ = 'action_configurations' - - id = Column(Integer, primary_key=True, autoincrement=True) - outcome_configuration_id = Column(ForeignKey('outcome_configurations.id'), nullable=False) - action_id = Column(ForeignKey('actions.id'), nullable=False) - action_template_id = Column(ForeignKey('action_templates.id'), nullable=True) - additional_params = Column(JSONB) - description = Column(String, nullable=True) - sort_order = Column(Integer, nullable=False) - - outcome_configuration = relationship('OutcomeConfiguration', foreign_keys=[outcome_configuration_id], lazy='select') - action = relationship('Action', foreign_keys=[action_id], lazy='select') - action_template = relationship('ActionTemplate', foreign_keys=[action_template_id]) - - @classmethod - def find_by_outcome_ids(cls, outcome_ids): - """Returns the event configurations based on phase ids""" - actions = db.session.query( - ActionConfiguration - ).filter( - ActionConfiguration.outcome_configuration_id.in_(outcome_ids), - ActionConfiguration.is_active.is_(True) - ).all() # pylint: disable=no-member - return actions +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Actions.""" +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned, db + + +class ActionConfiguration(BaseModelVersioned): + """Model class for Outcome.""" + + __tablename__ = 'action_configurations' + + id = Column(Integer, primary_key=True, autoincrement=True) + outcome_configuration_id = Column(ForeignKey('outcome_configurations.id'), nullable=False) + action_id = Column(ForeignKey('actions.id'), nullable=False) + action_template_id = Column(ForeignKey('action_templates.id'), nullable=True) + additional_params = Column(JSONB) + description = Column(String, nullable=True) + sort_order = Column(Integer, nullable=False) + + outcome_configuration = relationship('OutcomeConfiguration', foreign_keys=[outcome_configuration_id], lazy='select') + action = relationship('Action', foreign_keys=[action_id], lazy='select') + action_template = relationship('ActionTemplate', foreign_keys=[action_template_id]) + + @classmethod + def find_by_outcome_ids(cls, outcome_ids): + """Returns the event configurations based on phase ids""" + actions = db.session.query( + ActionConfiguration + ).filter( + ActionConfiguration.outcome_configuration_id.in_(outcome_ids), + ActionConfiguration.is_active.is_(True) + ).all() # pylint: disable=no-member + return actions diff --git a/epictrack-api/src/api/models/calendar_event.py b/epictrack-api/src/api/models/calendar_event.py index eb4e7de3f..2206f19f6 100644 --- a/epictrack-api/src/api/models/calendar_event.py +++ b/epictrack-api/src/api/models/calendar_event.py @@ -1,30 +1,30 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Calendar Events.""" - -import sqlalchemy as sa -from .base_model import BaseModelVersioned - - -class CalendarEvent(BaseModelVersioned): - """Model class for Event Types.""" - - __tablename__ = 'calendar_events' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - anticipated_date = sa.Column(sa.DateTime(timezone=True), nullable=False) - actual_date = sa.Column(sa.DateTime(timezone=True)) - number_of_days = sa.Column(sa.Integer) - link = sa.Column(sa.String) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Calendar Events.""" + +import sqlalchemy as sa +from .base_model import BaseModelVersioned + + +class CalendarEvent(BaseModelVersioned): + """Model class for Event Types.""" + + __tablename__ = 'calendar_events' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + anticipated_date = sa.Column(sa.DateTime(timezone=True), nullable=False) + actual_date = sa.Column(sa.DateTime(timezone=True)) + number_of_days = sa.Column(sa.Integer) + link = sa.Column(sa.String) diff --git a/epictrack-api/src/api/models/dashboard_seach_options.py b/epictrack-api/src/api/models/dashboard_seach_options.py index 424837929..cc14c23d6 100644 --- a/epictrack-api/src/api/models/dashboard_seach_options.py +++ b/epictrack-api/src/api/models/dashboard_seach_options.py @@ -1,18 +1,18 @@ -"""This module holds data classes.""" - -from typing import List, Optional - -from attr import dataclass - - -@dataclass -class WorkplanDashboardSearchOptions: # pylint: disable=too-many-instance-attributes - """Used to store work dashboard search options.""" - - text: str - teams: Optional[List[int]] - work_states: Optional[List[str]] - regions: Optional[List[int]] - project_types: Optional[List[int]] - work_types: Optional[List[int]] - staff_id: Optional[int] +"""This module holds data classes.""" + +from typing import List, Optional + +from attr import dataclass + + +@dataclass +class WorkplanDashboardSearchOptions: # pylint: disable=too-many-instance-attributes + """Used to store work dashboard search options.""" + + text: str + teams: Optional[List[int]] + work_states: Optional[List[str]] + regions: Optional[List[int]] + project_types: Optional[List[int]] + work_types: Optional[List[int]] + staff_id: Optional[int] diff --git a/epictrack-api/src/api/models/event_category.py b/epictrack-api/src/api/models/event_category.py index 01ef31992..444105f9e 100644 --- a/epictrack-api/src/api/models/event_category.py +++ b/epictrack-api/src/api/models/event_category.py @@ -1,50 +1,50 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Event Categories.""" - -import enum -import sqlalchemy as sa -from .base_model import BaseModelVersioned - - -class EventCategoryEnum(enum.Enum): - """Enum for EventCategory""" - - # pylint: disable=C0103 - MILESTONE = 1 - EXTENSION = 2 - SUSPENSION = 3 - DECISION = 4 - PCP = 5 - CALENDAR = 6 - FINANCE = 7 - SPECIAL_EXTENSION = 8 - - -class EventCategory(BaseModelVersioned): - """Model class for Event Categories.""" - - __tablename__ = 'event_categories' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String, nullable=False) - sort_order = sa.Column(sa.Integer, nullable=False) - - -PRIMARY_CATEGORIES = [EventCategoryEnum.MILESTONE, - EventCategoryEnum.DECISION, - EventCategoryEnum.EXTENSION, - EventCategoryEnum.SUSPENSION, - EventCategoryEnum.PCP, - EventCategoryEnum.FINANCE] +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Event Categories.""" + +import enum +import sqlalchemy as sa +from .base_model import BaseModelVersioned + + +class EventCategoryEnum(enum.Enum): + """Enum for EventCategory""" + + # pylint: disable=C0103 + MILESTONE = 1 + EXTENSION = 2 + SUSPENSION = 3 + DECISION = 4 + PCP = 5 + CALENDAR = 6 + FINANCE = 7 + SPECIAL_EXTENSION = 8 + + +class EventCategory(BaseModelVersioned): + """Model class for Event Categories.""" + + __tablename__ = 'event_categories' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String, nullable=False) + sort_order = sa.Column(sa.Integer, nullable=False) + + +PRIMARY_CATEGORIES = [EventCategoryEnum.MILESTONE, + EventCategoryEnum.DECISION, + EventCategoryEnum.EXTENSION, + EventCategoryEnum.SUSPENSION, + EventCategoryEnum.PCP, + EventCategoryEnum.FINANCE] diff --git a/epictrack-api/src/api/models/event_configuration.py b/epictrack-api/src/api/models/event_configuration.py index c9feafd07..a2fb8eece 100644 --- a/epictrack-api/src/api/models/event_configuration.py +++ b/epictrack-api/src/api/models/event_configuration.py @@ -1,61 +1,61 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Event Template.""" - -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from api.models.event_template import EventPositionEnum, EventTemplateVisibilityEnum - -from .base_model import BaseModelVersioned -from .db import db - - -class EventConfiguration(BaseModelVersioned): - """Model class for Event Template.""" - - __tablename__ = 'event_configurations' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - parent_id = sa.Column(sa.Integer, nullable=True) - template_id = sa.Column(sa.ForeignKey('event_templates.id'), nullable=False) - event_type_id = sa.Column(sa.ForeignKey('event_types.id'), nullable=False) - event_position = sa.Column(sa.Enum(EventPositionEnum)) - multiple_days = sa.Column(sa.Boolean, default=False) - event_category_id = sa.Column(sa.ForeignKey('event_categories.id'), nullable=False) - start_at = sa.Column(sa.String, nullable=True) - number_of_days = sa.Column(sa.Integer, default=0, nullable=False) - sort_order = sa.Column(sa.Integer, nullable=False) - work_phase_id = sa.Column(sa.ForeignKey('work_phases.id'), nullable=True) - visibility = sa.Column(sa.Enum(EventTemplateVisibilityEnum), nullable=False) - repeat_count = sa.Column(sa.Integer, nullable=False, default=0) - - event_type = relationship('EventType', foreign_keys=[event_type_id], lazy='select') - event_category = relationship('EventCategory', foreign_keys=[event_category_id], lazy='select') - event_template = relationship('EventTemplate', foreign_keys=[template_id], lazy='select') - work_phase = relationship('WorkPhase', foreign_keys=[work_phase_id], lazy='select') - - @classmethod - def find_by_work_phase_id(cls, _work_phase_id): - """Returns the event configurations based on phase id""" - events = db.session.query( - EventConfiguration - ).filter_by( - work_phase_id=_work_phase_id, - is_active=True - ).order_by( - EventConfiguration.sort_order.asc() - ).all() # pylint: disable=no-member - return events +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Event Template.""" + +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from api.models.event_template import EventPositionEnum, EventTemplateVisibilityEnum + +from .base_model import BaseModelVersioned +from .db import db + + +class EventConfiguration(BaseModelVersioned): + """Model class for Event Template.""" + + __tablename__ = 'event_configurations' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + parent_id = sa.Column(sa.Integer, nullable=True) + template_id = sa.Column(sa.ForeignKey('event_templates.id'), nullable=False) + event_type_id = sa.Column(sa.ForeignKey('event_types.id'), nullable=False) + event_position = sa.Column(sa.Enum(EventPositionEnum)) + multiple_days = sa.Column(sa.Boolean, default=False) + event_category_id = sa.Column(sa.ForeignKey('event_categories.id'), nullable=False) + start_at = sa.Column(sa.String, nullable=True) + number_of_days = sa.Column(sa.Integer, default=0, nullable=False) + sort_order = sa.Column(sa.Integer, nullable=False) + work_phase_id = sa.Column(sa.ForeignKey('work_phases.id'), nullable=True) + visibility = sa.Column(sa.Enum(EventTemplateVisibilityEnum), nullable=False) + repeat_count = sa.Column(sa.Integer, nullable=False, default=0) + + event_type = relationship('EventType', foreign_keys=[event_type_id], lazy='select') + event_category = relationship('EventCategory', foreign_keys=[event_category_id], lazy='select') + event_template = relationship('EventTemplate', foreign_keys=[template_id], lazy='select') + work_phase = relationship('WorkPhase', foreign_keys=[work_phase_id], lazy='select') + + @classmethod + def find_by_work_phase_id(cls, _work_phase_id): + """Returns the event configurations based on phase id""" + events = db.session.query( + EventConfiguration + ).filter_by( + work_phase_id=_work_phase_id, + is_active=True + ).order_by( + EventConfiguration.sort_order.asc() + ).all() # pylint: disable=no-member + return events diff --git a/epictrack-api/src/api/models/event_template.py b/epictrack-api/src/api/models/event_template.py index 5f2c5c330..2c3ba3ed5 100644 --- a/epictrack-api/src/api/models/event_template.py +++ b/epictrack-api/src/api/models/event_template.py @@ -1,94 +1,94 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Event Template.""" -import enum -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned -from .db import db - - -class EventPositionEnum(enum.Enum): - """Event position enum""" - - START = "START" - INTERMEDIATE = "INTERMEDIATE" - END = "END" - - -class EventTemplateVisibilityEnum(enum.Enum): - """Decide whether to show the events using this template or not""" - - MANDATORY = "MANDATORY" - OPTIONAL = "OPTIONAL" - HIDDEN = "HIDDEN" - SUGGESTED = "SUGGESTED" - - -class EventTemplate(BaseModelVersioned): - """Model class for Event Template.""" - - __tablename__ = "event_templates" - - id = sa.Column( - sa.Integer, primary_key=True, autoincrement=True - ) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - parent_id = sa.Column(sa.Integer, nullable=True) - phase_id = sa.Column(sa.ForeignKey("phase_codes.id"), nullable=False) - event_type_id = sa.Column(sa.ForeignKey("event_types.id"), nullable=False) - event_position = sa.Column(sa.Enum(EventPositionEnum)) - multiple_days = sa.Column(sa.Boolean, default=False) - event_category_id = sa.Column(sa.ForeignKey("event_categories.id"), nullable=False) - start_at = sa.Column(sa.String, nullable=True) - number_of_days = sa.Column(sa.Integer, default=0, nullable=False) - sort_order = sa.Column(sa.Integer, nullable=False) - visibility = sa.Column( - sa.Enum(EventTemplateVisibilityEnum), - nullable=False, - comment="Indicate whether the event generated with this template should be\ - autogenerated or available for optional events or added in the back end using actions", - ) - - phase = relationship("PhaseCode", foreign_keys=[phase_id], lazy="select") - event_type = relationship("EventType", foreign_keys=[event_type_id], lazy="select") - event_category = relationship( - "EventCategory", foreign_keys=[event_category_id], lazy="select" - ) - - @classmethod - def find_by_phase_id(cls, _phase_id): - """Returns the event configurations based on phase id""" - events = ( - db.session.query(EventTemplate) - .filter_by(phase_id=_phase_id, is_active=True) - .order_by(EventTemplate.sort_order.asc()) - .all() - ) # pylint: disable=no-member - return events - - @classmethod - def find_by_phase_ids(cls, _phase_ids): - """Returns the event configurations based on phase ids""" - events = ( - db.session.query(EventTemplate) - .filter( - EventTemplate.phase_id.in_(_phase_ids), - EventTemplate.is_active.is_(True), - ) - .order_by(EventTemplate.sort_order.asc()) - .all() - ) # pylint: disable=no-member - return events +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Event Template.""" +import enum +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned +from .db import db + + +class EventPositionEnum(enum.Enum): + """Event position enum""" + + START = "START" + INTERMEDIATE = "INTERMEDIATE" + END = "END" + + +class EventTemplateVisibilityEnum(enum.Enum): + """Decide whether to show the events using this template or not""" + + MANDATORY = "MANDATORY" + OPTIONAL = "OPTIONAL" + HIDDEN = "HIDDEN" + SUGGESTED = "SUGGESTED" + + +class EventTemplate(BaseModelVersioned): + """Model class for Event Template.""" + + __tablename__ = "event_templates" + + id = sa.Column( + sa.Integer, primary_key=True, autoincrement=True + ) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + parent_id = sa.Column(sa.Integer, nullable=True) + phase_id = sa.Column(sa.ForeignKey("phase_codes.id"), nullable=False) + event_type_id = sa.Column(sa.ForeignKey("event_types.id"), nullable=False) + event_position = sa.Column(sa.Enum(EventPositionEnum)) + multiple_days = sa.Column(sa.Boolean, default=False) + event_category_id = sa.Column(sa.ForeignKey("event_categories.id"), nullable=False) + start_at = sa.Column(sa.String, nullable=True) + number_of_days = sa.Column(sa.Integer, default=0, nullable=False) + sort_order = sa.Column(sa.Integer, nullable=False) + visibility = sa.Column( + sa.Enum(EventTemplateVisibilityEnum), + nullable=False, + comment="Indicate whether the event generated with this template should be\ + autogenerated or available for optional events or added in the back end using actions", + ) + + phase = relationship("PhaseCode", foreign_keys=[phase_id], lazy="select") + event_type = relationship("EventType", foreign_keys=[event_type_id], lazy="select") + event_category = relationship( + "EventCategory", foreign_keys=[event_category_id], lazy="select" + ) + + @classmethod + def find_by_phase_id(cls, _phase_id): + """Returns the event configurations based on phase id""" + events = ( + db.session.query(EventTemplate) + .filter_by(phase_id=_phase_id, is_active=True) + .order_by(EventTemplate.sort_order.asc()) + .all() + ) # pylint: disable=no-member + return events + + @classmethod + def find_by_phase_ids(cls, _phase_ids): + """Returns the event configurations based on phase ids""" + events = ( + db.session.query(EventTemplate) + .filter( + EventTemplate.phase_id.in_(_phase_ids), + EventTemplate.is_active.is_(True), + ) + .order_by(EventTemplate.sort_order.asc()) + .all() + ) # pylint: disable=no-member + return events diff --git a/epictrack-api/src/api/models/event_type.py b/epictrack-api/src/api/models/event_type.py index 4bafd9bba..fe3504eb5 100644 --- a/epictrack-api/src/api/models/event_type.py +++ b/epictrack-api/src/api/models/event_type.py @@ -1,49 +1,49 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Event Types.""" - -import enum - -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -class EventTypeEnum(enum.Enum): - """Enum for event type""" - - # pylint: disable=C0103 - TIME_LIMIT_SUSPENSION = 12 - TIME_LIMIT_RESUMPTION = 38 - REFERRAL = 5 - MINISTER_DECISION = 14 - CEAO_DECISION = 15 - - -class EventType(BaseModelVersioned): - """Model class for Event Types.""" - - __tablename__ = "event_types" - - id = sa.Column( - sa.Integer, primary_key=True, autoincrement=True - ) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - event_category_id = sa.Column(sa.ForeignKey("event_categories.id"), nullable=False) - sort_order = sa.Column(sa.Integer, nullable=False) - - event_category = relationship( - "EventCategory", foreign_keys=[event_category_id], lazy="select" - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Event Types.""" + +import enum + +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +class EventTypeEnum(enum.Enum): + """Enum for event type""" + + # pylint: disable=C0103 + TIME_LIMIT_SUSPENSION = 12 + TIME_LIMIT_RESUMPTION = 38 + REFERRAL = 5 + MINISTER_DECISION = 14 + CEAO_DECISION = 15 + + +class EventType(BaseModelVersioned): + """Model class for Event Types.""" + + __tablename__ = "event_types" + + id = sa.Column( + sa.Integer, primary_key=True, autoincrement=True + ) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + event_category_id = sa.Column(sa.ForeignKey("event_categories.id"), nullable=False) + sort_order = sa.Column(sa.Integer, nullable=False) + + event_category = relationship( + "EventCategory", foreign_keys=[event_category_id], lazy="select" + ) diff --git a/epictrack-api/src/api/models/outcome_configuration.py b/epictrack-api/src/api/models/outcome_configuration.py index a283d9186..4413d9247 100644 --- a/epictrack-api/src/api/models/outcome_configuration.py +++ b/epictrack-api/src/api/models/outcome_configuration.py @@ -1,45 +1,45 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Outcome Configuration.""" - -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned, db - - -class OutcomeConfiguration(BaseModelVersioned): - """Model class for Outcome.""" - - __tablename__ = 'outcome_configurations' - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String, nullable=False) - event_configuration_id = Column(ForeignKey('event_configurations.id'), nullable=False) - outcome_template_id = Column(ForeignKey('outcome_templates.id'), nullable=True) - sort_order = Column(Integer, nullable=False) - - event_configuration = relationship('EventConfiguration', foreign_keys=[event_configuration_id], lazy='select') - outcome_template = relationship('OutcomeTemplate', foreign_keys=[outcome_template_id]) - - @classmethod - def find_by_configuration_ids(cls, configuration_ids): - """Returns the event configurations based on phase ids""" - outcomes = db.session.query( - OutcomeConfiguration - ).filter( - OutcomeConfiguration.event_template_id.in_(configuration_ids), - OutcomeConfiguration.is_active.is_(True) - ).all() # pylint: disable=no-member - return outcomes +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Outcome Configuration.""" + +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned, db + + +class OutcomeConfiguration(BaseModelVersioned): + """Model class for Outcome.""" + + __tablename__ = 'outcome_configurations' + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, nullable=False) + event_configuration_id = Column(ForeignKey('event_configurations.id'), nullable=False) + outcome_template_id = Column(ForeignKey('outcome_templates.id'), nullable=True) + sort_order = Column(Integer, nullable=False) + + event_configuration = relationship('EventConfiguration', foreign_keys=[event_configuration_id], lazy='select') + outcome_template = relationship('OutcomeTemplate', foreign_keys=[outcome_template_id]) + + @classmethod + def find_by_configuration_ids(cls, configuration_ids): + """Returns the event configurations based on phase ids""" + outcomes = db.session.query( + OutcomeConfiguration + ).filter( + OutcomeConfiguration.event_template_id.in_(configuration_ids), + OutcomeConfiguration.is_active.is_(True) + ).all() # pylint: disable=no-member + return outcomes diff --git a/epictrack-api/src/api/models/phase_code.py b/epictrack-api/src/api/models/phase_code.py index cc17d595a..e4dfe9b76 100644 --- a/epictrack-api/src/api/models/phase_code.py +++ b/epictrack-api/src/api/models/phase_code.py @@ -1,73 +1,73 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Payment Disbursement status code.""" -import enum - -from sqlalchemy import Boolean, Column, Enum, ForeignKey, Integer, String -from sqlalchemy.orm import relationship - -from .code_table import CodeTableVersioned -from .db import db - - -class PhaseVisibilityEnum(enum.Enum): - """Decide whether to show the phase initially or not""" - - REGULAR = "REGULAR" - HIDDEN = "HIDDEN" - - -class PhaseCode(db.Model, CodeTableVersioned): - """Model class for Phase.""" - - __tablename__ = "phase_codes" - - id = Column( - Integer, primary_key=True, autoincrement=True - ) # TODO check how it can be inherited from parent - name = Column(String(250)) - work_type_id = Column(ForeignKey("work_types.id"), nullable=False) - ea_act_id = Column(ForeignKey("ea_acts.id"), nullable=False) - number_of_days = Column(Integer(), default=0) - legislated = Column(Boolean()) - sort_order = Column(Integer()) - color = Column(String(15)) - visibility = Column(Enum(PhaseVisibilityEnum), default=PhaseVisibilityEnum.REGULAR) - - work_type = relationship("WorkType", foreign_keys=[work_type_id], lazy="select") - ea_act = relationship("EAAct", foreign_keys=[ea_act_id], lazy="select") - - def as_dict(self): - """Return Json representation.""" - return { - "id": self.id, - "name": self.name, - "sort_order": self.sort_order, - "number_of_days": self.number_of_days, - "legislated": self.legislated, - "work_type": self.work_type.as_dict(), - "ea_act": self.ea_act.as_dict(), - "color": self.color, - } - - @classmethod - def find_by_ea_act_and_work_type(cls, _ea_act_id, _work_type_id): - """Given a id, this will return code master details.""" - code_table = ( - db.session.query(PhaseCode) - .filter_by(work_type_id=_work_type_id, ea_act_id=_ea_act_id, is_active=True) - .order_by(PhaseCode.sort_order.asc()) - .all() - ) # pylint: disable=no-member - return code_table +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Payment Disbursement status code.""" +import enum + +from sqlalchemy import Boolean, Column, Enum, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .code_table import CodeTableVersioned +from .db import db + + +class PhaseVisibilityEnum(enum.Enum): + """Decide whether to show the phase initially or not""" + + REGULAR = "REGULAR" + HIDDEN = "HIDDEN" + + +class PhaseCode(db.Model, CodeTableVersioned): + """Model class for Phase.""" + + __tablename__ = "phase_codes" + + id = Column( + Integer, primary_key=True, autoincrement=True + ) # TODO check how it can be inherited from parent + name = Column(String(250)) + work_type_id = Column(ForeignKey("work_types.id"), nullable=False) + ea_act_id = Column(ForeignKey("ea_acts.id"), nullable=False) + number_of_days = Column(Integer(), default=0) + legislated = Column(Boolean()) + sort_order = Column(Integer()) + color = Column(String(15)) + visibility = Column(Enum(PhaseVisibilityEnum), default=PhaseVisibilityEnum.REGULAR) + + work_type = relationship("WorkType", foreign_keys=[work_type_id], lazy="select") + ea_act = relationship("EAAct", foreign_keys=[ea_act_id], lazy="select") + + def as_dict(self): + """Return Json representation.""" + return { + "id": self.id, + "name": self.name, + "sort_order": self.sort_order, + "number_of_days": self.number_of_days, + "legislated": self.legislated, + "work_type": self.work_type.as_dict(), + "ea_act": self.ea_act.as_dict(), + "color": self.color, + } + + @classmethod + def find_by_ea_act_and_work_type(cls, _ea_act_id, _work_type_id): + """Given a id, this will return code master details.""" + code_table = ( + db.session.query(PhaseCode) + .filter_by(work_type_id=_work_type_id, ea_act_id=_ea_act_id, is_active=True) + .order_by(PhaseCode.sort_order.asc()) + .all() + ) # pylint: disable=no-member + return code_table diff --git a/epictrack-api/src/api/models/project.py b/epictrack-api/src/api/models/project.py index aa4026d30..ff450535a 100644 --- a/epictrack-api/src/api/models/project.py +++ b/epictrack-api/src/api/models/project.py @@ -1,107 +1,107 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to manage Project.""" -import enum - -from sqlalchemy import Boolean, Column, Date, Enum, Float, ForeignKey, Integer, String, Text, func -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -class ProjectStateEnum(enum.Enum): - """Enum for project state""" - - PRE_WORK = "PRE_WORK" - UNDER_EAC_ASSESSMENT = "UNDER_EAC_ASSESSMENT" - UNDER_EXEMPTION_REQUEST = "UNDER_EXEMPTION_REQUEST" - UNDER_AMENDMENT = "UNDER_AMENDMENT" - UNDER_DISPUTE_RESOLUTION = "UNDER_DISPUTE_RESOLUTION" - PRE_CONSTRUCTION = "PRE_CONSTRUCTION" - CONSTRUCTION = "CONSTRUCTION" - OPERATION = "OPERATION" - CARE_AND_MAINTENANCE = "CARE_AND_MAINTENANCE" - DECOMMISSION = "DECOMMISSION" - UNKNOWN = "UNKNOWN" - CLOSED = "CLOSED" - UNDER_DESIGNATION = "UNDER_DESIGNATION" - - -class Project(BaseModelVersioned): - """Model class for Project.""" - - __tablename__ = "projects" - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String()) - project_tracking_number = Column(String(), nullable=True, default=None) - description = Column(String()) - latitude = Column(String(), nullable=False) - longitude = Column(String(), nullable=False) - capital_investment = Column(Float()) - epic_guid = Column(String(), nullable=True, default=None) - is_project_closed = Column(Boolean(), default=False, nullable=False) - address = Column(Text, nullable=True, default=None) - fte_positions_construction = Column(Integer(), nullable=True) - fte_positions_operation = Column(Integer(), nullable=True) - project_state = Column(Enum(ProjectStateEnum)) - - ea_certificate = Column(String(255), nullable=True, default=None) - sub_type_id = Column(ForeignKey("sub_types.id"), nullable=False) - type_id = Column(ForeignKey("types.id"), nullable=False) - proponent_id = Column(ForeignKey("proponents.id"), nullable=False) - region_id_env = Column(ForeignKey("regions.id"), nullable=True) - region_id_flnro = Column(ForeignKey("regions.id"), nullable=True) - abbreviation = Column(String(10), nullable=True, unique=True) - eac_signed = Column(Date(), nullable=True) - eac_expires = Column(Date(), nullable=True) - sub_type = relationship("SubType", foreign_keys=[sub_type_id], lazy="select") - type = relationship("Type", foreign_keys=[type_id], lazy="select") - proponent = relationship("Proponent", foreign_keys=[proponent_id], lazy="select") - region_env = relationship("Region", foreign_keys=[region_id_env], lazy="select") - region_flnro = relationship("Region", foreign_keys=[region_id_flnro], lazy="select") - works = relationship('Work', lazy='dynamic') - - @classmethod - def find_all_projects(cls, with_works=False, is_active=None): - """Return all projects with works.""" - query = cls.query - if with_works: - query = query.filter(cls.works.any()) - if is_active is not None: - query = query.filter(cls.is_active.is_(is_active)) - return query.filter(cls.is_deleted.is_(False)).all() - - @classmethod - def check_existence(cls, name, project_id=None): - """Checks if a project exists with given name""" - query = Project.query.filter( - func.lower(Project.name) == func.lower(name), Project.is_deleted.is_(False) - ) - if project_id: - query = query.filter(Project.id != project_id) - if query.count() > 0: - return True - return False - - @classmethod - def get_by_abbreviation(cls, abbreviation: str): - """Get project by abbreviation.""" - return Project.query.filter_by(abbreviation=abbreviation).first() - - def as_dict(self, recursive=True): - """Return JSON Representation.""" - data = super().as_dict(recursive) - data["project_state"] = self.project_state.value - return data +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to manage Project.""" +import enum + +from sqlalchemy import Boolean, Column, Date, Enum, Float, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +class ProjectStateEnum(enum.Enum): + """Enum for project state""" + + PRE_WORK = "PRE_WORK" + UNDER_EAC_ASSESSMENT = "UNDER_EAC_ASSESSMENT" + UNDER_EXEMPTION_REQUEST = "UNDER_EXEMPTION_REQUEST" + UNDER_AMENDMENT = "UNDER_AMENDMENT" + UNDER_DISPUTE_RESOLUTION = "UNDER_DISPUTE_RESOLUTION" + PRE_CONSTRUCTION = "PRE_CONSTRUCTION" + CONSTRUCTION = "CONSTRUCTION" + OPERATION = "OPERATION" + CARE_AND_MAINTENANCE = "CARE_AND_MAINTENANCE" + DECOMMISSION = "DECOMMISSION" + UNKNOWN = "UNKNOWN" + CLOSED = "CLOSED" + UNDER_DESIGNATION = "UNDER_DESIGNATION" + + +class Project(BaseModelVersioned): + """Model class for Project.""" + + __tablename__ = "projects" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String()) + project_tracking_number = Column(String(), nullable=True, default=None) + description = Column(String()) + latitude = Column(String(), nullable=False) + longitude = Column(String(), nullable=False) + capital_investment = Column(Float()) + epic_guid = Column(String(), nullable=True, default=None) + is_project_closed = Column(Boolean(), default=False, nullable=False) + address = Column(Text, nullable=True, default=None) + fte_positions_construction = Column(Integer(), nullable=True) + fte_positions_operation = Column(Integer(), nullable=True) + project_state = Column(Enum(ProjectStateEnum)) + + ea_certificate = Column(String(255), nullable=True, default=None) + sub_type_id = Column(ForeignKey("sub_types.id"), nullable=False) + type_id = Column(ForeignKey("types.id"), nullable=False) + proponent_id = Column(ForeignKey("proponents.id"), nullable=False) + region_id_env = Column(ForeignKey("regions.id"), nullable=True) + region_id_flnro = Column(ForeignKey("regions.id"), nullable=True) + abbreviation = Column(String(10), nullable=True, unique=True) + eac_signed = Column(Date(), nullable=True) + eac_expires = Column(Date(), nullable=True) + sub_type = relationship("SubType", foreign_keys=[sub_type_id], lazy="select") + type = relationship("Type", foreign_keys=[type_id], lazy="select") + proponent = relationship("Proponent", foreign_keys=[proponent_id], lazy="select") + region_env = relationship("Region", foreign_keys=[region_id_env], lazy="select") + region_flnro = relationship("Region", foreign_keys=[region_id_flnro], lazy="select") + works = relationship('Work', lazy='dynamic') + + @classmethod + def find_all_projects(cls, with_works=False, is_active=None): + """Return all projects with works.""" + query = cls.query + if with_works: + query = query.filter(cls.works.any()) + if is_active is not None: + query = query.filter(cls.is_active.is_(is_active)) + return query.filter(cls.is_deleted.is_(False)).all() + + @classmethod + def check_existence(cls, name, project_id=None): + """Checks if a project exists with given name""" + query = Project.query.filter( + func.lower(Project.name) == func.lower(name), Project.is_deleted.is_(False) + ) + if project_id: + query = query.filter(Project.id != project_id) + if query.count() > 0: + return True + return False + + @classmethod + def get_by_abbreviation(cls, abbreviation: str): + """Get project by abbreviation.""" + return Project.query.filter_by(abbreviation=abbreviation).first() + + def as_dict(self, recursive=True): + """Return JSON Representation.""" + data = super().as_dict(recursive) + data["project_state"] = self.project_state.value + return data diff --git a/epictrack-api/src/api/models/region.py b/epictrack-api/src/api/models/region.py index cfe846e51..0aceb1f94 100644 --- a/epictrack-api/src/api/models/region.py +++ b/epictrack-api/src/api/models/region.py @@ -1,40 +1,40 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Region.""" - -from sqlalchemy import Column, Integer, String - -from .code_table import CodeTableVersioned -from .db import db - - -class Region(db.Model, CodeTableVersioned): - """Model class for Region.""" - - __tablename__ = 'regions' - - id = Column(Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - entity = Column(String()) - sort_order = Column(Integer, nullable=False) - - @classmethod - def find_all_by_region_type(cls, region_type: str): - """Find all regions by region type.""" - return cls.query.filter_by(entity=region_type).all() - - def as_dict(self): - """Return Json representation.""" - result = CodeTableVersioned.as_dict(self) - result['entity'] = self.entity - return result +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Region.""" + +from sqlalchemy import Column, Integer, String + +from .code_table import CodeTableVersioned +from .db import db + + +class Region(db.Model, CodeTableVersioned): + """Model class for Region.""" + + __tablename__ = 'regions' + + id = Column(Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + entity = Column(String()) + sort_order = Column(Integer, nullable=False) + + @classmethod + def find_all_by_region_type(cls, region_type: str): + """Find all regions by region type.""" + return cls.query.filter_by(entity=region_type).all() + + def as_dict(self): + """Return Json representation.""" + result = CodeTableVersioned.as_dict(self) + result['entity'] = self.entity + return result diff --git a/epictrack-api/src/api/models/reminder_configuration.py b/epictrack-api/src/api/models/reminder_configuration.py index 88a4cc42f..08a4d8ed0 100644 --- a/epictrack-api/src/api/models/reminder_configuration.py +++ b/epictrack-api/src/api/models/reminder_configuration.py @@ -1,49 +1,49 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Reminder Configuration.""" - -from sqlalchemy import Column, Integer, String, Text, func - -from .base_model import BaseModelVersioned - - -class ReminderConfiguration(BaseModelVersioned): - """Model class for Engagement.""" - - __tablename__ = "reminder_configurations" - - id = Column(Integer, primary_key=True, autoincrement=True) - reminder_type = Column(String(255), nullable=False) - interval = Column(Text, nullable=False) - email_addresses = Column(Text, nullable=False) - reminder_text = Column(Text, nullable=True) - position_id = Column(Integer, nullable=False) - - def as_dict(self, recursive=False): - """Return a JSON representation""" - return super().as_dict(recursive=recursive) - - @classmethod - def check_existence(cls, reminder_type, position_id, reminder_configuration_id): - """Checks if a reminder configuration exists for given reminder type and position""" - query = ReminderConfiguration.query.filter( - func.lower(ReminderConfiguration.reminder_type) == func.lower(reminder_type), - ReminderConfiguration.position_id == position_id, - ReminderConfiguration.is_deleted.is_(False), - ) - if reminder_configuration_id: - query = query.filter(ReminderConfiguration.id != reminder_configuration_id) - if query.count() > 0: - return True - return False +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Reminder Configuration.""" + +from sqlalchemy import Column, Integer, String, Text, func + +from .base_model import BaseModelVersioned + + +class ReminderConfiguration(BaseModelVersioned): + """Model class for Engagement.""" + + __tablename__ = "reminder_configurations" + + id = Column(Integer, primary_key=True, autoincrement=True) + reminder_type = Column(String(255), nullable=False) + interval = Column(Text, nullable=False) + email_addresses = Column(Text, nullable=False) + reminder_text = Column(Text, nullable=True) + position_id = Column(Integer, nullable=False) + + def as_dict(self, recursive=False): + """Return a JSON representation""" + return super().as_dict(recursive=recursive) + + @classmethod + def check_existence(cls, reminder_type, position_id, reminder_configuration_id): + """Checks if a reminder configuration exists for given reminder type and position""" + query = ReminderConfiguration.query.filter( + func.lower(ReminderConfiguration.reminder_type) == func.lower(reminder_type), + ReminderConfiguration.position_id == position_id, + ReminderConfiguration.is_deleted.is_(False), + ) + if reminder_configuration_id: + query = query.filter(ReminderConfiguration.id != reminder_configuration_id) + if query.count() > 0: + return True + return False diff --git a/epictrack-api/src/api/models/responsibility.py b/epictrack-api/src/api/models/responsibility.py index ea8d093cd..b671b8679 100644 --- a/epictrack-api/src/api/models/responsibility.py +++ b/epictrack-api/src/api/models/responsibility.py @@ -1,27 +1,27 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Staff.""" - -import sqlalchemy as sa - -from .base_model import BaseModelVersioned - - -class Responsibility(BaseModelVersioned): - """Model class for responsibilities""" - - __tablename__ = 'responsibilities' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Staff.""" + +import sqlalchemy as sa + +from .base_model import BaseModelVersioned + + +class Responsibility(BaseModelVersioned): + """Model class for responsibilities""" + + __tablename__ = 'responsibilities' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) diff --git a/epictrack-api/src/api/models/staff.py b/epictrack-api/src/api/models/staff.py index d8173d9af..1fd9a2ffa 100644 --- a/epictrack-api/src/api/models/staff.py +++ b/epictrack-api/src/api/models/staff.py @@ -1,103 +1,103 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Staff.""" - -from typing import List - -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, asc, func, DateTime -from sqlalchemy.orm import column_property, relationship - -from api.models.base_model import BaseModelVersioned -from api.utils.utcnow import utcnow - - -class Staff(BaseModelVersioned): - """Model class for Staff.""" - - __tablename__ = "staffs" - - id = Column(Integer, primary_key=True, autoincrement=True) - idir_user_id = Column(String(100), nullable=True) - first_name = Column(String(100), nullable=False) - last_name = Column(String(100), nullable=False) - phone = Column(String(), nullable=False) - email = Column(String(), nullable=False) - is_active = Column(Boolean(), default=True, nullable=False) - position_id = Column(ForeignKey("positions.id"), nullable=False) - is_deleted = Column(Boolean(), default=False, nullable=False) - position = relationship("Position", foreign_keys=[position_id], lazy="select") - - full_name = column_property(last_name + ", " + first_name) - last_active_at = Column(DateTime(timezone=True), server_default=utcnow()) - - # Define the excluded fields from versioning - __exclude_from_tracking_history__ = {"last_active_at"} - - def as_dict(self): # pylint: disable=arguments-differ - """Return Json representation.""" - return { - "id": self.id, - "idir_user_id": self.idir_user_id, - "first_name": self.first_name, - "last_name": self.last_name, - "full_name": self.full_name, - "phone": self.phone, - "email": self.email, - "is_active": self.is_active, - "position_id": self.position_id, - "position": self.position.as_dict(), - } - - @classmethod - def find_active_staff_by_position(cls, position_id: int): - """Return active staff by position id.""" - return cls.query.filter_by(position_id=position_id, is_active=True) - - @classmethod - def find_active_staff_by_positions(cls, position_ids: List[int]): - """Return active staffs by position ids.""" - return cls.query.filter( - Staff.position_id.in_(position_ids), Staff.is_active.is_(True) - ).order_by(asc(Staff.last_name), asc(Staff.first_name)) - - @classmethod - def find_all_active_staff(cls): - """Return all active staff.""" - return cls.query.filter_by(is_active=True, is_deleted=False) - - @classmethod - def find_all_non_deleted_staff(cls, is_active=False): - """Return all non-deleted staff""" - query = {"is_deleted": False} - if is_active: - query["is_active"] = is_active - return cls.query.filter_by(**query) - - @classmethod - def check_existence(cls, email, staff_id): - """Checks if a staff exists with given email address""" - query = cls.query.filter( - func.lower(Staff.email) == func.lower(email), - Staff.is_deleted.is_(False), - ) - if staff_id: - query = query.filter(Staff.id != staff_id) - if query.count() > 0: - return True - return False - - @classmethod - def find_by_email(cls, email): - """Returns a staff by email""" - return cls.query.filter(Staff.email == email).first() +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Staff.""" + +from typing import List + +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, asc, func, DateTime +from sqlalchemy.orm import column_property, relationship + +from api.models.base_model import BaseModelVersioned +from api.utils.utcnow import utcnow + + +class Staff(BaseModelVersioned): + """Model class for Staff.""" + + __tablename__ = "staffs" + + id = Column(Integer, primary_key=True, autoincrement=True) + idir_user_id = Column(String(100), nullable=True) + first_name = Column(String(100), nullable=False) + last_name = Column(String(100), nullable=False) + phone = Column(String(), nullable=False) + email = Column(String(), nullable=False) + is_active = Column(Boolean(), default=True, nullable=False) + position_id = Column(ForeignKey("positions.id"), nullable=False) + is_deleted = Column(Boolean(), default=False, nullable=False) + position = relationship("Position", foreign_keys=[position_id], lazy="select") + + full_name = column_property(last_name + ", " + first_name) + last_active_at = Column(DateTime(timezone=True), server_default=utcnow()) + + # Define the excluded fields from versioning + __exclude_from_tracking_history__ = {"last_active_at"} + + def as_dict(self): # pylint: disable=arguments-differ + """Return Json representation.""" + return { + "id": self.id, + "idir_user_id": self.idir_user_id, + "first_name": self.first_name, + "last_name": self.last_name, + "full_name": self.full_name, + "phone": self.phone, + "email": self.email, + "is_active": self.is_active, + "position_id": self.position_id, + "position": self.position.as_dict(), + } + + @classmethod + def find_active_staff_by_position(cls, position_id: int): + """Return active staff by position id.""" + return cls.query.filter_by(position_id=position_id, is_active=True) + + @classmethod + def find_active_staff_by_positions(cls, position_ids: List[int]): + """Return active staffs by position ids.""" + return cls.query.filter( + Staff.position_id.in_(position_ids), Staff.is_active.is_(True) + ).order_by(asc(Staff.last_name), asc(Staff.first_name)) + + @classmethod + def find_all_active_staff(cls): + """Return all active staff.""" + return cls.query.filter_by(is_active=True, is_deleted=False) + + @classmethod + def find_all_non_deleted_staff(cls, is_active=False): + """Return all non-deleted staff""" + query = {"is_deleted": False} + if is_active: + query["is_active"] = is_active + return cls.query.filter_by(**query) + + @classmethod + def check_existence(cls, email, staff_id): + """Checks if a staff exists with given email address""" + query = cls.query.filter( + func.lower(Staff.email) == func.lower(email), + Staff.is_deleted.is_(False), + ) + if staff_id: + query = query.filter(Staff.id != staff_id) + if query.count() > 0: + return True + return False + + @classmethod + def find_by_email(cls, email): + """Returns a staff by email""" + return cls.query.filter(Staff.email == email).first() diff --git a/epictrack-api/src/api/models/staff_work_role.py b/epictrack-api/src/api/models/staff_work_role.py index 3d92f8bb7..a5c36582b 100644 --- a/epictrack-api/src/api/models/staff_work_role.py +++ b/epictrack-api/src/api/models/staff_work_role.py @@ -1,109 +1,109 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to StaffWorkRole.""" - -from sqlalchemy import Boolean, Column, ForeignKey, Integer -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -class StaffWorkRole(BaseModelVersioned): - """Model class for StaffWorkRole.""" - - __tablename__ = 'staff_work_roles' - - id = Column(Integer, primary_key=True, autoincrement=True) - is_deleted = Column(Boolean(), default=False, nullable=False) - work_id = Column(ForeignKey('works.id'), nullable=False) - role_id = Column(ForeignKey('roles.id'), nullable=False) - staff_id = Column(ForeignKey('staffs.id'), nullable=False) - - work = relationship('Work', foreign_keys=[work_id], lazy='select') - role = relationship('Role', foreign_keys=[role_id], lazy='select') - staff = relationship('Staff', foreign_keys=[staff_id], lazy='select') - - def as_dict(self): # pylint:disable=arguments-differ - """Return Json representation.""" - return { - 'id': self.id, - 'work_id': self.work_id, - 'role': self.role.as_dict(), - 'staff': self.staff.as_dict() - } - - @classmethod - def find_by_work_id(cls, work_id: int): - """Return by work id.""" - return cls.query.filter_by(work_id=work_id) - - @classmethod - def find_by_role_and_work(cls, work_id: int, role_id): - """Return by work and role ids.""" - return cls.query.filter( - StaffWorkRole.work_id == work_id, - StaffWorkRole.role_id == role_id, - StaffWorkRole.is_deleted.is_(False), - StaffWorkRole.is_active.is_(True), - ).all() - - @classmethod - def find_one_by_role_and_work(cls, work_id: int, role_id): - """Return by work and role ids.""" - return cls.query.filter( - StaffWorkRole.work_id == work_id, - StaffWorkRole.role_id == role_id, - StaffWorkRole.is_deleted.is_(False), - StaffWorkRole.is_active.is_(True), - ).first() - - @classmethod - def find_by_work_and_staff_and_role(cls, work_id: int, staff_id: object, role_id: object, work_staff_id: object): - """Return by work, staff and role ids.""" - query = cls.query.filter( - StaffWorkRole.work_id == work_id, - StaffWorkRole.staff_id == staff_id, - StaffWorkRole.is_deleted.is_(False), - StaffWorkRole.role_id == role_id, - ) - - if work_staff_id: - query = query.filter(StaffWorkRole.id != work_staff_id) - - return query.all() - - @classmethod - def find_one_by_work_and_staff_and_role( # pylint:disable=too-many-arguments - cls, - work_id: int, - staff_id: object, - role_id: object, - work_staff_id: object, - is_active: bool = None - ): - """Return by work, staff and role ids.""" - query = cls.query.filter( - StaffWorkRole.work_id == work_id, - StaffWorkRole.staff_id == staff_id, - StaffWorkRole.is_deleted.is_(False), - StaffWorkRole.role_id == role_id, - ) - - if is_active is not None: - query = query.filter(StaffWorkRole.is_active.is_(is_active)) - - if work_staff_id: - query = query.filter(StaffWorkRole.id != work_staff_id) - - return query.first() +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to StaffWorkRole.""" + +from sqlalchemy import Boolean, Column, ForeignKey, Integer +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +class StaffWorkRole(BaseModelVersioned): + """Model class for StaffWorkRole.""" + + __tablename__ = 'staff_work_roles' + + id = Column(Integer, primary_key=True, autoincrement=True) + is_deleted = Column(Boolean(), default=False, nullable=False) + work_id = Column(ForeignKey('works.id'), nullable=False) + role_id = Column(ForeignKey('roles.id'), nullable=False) + staff_id = Column(ForeignKey('staffs.id'), nullable=False) + + work = relationship('Work', foreign_keys=[work_id], lazy='select') + role = relationship('Role', foreign_keys=[role_id], lazy='select') + staff = relationship('Staff', foreign_keys=[staff_id], lazy='select') + + def as_dict(self): # pylint:disable=arguments-differ + """Return Json representation.""" + return { + 'id': self.id, + 'work_id': self.work_id, + 'role': self.role.as_dict(), + 'staff': self.staff.as_dict() + } + + @classmethod + def find_by_work_id(cls, work_id: int): + """Return by work id.""" + return cls.query.filter_by(work_id=work_id) + + @classmethod + def find_by_role_and_work(cls, work_id: int, role_id): + """Return by work and role ids.""" + return cls.query.filter( + StaffWorkRole.work_id == work_id, + StaffWorkRole.role_id == role_id, + StaffWorkRole.is_deleted.is_(False), + StaffWorkRole.is_active.is_(True), + ).all() + + @classmethod + def find_one_by_role_and_work(cls, work_id: int, role_id): + """Return by work and role ids.""" + return cls.query.filter( + StaffWorkRole.work_id == work_id, + StaffWorkRole.role_id == role_id, + StaffWorkRole.is_deleted.is_(False), + StaffWorkRole.is_active.is_(True), + ).first() + + @classmethod + def find_by_work_and_staff_and_role(cls, work_id: int, staff_id: object, role_id: object, work_staff_id: object): + """Return by work, staff and role ids.""" + query = cls.query.filter( + StaffWorkRole.work_id == work_id, + StaffWorkRole.staff_id == staff_id, + StaffWorkRole.is_deleted.is_(False), + StaffWorkRole.role_id == role_id, + ) + + if work_staff_id: + query = query.filter(StaffWorkRole.id != work_staff_id) + + return query.all() + + @classmethod + def find_one_by_work_and_staff_and_role( # pylint:disable=too-many-arguments + cls, + work_id: int, + staff_id: object, + role_id: object, + work_staff_id: object, + is_active: bool = None + ): + """Return by work, staff and role ids.""" + query = cls.query.filter( + StaffWorkRole.work_id == work_id, + StaffWorkRole.staff_id == staff_id, + StaffWorkRole.is_deleted.is_(False), + StaffWorkRole.role_id == role_id, + ) + + if is_active is not None: + query = query.filter(StaffWorkRole.is_active.is_(is_active)) + + if work_staff_id: + query = query.filter(StaffWorkRole.id != work_staff_id) + + return query.first() diff --git a/epictrack-api/src/api/models/task.py b/epictrack-api/src/api/models/task.py index 732639541..43d660e73 100644 --- a/epictrack-api/src/api/models/task.py +++ b/epictrack-api/src/api/models/task.py @@ -1,36 +1,36 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Tasks.""" - -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -class Task(BaseModelVersioned): - """Model class for Tasks.""" - - __tablename__ = 'tasks' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - start_at = sa.Column(sa.Integer, default=0, nullable=False) - number_of_days = sa.Column(sa.Integer, default=1, nullable=False) - template_id = sa.Column(sa.ForeignKey('task_templates.id'), nullable=False) - responsibility_id = sa.Column(sa.Integer, sa.ForeignKey('responsibilities.id'), nullable=False) - tips = sa.Column(sa.String) - - template = relationship('TaskTemplate', foreign_keys=[template_id], lazy='select') - responsibility = relationship('Responsibility', foreign_keys=[responsibility_id], lazy='select') +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Tasks.""" + +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +class Task(BaseModelVersioned): + """Model class for Tasks.""" + + __tablename__ = 'tasks' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + start_at = sa.Column(sa.Integer, default=0, nullable=False) + number_of_days = sa.Column(sa.Integer, default=1, nullable=False) + template_id = sa.Column(sa.ForeignKey('task_templates.id'), nullable=False) + responsibility_id = sa.Column(sa.Integer, sa.ForeignKey('responsibilities.id'), nullable=False) + tips = sa.Column(sa.String) + + template = relationship('TaskTemplate', foreign_keys=[template_id], lazy='select') + responsibility = relationship('Responsibility', foreign_keys=[responsibility_id], lazy='select') diff --git a/epictrack-api/src/api/models/task_event.py b/epictrack-api/src/api/models/task_event.py index a8affb562..cffb9a845 100644 --- a/epictrack-api/src/api/models/task_event.py +++ b/epictrack-api/src/api/models/task_event.py @@ -1,80 +1,80 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Task Events.""" - -import enum -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -# class ResponsibleEntityEnum(enum.Enum): -# """Enum for responsible entity""" - -# # pylint: disable=C0103 -# Proponent = 1 -# PIN = 2 -# EAO = 3 -# # pylint: disable=C0103 -# FederalAgencies = 3 - - -class StatusEnum(enum.Enum): - """Enum for responsible entity""" - - NOT_STARTED = "NOT_STARTED" - INPROGRESS = "INPROGRESS" - COMPLETED = "COMPLETED" - - -class TaskEvent(BaseModelVersioned): - """Model class for Tasks.""" - - __tablename__ = "task_events" - - id = sa.Column( - sa.Integer, primary_key=True, autoincrement=True - ) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - work_phase_id = sa.Column(sa.ForeignKey('work_phases.id'), nullable=True) - - start_date = sa.Column(sa.DateTime(timezone=True)) - number_of_days = sa.Column(sa.Integer, default=1, nullable=False) - tips = sa.Column(sa.String) - notes = sa.Column(sa.String) - status = sa.Column(sa.Enum(StatusEnum), default=StatusEnum.NOT_STARTED) - - work_phase = relationship('WorkPhase', foreign_keys=[work_phase_id], lazy='select') - - assignees = relationship( - "TaskEventAssignee", - primaryjoin="and_(TaskEvent.id==TaskEventAssignee.task_event_id,\ - TaskEventAssignee.is_active.is_(True), \ - TaskEventAssignee.is_deleted.is_(False))", - back_populates="task_event", - ) - - responsibilities = relationship( - "TaskEventResponsibility", - primaryjoin="and_(TaskEvent.id==TaskEventResponsibility.task_event_id,\ - TaskEventResponsibility.is_active.is_(True), \ - TaskEventResponsibility.is_deleted.is_(False))", - back_populates="task_event", - ) - - @classmethod - def find_by_work_phase(cls, work_id: int, phase_id: int): - """Find task events by work id and phase id""" - return cls.query.filter_by(work_id=work_id, phase_id=phase_id).all() +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Task Events.""" + +import enum +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +# class ResponsibleEntityEnum(enum.Enum): +# """Enum for responsible entity""" + +# # pylint: disable=C0103 +# Proponent = 1 +# PIN = 2 +# EAO = 3 +# # pylint: disable=C0103 +# FederalAgencies = 3 + + +class StatusEnum(enum.Enum): + """Enum for responsible entity""" + + NOT_STARTED = "NOT_STARTED" + INPROGRESS = "INPROGRESS" + COMPLETED = "COMPLETED" + + +class TaskEvent(BaseModelVersioned): + """Model class for Tasks.""" + + __tablename__ = "task_events" + + id = sa.Column( + sa.Integer, primary_key=True, autoincrement=True + ) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + work_phase_id = sa.Column(sa.ForeignKey('work_phases.id'), nullable=True) + + start_date = sa.Column(sa.DateTime(timezone=True)) + number_of_days = sa.Column(sa.Integer, default=1, nullable=False) + tips = sa.Column(sa.String) + notes = sa.Column(sa.String) + status = sa.Column(sa.Enum(StatusEnum), default=StatusEnum.NOT_STARTED) + + work_phase = relationship('WorkPhase', foreign_keys=[work_phase_id], lazy='select') + + assignees = relationship( + "TaskEventAssignee", + primaryjoin="and_(TaskEvent.id==TaskEventAssignee.task_event_id,\ + TaskEventAssignee.is_active.is_(True), \ + TaskEventAssignee.is_deleted.is_(False))", + back_populates="task_event", + ) + + responsibilities = relationship( + "TaskEventResponsibility", + primaryjoin="and_(TaskEvent.id==TaskEventResponsibility.task_event_id,\ + TaskEventResponsibility.is_active.is_(True), \ + TaskEventResponsibility.is_deleted.is_(False))", + back_populates="task_event", + ) + + @classmethod + def find_by_work_phase(cls, work_id: int, phase_id: int): + """Find task events by work id and phase id""" + return cls.query.filter_by(work_id=work_id, phase_id=phase_id).all() diff --git a/epictrack-api/src/api/models/task_event_assignee.py b/epictrack-api/src/api/models/task_event_assignee.py index ce13d59b6..ed7fface0 100644 --- a/epictrack-api/src/api/models/task_event_assignee.py +++ b/epictrack-api/src/api/models/task_event_assignee.py @@ -1,32 +1,32 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Task Event Assignees.""" - -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -class TaskEventAssignee(BaseModelVersioned): - """Model class for Task Event Assignees.""" - - __tablename__ = 'task_event_assignees' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - task_event_id = sa.Column(sa.ForeignKey('task_events.id'), nullable=False) - assignee_id = sa.Column(sa.ForeignKey('staffs.id'), nullable=False) - - task_event = relationship('TaskEvent', foreign_keys=[task_event_id], lazy='select') - assignee = relationship('Staff', foreign_keys=[assignee_id], lazy='select') +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Task Event Assignees.""" + +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +class TaskEventAssignee(BaseModelVersioned): + """Model class for Task Event Assignees.""" + + __tablename__ = 'task_event_assignees' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + task_event_id = sa.Column(sa.ForeignKey('task_events.id'), nullable=False) + assignee_id = sa.Column(sa.ForeignKey('staffs.id'), nullable=False) + + task_event = relationship('TaskEvent', foreign_keys=[task_event_id], lazy='select') + assignee = relationship('Staff', foreign_keys=[assignee_id], lazy='select') diff --git a/epictrack-api/src/api/models/task_template.py b/epictrack-api/src/api/models/task_template.py index da6e478de..773ad453b 100644 --- a/epictrack-api/src/api/models/task_template.py +++ b/epictrack-api/src/api/models/task_template.py @@ -1,40 +1,40 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Task Templates.""" - -import sqlalchemy as sa -from sqlalchemy.orm import relationship -from .base_model import BaseModelVersioned - - -class TaskTemplate(BaseModelVersioned): - """Model class for Tasks.""" - - __tablename__ = 'task_templates' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - name = sa.Column(sa.String) - ea_act_id = sa.Column(sa.ForeignKey('ea_acts.id'), nullable=False) - phase_id = sa.Column(sa.ForeignKey('phase_codes.id'), nullable=False) - work_type_id = sa.Column(sa.ForeignKey('work_types.id'), nullable=False) - - phase = relationship('PhaseCode', foreign_keys=[phase_id], lazy='select') - work_type = relationship('WorkType', foreign_keys=[work_type_id], lazy='select') - ea_act = relationship('EAAct', foreign_keys=[ea_act_id], lazy='select') - - tasks = relationship( - "Task", - primaryjoin="TaskTemplate.id==Task.template_id", - back_populates="template", - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Task Templates.""" + +import sqlalchemy as sa +from sqlalchemy.orm import relationship +from .base_model import BaseModelVersioned + + +class TaskTemplate(BaseModelVersioned): + """Model class for Tasks.""" + + __tablename__ = 'task_templates' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + name = sa.Column(sa.String) + ea_act_id = sa.Column(sa.ForeignKey('ea_acts.id'), nullable=False) + phase_id = sa.Column(sa.ForeignKey('phase_codes.id'), nullable=False) + work_type_id = sa.Column(sa.ForeignKey('work_types.id'), nullable=False) + + phase = relationship('PhaseCode', foreign_keys=[phase_id], lazy='select') + work_type = relationship('WorkType', foreign_keys=[work_type_id], lazy='select') + ea_act = relationship('EAAct', foreign_keys=[ea_act_id], lazy='select') + + tasks = relationship( + "Task", + primaryjoin="TaskTemplate.id==Task.template_id", + back_populates="template", + ) diff --git a/epictrack-api/src/api/models/work_calendar_event.py b/epictrack-api/src/api/models/work_calendar_event.py index dda7f3857..11a73aeea 100644 --- a/epictrack-api/src/api/models/work_calendar_event.py +++ b/epictrack-api/src/api/models/work_calendar_event.py @@ -1,33 +1,33 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Work Calendar Events.""" - -import sqlalchemy as sa -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned - - -class WorkCalendarEvent(BaseModelVersioned): - """Model class for Work Calendar Events.""" - - __tablename__ = 'work_calendar_events' - - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent - calendar_event_id = sa.Column(sa.ForeignKey('calendar_events.id'), nullable=False) - source_event_id = sa.Column(sa.Integer, nullable=True) - event_configuration_id = sa.Column(sa.ForeignKey('event_configurations.id'), nullable=False) - - event_configuration = relationship('EventConfiguration', foreign_keys=[event_configuration_id], lazy='select') - calendar_event = relationship('CalendarEvent', foreign_keys=[calendar_event_id], lazy='select') +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Work Calendar Events.""" + +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned + + +class WorkCalendarEvent(BaseModelVersioned): + """Model class for Work Calendar Events.""" + + __tablename__ = 'work_calendar_events' + + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) # TODO check how it can be inherited from parent + calendar_event_id = sa.Column(sa.ForeignKey('calendar_events.id'), nullable=False) + source_event_id = sa.Column(sa.Integer, nullable=True) + event_configuration_id = sa.Column(sa.ForeignKey('event_configurations.id'), nullable=False) + + event_configuration = relationship('EventConfiguration', foreign_keys=[event_configuration_id], lazy='select') + calendar_event = relationship('CalendarEvent', foreign_keys=[calendar_event_id], lazy='select') diff --git a/epictrack-api/src/api/models/work_issue_updates.py b/epictrack-api/src/api/models/work_issue_updates.py index 92ba0fe45..4a080a3f6 100644 --- a/epictrack-api/src/api/models/work_issue_updates.py +++ b/epictrack-api/src/api/models/work_issue_updates.py @@ -1,44 +1,44 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Model to handle all operations related to Issues.""" - -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime -from sqlalchemy.orm import relationship - -from .base_model import BaseModelVersioned -from ..utils.utcnow import utcnow - - -class WorkIssueUpdates(BaseModelVersioned): - """Model class for updates Connected to an issue.""" - - __tablename__ = 'work_issue_updates' - - id = Column(Integer, primary_key=True, autoincrement=True) - description = Column(String(2000), nullable=False) - - is_approved = Column(Boolean(), default=False, nullable=True) - approved_by = Column(String(255), default=None, nullable=True) - - work_issue_id = Column(ForeignKey('work_issues.id'), nullable=False) - work_issue = relationship('WorkIssues', back_populates='updates') - posted_date = Column(DateTime(timezone=True), nullable=False, server_default=utcnow()) - - def as_dict(self): # pylint:disable=arguments-differ - """Return Json representation.""" - return { - 'id': self.id, - 'description': self.description, - 'work_issue_id': self.work_issue_id, - } +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Model to handle all operations related to Issues.""" + +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime +from sqlalchemy.orm import relationship + +from .base_model import BaseModelVersioned +from ..utils.utcnow import utcnow + + +class WorkIssueUpdates(BaseModelVersioned): + """Model class for updates Connected to an issue.""" + + __tablename__ = 'work_issue_updates' + + id = Column(Integer, primary_key=True, autoincrement=True) + description = Column(String(2000), nullable=False) + + is_approved = Column(Boolean(), default=False, nullable=True) + approved_by = Column(String(255), default=None, nullable=True) + + work_issue_id = Column(ForeignKey('work_issues.id'), nullable=False) + work_issue = relationship('WorkIssues', back_populates='updates') + posted_date = Column(DateTime(timezone=True), nullable=False, server_default=utcnow()) + + def as_dict(self): # pylint:disable=arguments-differ + """Return Json representation.""" + return { + 'id': self.id, + 'description': self.description, + 'work_issue_id': self.work_issue_id, + } diff --git a/epictrack-api/src/api/resources/__init__.py b/epictrack-api/src/api/resources/__init__.py index efbe7728e..f9dbe66f1 100644 --- a/epictrack-api/src/api/resources/__init__.py +++ b/epictrack-api/src/api/resources/__init__.py @@ -1,140 +1,140 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Exposes all of the resource endpoints mounted in Flask-Blueprint style. - -Uses restplus namespaces to mount individual api endpoints into the service. - -All services have 2 defaults sets of endpoints: - - ops - - meta -That are used to expose operational health information about the service, and meta information. -""" - -from flask import Blueprint - -from .act_section import API as ACT_SECTION_API -from .apihelper import Api -from .code import API as CODES_API -from .ea_act import API as EA_ACT_API -from .eao_team import API as EAO_TEAM_API -from .event import API as EVENT_API -from .event_configuration import API as EVENT_CONFIGURATION_API -from .event_template import API as EVENT_TEMPLATE_API -from .federal_involvement import API as FEDERAL_INVOLVEMENT_API -from .indigenous_consultation_level import API as INDIGENOUS_CONSULTATION_LEVEL_API -from .indigenous_nation import API as INDIGENOUS_NATION_API -from .insights import API as INSIGHTS_API -from .inspection import API as INSPECTION_API -from .lookup_data_generator import API as LOOKUP_API -from .meta import API as META_API -from .ministry import API as MINISTRY_API -from .ops import API as OPS_API -from .outcome import API as OUTCOME_API -from .outcome_configuration import API as OUTCOME_CONFIGURATION_API -from .phase import API as PHASE_API -from .pip_org_type import API as PIP_ORG_TYPES_API -from .position import API as POSITION_API -from .project import API as PROJECTS_API -from .project_type import API as PROJECT_TYPES_API -from .proponent import API as PROPONENT_API -from .region import API as REGION_API -from .reminder_configuration import API as REMINDER_CONFIGURATION_API -from .reports import API as REPORTS_API -from .responsibility import API as RESPONSIBILITY_API -from .role import API as ROLES_API -from .special_field import API as SPECIAL_FIELD_API -from .staff import API as STAFF_API -from .sub_types import API as SUB_TYPES_API -from .substitution_act import API as SUBSTITUTION_ACTS_API -from .sync_form_data import API as SYNC_FORM_DATA_API -from .task import API as TASK_API -from .task_template import API as TASK_TEMPLATE_API -from .types import API as TYPES_API -from .user import API as USER_API -from .work import API as WORK_API -from .work_issues import API as WORK_ISSUES_API -from .work_status import API as WORK_STATUS_API -from .work_type import API as WORK_TYPES_API - - -__all__ = ("API_BLUEPRINT", "OPS_BLUEPRINT") - -# This will add the Authorize button to the swagger docs -AUTHORIZATIONS = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}} - -OPS_BLUEPRINT = Blueprint("API_OPS", __name__, url_prefix="/ops") - -API_OPS = Api( - OPS_BLUEPRINT, - title="Service OPS API", - version="1.0", - description="The Core API for the Reports System", - security=["apikey"], - authorizations=AUTHORIZATIONS, -) - -API_OPS.add_namespace(OPS_API, path="/") - -API_BLUEPRINT = Blueprint("API", __name__, url_prefix="/api/v1") - -API = Api( - API_BLUEPRINT, - title="EAO Reports API", - version="1.0", - description="The Core API for the Reports System", - security=["apikey"], - authorizations=AUTHORIZATIONS, -) - -API.add_namespace(META_API, path="/meta") -API.add_namespace(CODES_API, path="/codes") -API.add_namespace(PROJECTS_API, path="/projects") -API.add_namespace(PROJECT_TYPES_API, path="/project-types") -API.add_namespace(SYNC_FORM_DATA_API, path="/sync-form-data") -API.add_namespace(PHASE_API, path="/phases") -API.add_namespace(STAFF_API, path="/staffs") -API.add_namespace(OUTCOME_API, path="/outcomes") -API.add_namespace(SUB_TYPES_API, path="/sub-types") -API.add_namespace(INSPECTION_API, path="/inspections") -API.add_namespace(WORK_API, path="/works") -API.add_namespace(LOOKUP_API, path="/lookups") -API.add_namespace(REPORTS_API, path="/reports") -API.add_namespace(INDIGENOUS_NATION_API, path="/indigenous-nations") -API.add_namespace(PROPONENT_API, path="/proponents") -API.add_namespace(REMINDER_CONFIGURATION_API, path="/reminder-configurations") -API.add_namespace(USER_API, path='/users') -API.add_namespace(TASK_TEMPLATE_API, path="/task-templates") -API.add_namespace(EVENT_TEMPLATE_API, path="/event-templates") -API.add_namespace(TASK_API, path="/tasks") -API.add_namespace(EVENT_API, path="/milestones") -API.add_namespace(EVENT_CONFIGURATION_API, path="/event-configurations") -API.add_namespace(RESPONSIBILITY_API, path="/responsibilities") -API.add_namespace(OUTCOME_CONFIGURATION_API, path="/outcome-configurations") -API.add_namespace(ACT_SECTION_API, path="/act-sections") -API.add_namespace(WORK_STATUS_API, path='/work//statuses') -API.add_namespace(WORK_TYPES_API, path='/work-types') -API.add_namespace(WORK_ISSUES_API, path='/work//issues') -API.add_namespace(SPECIAL_FIELD_API, path='/special-fields') -API.add_namespace(POSITION_API, path='/positions') -API.add_namespace(REGION_API, path='/regions') -API.add_namespace(EAO_TEAM_API, path='/eao-teams') -API.add_namespace(INDIGENOUS_CONSULTATION_LEVEL_API, path='/indigenous-nations-consultation-levels') -API.add_namespace(MINISTRY_API, path='/ministries') -API.add_namespace(EA_ACT_API, path='/ea-acts') -API.add_namespace(TYPES_API, path='/types') -API.add_namespace(FEDERAL_INVOLVEMENT_API, path='/federal-involvements') -API.add_namespace(ROLES_API, path='/roles') -API.add_namespace(SUBSTITUTION_ACTS_API, path='/substitution-acts') -API.add_namespace(PIP_ORG_TYPES_API, path='/pip-org-types') -API.add_namespace(INSIGHTS_API, path='/insights') +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Exposes all of the resource endpoints mounted in Flask-Blueprint style. + +Uses restplus namespaces to mount individual api endpoints into the service. + +All services have 2 defaults sets of endpoints: + - ops + - meta +That are used to expose operational health information about the service, and meta information. +""" + +from flask import Blueprint + +from .act_section import API as ACT_SECTION_API +from .apihelper import Api +from .code import API as CODES_API +from .ea_act import API as EA_ACT_API +from .eao_team import API as EAO_TEAM_API +from .event import API as EVENT_API +from .event_configuration import API as EVENT_CONFIGURATION_API +from .event_template import API as EVENT_TEMPLATE_API +from .federal_involvement import API as FEDERAL_INVOLVEMENT_API +from .indigenous_consultation_level import API as INDIGENOUS_CONSULTATION_LEVEL_API +from .indigenous_nation import API as INDIGENOUS_NATION_API +from .insights import API as INSIGHTS_API +from .inspection import API as INSPECTION_API +from .lookup_data_generator import API as LOOKUP_API +from .meta import API as META_API +from .ministry import API as MINISTRY_API +from .ops import API as OPS_API +from .outcome import API as OUTCOME_API +from .outcome_configuration import API as OUTCOME_CONFIGURATION_API +from .phase import API as PHASE_API +from .pip_org_type import API as PIP_ORG_TYPES_API +from .position import API as POSITION_API +from .project import API as PROJECTS_API +from .project_type import API as PROJECT_TYPES_API +from .proponent import API as PROPONENT_API +from .region import API as REGION_API +from .reminder_configuration import API as REMINDER_CONFIGURATION_API +from .reports import API as REPORTS_API +from .responsibility import API as RESPONSIBILITY_API +from .role import API as ROLES_API +from .special_field import API as SPECIAL_FIELD_API +from .staff import API as STAFF_API +from .sub_types import API as SUB_TYPES_API +from .substitution_act import API as SUBSTITUTION_ACTS_API +from .sync_form_data import API as SYNC_FORM_DATA_API +from .task import API as TASK_API +from .task_template import API as TASK_TEMPLATE_API +from .types import API as TYPES_API +from .user import API as USER_API +from .work import API as WORK_API +from .work_issues import API as WORK_ISSUES_API +from .work_status import API as WORK_STATUS_API +from .work_type import API as WORK_TYPES_API + + +__all__ = ("API_BLUEPRINT", "OPS_BLUEPRINT") + +# This will add the Authorize button to the swagger docs +AUTHORIZATIONS = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}} + +OPS_BLUEPRINT = Blueprint("API_OPS", __name__, url_prefix="/ops") + +API_OPS = Api( + OPS_BLUEPRINT, + title="Service OPS API", + version="1.0", + description="The Core API for the Reports System", + security=["apikey"], + authorizations=AUTHORIZATIONS, +) + +API_OPS.add_namespace(OPS_API, path="/") + +API_BLUEPRINT = Blueprint("API", __name__, url_prefix="/api/v1") + +API = Api( + API_BLUEPRINT, + title="EAO Reports API", + version="1.0", + description="The Core API for the Reports System", + security=["apikey"], + authorizations=AUTHORIZATIONS, +) + +API.add_namespace(META_API, path="/meta") +API.add_namespace(CODES_API, path="/codes") +API.add_namespace(PROJECTS_API, path="/projects") +API.add_namespace(PROJECT_TYPES_API, path="/project-types") +API.add_namespace(SYNC_FORM_DATA_API, path="/sync-form-data") +API.add_namespace(PHASE_API, path="/phases") +API.add_namespace(STAFF_API, path="/staffs") +API.add_namespace(OUTCOME_API, path="/outcomes") +API.add_namespace(SUB_TYPES_API, path="/sub-types") +API.add_namespace(INSPECTION_API, path="/inspections") +API.add_namespace(WORK_API, path="/works") +API.add_namespace(LOOKUP_API, path="/lookups") +API.add_namespace(REPORTS_API, path="/reports") +API.add_namespace(INDIGENOUS_NATION_API, path="/indigenous-nations") +API.add_namespace(PROPONENT_API, path="/proponents") +API.add_namespace(REMINDER_CONFIGURATION_API, path="/reminder-configurations") +API.add_namespace(USER_API, path='/users') +API.add_namespace(TASK_TEMPLATE_API, path="/task-templates") +API.add_namespace(EVENT_TEMPLATE_API, path="/event-templates") +API.add_namespace(TASK_API, path="/tasks") +API.add_namespace(EVENT_API, path="/milestones") +API.add_namespace(EVENT_CONFIGURATION_API, path="/event-configurations") +API.add_namespace(RESPONSIBILITY_API, path="/responsibilities") +API.add_namespace(OUTCOME_CONFIGURATION_API, path="/outcome-configurations") +API.add_namespace(ACT_SECTION_API, path="/act-sections") +API.add_namespace(WORK_STATUS_API, path='/work//statuses') +API.add_namespace(WORK_TYPES_API, path='/work-types') +API.add_namespace(WORK_ISSUES_API, path='/work//issues') +API.add_namespace(SPECIAL_FIELD_API, path='/special-fields') +API.add_namespace(POSITION_API, path='/positions') +API.add_namespace(REGION_API, path='/regions') +API.add_namespace(EAO_TEAM_API, path='/eao-teams') +API.add_namespace(INDIGENOUS_CONSULTATION_LEVEL_API, path='/indigenous-nations-consultation-levels') +API.add_namespace(MINISTRY_API, path='/ministries') +API.add_namespace(EA_ACT_API, path='/ea-acts') +API.add_namespace(TYPES_API, path='/types') +API.add_namespace(FEDERAL_INVOLVEMENT_API, path='/federal-involvements') +API.add_namespace(ROLES_API, path='/roles') +API.add_namespace(SUBSTITUTION_ACTS_API, path='/substitution-acts') +API.add_namespace(PIP_ORG_TYPES_API, path='/pip-org-types') +API.add_namespace(INSIGHTS_API, path='/insights') diff --git a/epictrack-api/src/api/resources/act_section.py b/epictrack-api/src/api/resources/act_section.py index 1a1112794..0d6cd6ce9 100644 --- a/epictrack-api/src/api/resources/act_section.py +++ b/epictrack-api/src/api/resources/act_section.py @@ -1,45 +1,45 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for Act Section endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.services import ActSectionService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - -from api.schemas import request as req -from api.schemas import response as res - -API = Namespace("act-sections", description="ActSections") - - -@cors_preflight("GET") -@API.route("", methods=["GET", "OPTIONS"]) -class ActSections(Resource): - """Endpoint resource to return sub types based on type id""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) - def get(): - """Return all sub_types based on type_id.""" - args = req.ActSectionQueryParameterSchema().load(request.args) - act_sections = ActSectionService.find_by_ea_act(args) - return jsonify(res.ActSectionResponseSchema(many=True).dump(act_sections)), HTTPStatus.OK +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for Act Section endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.services import ActSectionService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + +from api.schemas import request as req +from api.schemas import response as res + +API = Namespace("act-sections", description="ActSections") + + +@cors_preflight("GET") +@API.route("", methods=["GET", "OPTIONS"]) +class ActSections(Resource): + """Endpoint resource to return sub types based on type id""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) + def get(): + """Return all sub_types based on type_id.""" + args = req.ActSectionQueryParameterSchema().load(request.args) + act_sections = ActSectionService.find_by_ea_act(args) + return jsonify(res.ActSectionResponseSchema(many=True).dump(act_sections)), HTTPStatus.OK diff --git a/epictrack-api/src/api/resources/eao_team.py b/epictrack-api/src/api/resources/eao_team.py index 080ae3765..8ced90276 100644 --- a/epictrack-api/src/api/resources/eao_team.py +++ b/epictrack-api/src/api/resources/eao_team.py @@ -1,43 +1,43 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for Sub Sector endpoints.""" -from http import HTTPStatus - -from flask import jsonify -from flask_restx import Namespace, Resource, cors - -from api.schemas.eao_team import EAOTeamSchema -from api.services.eao_team_service import EAOTeamService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - - -API = Namespace("eao-teams", description="EAOTeams") - - -@cors_preflight("GET") -@API.route("", methods=["GET", "OPTIONS"]) -class EAOTEAM(Resource): - """Endpoint resource to return eao teams""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT) - def get(): - """Return all eao teams.""" - eao_teams = EAOTeamService.find_all_teams() - return jsonify(EAOTeamSchema(many=True).dump(eao_teams)), HTTPStatus.OK +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for Sub Sector endpoints.""" +from http import HTTPStatus + +from flask import jsonify +from flask_restx import Namespace, Resource, cors + +from api.schemas.eao_team import EAOTeamSchema +from api.services.eao_team_service import EAOTeamService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + + +API = Namespace("eao-teams", description="EAOTeams") + + +@cors_preflight("GET") +@API.route("", methods=["GET", "OPTIONS"]) +class EAOTEAM(Resource): + """Endpoint resource to return eao teams""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT) + def get(): + """Return all eao teams.""" + eao_teams = EAOTeamService.find_all_teams() + return jsonify(EAOTeamSchema(many=True).dump(eao_teams)), HTTPStatus.OK diff --git a/epictrack-api/src/api/resources/event_configuration.py b/epictrack-api/src/api/resources/event_configuration.py index ce0304829..3822e4996 100644 --- a/epictrack-api/src/api/resources/event_configuration.py +++ b/epictrack-api/src/api/resources/event_configuration.py @@ -1,61 +1,61 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for Event Configuration endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.schemas import request as req -from api.schemas import response as res -from api.services.event_configuration import EventConfigurationService -from api.models.event_category import PRIMARY_CATEGORIES -from api.utils import auth, profiletime -from api.utils.util import cors_preflight - - -API = Namespace("event-configurations", description="Event Configurations") - - -@cors_preflight("GET") -@API.route("/", methods=["GET", "OPTIONS"]) -class EventConfigurations(Resource): - """Endpoint resource to return all event configurations for given parameters""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(): - """Return all task templates.""" - args = req.EventConfigurationQueryParamSchema().load(request.args) - work_phase_id = args.get("work_phase_id") - visibility_modes = args.get("visibility_modes") - configurable = args.get("configurable", False) - if configurable: - configurations = ( - EventConfigurationService.find_configurations_for_adding_new_milestone( - work_phase_id, visibility_modes, PRIMARY_CATEGORIES - ) - ) - else: - configurations = EventConfigurationService.find_configurations( - work_phase_id, visibility_modes, PRIMARY_CATEGORIES - ) - return ( - jsonify( - res.EventConfigurationResponseSchema(many=True).dump(configurations) - ), - HTTPStatus.OK, - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for Event Configuration endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.schemas import request as req +from api.schemas import response as res +from api.services.event_configuration import EventConfigurationService +from api.models.event_category import PRIMARY_CATEGORIES +from api.utils import auth, profiletime +from api.utils.util import cors_preflight + + +API = Namespace("event-configurations", description="Event Configurations") + + +@cors_preflight("GET") +@API.route("/", methods=["GET", "OPTIONS"]) +class EventConfigurations(Resource): + """Endpoint resource to return all event configurations for given parameters""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(): + """Return all task templates.""" + args = req.EventConfigurationQueryParamSchema().load(request.args) + work_phase_id = args.get("work_phase_id") + visibility_modes = args.get("visibility_modes") + configurable = args.get("configurable", False) + if configurable: + configurations = ( + EventConfigurationService.find_configurations_for_adding_new_milestone( + work_phase_id, visibility_modes, PRIMARY_CATEGORIES + ) + ) + else: + configurations = EventConfigurationService.find_configurations( + work_phase_id, visibility_modes, PRIMARY_CATEGORIES + ) + return ( + jsonify( + res.EventConfigurationResponseSchema(many=True).dump(configurations) + ), + HTTPStatus.OK, + ) diff --git a/epictrack-api/src/api/resources/event_template.py b/epictrack-api/src/api/resources/event_template.py index f3c42a6e9..92e686824 100644 --- a/epictrack-api/src/api/resources/event_template.py +++ b/epictrack-api/src/api/resources/event_template.py @@ -1,41 +1,41 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for Event Template endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.services import EventTemplateService -from api.utils import auth, profiletime -from api.utils.util import cors_preflight - - -API = Namespace("tasks", description="Tasks") - - -@cors_preflight("POST") -@API.route("", methods=["POST", "OPTIONS"]) -class EventTemplates(Resource): - """Endpoints for EventTemplates""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(): - """Create new task template""" - template_file = request.files["event_template"] - event_template = EventTemplateService.import_events_template(template_file) - return jsonify(event_template), HTTPStatus.CREATED +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for Event Template endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.services import EventTemplateService +from api.utils import auth, profiletime +from api.utils.util import cors_preflight + + +API = Namespace("tasks", description="Tasks") + + +@cors_preflight("POST") +@API.route("", methods=["POST", "OPTIONS"]) +class EventTemplates(Resource): + """Endpoints for EventTemplates""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Create new task template""" + template_file = request.files["event_template"] + event_template = EventTemplateService.import_events_template(template_file) + return jsonify(event_template), HTTPStatus.CREATED diff --git a/epictrack-api/src/api/resources/outcome_configuration.py b/epictrack-api/src/api/resources/outcome_configuration.py index 95038aa77..4883aec11 100644 --- a/epictrack-api/src/api/resources/outcome_configuration.py +++ b/epictrack-api/src/api/resources/outcome_configuration.py @@ -1,45 +1,45 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for Outcome Configuration endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.schemas import request as req -from api.schemas import response as res -from api.services.outcome_configuration import OutcomeConfigurationService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - - -API = Namespace('outcome_configurations', description='Outcome Configurations') - - -@cors_preflight('GET') -@API.route('/', methods=['GET', 'OPTIONS']) -class OutcomeConfigurations(Resource): - """Endpoints for the outcome configurations""" - - @staticmethod - @cors.crossdomain(origin='*') - @auth.require - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) - @profiletime - def get(): - """Return all outcomes based on milestone_id.""" - args = req.OutcomeConfigurationQueryParameterSchema().load(request.args) - outcomes = OutcomeConfigurationService.find_by_configuration_id(args.get("configuration_id")) - return jsonify(res.OutcomeConfigurationResponseSchema(many=True).dump(outcomes)), HTTPStatus.OK +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for Outcome Configuration endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.schemas import request as req +from api.schemas import response as res +from api.services.outcome_configuration import OutcomeConfigurationService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + + +API = Namespace('outcome_configurations', description='Outcome Configurations') + + +@cors_preflight('GET') +@API.route('/', methods=['GET', 'OPTIONS']) +class OutcomeConfigurations(Resource): + """Endpoints for the outcome configurations""" + + @staticmethod + @cors.crossdomain(origin='*') + @auth.require + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) + @profiletime + def get(): + """Return all outcomes based on milestone_id.""" + args = req.OutcomeConfigurationQueryParameterSchema().load(request.args) + outcomes = OutcomeConfigurationService.find_by_configuration_id(args.get("configuration_id")) + return jsonify(res.OutcomeConfigurationResponseSchema(many=True).dump(outcomes)), HTTPStatus.OK diff --git a/epictrack-api/src/api/resources/project.py b/epictrack-api/src/api/resources/project.py index 2ba01f8b1..30d04517b 100644 --- a/epictrack-api/src/api/resources/project.py +++ b/epictrack-api/src/api/resources/project.py @@ -1,235 +1,235 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for project endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.schemas import request as req -from api.schemas import response as res -from api.schemas.work_type import WorkTypeSchema -from api.services import ProjectService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - - -API = Namespace("projects", description="Projects") - - -@cors_preflight("GET, DELETE, POST") -@API.route("", methods=["GET", "POST", "OPTIONS"]) -class Projects(Resource): - """Endpoint resource to manage projects.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(): - """Return all projects.""" - with_works = request.args.get( - 'with_works', - default=False, - type=lambda v: v.lower() == 'true' - ) - is_active = request.args.get("is_active", None, bool) - projects = ProjectService.find_all(with_works, is_active) - return_type = request.args.get("return_type", None) - if return_type == "list_type": - schema = res.ListTypeResponseSchema(many=True) - else: - schema = res.ProjectResponseSchema(many=True) - return jsonify(schema.dump(projects)), HTTPStatus.OK - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(): - """Create new project""" - request_json = req.ProjectBodyParameterSchema().load(API.payload) - project = ProjectService.create_project(request_json) - return res.ProjectResponseSchema().dump(project), HTTPStatus.CREATED - - -@cors_preflight("GET, DELETE, PUT") -@API.route("/", methods=["GET", "PUT", "DELETE", "OPTIONS"]) -class Project(Resource): - """Endpoint resource to manage a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Return details of a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - project = ProjectService.find(project_id, exclude_deleted=True) - return res.ProjectResponseSchema().dump(project), HTTPStatus.OK - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def put(project_id): - """Update and return a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - request_json = req.ProjectBodyParameterSchema().load(API.payload) - project = ProjectService.update_project(project_id, request_json) - return res.ProjectResponseSchema().dump(project), HTTPStatus.OK - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def delete(project_id): - """Delete a project""" - req.ProjectIdPathParameterSchema().load(request.view_args) - ProjectService.delete_project(project_id) - return "Project successfully deleted", HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("/exists", methods=["GET", "OPTIONS"]) -class ValidateProject(Resource): - """Endpoint resource to check for existing project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(): - """Checks for existing projects.""" - args = req.ProjectExistenceQueryParamSchema().load(request.args) - name = args["name"] - project_id = args["project_id"] - exists = ProjectService.check_existence(name=name, project_id=project_id) - return {"exists": exists}, HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("//work-types", methods=["GET", "OPTIONS"]) -class ProjectWorkTypes(Resource): - """Endpoint resource to get all work types associated with a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Return work types associated with a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - args = req.WorkIdPathParameterSchema().load(request.args) - work_id = args["work_id"] - work_types = ProjectService.find_project_work_types(project_id, work_id) - return WorkTypeSchema(many=True).dump(work_types), HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("//first-nations", methods=["GET", "OPTIONS"]) -class ProjectFirstNations(Resource): - """Endpoint resource to get all first nations associated with a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Return first nations associated with a project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - args = req.ProjectFirstNationsQueryParamSchema().load(request.args) - work_type_id = args["work_type_id"] - work_id = args["work_id"] - first_nations = ProjectService.find_first_nations( - project_id, work_id, work_type_id - ) - return ( - res.IndigenousResponseNationSchema(many=True).dump(first_nations), - HTTPStatus.OK, - ) - - -@cors_preflight("GET") -@API.route("//first-nation-available", methods=["GET", "OPTIONS"]) -class ProjectFirstNationAvailableStatus(Resource): - """Endpoint resource to check if there are first nations associated with a project.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def get(project_id): - """Check if any first nations associated with given project.""" - req.ProjectIdPathParameterSchema().load(request.view_args) - args = req.ProjectFirstNationsQueryParamSchema().load(request.args) - work_id = args["work_id"] - first_nation_availability = ProjectService.check_first_nation_available( - project_id, work_id - ) - return first_nation_availability, HTTPStatus.OK - - -@cors_preflight("POST") -@API.route("/import", methods=["POST", "OPTIONS"]) -class ImportProjects(Resource): - """Endpoint resource to import projects.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(): - """Import projects""" - file = request.files["file"] - response = ProjectService.import_projects(file) - return response, HTTPStatus.CREATED - - -@cors_preflight("POST") -@API.route("/abbreviation", methods=["POST", "OPTIONS"]) -class ProjectAbbreviation(Resource): - """Endpoint resource to import projects.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(): - """Create new project abbreviation""" - request_json = req.ProjectAbbreviationParameterSchema().load(API.payload) - project_abbreviation = ProjectService.create_project_abbreviation( - request_json.get("name") - ) - return project_abbreviation, HTTPStatus.CREATED - - -@cors_preflight("GET, DELETE, POST") -@API.route("/types", methods=["GET", "POST", "OPTIONS"]) -class ProjectTypes(Resource): - """Endpoint resource to manage projects.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) - def get(): - """Return all project types.""" - project_types = ProjectService.find_all_project_types() - return ( - jsonify(project_types), - HTTPStatus.OK, - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for project endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.schemas import request as req +from api.schemas import response as res +from api.schemas.work_type import WorkTypeSchema +from api.services import ProjectService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + + +API = Namespace("projects", description="Projects") + + +@cors_preflight("GET, DELETE, POST") +@API.route("", methods=["GET", "POST", "OPTIONS"]) +class Projects(Resource): + """Endpoint resource to manage projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(): + """Return all projects.""" + with_works = request.args.get( + 'with_works', + default=False, + type=lambda v: v.lower() == 'true' + ) + is_active = request.args.get("is_active", None, bool) + projects = ProjectService.find_all(with_works, is_active) + return_type = request.args.get("return_type", None) + if return_type == "list_type": + schema = res.ListTypeResponseSchema(many=True) + else: + schema = res.ProjectResponseSchema(many=True) + return jsonify(schema.dump(projects)), HTTPStatus.OK + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Create new project""" + request_json = req.ProjectBodyParameterSchema().load(API.payload) + project = ProjectService.create_project(request_json) + return res.ProjectResponseSchema().dump(project), HTTPStatus.CREATED + + +@cors_preflight("GET, DELETE, PUT") +@API.route("/", methods=["GET", "PUT", "DELETE", "OPTIONS"]) +class Project(Resource): + """Endpoint resource to manage a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Return details of a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + project = ProjectService.find(project_id, exclude_deleted=True) + return res.ProjectResponseSchema().dump(project), HTTPStatus.OK + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def put(project_id): + """Update and return a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + request_json = req.ProjectBodyParameterSchema().load(API.payload) + project = ProjectService.update_project(project_id, request_json) + return res.ProjectResponseSchema().dump(project), HTTPStatus.OK + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def delete(project_id): + """Delete a project""" + req.ProjectIdPathParameterSchema().load(request.view_args) + ProjectService.delete_project(project_id) + return "Project successfully deleted", HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("/exists", methods=["GET", "OPTIONS"]) +class ValidateProject(Resource): + """Endpoint resource to check for existing project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(): + """Checks for existing projects.""" + args = req.ProjectExistenceQueryParamSchema().load(request.args) + name = args["name"] + project_id = args["project_id"] + exists = ProjectService.check_existence(name=name, project_id=project_id) + return {"exists": exists}, HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("//work-types", methods=["GET", "OPTIONS"]) +class ProjectWorkTypes(Resource): + """Endpoint resource to get all work types associated with a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Return work types associated with a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + args = req.WorkIdPathParameterSchema().load(request.args) + work_id = args["work_id"] + work_types = ProjectService.find_project_work_types(project_id, work_id) + return WorkTypeSchema(many=True).dump(work_types), HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("//first-nations", methods=["GET", "OPTIONS"]) +class ProjectFirstNations(Resource): + """Endpoint resource to get all first nations associated with a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Return first nations associated with a project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + args = req.ProjectFirstNationsQueryParamSchema().load(request.args) + work_type_id = args["work_type_id"] + work_id = args["work_id"] + first_nations = ProjectService.find_first_nations( + project_id, work_id, work_type_id + ) + return ( + res.IndigenousResponseNationSchema(many=True).dump(first_nations), + HTTPStatus.OK, + ) + + +@cors_preflight("GET") +@API.route("//first-nation-available", methods=["GET", "OPTIONS"]) +class ProjectFirstNationAvailableStatus(Resource): + """Endpoint resource to check if there are first nations associated with a project.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def get(project_id): + """Check if any first nations associated with given project.""" + req.ProjectIdPathParameterSchema().load(request.view_args) + args = req.ProjectFirstNationsQueryParamSchema().load(request.args) + work_id = args["work_id"] + first_nation_availability = ProjectService.check_first_nation_available( + project_id, work_id + ) + return first_nation_availability, HTTPStatus.OK + + +@cors_preflight("POST") +@API.route("/import", methods=["POST", "OPTIONS"]) +class ImportProjects(Resource): + """Endpoint resource to import projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Import projects""" + file = request.files["file"] + response = ProjectService.import_projects(file) + return response, HTTPStatus.CREATED + + +@cors_preflight("POST") +@API.route("/abbreviation", methods=["POST", "OPTIONS"]) +class ProjectAbbreviation(Resource): + """Endpoint resource to import projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(): + """Create new project abbreviation""" + request_json = req.ProjectAbbreviationParameterSchema().load(API.payload) + project_abbreviation = ProjectService.create_project_abbreviation( + request_json.get("name") + ) + return project_abbreviation, HTTPStatus.CREATED + + +@cors_preflight("GET, DELETE, POST") +@API.route("/types", methods=["GET", "POST", "OPTIONS"]) +class ProjectTypes(Resource): + """Endpoint resource to manage projects.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) + def get(): + """Return all project types.""" + project_types = ProjectService.find_all_project_types() + return ( + jsonify(project_types), + HTTPStatus.OK, + ) diff --git a/epictrack-api/src/api/resources/project_type.py b/epictrack-api/src/api/resources/project_type.py index 3e15d1b7c..56433a562 100644 --- a/epictrack-api/src/api/resources/project_type.py +++ b/epictrack-api/src/api/resources/project_type.py @@ -1,45 +1,45 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for work endpoints.""" -from http import HTTPStatus - -from flask import jsonify -from flask_restx import Namespace, Resource, cors - -from api.services import ProjectService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - - -API = Namespace("project types", description="Project Types") - - -@cors_preflight("GET") -@API.route("", methods=["GET", "OPTIONS"]) -class ProjectTypes(Resource): - """Endpoint resource to manage project types.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) - def get(): - """Return all project types.""" - project_types = ProjectService.find_all_project_types() - return ( - jsonify(project_types), - HTTPStatus.OK, - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for work endpoints.""" +from http import HTTPStatus + +from flask import jsonify +from flask_restx import Namespace, Resource, cors + +from api.services import ProjectService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + + +API = Namespace("project types", description="Project Types") + + +@cors_preflight("GET") +@API.route("", methods=["GET", "OPTIONS"]) +class ProjectTypes(Resource): + """Endpoint resource to manage project types.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) + def get(): + """Return all project types.""" + project_types = ProjectService.find_all_project_types() + return ( + jsonify(project_types), + HTTPStatus.OK, + ) diff --git a/epictrack-api/src/api/resources/region.py b/epictrack-api/src/api/resources/region.py index 14c22e3b1..3568db153 100644 --- a/epictrack-api/src/api/resources/region.py +++ b/epictrack-api/src/api/resources/region.py @@ -1,46 +1,46 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for Sub Sector endpoints.""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors - -from api.schemas import request as req -from api.schemas.region import RegionSchema -from api.services.region import RegionService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - - -API = Namespace("regions", description="Regions") - - -@cors_preflight("GET") -@API.route("", methods=["GET", "OPTIONS"]) -class Regions(Resource): - """Endpoint resource to return regions based on region type.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) - def get(): - """Return all regions based on region type.""" - req.RegionTypePathParameterSchema().load(request.args) - region_type = request.args.get("type", None) - regions = RegionService.find_regions_by_type(region_type) - return jsonify(RegionSchema(many=True).dump(regions)), HTTPStatus.OK +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for Sub Sector endpoints.""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors + +from api.schemas import request as req +from api.schemas.region import RegionSchema +from api.services.region import RegionService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + + +API = Namespace("regions", description="Regions") + + +@cors_preflight("GET") +@API.route("", methods=["GET", "OPTIONS"]) +class Regions(Resource): + """Endpoint resource to return regions based on region type.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True) + def get(): + """Return all regions based on region type.""" + req.RegionTypePathParameterSchema().load(request.args) + region_type = request.args.get("type", None) + regions = RegionService.find_regions_by_type(region_type) + return jsonify(RegionSchema(many=True).dump(regions)), HTTPStatus.OK diff --git a/epictrack-api/src/api/resources/user.py b/epictrack-api/src/api/resources/user.py index 4b14f428f..f7554908c 100644 --- a/epictrack-api/src/api/resources/user.py +++ b/epictrack-api/src/api/resources/user.py @@ -1,77 +1,77 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""User resource""" -from http import HTTPStatus - -from flask import jsonify, request -from flask_restx import Namespace, Resource, cors -from api.exceptions import BusinessError -from api.schemas import request as req -from api.schemas import response as res -from api.services import UserService -from api.utils import auth, profiletime -from api.utils.roles import Role -from api.utils.util import cors_preflight - -API = Namespace("users", description="Users") - - -@cors_preflight("GET") -@API.route("", methods=["GET", "OPTIONS"]) -class Users(Resource): - """Users resource""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.has_one_of_roles([Role.MANAGE_USERS.value]) - @profiletime - def get(): - """Get all users""" - user_schema = res.UserResponseSchema(many=True) - return jsonify(user_schema.dump(UserService.get_all_users())), HTTPStatus.OK - - -@cors_preflight("GET") -@API.route("/groups", methods=["GET", "OPTIONS"]) -class Groups(Resource): - """Group resource""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.has_one_of_roles([Role.MANAGE_USERS.value]) - @profiletime - def get(): - """Get all groups""" - reponse_schema = res.UserGroupResponseSchema(many=True) - return jsonify(reponse_schema.dump(UserService.get_groups())), HTTPStatus.OK - - -@cors_preflight("PUT") -@API.route("//groups", methods=["PUT", "OPTIONS"]) -class UserGroups(Resource): - """UserGroup resource""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.has_one_of_roles([Role.MANAGE_USERS.value]) - @profiletime - def put(user_id): - """Update the group of the user""" - req.UserGroupPathParamSchema().load(request.view_args) - user_group_request = req.UserGroupBodyParamSchema().load(API.payload) - - response = UserService.update_user_group(user_id, user_group_request) - if response.status_code == 204: - return '', HTTPStatus.NO_CONTENT - raise BusinessError('Update failed', 500) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""User resource""" +from http import HTTPStatus + +from flask import jsonify, request +from flask_restx import Namespace, Resource, cors +from api.exceptions import BusinessError +from api.schemas import request as req +from api.schemas import response as res +from api.services import UserService +from api.utils import auth, profiletime +from api.utils.roles import Role +from api.utils.util import cors_preflight + +API = Namespace("users", description="Users") + + +@cors_preflight("GET") +@API.route("", methods=["GET", "OPTIONS"]) +class Users(Resource): + """Users resource""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.has_one_of_roles([Role.MANAGE_USERS.value]) + @profiletime + def get(): + """Get all users""" + user_schema = res.UserResponseSchema(many=True) + return jsonify(user_schema.dump(UserService.get_all_users())), HTTPStatus.OK + + +@cors_preflight("GET") +@API.route("/groups", methods=["GET", "OPTIONS"]) +class Groups(Resource): + """Group resource""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.has_one_of_roles([Role.MANAGE_USERS.value]) + @profiletime + def get(): + """Get all groups""" + reponse_schema = res.UserGroupResponseSchema(many=True) + return jsonify(reponse_schema.dump(UserService.get_groups())), HTTPStatus.OK + + +@cors_preflight("PUT") +@API.route("//groups", methods=["PUT", "OPTIONS"]) +class UserGroups(Resource): + """UserGroup resource""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.has_one_of_roles([Role.MANAGE_USERS.value]) + @profiletime + def put(user_id): + """Update the group of the user""" + req.UserGroupPathParamSchema().load(request.view_args) + user_group_request = req.UserGroupBodyParamSchema().load(API.payload) + + response = UserService.update_user_group(user_id, user_group_request) + if response.status_code == 204: + return '', HTTPStatus.NO_CONTENT + raise BusinessError('Update failed', 500) diff --git a/epictrack-api/src/api/resources/work_issues.py b/epictrack-api/src/api/resources/work_issues.py index 714e8de49..8649b87da 100644 --- a/epictrack-api/src/api/resources/work_issues.py +++ b/epictrack-api/src/api/resources/work_issues.py @@ -1,117 +1,117 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for work issues endpoints.""" -from http import HTTPStatus - -from flask import jsonify -from flask_restx import Namespace, Resource, cors - -from api.schemas import request as req -from api.schemas import response as res -from api.services import WorkIssuesService -from api.utils import auth, profiletime -from api.utils.util import cors_preflight - -API = Namespace("work-issues", description="Work Issues") - - -@cors_preflight("GET, POST") -@API.route("", methods=["GET", "POST", "OPTIONS"]) -class WorkStatus(Resource): - """Endpoint resource to manage work issues.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - def get(work_id): - """Return all active works.""" - works = WorkIssuesService.find_all_work_issues(work_id) - return jsonify(res.WorkIssuesResponseSchema(many=True).dump(works)), HTTPStatus.OK - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(work_id): - """Create new work status""" - request_dict = req.WorkIssuesCreateParameterSchema().load(API.payload) - work_issues = WorkIssuesService.create_work_issue_and_updates(work_id, request_dict) - return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED - - -@cors_preflight("PATCH") -@API.route("/", methods=["PATCH", "OPTIONS"]) -class IssueUpdateEdits(Resource): - """Endpoint resource to manage updates/edits for a specific issue.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def patch(work_id, issue_id): - """Create a new update for the specified issue.""" - request_dict = req.WorkIssuesParameterSchema().load(API.payload) - work_issues = WorkIssuesService.edit_issue(work_id, issue_id, request_dict) - return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED - - -@cors_preflight("POST") -@API.route("//update", methods=["POST", "OPTIONS"]) -class WorkIssueUpdate(Resource): - """Endpoint resource to manage updates for a specific issue.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @profiletime - def post(work_id, issue_id): - """Create a new update for the specified issue.""" - request_dict = req.WorkIssuesUpdateCloneSchema().load(API.payload) - work_issues = WorkIssuesService.add_work_issue_update(work_id, issue_id, request_dict) - return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED - - -@cors_preflight("PATCH") -@API.route("//update/", methods=["PATCH", "OPTIONS"]) -class EditIssues(Resource): - """Endpoint resource to manage approving of work status.""" - - @staticmethod - @cors.crossdomain(origin="*") - @profiletime - @auth.require - # pylint: disable=unused-argument - def patch(work_id, issue_id, update_id): - """Approve a work status.""" - request_dict = req.WorkIssuesUpdateEditSchema().load(API.payload) - edited_issue_update = WorkIssuesService.edit_issue_update(issue_id, update_id, request_dict) - - return res.WorkIssueUpdatesResponseSchema().dump(edited_issue_update), HTTPStatus.OK - - -@cors_preflight("PATCH") -@API.route("//update//approve", methods=["PATCH", "OPTIONS"]) -class ApproveIssueUpdate(Resource): - """Endpoint resource to manage approving of work status.""" - - @staticmethod - @cors.crossdomain(origin="*") - @profiletime - @auth.require - # pylint: disable=unused-argument - def patch(work_id, issue_id, update_id): - """Approve a work status.""" - approved_work_issues = WorkIssuesService.approve_work_issues(issue_id, update_id) - - return res.WorkIssueUpdatesResponseSchema().dump(approved_work_issues), HTTPStatus.OK +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for work issues endpoints.""" +from http import HTTPStatus + +from flask import jsonify +from flask_restx import Namespace, Resource, cors + +from api.schemas import request as req +from api.schemas import response as res +from api.services import WorkIssuesService +from api.utils import auth, profiletime +from api.utils.util import cors_preflight + +API = Namespace("work-issues", description="Work Issues") + + +@cors_preflight("GET, POST") +@API.route("", methods=["GET", "POST", "OPTIONS"]) +class WorkStatus(Resource): + """Endpoint resource to manage work issues.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + def get(work_id): + """Return all active works.""" + works = WorkIssuesService.find_all_work_issues(work_id) + return jsonify(res.WorkIssuesResponseSchema(many=True).dump(works)), HTTPStatus.OK + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(work_id): + """Create new work status""" + request_dict = req.WorkIssuesCreateParameterSchema().load(API.payload) + work_issues = WorkIssuesService.create_work_issue_and_updates(work_id, request_dict) + return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED + + +@cors_preflight("PATCH") +@API.route("/", methods=["PATCH", "OPTIONS"]) +class IssueUpdateEdits(Resource): + """Endpoint resource to manage updates/edits for a specific issue.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def patch(work_id, issue_id): + """Create a new update for the specified issue.""" + request_dict = req.WorkIssuesParameterSchema().load(API.payload) + work_issues = WorkIssuesService.edit_issue(work_id, issue_id, request_dict) + return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED + + +@cors_preflight("POST") +@API.route("//update", methods=["POST", "OPTIONS"]) +class WorkIssueUpdate(Resource): + """Endpoint resource to manage updates for a specific issue.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @profiletime + def post(work_id, issue_id): + """Create a new update for the specified issue.""" + request_dict = req.WorkIssuesUpdateCloneSchema().load(API.payload) + work_issues = WorkIssuesService.add_work_issue_update(work_id, issue_id, request_dict) + return res.WorkIssuesResponseSchema().dump(work_issues), HTTPStatus.CREATED + + +@cors_preflight("PATCH") +@API.route("//update/", methods=["PATCH", "OPTIONS"]) +class EditIssues(Resource): + """Endpoint resource to manage approving of work status.""" + + @staticmethod + @cors.crossdomain(origin="*") + @profiletime + @auth.require + # pylint: disable=unused-argument + def patch(work_id, issue_id, update_id): + """Approve a work status.""" + request_dict = req.WorkIssuesUpdateEditSchema().load(API.payload) + edited_issue_update = WorkIssuesService.edit_issue_update(issue_id, update_id, request_dict) + + return res.WorkIssueUpdatesResponseSchema().dump(edited_issue_update), HTTPStatus.OK + + +@cors_preflight("PATCH") +@API.route("//update//approve", methods=["PATCH", "OPTIONS"]) +class ApproveIssueUpdate(Resource): + """Endpoint resource to manage approving of work status.""" + + @staticmethod + @cors.crossdomain(origin="*") + @profiletime + @auth.require + # pylint: disable=unused-argument + def patch(work_id, issue_id, update_id): + """Approve a work status.""" + approved_work_issues = WorkIssuesService.approve_work_issues(issue_id, update_id) + + return res.WorkIssueUpdatesResponseSchema().dump(approved_work_issues), HTTPStatus.OK diff --git a/epictrack-api/src/api/resources/work_type.py b/epictrack-api/src/api/resources/work_type.py index 7cfcd83c1..c5e5f5722 100644 --- a/epictrack-api/src/api/resources/work_type.py +++ b/epictrack-api/src/api/resources/work_type.py @@ -1,42 +1,42 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Resource for work endpoints.""" -from http import HTTPStatus - -from flask import jsonify -from flask_restx import Namespace, Resource, cors - -from api.services import WorkService -from api.utils import auth, constants, profiletime -from api.utils.caching import AppCache -from api.utils.util import cors_preflight - - -API = Namespace("work-types", description="Work types") - - -@cors_preflight("GET") -@API.route("", methods=["GET", "OPTIONS"]) -class WorkTypes(Resource): - """Endpoint resource to manage works.""" - - @staticmethod - @cors.crossdomain(origin="*") - @auth.require - @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT) - @profiletime - def get(): - """Return all active work types.""" - work_types = WorkService.find_all_work_types() - return jsonify(work_types), HTTPStatus.OK +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resource for work endpoints.""" +from http import HTTPStatus + +from flask import jsonify +from flask_restx import Namespace, Resource, cors + +from api.services import WorkService +from api.utils import auth, constants, profiletime +from api.utils.caching import AppCache +from api.utils.util import cors_preflight + + +API = Namespace("work-types", description="Work types") + + +@cors_preflight("GET") +@API.route("", methods=["GET", "OPTIONS"]) +class WorkTypes(Resource): + """Endpoint resource to manage works.""" + + @staticmethod + @cors.crossdomain(origin="*") + @auth.require + @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT) + @profiletime + def get(): + """Return all active work types.""" + work_types = WorkService.find_all_work_types() + return jsonify(work_types), HTTPStatus.OK diff --git a/epictrack-api/src/api/schemas/request/__init__.py b/epictrack-api/src/api/schemas/request/__init__.py index 8b48b6427..1fe577dbf 100644 --- a/epictrack-api/src/api/schemas/request/__init__.py +++ b/epictrack-api/src/api/schemas/request/__init__.py @@ -1,115 +1,115 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Exposes all the request validation schemas""" -from api.schemas.request.region_request import RegionTypePathParameterSchema - -from .act_section_request import ActSectionQueryParameterSchema -from .action_configuration_request import ActionConfigurationBodyParameterSchema -from .action_template_request import ActionTemplateBodyParameterSchema -from .base import BasicRequestQueryParameterSchema -from .event_configuration_request import ( - EventConfigurationBodyParamSchema, - EventConfigurationQueryParamSchema, -) -from .event_request import ( - MilestoneEventBodyParameterSchema, - MilestoneEventBulkDeleteQueryParamSchema, - MilestoneEventCheckQueryParameterSchema, - MilestoneEventPathParameterSchema, - MilestoneEventPushEventQueryParameterSchema, -) -from .event_template_request import EventTemplateBodyParameterSchema -from .indigenous_nation_request import ( - IndigenousNationBodyParameterSchema, - IndigenousNationExistenceQueryParamSchema, - IndigenousNationIdPathParameterSchema, - IndigenousWorkBodyParameterSchema, - WorkIndigenousNationIdPathParameterSchema, - WorkNationExistenceCheckQueryParamSchema, -) -from .insight_request import ( - ProjectInsightRequestQueryParameterSchema, - WorkInsightRequestQueryParameterSchema, -) -from .outcome_configuration_request import ( - OutcomeConfigurationBodyParameterSchema, - OutcomeConfigurationQueryParameterSchema, -) -from .outcome_template_request import OutcomeTemplateBodyParameterSchema -from .phase_request import PhaseBodyParameterSchema -from .project_request import ( - ProjectAbbreviationParameterSchema, - ProjectBodyParameterSchema, - ProjectExistenceQueryParamSchema, - ProjectFirstNationsQueryParamSchema, - ProjectIdPathParameterSchema, -) -from .proponent_request import ( - ProponentBodyParameterSchema, - ProponentExistenceQueryParamSchema, - ProponentIdPathParameterSchema, -) -from .reminder_configuration_request import ( - ReminderConfigurationExistenceQueryParamSchema, -) -from .special_field_request import ( - SpecialFieldBodyParameterSchema, - SpecialFieldIdPathParameterSchema, - SpecialFieldQueryParamSchema, -) -from .staff_request import ( - StaffBodyParameterSchema, - StaffByPositionsQueryParamSchema, - StaffEmailPathParameterSchema, - StaffExistanceQueryParamSchema, - StaffIdPathParameterSchema, -) -from .staff_work_role_request import ( - StaffWorkBodyParamSchema, - StaffWorkExistenceCheckQueryParamSchema, - StaffWorkPathParamSchema, -) -from .task_request import ( - TaskTemplateBodyParameterSchema, - TaskTemplateIdPathParameterSchema, - TaskTemplateImportEventsBodyParamSchema, - TaskBodyParameterSchema, - CopyTaskEventBodyParameterSchema, - TaskEventBodyParamSchema, - TaskEventBulkUpdateBodyParamSchema, - TaskEventIdPathParameterSchema, - TaskEventQueryParamSchema, - TasksBulkDeleteQueryParamSchema, - TaskTemplateQueryParamSchema, - TaskEventByStaffQueryParamSchema, -) -from .type_request import TypeIdPathParameterSchema -from .user_group_request import UserGroupBodyParamSchema, UserGroupPathParamSchema -from .work_request import ( - WorkBodyParameterSchema, - WorkExistenceQueryParamSchema, - WorkFirstNationImportBodyParamSchema, - WorkFirstNationNotesBodySchema, - WorkIdPathParameterSchema, - WorkIdPhaseIdPathParameterSchema, - WorkIssuesCreateParameterSchema, - WorkIssuesParameterSchema, - WorkIssuesUpdateCloneSchema, - WorkIssuesUpdateEditSchema, - WorkNotesBodySchema, - WorkPlanDownloadQueryParamSchema, - WorkStatusParameterSchema, - WorkTypeIdQueryParamSchema, - WorkQueryParameterSchema, -) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Exposes all the request validation schemas""" +from api.schemas.request.region_request import RegionTypePathParameterSchema + +from .act_section_request import ActSectionQueryParameterSchema +from .action_configuration_request import ActionConfigurationBodyParameterSchema +from .action_template_request import ActionTemplateBodyParameterSchema +from .base import BasicRequestQueryParameterSchema +from .event_configuration_request import ( + EventConfigurationBodyParamSchema, + EventConfigurationQueryParamSchema, +) +from .event_request import ( + MilestoneEventBodyParameterSchema, + MilestoneEventBulkDeleteQueryParamSchema, + MilestoneEventCheckQueryParameterSchema, + MilestoneEventPathParameterSchema, + MilestoneEventPushEventQueryParameterSchema, +) +from .event_template_request import EventTemplateBodyParameterSchema +from .indigenous_nation_request import ( + IndigenousNationBodyParameterSchema, + IndigenousNationExistenceQueryParamSchema, + IndigenousNationIdPathParameterSchema, + IndigenousWorkBodyParameterSchema, + WorkIndigenousNationIdPathParameterSchema, + WorkNationExistenceCheckQueryParamSchema, +) +from .insight_request import ( + ProjectInsightRequestQueryParameterSchema, + WorkInsightRequestQueryParameterSchema, +) +from .outcome_configuration_request import ( + OutcomeConfigurationBodyParameterSchema, + OutcomeConfigurationQueryParameterSchema, +) +from .outcome_template_request import OutcomeTemplateBodyParameterSchema +from .phase_request import PhaseBodyParameterSchema +from .project_request import ( + ProjectAbbreviationParameterSchema, + ProjectBodyParameterSchema, + ProjectExistenceQueryParamSchema, + ProjectFirstNationsQueryParamSchema, + ProjectIdPathParameterSchema, +) +from .proponent_request import ( + ProponentBodyParameterSchema, + ProponentExistenceQueryParamSchema, + ProponentIdPathParameterSchema, +) +from .reminder_configuration_request import ( + ReminderConfigurationExistenceQueryParamSchema, +) +from .special_field_request import ( + SpecialFieldBodyParameterSchema, + SpecialFieldIdPathParameterSchema, + SpecialFieldQueryParamSchema, +) +from .staff_request import ( + StaffBodyParameterSchema, + StaffByPositionsQueryParamSchema, + StaffEmailPathParameterSchema, + StaffExistanceQueryParamSchema, + StaffIdPathParameterSchema, +) +from .staff_work_role_request import ( + StaffWorkBodyParamSchema, + StaffWorkExistenceCheckQueryParamSchema, + StaffWorkPathParamSchema, +) +from .task_request import ( + TaskTemplateBodyParameterSchema, + TaskTemplateIdPathParameterSchema, + TaskTemplateImportEventsBodyParamSchema, + TaskBodyParameterSchema, + CopyTaskEventBodyParameterSchema, + TaskEventBodyParamSchema, + TaskEventBulkUpdateBodyParamSchema, + TaskEventIdPathParameterSchema, + TaskEventQueryParamSchema, + TasksBulkDeleteQueryParamSchema, + TaskTemplateQueryParamSchema, + TaskEventByStaffQueryParamSchema, +) +from .type_request import TypeIdPathParameterSchema +from .user_group_request import UserGroupBodyParamSchema, UserGroupPathParamSchema +from .work_request import ( + WorkBodyParameterSchema, + WorkExistenceQueryParamSchema, + WorkFirstNationImportBodyParamSchema, + WorkFirstNationNotesBodySchema, + WorkIdPathParameterSchema, + WorkIdPhaseIdPathParameterSchema, + WorkIssuesCreateParameterSchema, + WorkIssuesParameterSchema, + WorkIssuesUpdateCloneSchema, + WorkIssuesUpdateEditSchema, + WorkNotesBodySchema, + WorkPlanDownloadQueryParamSchema, + WorkStatusParameterSchema, + WorkTypeIdQueryParamSchema, + WorkQueryParameterSchema, +) diff --git a/epictrack-api/src/api/schemas/request/act_section_request.py b/epictrack-api/src/api/schemas/request/act_section_request.py index 82da0d7a6..ec39ddf39 100644 --- a/epictrack-api/src/api/schemas/request/act_section_request.py +++ b/epictrack-api/src/api/schemas/request/act_section_request.py @@ -1,26 +1,26 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Act Sections resource's input validations""" -from marshmallow import fields, validate - -from .base import BasicRequestQueryParameterSchema - - -class ActSectionQueryParameterSchema(BasicRequestQueryParameterSchema): - """Type id path parameter schema""" - - ea_act_id = fields.Int( - metadata={"description": "The id of the EA Act"}, - validate=validate.Range(min=1) - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Act Sections resource's input validations""" +from marshmallow import fields, validate + +from .base import BasicRequestQueryParameterSchema + + +class ActSectionQueryParameterSchema(BasicRequestQueryParameterSchema): + """Type id path parameter schema""" + + ea_act_id = fields.Int( + metadata={"description": "The id of the EA Act"}, + validate=validate.Range(min=1) + ) diff --git a/epictrack-api/src/api/schemas/request/action_configuration_request.py b/epictrack-api/src/api/schemas/request/action_configuration_request.py index 442ed3927..bae75e862 100644 --- a/epictrack-api/src/api/schemas/request/action_configuration_request.py +++ b/epictrack-api/src/api/schemas/request/action_configuration_request.py @@ -1,47 +1,47 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Action Configuration resource's input validations""" -from marshmallow import fields - -from .base import RequestBodyParameterSchema - - -class ActionConfigurationBodyParameterSchema(RequestBodyParameterSchema): - """ActionTemplate body schema""" - - action_id = fields.Int( - metadata={"description": "Id of the action"}, - required=True - ) - - outcome_configuration_id = fields.Int( - metadata={"description": "Outcome configuration id item"}, - required=True - ) - - action_template_id = fields.Int( - metadata={"description": "Action template Id corresponding to the configuration"} - ) - - additional_params = fields.Raw( - metadata={"description": "Additional parameters for the action"} - ) - - description = fields.Str( - metadata={"description": "Description of the action"} - ) - - sort_order = fields.Int( - metadata={"description": "Sort order of the event template item"} - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Action Configuration resource's input validations""" +from marshmallow import fields + +from .base import RequestBodyParameterSchema + + +class ActionConfigurationBodyParameterSchema(RequestBodyParameterSchema): + """ActionTemplate body schema""" + + action_id = fields.Int( + metadata={"description": "Id of the action"}, + required=True + ) + + outcome_configuration_id = fields.Int( + metadata={"description": "Outcome configuration id item"}, + required=True + ) + + action_template_id = fields.Int( + metadata={"description": "Action template Id corresponding to the configuration"} + ) + + additional_params = fields.Raw( + metadata={"description": "Additional parameters for the action"} + ) + + description = fields.Str( + metadata={"description": "Description of the action"} + ) + + sort_order = fields.Int( + metadata={"description": "Sort order of the event template item"} + ) diff --git a/epictrack-api/src/api/schemas/request/action_template_request.py b/epictrack-api/src/api/schemas/request/action_template_request.py index e9b2e38c8..07ec559ab 100644 --- a/epictrack-api/src/api/schemas/request/action_template_request.py +++ b/epictrack-api/src/api/schemas/request/action_template_request.py @@ -1,43 +1,43 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Action Template resource's input validations""" -from marshmallow import fields - -from .base import RequestBodyParameterSchema - - -class ActionTemplateBodyParameterSchema(RequestBodyParameterSchema): - """ActionTemplate body schema""" - - action_id = fields.Int( - metadata={"description": "Id of the action"}, - required=True - ) - - outcome_id = fields.Int( - metadata={"description": "outcome id item"}, - required=True - ) - - additional_params = fields.Raw( - metadata={"description": "Additional parameters for the action"} - ) - - description = fields.Str( - metadata={"description": "Description of the action"} - ) - - sort_order = fields.Int( - metadata={"description": "Sort order of the event template item"} - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Action Template resource's input validations""" +from marshmallow import fields + +from .base import RequestBodyParameterSchema + + +class ActionTemplateBodyParameterSchema(RequestBodyParameterSchema): + """ActionTemplate body schema""" + + action_id = fields.Int( + metadata={"description": "Id of the action"}, + required=True + ) + + outcome_id = fields.Int( + metadata={"description": "outcome id item"}, + required=True + ) + + additional_params = fields.Raw( + metadata={"description": "Additional parameters for the action"} + ) + + description = fields.Str( + metadata={"description": "Description of the action"} + ) + + sort_order = fields.Int( + metadata={"description": "Sort order of the event template item"} + ) diff --git a/epictrack-api/src/api/schemas/request/base.py b/epictrack-api/src/api/schemas/request/base.py index c6f9cb0c5..c4b5de392 100644 --- a/epictrack-api/src/api/schemas/request/base.py +++ b/epictrack-api/src/api/schemas/request/base.py @@ -1,64 +1,64 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Base schema for path parameter validation""" -import json - -from marshmallow import EXCLUDE -from marshmallow import Schema as MarshmallowSchema -from marshmallow import fields, pre_load - -from api.exceptions import BadRequestError - - -class Schema(MarshmallowSchema): - """Base Schema""" - - def handle_error(self, error, data, **kwargs): - """Log and raise our custom exception when validation fails.""" - raise BadRequestError(json.dumps(error.messages)) - - -class RequestPathParameterSchema(Schema): - """Request path parameter schema base class""" - - -class RequestBodyParameterSchema(Schema): # pylint: disable=too-few-public-methods - """Request body parameter schema base class""" - - class Meta: # pylint: disable=too-few-public-methods - """Meta""" - - unknown = EXCLUDE - - @pre_load - def parse_empty_string(self, data, **kwargs): # pylint: disable=unused-argument - """Parse the input and convert empty strings to None for integer fields""" - for field in data: - if ( - data[field] == "" and isinstance(self.load_fields.get(field, None), fields.Integer) - ): - data[field] = None - return data - - -class RequestQueryParameterSchema(Schema): - """Request query parameter schema base class""" - - -class BasicRequestQueryParameterSchema(RequestQueryParameterSchema): - """Request query parameter schema for basic query""" - - is_active = fields.Bool( - metadata={"description": "Active/Inactive"}, - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Base schema for path parameter validation""" +import json + +from marshmallow import EXCLUDE +from marshmallow import Schema as MarshmallowSchema +from marshmallow import fields, pre_load + +from api.exceptions import BadRequestError + + +class Schema(MarshmallowSchema): + """Base Schema""" + + def handle_error(self, error, data, **kwargs): + """Log and raise our custom exception when validation fails.""" + raise BadRequestError(json.dumps(error.messages)) + + +class RequestPathParameterSchema(Schema): + """Request path parameter schema base class""" + + +class RequestBodyParameterSchema(Schema): # pylint: disable=too-few-public-methods + """Request body parameter schema base class""" + + class Meta: # pylint: disable=too-few-public-methods + """Meta""" + + unknown = EXCLUDE + + @pre_load + def parse_empty_string(self, data, **kwargs): # pylint: disable=unused-argument + """Parse the input and convert empty strings to None for integer fields""" + for field in data: + if ( + data[field] == "" and isinstance(self.load_fields.get(field, None), fields.Integer) + ): + data[field] = None + return data + + +class RequestQueryParameterSchema(Schema): + """Request query parameter schema base class""" + + +class BasicRequestQueryParameterSchema(RequestQueryParameterSchema): + """Request query parameter schema for basic query""" + + is_active = fields.Bool( + metadata={"description": "Active/Inactive"}, + ) diff --git a/epictrack-api/src/api/schemas/request/custom_fields.py b/epictrack-api/src/api/schemas/request/custom_fields.py index f28dd2d85..c94d3b9c0 100644 --- a/epictrack-api/src/api/schemas/request/custom_fields.py +++ b/epictrack-api/src/api/schemas/request/custom_fields.py @@ -1,64 +1,64 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Custom marshmallow fields""" -from typing import Any, Mapping, Union - -from marshmallow import ValidationError, fields - - -class IntegerList(fields.Field): - """List of comma separated values""" - - def _deserialize( - self, - value: Any, - attr: Union[str, None], - data: Union[Mapping[str, Any], None], - **kwargs, - ): - try: - return [int(v.strip()) for v in value.split(",")] - except ValueError as err: - raise ValidationError(str(err)) from err - - -class CommaSeparatedEnumList(fields.Field): - """Custom marshmallow field for comma separated enum list""" - - def __init__(self, enum, **kwargs): - """Init""" - self.enum = enum - super().__init__(**kwargs) - - def _deserialize(self, value, attr, data, **kwargs): - """Deserialize""" - if not value: - return [] - - if not isinstance(value, str): - raise ValidationError("Invalid input type.") - - values = value.split(",") - enum_values = [] - - for val in values: - try: - enum_value = val - enum_values.append(enum_value) - except ValueError as exc: - raise ValidationError( - f"'{val}' is not a valid {self.enum.__name__} value." - ) from exc - - return enum_values +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Custom marshmallow fields""" +from typing import Any, Mapping, Union + +from marshmallow import ValidationError, fields + + +class IntegerList(fields.Field): + """List of comma separated values""" + + def _deserialize( + self, + value: Any, + attr: Union[str, None], + data: Union[Mapping[str, Any], None], + **kwargs, + ): + try: + return [int(v.strip()) for v in value.split(",")] + except ValueError as err: + raise ValidationError(str(err)) from err + + +class CommaSeparatedEnumList(fields.Field): + """Custom marshmallow field for comma separated enum list""" + + def __init__(self, enum, **kwargs): + """Init""" + self.enum = enum + super().__init__(**kwargs) + + def _deserialize(self, value, attr, data, **kwargs): + """Deserialize""" + if not value: + return [] + + if not isinstance(value, str): + raise ValidationError("Invalid input type.") + + values = value.split(",") + enum_values = [] + + for val in values: + try: + enum_value = val + enum_values.append(enum_value) + except ValueError as exc: + raise ValidationError( + f"'{val}' is not a valid {self.enum.__name__} value." + ) from exc + + return enum_values diff --git a/epictrack-api/src/api/schemas/request/event_template_request.py b/epictrack-api/src/api/schemas/request/event_template_request.py index daf73c4d4..d15f201de 100644 --- a/epictrack-api/src/api/schemas/request/event_template_request.py +++ b/epictrack-api/src/api/schemas/request/event_template_request.py @@ -1,78 +1,78 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Event Template resource's input validations""" -from marshmallow import fields, validate - -from .base import RequestBodyParameterSchema - - -class EventTemplateBodyParameterSchema(RequestBodyParameterSchema): - """EventTemplate body schema""" - - name = fields.Str( - metadata={"description": "Name of the event"}, - required=True - ) - - parent_id = fields.Int( - metadata={"description": "parent event template id item"}, - allow_none=True - ) - - phase_id = fields.Int( - metadata={"description": "Phase id of the event"}, - validate=validate.Range(min=1), - required=True - ) - - event_type_id = fields.Int( - metadata={"description": "Event type of the event template"}, - validate=validate.Range(min=1), - required=True - ) - - event_category_id = fields.Int( - metadata={"description": "Event category of the event template"}, - validate=validate.Range(min=1), - required=True - ) - - start_at = fields.Str( - metadata={"description": "Number of days after which the event to be started"} - ) - - number_of_days = fields.Int( - metadata={"description": "Duration of the event"}, - allow_none=True - ) - - mandatory = fields.Bool( - metadata={"description": "Indicate the event is a mandatory one or not"} - ) - - sort_order = fields.Int( - metadata={"description": "Sort order of the event template item"} - ) - - event_position = fields.Str( - metadata={"description": "position of the event in the phase"} - ) - - multiple_days = fields.Bool( - metadata={"description": "Indicate if it is a multi day event"} - ) - - visibility = fields.Str( - metadata={"description": "Indicate whether the event to be shown in the workplan or not"} - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Event Template resource's input validations""" +from marshmallow import fields, validate + +from .base import RequestBodyParameterSchema + + +class EventTemplateBodyParameterSchema(RequestBodyParameterSchema): + """EventTemplate body schema""" + + name = fields.Str( + metadata={"description": "Name of the event"}, + required=True + ) + + parent_id = fields.Int( + metadata={"description": "parent event template id item"}, + allow_none=True + ) + + phase_id = fields.Int( + metadata={"description": "Phase id of the event"}, + validate=validate.Range(min=1), + required=True + ) + + event_type_id = fields.Int( + metadata={"description": "Event type of the event template"}, + validate=validate.Range(min=1), + required=True + ) + + event_category_id = fields.Int( + metadata={"description": "Event category of the event template"}, + validate=validate.Range(min=1), + required=True + ) + + start_at = fields.Str( + metadata={"description": "Number of days after which the event to be started"} + ) + + number_of_days = fields.Int( + metadata={"description": "Duration of the event"}, + allow_none=True + ) + + mandatory = fields.Bool( + metadata={"description": "Indicate the event is a mandatory one or not"} + ) + + sort_order = fields.Int( + metadata={"description": "Sort order of the event template item"} + ) + + event_position = fields.Str( + metadata={"description": "position of the event in the phase"} + ) + + multiple_days = fields.Bool( + metadata={"description": "Indicate if it is a multi day event"} + ) + + visibility = fields.Str( + metadata={"description": "Indicate whether the event to be shown in the workplan or not"} + ) diff --git a/epictrack-api/src/api/schemas/request/outcome_configuration_request.py b/epictrack-api/src/api/schemas/request/outcome_configuration_request.py index 8ab895a2a..86ec58b01 100644 --- a/epictrack-api/src/api/schemas/request/outcome_configuration_request.py +++ b/epictrack-api/src/api/schemas/request/outcome_configuration_request.py @@ -1,49 +1,49 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Outcome Configuration resource's input validations""" -from marshmallow import fields, validate - -from .base import RequestBodyParameterSchema, RequestQueryParameterSchema - - -class OutcomeConfigurationQueryParameterSchema(RequestQueryParameterSchema): - """Outcome Configuration query by configuration id""" - - configuration_id = fields.Int( - metadata={"description": "Event configuration id"}, - required=True, - validate=validate.Range(min=1) - ) - - -class OutcomeConfigurationBodyParameterSchema(RequestBodyParameterSchema): - """Outcome Configuration body schema""" - - name = fields.Str( - metadata={"description": "Name of the outcome"}, - required=True - ) - - event_configuration_id = fields.Int( - metadata={"description": "Configuration id item"}, - required=True - ) - - outcome_template_id = fields.Int( - metadata={"description": "Template id correspnding to the configuration"} - ) - - sort_order = fields.Int( - metadata={"description": "Sort order of the event template item"} - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Outcome Configuration resource's input validations""" +from marshmallow import fields, validate + +from .base import RequestBodyParameterSchema, RequestQueryParameterSchema + + +class OutcomeConfigurationQueryParameterSchema(RequestQueryParameterSchema): + """Outcome Configuration query by configuration id""" + + configuration_id = fields.Int( + metadata={"description": "Event configuration id"}, + required=True, + validate=validate.Range(min=1) + ) + + +class OutcomeConfigurationBodyParameterSchema(RequestBodyParameterSchema): + """Outcome Configuration body schema""" + + name = fields.Str( + metadata={"description": "Name of the outcome"}, + required=True + ) + + event_configuration_id = fields.Int( + metadata={"description": "Configuration id item"}, + required=True + ) + + outcome_template_id = fields.Int( + metadata={"description": "Template id correspnding to the configuration"} + ) + + sort_order = fields.Int( + metadata={"description": "Sort order of the event template item"} + ) diff --git a/epictrack-api/src/api/schemas/request/outcome_template_request.py b/epictrack-api/src/api/schemas/request/outcome_template_request.py index 152d9bcdf..d2060e0ab 100644 --- a/epictrack-api/src/api/schemas/request/outcome_template_request.py +++ b/epictrack-api/src/api/schemas/request/outcome_template_request.py @@ -1,35 +1,35 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Outcome Template resource's input validations""" -from marshmallow import fields - -from .base import RequestBodyParameterSchema - - -class OutcomeTemplateBodyParameterSchema(RequestBodyParameterSchema): - """EventTemplate body schema""" - - name = fields.Str( - metadata={"description": "Name of the outcome"}, - required=True - ) - - event_template_id = fields.Int( - metadata={"description": "template id item"}, - required=True - ) - - sort_order = fields.Int( - metadata={"description": "Sort order of the event template item"} - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Outcome Template resource's input validations""" +from marshmallow import fields + +from .base import RequestBodyParameterSchema + + +class OutcomeTemplateBodyParameterSchema(RequestBodyParameterSchema): + """EventTemplate body schema""" + + name = fields.Str( + metadata={"description": "Name of the outcome"}, + required=True + ) + + event_template_id = fields.Int( + metadata={"description": "template id item"}, + required=True + ) + + sort_order = fields.Int( + metadata={"description": "Sort order of the event template item"} + ) diff --git a/epictrack-api/src/api/schemas/request/phase_request.py b/epictrack-api/src/api/schemas/request/phase_request.py index 1833e4c7e..96dcf6d59 100644 --- a/epictrack-api/src/api/schemas/request/phase_request.py +++ b/epictrack-api/src/api/schemas/request/phase_request.py @@ -1,66 +1,66 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Phase resource's input validations""" -from marshmallow import fields, validate - -from .base import RequestBodyParameterSchema - - -class PhaseBodyParameterSchema(RequestBodyParameterSchema): - """Phase request body schema""" - - name = fields.Str( - metadata={"description": "Name of phase"}, - validate=validate.Length(max=250), - required=True, - ) - - work_type_id = fields.Int( - metadata={"description": "Work type of the phase"}, - validate=validate.Range(min=1), - required=True - ) - - ea_act_id = fields.Int( - metadata={"description": "EA Act of the phase"}, - validate=validate.Range(min=1), - required=True - ) - - number_of_days = fields.Int( - metadata={"description": "Number of days of the phase"} - ) - - legislated = fields.Bool( - metadata={"description": "Indicate if the phase is legislated or not"}, - required=True - ) - - sort_order = fields.Int( - metadata={"description": "Order of the phase"} - ) - - color = fields.String( - metadata={"dscription": "Color of the phase"}, - required=True - ) - - visibility = fields.String( - metadata={"description": "Indicate if the phase is visible in the work plan"}, - required=True, - ) - - is_active = fields.Bool( - metadata={"description": "Active state of the task"}, - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Phase resource's input validations""" +from marshmallow import fields, validate + +from .base import RequestBodyParameterSchema + + +class PhaseBodyParameterSchema(RequestBodyParameterSchema): + """Phase request body schema""" + + name = fields.Str( + metadata={"description": "Name of phase"}, + validate=validate.Length(max=250), + required=True, + ) + + work_type_id = fields.Int( + metadata={"description": "Work type of the phase"}, + validate=validate.Range(min=1), + required=True + ) + + ea_act_id = fields.Int( + metadata={"description": "EA Act of the phase"}, + validate=validate.Range(min=1), + required=True + ) + + number_of_days = fields.Int( + metadata={"description": "Number of days of the phase"} + ) + + legislated = fields.Bool( + metadata={"description": "Indicate if the phase is legislated or not"}, + required=True + ) + + sort_order = fields.Int( + metadata={"description": "Order of the phase"} + ) + + color = fields.String( + metadata={"dscription": "Color of the phase"}, + required=True + ) + + visibility = fields.String( + metadata={"description": "Indicate if the phase is visible in the work plan"}, + required=True, + ) + + is_active = fields.Bool( + metadata={"description": "Active state of the task"}, + ) diff --git a/epictrack-api/src/api/schemas/request/project_request.py b/epictrack-api/src/api/schemas/request/project_request.py index bbd6962b1..ff966c6a5 100644 --- a/epictrack-api/src/api/schemas/request/project_request.py +++ b/epictrack-api/src/api/schemas/request/project_request.py @@ -1,187 +1,187 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Project resource's input validations""" -from marshmallow import fields, validate - -from api.schemas.validators import is_uppercase - -from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema - - -class ProjectBodyParameterSchema(RequestBodyParameterSchema): - """Project request body schema""" - - name = fields.Str( - metadata={"description": "Name of project"}, - validate=validate.Length(max=150), - required=True, - ) - latitude = fields.Float( - metadata={"description": "Latitude of project location"}, - validate=validate.Range(min=-90, max=90), - required=True, - ) - longitude = fields.Float( - metadata={"description": "Longitude of project location"}, - validate=validate.Range(min=-180, max=180), - required=True, - ) - - capital_investment = fields.Int( - metadata={"description": "Capital investment of project"}, - validate=validate.Range(min=0), - allow_none=True, - load_default=None, - ) - epic_guid = fields.Str( - metadata={"description": "EPIC GUID of project"}, - validate=validate.Length(max=150), - allow_none=True, - load_default=None, - ) - ea_certificate = fields.Str( - metadata={"description": "EA Certificate # of project"}, - validate=validate.Length(max=150), - allow_none=True, - load_default=None, - ) - abbreviation = fields.Str( - metadata={"description": "Abbreviation of the project"}, - validate=[validate.Length(max=150), is_uppercase], - required=True, - ) - description = fields.Str( - metadata={"description": "Description of project"}, - validate=validate.Length(max=2000), - required=True, - ) - address = fields.Str( - metadata={"description": "Location description of project"}, - validate=validate.Length(max=2000), - allow_none=True, - load_default=None, - ) - - proponent_id = fields.Int( - metadata={"description": "Proponent id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - type_id = fields.Int( - metadata={"description": "Type id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - sub_type_id = fields.Int( - metadata={"description": "SubType id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - region_id_env = fields.Int( - metadata={"description": "ENV Region id of the project"}, - validate=validate.Range(min=1), - allow_none=True, - load_default=None - ) - region_id_flnro = fields.Int( - metadata={"description": "NRS Region id of the project"}, - validate=validate.Range(min=1), - allow_none=True, - load_default=None - ) - - fte_positions_construction = fields.Int( - metadata={"description": "FTE Positions created during construction on project"}, - validate=validate.Range(min=0), - allow_none=True, - load_default=None - ) - - fte_positions_operation = fields.Int( - metadata={"description": "FTE Positions created during operation on project"}, - validate=validate.Range(min=0), - allow_none=True, - load_default=None - ) - - eac_signed = fields.Date( - metadata={"description": "Date the EAC was signed on"}, - allow_none=True, - load_default=None - ) - - eac_expires = fields.Date( - metadata={"description": "Date the EAC expires on"}, - allow_none=True, - load_default=None - ) - - is_active = fields.Bool(metadata={"description": "Active state of the project"}) - is_project_closed = fields.Bool(metadata={"description": "Closed state of the project"}, default=False) - - -class ProjectExistenceQueryParamSchema(RequestQueryParameterSchema): - """Project existence check query parameters""" - - name = fields.Str( - metadata={"description": "Name of the project"}, - validate=validate.Length(max=150), - required=True - ) - - project_id = fields.Int( - metadata={"description": "The id of the project"}, - validate=validate.Range(min=1), - load_default=None - ) - - -class ProjectIdPathParameterSchema(RequestPathParameterSchema): - """project id path parameter schema""" - - project_id = fields.Int( - metadata={"description": "The id of the project"}, - validate=validate.Range(min=1), - required=True - ) - - -class ProjectFirstNationsQueryParamSchema(RequestQueryParameterSchema): - """Project first nations query parameters""" - - work_id = fields.Int( - metadata={"description": "The id of the work"}, - validate=validate.Range(min=1), - required=True - ) - work_type_id = fields.Int( - metadata={"description": "The id of the work type"}, - validate=validate.Range(min=1), - load_default=None, - allow_none=True, - missing=None - ) - - -class ProjectAbbreviationParameterSchema(RequestPathParameterSchema): - """project id path parameter schema""" - - name = fields.Str( - metadata={"description": "Name of project"}, - validate=validate.Length(max=150), - required=True, - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Project resource's input validations""" +from marshmallow import fields, validate + +from api.schemas.validators import is_uppercase + +from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema + + +class ProjectBodyParameterSchema(RequestBodyParameterSchema): + """Project request body schema""" + + name = fields.Str( + metadata={"description": "Name of project"}, + validate=validate.Length(max=150), + required=True, + ) + latitude = fields.Float( + metadata={"description": "Latitude of project location"}, + validate=validate.Range(min=-90, max=90), + required=True, + ) + longitude = fields.Float( + metadata={"description": "Longitude of project location"}, + validate=validate.Range(min=-180, max=180), + required=True, + ) + + capital_investment = fields.Int( + metadata={"description": "Capital investment of project"}, + validate=validate.Range(min=0), + allow_none=True, + load_default=None, + ) + epic_guid = fields.Str( + metadata={"description": "EPIC GUID of project"}, + validate=validate.Length(max=150), + allow_none=True, + load_default=None, + ) + ea_certificate = fields.Str( + metadata={"description": "EA Certificate # of project"}, + validate=validate.Length(max=150), + allow_none=True, + load_default=None, + ) + abbreviation = fields.Str( + metadata={"description": "Abbreviation of the project"}, + validate=[validate.Length(max=150), is_uppercase], + required=True, + ) + description = fields.Str( + metadata={"description": "Description of project"}, + validate=validate.Length(max=2000), + required=True, + ) + address = fields.Str( + metadata={"description": "Location description of project"}, + validate=validate.Length(max=2000), + allow_none=True, + load_default=None, + ) + + proponent_id = fields.Int( + metadata={"description": "Proponent id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + type_id = fields.Int( + metadata={"description": "Type id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + sub_type_id = fields.Int( + metadata={"description": "SubType id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + region_id_env = fields.Int( + metadata={"description": "ENV Region id of the project"}, + validate=validate.Range(min=1), + allow_none=True, + load_default=None + ) + region_id_flnro = fields.Int( + metadata={"description": "NRS Region id of the project"}, + validate=validate.Range(min=1), + allow_none=True, + load_default=None + ) + + fte_positions_construction = fields.Int( + metadata={"description": "FTE Positions created during construction on project"}, + validate=validate.Range(min=0), + allow_none=True, + load_default=None + ) + + fte_positions_operation = fields.Int( + metadata={"description": "FTE Positions created during operation on project"}, + validate=validate.Range(min=0), + allow_none=True, + load_default=None + ) + + eac_signed = fields.Date( + metadata={"description": "Date the EAC was signed on"}, + allow_none=True, + load_default=None + ) + + eac_expires = fields.Date( + metadata={"description": "Date the EAC expires on"}, + allow_none=True, + load_default=None + ) + + is_active = fields.Bool(metadata={"description": "Active state of the project"}) + is_project_closed = fields.Bool(metadata={"description": "Closed state of the project"}, default=False) + + +class ProjectExistenceQueryParamSchema(RequestQueryParameterSchema): + """Project existence check query parameters""" + + name = fields.Str( + metadata={"description": "Name of the project"}, + validate=validate.Length(max=150), + required=True + ) + + project_id = fields.Int( + metadata={"description": "The id of the project"}, + validate=validate.Range(min=1), + load_default=None + ) + + +class ProjectIdPathParameterSchema(RequestPathParameterSchema): + """project id path parameter schema""" + + project_id = fields.Int( + metadata={"description": "The id of the project"}, + validate=validate.Range(min=1), + required=True + ) + + +class ProjectFirstNationsQueryParamSchema(RequestQueryParameterSchema): + """Project first nations query parameters""" + + work_id = fields.Int( + metadata={"description": "The id of the work"}, + validate=validate.Range(min=1), + required=True + ) + work_type_id = fields.Int( + metadata={"description": "The id of the work type"}, + validate=validate.Range(min=1), + load_default=None, + allow_none=True, + missing=None + ) + + +class ProjectAbbreviationParameterSchema(RequestPathParameterSchema): + """project id path parameter schema""" + + name = fields.Str( + metadata={"description": "Name of project"}, + validate=validate.Length(max=150), + required=True, + ) diff --git a/epictrack-api/src/api/schemas/request/region_request.py b/epictrack-api/src/api/schemas/request/region_request.py index 5bfa9481f..20fd4cadc 100644 --- a/epictrack-api/src/api/schemas/request/region_request.py +++ b/epictrack-api/src/api/schemas/request/region_request.py @@ -1,26 +1,26 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Type resource's input validations""" -from marshmallow import fields, validate - -from .base import RequestPathParameterSchema - - -class RegionTypePathParameterSchema(RequestPathParameterSchema): - """Type id path parameter schema""" - - type = fields.Str( - metadata={"description": "The type of the region"}, - validate=validate.Length(min=1), required=True - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Type resource's input validations""" +from marshmallow import fields, validate + +from .base import RequestPathParameterSchema + + +class RegionTypePathParameterSchema(RequestPathParameterSchema): + """Type id path parameter schema""" + + type = fields.Str( + metadata={"description": "The type of the region"}, + validate=validate.Length(min=1), required=True + ) diff --git a/epictrack-api/src/api/schemas/request/staff_request.py b/epictrack-api/src/api/schemas/request/staff_request.py index c8bf2e6ad..c5ab4dfdf 100644 --- a/epictrack-api/src/api/schemas/request/staff_request.py +++ b/epictrack-api/src/api/schemas/request/staff_request.py @@ -1,125 +1,125 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Staff resource's input validations""" -from marshmallow import fields, pre_load, validate - -from api.schemas.validators import Phone - -from .base import ( - BasicRequestQueryParameterSchema, RequestBodyParameterSchema, RequestPathParameterSchema, - RequestQueryParameterSchema) -from .custom_fields import IntegerList - - -class StaffIdPathParameterSchema(RequestPathParameterSchema): - """Staff id path parameter schema""" - - staff_id = fields.Int( - metadata={"description": "The id of the staff"}, - validate=validate.Range(min=1), - required=True, - ) - - -class StaffExistanceQueryParamSchema(RequestQueryParameterSchema): - """Staff existance check query parameters""" - - email = fields.Str( - metadata={"description": "Email address of the staff"}, - validate=validate.Email(), - required=True, - ) - - staff_id = fields.Int( - metadata={"description": "The id of the staff"}, - validate=validate.Range(min=1), - missing=None, - ) - - @pre_load - def convert_email_to_lower(self, data, **kwargs): # pylint: disable=unused-argument - """Converts staff email into lower case string""" - data = dict(data) - if "email" in data: - data["email"] = data["email"].lower() - return data - - -class StaffByPositionsQueryParamSchema(BasicRequestQueryParameterSchema): - """Staff by positions query parameter""" - - positions = IntegerList(metadata={"description": "comma separated position ids"}) - - -class StaffBodyParameterSchema(RequestBodyParameterSchema): - """Staff request body schema""" - - first_name = fields.Str( - metadata={"description": "First name of staff"}, - validate=validate.Length(max=150), - required=True, - ) - - last_name = fields.Str( - metadata={"description": "Last name of staff"}, - validate=validate.Length(max=150), - required=True, - ) - - email = fields.Str( - metadata={"description": "Email address of the staff"}, - validate=validate.Email(), - required=True, - ) - - phone = fields.Str( - metadata={"description": "Phone number of staff"}, - validate=Phone(), - required=True, - ) - - position_id = fields.Int( - metadata={"description": "Position id of the staff"}, - validate=validate.Range(min=1), - required=True, - ) - - is_active = fields.Boolean( - metadata={"description": "Active status of the staff"}, required=True - ) - - @pre_load - def convert_email_to_lower(self, data, **kwargs): # pylint: disable=unused-argument - """Converts staff email into lower case string""" - if "email" in data: - data["email"] = data["email"].lower() - return data - - -class StaffEmailPathParameterSchema(RequestPathParameterSchema): - """Staff email path parameter schema""" - - email = fields.Str( - metadata={"description": "The email of the staff"}, - validate=validate.Email(), - required=True, - ) - - @pre_load - def convert_email_to_lower(self, data, **kwargs): # pylint: disable=unused-argument - """Converts staff email into lower case string""" - data = dict(data) - if "email" in data: - data["email"] = data["email"].lower() - return data +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Staff resource's input validations""" +from marshmallow import fields, pre_load, validate + +from api.schemas.validators import Phone + +from .base import ( + BasicRequestQueryParameterSchema, RequestBodyParameterSchema, RequestPathParameterSchema, + RequestQueryParameterSchema) +from .custom_fields import IntegerList + + +class StaffIdPathParameterSchema(RequestPathParameterSchema): + """Staff id path parameter schema""" + + staff_id = fields.Int( + metadata={"description": "The id of the staff"}, + validate=validate.Range(min=1), + required=True, + ) + + +class StaffExistanceQueryParamSchema(RequestQueryParameterSchema): + """Staff existance check query parameters""" + + email = fields.Str( + metadata={"description": "Email address of the staff"}, + validate=validate.Email(), + required=True, + ) + + staff_id = fields.Int( + metadata={"description": "The id of the staff"}, + validate=validate.Range(min=1), + missing=None, + ) + + @pre_load + def convert_email_to_lower(self, data, **kwargs): # pylint: disable=unused-argument + """Converts staff email into lower case string""" + data = dict(data) + if "email" in data: + data["email"] = data["email"].lower() + return data + + +class StaffByPositionsQueryParamSchema(BasicRequestQueryParameterSchema): + """Staff by positions query parameter""" + + positions = IntegerList(metadata={"description": "comma separated position ids"}) + + +class StaffBodyParameterSchema(RequestBodyParameterSchema): + """Staff request body schema""" + + first_name = fields.Str( + metadata={"description": "First name of staff"}, + validate=validate.Length(max=150), + required=True, + ) + + last_name = fields.Str( + metadata={"description": "Last name of staff"}, + validate=validate.Length(max=150), + required=True, + ) + + email = fields.Str( + metadata={"description": "Email address of the staff"}, + validate=validate.Email(), + required=True, + ) + + phone = fields.Str( + metadata={"description": "Phone number of staff"}, + validate=Phone(), + required=True, + ) + + position_id = fields.Int( + metadata={"description": "Position id of the staff"}, + validate=validate.Range(min=1), + required=True, + ) + + is_active = fields.Boolean( + metadata={"description": "Active status of the staff"}, required=True + ) + + @pre_load + def convert_email_to_lower(self, data, **kwargs): # pylint: disable=unused-argument + """Converts staff email into lower case string""" + if "email" in data: + data["email"] = data["email"].lower() + return data + + +class StaffEmailPathParameterSchema(RequestPathParameterSchema): + """Staff email path parameter schema""" + + email = fields.Str( + metadata={"description": "The email of the staff"}, + validate=validate.Email(), + required=True, + ) + + @pre_load + def convert_email_to_lower(self, data, **kwargs): # pylint: disable=unused-argument + """Converts staff email into lower case string""" + data = dict(data) + if "email" in data: + data["email"] = data["email"].lower() + return data diff --git a/epictrack-api/src/api/schemas/request/staff_work_role_request.py b/epictrack-api/src/api/schemas/request/staff_work_role_request.py index e17cd9e67..e8d5bbcd6 100644 --- a/epictrack-api/src/api/schemas/request/staff_work_role_request.py +++ b/epictrack-api/src/api/schemas/request/staff_work_role_request.py @@ -1,69 +1,69 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Input validation for staff work role""" -from marshmallow import fields, validate - -from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema - - -class StaffWorkExistenceCheckQueryParamSchema(RequestQueryParameterSchema): - """StaffWork Existence check query parameter""" - - staff_id = fields.Int( - metadata={"description": "Staff ID"}, - required=True, - validate=validate.Range(min=1) - ) - - role_id = fields.Int( - metadata={"description": "Role ID"}, - required=True, - validate=validate.Range(min=1) - ) - - work_staff_id = fields.Int( - metadata={"description": "Id"}, - load_default=None - ) - - -class StaffWorkPathParamSchema(RequestPathParameterSchema): - """StaffWork path parameter schema""" - - work_staff_id = fields.Int( - metadata={"description": "Staff Work Id"}, - validate=validate.Range(min=1), - required=True - ) - - -class StaffWorkBodyParamSchema(RequestBodyParameterSchema): - """StaffWork body parameter schema""" - - staff_id = fields.Int( - metadata={"description": "Staff ID"}, - required=True, - validate=validate.Range(min=1) - ) - - role_id = fields.Int( - metadata={"description": "Role ID"}, - required=True, - validate=validate.Range(min=1) - ) - - is_active = fields.Bool( - metadata={"description": "Staff Work assocation is active or not"}, - required=True - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Input validation for staff work role""" +from marshmallow import fields, validate + +from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema + + +class StaffWorkExistenceCheckQueryParamSchema(RequestQueryParameterSchema): + """StaffWork Existence check query parameter""" + + staff_id = fields.Int( + metadata={"description": "Staff ID"}, + required=True, + validate=validate.Range(min=1) + ) + + role_id = fields.Int( + metadata={"description": "Role ID"}, + required=True, + validate=validate.Range(min=1) + ) + + work_staff_id = fields.Int( + metadata={"description": "Id"}, + load_default=None + ) + + +class StaffWorkPathParamSchema(RequestPathParameterSchema): + """StaffWork path parameter schema""" + + work_staff_id = fields.Int( + metadata={"description": "Staff Work Id"}, + validate=validate.Range(min=1), + required=True + ) + + +class StaffWorkBodyParamSchema(RequestBodyParameterSchema): + """StaffWork body parameter schema""" + + staff_id = fields.Int( + metadata={"description": "Staff ID"}, + required=True, + validate=validate.Range(min=1) + ) + + role_id = fields.Int( + metadata={"description": "Role ID"}, + required=True, + validate=validate.Range(min=1) + ) + + is_active = fields.Bool( + metadata={"description": "Staff Work assocation is active or not"}, + required=True + ) diff --git a/epictrack-api/src/api/schemas/request/user_group_request.py b/epictrack-api/src/api/schemas/request/user_group_request.py index 8440cd8bb..9bcc3dffa 100644 --- a/epictrack-api/src/api/schemas/request/user_group_request.py +++ b/epictrack-api/src/api/schemas/request/user_group_request.py @@ -1,38 +1,38 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""User group request schema""" -from marshmallow import fields -from .base import RequestBodyParameterSchema, RequestPathParameterSchema - - -class UserGroupBodyParamSchema(RequestBodyParameterSchema): - """User group body parameter schema""" - - group_id_to_update = fields.Str( - metadata={"description": "Group id to be updated"}, - required=True - ) - - existing_group_id = fields.Str( - metadata={"description": "Existing group id of the user"}, - ) - - -class UserGroupPathParamSchema(RequestPathParameterSchema): - """User group path parameter schema""" - - user_id = fields.UUID( - metadata={"description": "Id of the user"}, - required=True - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""User group request schema""" +from marshmallow import fields +from .base import RequestBodyParameterSchema, RequestPathParameterSchema + + +class UserGroupBodyParamSchema(RequestBodyParameterSchema): + """User group body parameter schema""" + + group_id_to_update = fields.Str( + metadata={"description": "Group id to be updated"}, + required=True + ) + + existing_group_id = fields.Str( + metadata={"description": "Existing group id of the user"}, + ) + + +class UserGroupPathParamSchema(RequestPathParameterSchema): + """User group path parameter schema""" + + user_id = fields.UUID( + metadata={"description": "Id of the user"}, + required=True + ) diff --git a/epictrack-api/src/api/schemas/request/work_request.py b/epictrack-api/src/api/schemas/request/work_request.py index b5382cb32..392a83659 100644 --- a/epictrack-api/src/api/schemas/request/work_request.py +++ b/epictrack-api/src/api/schemas/request/work_request.py @@ -1,302 +1,302 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Work resource's input validations""" -from marshmallow import fields, validate - -from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema - - -class WorkBodyParameterSchema(RequestBodyParameterSchema): - """Work request body schema""" - - simple_title = fields.Str( - metadata={"description": "Simple title of work"}, - validate=validate.Length(max=150), - required=True, - ) - - report_description = fields.Str( - metadata={"description": "Report description of work"}, - required=True, - ) - epic_description = fields.Str( - metadata={"description": "EPIC description of work"}, - validate=validate.Length(max=2000), - allow_none=True, - load_default=None, - ) - start_date = fields.DateTime( - metadata={"description": "Start date for the work"}, required=True - ) - - ea_act_id = fields.Int( - metadata={"description": "EA Act id of the work"}, - validate=validate.Range(min=1), - required=True, - ) - - work_type_id = fields.Int( - metadata={"description": "WorkType id of the work"}, - validate=validate.Range(min=1), - required=True, - ) - - project_id = fields.Int( - metadata={"description": "Project id of the work"}, - validate=validate.Range(min=1), - required=True, - ) - - ministry_id = fields.Int( - metadata={"description": "Ministry id of the work"}, - validate=validate.Range(min=1), - required=False, - ) - federal_involvement_id = fields.Int( - metadata={"description": "Federal involvement id of the work"}, - validate=validate.Range(min=1), - required=True, - ) - - substitution_act_id = fields.Int( - metadata={"description": "Substitution act id of the work"}, - validate=validate.Range(min=1), - allow_none=True, - ) - - eao_team_id = fields.Int( - metadata={"description": "EAO Team id of the work"}, - validate=validate.Range(min=1), - allow_none=True, - ) - - responsible_epd_id = fields.Int( - metadata={"description": "Responsible EPD id of the work"}, - validate=validate.Range(min=1), - allow_none=True, - ) - - work_lead_id = fields.Int( - metadata={"description": "Work lead id of the work"}, - validate=validate.Range(min=1), - allow_none=True, - ) - - decision_by_id = fields.Int( - metadata={"description": "Decision maker id of the work"}, - validate=validate.Range(min=1), - allow_none=True, - ) - - is_active = fields.Bool(metadata={"description": "Active state of the work"}) - is_high_priority = fields.Bool( - metadata={"description": "Is a high priority work"}, default=False - ) - is_cac_recommended = fields.Bool( - metadata={"description": "Is CAC recommended for the work"}, default=False - ) - - -class WorkExistenceQueryParamSchema(RequestQueryParameterSchema): - """Work existence check query parameters""" - - title = fields.Str( - metadata={"description": "Title of the work"}, - validate=validate.Length(max=150), - required=True, - ) - - work_id = fields.Int( - metadata={"description": "The id of the work"}, - validate=validate.Range(min=1), - load_default=None, - ) - - -class WorkIdPathParameterSchema(RequestPathParameterSchema): - """work id path parameter schema""" - - work_id = fields.Int( - metadata={"description": "The id of the work"}, - validate=validate.Range(min=1), - required=True, - ) - - -class WorkPlanDownloadQueryParamSchema(RequestQueryParameterSchema): - """Workplan download query parameters""" - - work_phase_id = fields.Int( - metadata={"description": "Id of work phase"}, - validate=validate.Range(min=1), - required=True, - ) - - -class WorkIdPhaseIdPathParameterSchema(RequestPathParameterSchema): - """Work id and phase id path parameter schema""" - - work_phase_id = fields.Int( - metadata={"description": "Work phase ID"}, - validate=validate.Range(min=1), - required=True, - ) - - -class WorkFirstNationNotesBodySchema(RequestBodyParameterSchema): - """Work first nation notes body parameter schema""" - - notes = fields.Str( - metadata={"description": "First nation notes"}, - validate=validate.Length(min=1), - required=True, - ) - - -class WorkTypeIdQueryParamSchema(RequestQueryParameterSchema): - """Work type id query parameters""" - - work_type_id = fields.Int( - metadata={"description": "The id of the work type"}, - validate=validate.Range(min=1), - load_default=None, - allow_none=True - ) - - -class WorkFirstNationImportBodyParamSchema(RequestBodyParameterSchema): - """Work First nation import body parameter schema""" - - indigenous_nation_ids = fields.List( - fields.Int(validate=validate.Range(min=1)), - metadata={"description": "Ids of the first nations"}, - required=True, - validate=validate.Length(min=1), - ) - - -class WorkStatusParameterSchema(RequestBodyParameterSchema): - """Work status request body schema""" - - description = fields.Str( - metadata={"description": "description of status"}, - validate=validate.Length(max=1000), - required=True, - ) - - notes = fields.Str( - metadata={"description": "Notes for the work status "}, - allow_none=True - ) - - posted_date = fields.DateTime( - metadata={"description": "posted date for the work status"}, required=False - ) - - -class WorkIssuesParameterSchema(RequestBodyParameterSchema): - """Work issues request body schema""" - - title = fields.Str( - metadata={"description": "Title Of the issue"}, - validate=validate.Length(max=50), - required=True, - ) - - is_active: bool = fields.Bool( - default=True, - description="Flag indicating whether the issue is active", - ) - - is_high_priority: bool = fields.Bool( - default=False, - description="Flag indicating whether the issue is of high priority", - ) - - start_date = fields.DateTime( - metadata={"description": "Start date for the issue"}, required=False - ) - - expected_resolution_date = fields.DateTime( - metadata={"description": "Expected Resolution date for the issue"}, required=False - ) - - -class WorkIssuesCreateParameterSchema(WorkIssuesParameterSchema): - """Work issues create request body schema""" - - updates = fields.List( - fields.Str, - metadata={"description": "List of updates for the issue"}, - required=False, - ) - - -class WorkIssuesUpdateEditSchema(RequestBodyParameterSchema): - """Work status update request body schema for PUT requests""" - - posted_date = fields.DateTime( - metadata={"description": "posted date for the update"}, - required=True - ) - - description = fields.Str( - metadata={"description": "Description of the update"}, - validate=validate.Length(max=1000), - required=True - ) - - -class WorkIssuesUpdateCloneSchema(RequestBodyParameterSchema): - """Work status update request body schema for PUT requests""" - - posted_date = fields.DateTime( - metadata={"description": "posted date for the update"}, - required=True - ) - - description = fields.Str( - metadata={"description": "Description of the update"}, - validate=validate.Length(max=500), - required=True - ) - - -class WorkNotesBodySchema(RequestBodyParameterSchema): - """Work notes body parameter schema""" - - notes = fields.Str( - metadata={"description": "Work status notes"}, - validate=validate.Length(min=1), - required=True, - ) - - note_type = fields.Str( - metadata={"description": "Type of work status notes"}, - validate=validate.OneOf(['status_notes', 'issue_notes']), # Add your predefined types - required=True, - ) - - -class WorkQueryParameterSchema(RequestQueryParameterSchema): - """Work Query parameters""" - - is_active = fields.Bool( - metadata={"description": "query active or inactive ones"}, - allow_none=True - ) - - include_indigenous_nations = fields.Bool( - metadata={"description": "Indicate if the result should have indigenous nations"} - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Work resource's input validations""" +from marshmallow import fields, validate + +from .base import RequestBodyParameterSchema, RequestPathParameterSchema, RequestQueryParameterSchema + + +class WorkBodyParameterSchema(RequestBodyParameterSchema): + """Work request body schema""" + + simple_title = fields.Str( + metadata={"description": "Simple title of work"}, + validate=validate.Length(max=150), + required=True, + ) + + report_description = fields.Str( + metadata={"description": "Report description of work"}, + required=True, + ) + epic_description = fields.Str( + metadata={"description": "EPIC description of work"}, + validate=validate.Length(max=2000), + allow_none=True, + load_default=None, + ) + start_date = fields.DateTime( + metadata={"description": "Start date for the work"}, required=True + ) + + ea_act_id = fields.Int( + metadata={"description": "EA Act id of the work"}, + validate=validate.Range(min=1), + required=True, + ) + + work_type_id = fields.Int( + metadata={"description": "WorkType id of the work"}, + validate=validate.Range(min=1), + required=True, + ) + + project_id = fields.Int( + metadata={"description": "Project id of the work"}, + validate=validate.Range(min=1), + required=True, + ) + + ministry_id = fields.Int( + metadata={"description": "Ministry id of the work"}, + validate=validate.Range(min=1), + required=False, + ) + federal_involvement_id = fields.Int( + metadata={"description": "Federal involvement id of the work"}, + validate=validate.Range(min=1), + required=True, + ) + + substitution_act_id = fields.Int( + metadata={"description": "Substitution act id of the work"}, + validate=validate.Range(min=1), + allow_none=True, + ) + + eao_team_id = fields.Int( + metadata={"description": "EAO Team id of the work"}, + validate=validate.Range(min=1), + allow_none=True, + ) + + responsible_epd_id = fields.Int( + metadata={"description": "Responsible EPD id of the work"}, + validate=validate.Range(min=1), + allow_none=True, + ) + + work_lead_id = fields.Int( + metadata={"description": "Work lead id of the work"}, + validate=validate.Range(min=1), + allow_none=True, + ) + + decision_by_id = fields.Int( + metadata={"description": "Decision maker id of the work"}, + validate=validate.Range(min=1), + allow_none=True, + ) + + is_active = fields.Bool(metadata={"description": "Active state of the work"}) + is_high_priority = fields.Bool( + metadata={"description": "Is a high priority work"}, default=False + ) + is_cac_recommended = fields.Bool( + metadata={"description": "Is CAC recommended for the work"}, default=False + ) + + +class WorkExistenceQueryParamSchema(RequestQueryParameterSchema): + """Work existence check query parameters""" + + title = fields.Str( + metadata={"description": "Title of the work"}, + validate=validate.Length(max=150), + required=True, + ) + + work_id = fields.Int( + metadata={"description": "The id of the work"}, + validate=validate.Range(min=1), + load_default=None, + ) + + +class WorkIdPathParameterSchema(RequestPathParameterSchema): + """work id path parameter schema""" + + work_id = fields.Int( + metadata={"description": "The id of the work"}, + validate=validate.Range(min=1), + required=True, + ) + + +class WorkPlanDownloadQueryParamSchema(RequestQueryParameterSchema): + """Workplan download query parameters""" + + work_phase_id = fields.Int( + metadata={"description": "Id of work phase"}, + validate=validate.Range(min=1), + required=True, + ) + + +class WorkIdPhaseIdPathParameterSchema(RequestPathParameterSchema): + """Work id and phase id path parameter schema""" + + work_phase_id = fields.Int( + metadata={"description": "Work phase ID"}, + validate=validate.Range(min=1), + required=True, + ) + + +class WorkFirstNationNotesBodySchema(RequestBodyParameterSchema): + """Work first nation notes body parameter schema""" + + notes = fields.Str( + metadata={"description": "First nation notes"}, + validate=validate.Length(min=1), + required=True, + ) + + +class WorkTypeIdQueryParamSchema(RequestQueryParameterSchema): + """Work type id query parameters""" + + work_type_id = fields.Int( + metadata={"description": "The id of the work type"}, + validate=validate.Range(min=1), + load_default=None, + allow_none=True + ) + + +class WorkFirstNationImportBodyParamSchema(RequestBodyParameterSchema): + """Work First nation import body parameter schema""" + + indigenous_nation_ids = fields.List( + fields.Int(validate=validate.Range(min=1)), + metadata={"description": "Ids of the first nations"}, + required=True, + validate=validate.Length(min=1), + ) + + +class WorkStatusParameterSchema(RequestBodyParameterSchema): + """Work status request body schema""" + + description = fields.Str( + metadata={"description": "description of status"}, + validate=validate.Length(max=1000), + required=True, + ) + + notes = fields.Str( + metadata={"description": "Notes for the work status "}, + allow_none=True + ) + + posted_date = fields.DateTime( + metadata={"description": "posted date for the work status"}, required=False + ) + + +class WorkIssuesParameterSchema(RequestBodyParameterSchema): + """Work issues request body schema""" + + title = fields.Str( + metadata={"description": "Title Of the issue"}, + validate=validate.Length(max=50), + required=True, + ) + + is_active: bool = fields.Bool( + default=True, + description="Flag indicating whether the issue is active", + ) + + is_high_priority: bool = fields.Bool( + default=False, + description="Flag indicating whether the issue is of high priority", + ) + + start_date = fields.DateTime( + metadata={"description": "Start date for the issue"}, required=False + ) + + expected_resolution_date = fields.DateTime( + metadata={"description": "Expected Resolution date for the issue"}, required=False + ) + + +class WorkIssuesCreateParameterSchema(WorkIssuesParameterSchema): + """Work issues create request body schema""" + + updates = fields.List( + fields.Str, + metadata={"description": "List of updates for the issue"}, + required=False, + ) + + +class WorkIssuesUpdateEditSchema(RequestBodyParameterSchema): + """Work status update request body schema for PUT requests""" + + posted_date = fields.DateTime( + metadata={"description": "posted date for the update"}, + required=True + ) + + description = fields.Str( + metadata={"description": "Description of the update"}, + validate=validate.Length(max=1000), + required=True + ) + + +class WorkIssuesUpdateCloneSchema(RequestBodyParameterSchema): + """Work status update request body schema for PUT requests""" + + posted_date = fields.DateTime( + metadata={"description": "posted date for the update"}, + required=True + ) + + description = fields.Str( + metadata={"description": "Description of the update"}, + validate=validate.Length(max=500), + required=True + ) + + +class WorkNotesBodySchema(RequestBodyParameterSchema): + """Work notes body parameter schema""" + + notes = fields.Str( + metadata={"description": "Work status notes"}, + validate=validate.Length(min=1), + required=True, + ) + + note_type = fields.Str( + metadata={"description": "Type of work status notes"}, + validate=validate.OneOf(['status_notes', 'issue_notes']), # Add your predefined types + required=True, + ) + + +class WorkQueryParameterSchema(RequestQueryParameterSchema): + """Work Query parameters""" + + is_active = fields.Bool( + metadata={"description": "query active or inactive ones"}, + allow_none=True + ) + + include_indigenous_nations = fields.Bool( + metadata={"description": "Indicate if the result should have indigenous nations"} + ) diff --git a/epictrack-api/src/api/schemas/response/__init__.py b/epictrack-api/src/api/schemas/response/__init__.py index d643db92e..2a4571df6 100644 --- a/epictrack-api/src/api/schemas/response/__init__.py +++ b/epictrack-api/src/api/schemas/response/__init__.py @@ -1,60 +1,60 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Exposes all the response validation schemas""" -from .act_section_response import ActSectionResponseSchema -from .action_template_response import ActionTemplateResponseSchema -from .event_configuration_response import EventConfigurationResponseSchema -from .event_response import ( - EventDateChangePosibilityCheckResponseSchema, - EventResponseSchema, -) -from .event_template_response import EventTemplateResponseSchema -from .indigenous_nation_response import ( - IndigenousResponseNationSchema, - WorkIndigenousNationResponseSchema, - IndigenousNationConsultationResponseSchema, -) -from .list_type_response import ListTypeResponseSchema -from .outcome_configuration_response import OutcomeConfigurationResponseSchema -from .outcome_template_response import OutcomeTemplateResponseSchema -from .phase_response import PhaseResponseSchema -from .project_response import ProjectResponseSchema -from .proponent_response import ProponentResponseSchema -from .responsibility_response import ResponsibilityResponseSchema -from .role_response import RoleResponseSchema -from .special_field_response import SpecialFieldResponseSchema -from .staff_response import StaffResponseSchema -from .staff_work_role_response import StaffWorkRoleResponseSchema -from .task_response import ( - TaskEventResponseSchema, - TaskResponseSchema, - TaskTemplateResponseSchema, - TaskEventByStaffResponseSchema, -) -from .types_response import SubTypeResponseSchema, TypeResponseSchema -from .user_group_response import UserGroupResponseSchema -from .user_response import UserResponseSchema -from .work_response import ( - WorkIssuesResponseSchema, - WorkIssueUpdatesResponseSchema, - WorkPhaseAdditionalInfoResponseSchema, - WorkPhaseResponseSchema, - WorkPhaseTemplateAvailableResponse, - WorkResourceResponseSchema, - WorkResponseSchema, - WorkStaffRoleReponseSchema, - WorkStatusResponseSchema, - WorkPhaseByIdResponseSchema, - WorkIssuesLatestUpdateResponseSchema, -) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Exposes all the response validation schemas""" +from .act_section_response import ActSectionResponseSchema +from .action_template_response import ActionTemplateResponseSchema +from .event_configuration_response import EventConfigurationResponseSchema +from .event_response import ( + EventDateChangePosibilityCheckResponseSchema, + EventResponseSchema, +) +from .event_template_response import EventTemplateResponseSchema +from .indigenous_nation_response import ( + IndigenousResponseNationSchema, + WorkIndigenousNationResponseSchema, + IndigenousNationConsultationResponseSchema, +) +from .list_type_response import ListTypeResponseSchema +from .outcome_configuration_response import OutcomeConfigurationResponseSchema +from .outcome_template_response import OutcomeTemplateResponseSchema +from .phase_response import PhaseResponseSchema +from .project_response import ProjectResponseSchema +from .proponent_response import ProponentResponseSchema +from .responsibility_response import ResponsibilityResponseSchema +from .role_response import RoleResponseSchema +from .special_field_response import SpecialFieldResponseSchema +from .staff_response import StaffResponseSchema +from .staff_work_role_response import StaffWorkRoleResponseSchema +from .task_response import ( + TaskEventResponseSchema, + TaskResponseSchema, + TaskTemplateResponseSchema, + TaskEventByStaffResponseSchema, +) +from .types_response import SubTypeResponseSchema, TypeResponseSchema +from .user_group_response import UserGroupResponseSchema +from .user_response import UserResponseSchema +from .work_response import ( + WorkIssuesResponseSchema, + WorkIssueUpdatesResponseSchema, + WorkPhaseAdditionalInfoResponseSchema, + WorkPhaseResponseSchema, + WorkPhaseTemplateAvailableResponse, + WorkResourceResponseSchema, + WorkResponseSchema, + WorkStaffRoleReponseSchema, + WorkStatusResponseSchema, + WorkPhaseByIdResponseSchema, + WorkIssuesLatestUpdateResponseSchema, +) diff --git a/epictrack-api/src/api/schemas/response/act_section_response.py b/epictrack-api/src/api/schemas/response/act_section_response.py index 73d2b0f8f..6bb249500 100644 --- a/epictrack-api/src/api/schemas/response/act_section_response.py +++ b/epictrack-api/src/api/schemas/response/act_section_response.py @@ -1,17 +1,17 @@ -"""ActSection model schema""" -from marshmallow import EXCLUDE - -from api.models import ActSection -from api.schemas.base import AutoSchemaBase - - -class ActSectionResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Type model schema class""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = ActSection - unknown = EXCLUDE +"""ActSection model schema""" +from marshmallow import EXCLUDE + +from api.models import ActSection +from api.schemas.base import AutoSchemaBase + + +class ActSectionResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Type model schema class""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = ActSection + unknown = EXCLUDE diff --git a/epictrack-api/src/api/schemas/response/action_template_response.py b/epictrack-api/src/api/schemas/response/action_template_response.py index 3a8c6cba3..847a59a91 100644 --- a/epictrack-api/src/api/schemas/response/action_template_response.py +++ b/epictrack-api/src/api/schemas/response/action_template_response.py @@ -1,31 +1,31 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Action Template Response response schema""" -from marshmallow import EXCLUDE - -from api.models import ActionTemplate -from api.schemas.base import AutoSchemaBase - - -class ActionTemplateResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Task model schema class""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = ActionTemplate - include_fk = True - unknown = EXCLUDE +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Action Template Response response schema""" +from marshmallow import EXCLUDE + +from api.models import ActionTemplate +from api.schemas.base import AutoSchemaBase + + +class ActionTemplateResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Task model schema class""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = ActionTemplate + include_fk = True + unknown = EXCLUDE diff --git a/epictrack-api/src/api/schemas/response/event_configuration_response.py b/epictrack-api/src/api/schemas/response/event_configuration_response.py index 44ee0678c..0d019ee69 100644 --- a/epictrack-api/src/api/schemas/response/event_configuration_response.py +++ b/epictrack-api/src/api/schemas/response/event_configuration_response.py @@ -1,42 +1,42 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Event Template Response response schema""" -from marshmallow import EXCLUDE, fields - -from api.models import EventConfiguration -from api.schemas.base import AutoSchemaBase - - -class EventConfigurationResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Task model schema class""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = EventConfiguration - include_fk = True - unknown = EXCLUDE - event_position = fields.Method("get_event_position") - - visibility = fields.Method("get_visibility") - - def get_visibility(self, obj: EventConfiguration) -> str: - """Return value for the visibility""" - return obj.visibility if isinstance(obj.visibility, str) else obj.visibility.value - - def get_event_position(self, obj: EventConfiguration) -> str: - """Return the work state""" - return obj.event_position if isinstance(obj.event_position, str) else obj.event_position.value +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Event Template Response response schema""" +from marshmallow import EXCLUDE, fields + +from api.models import EventConfiguration +from api.schemas.base import AutoSchemaBase + + +class EventConfigurationResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Task model schema class""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = EventConfiguration + include_fk = True + unknown = EXCLUDE + event_position = fields.Method("get_event_position") + + visibility = fields.Method("get_visibility") + + def get_visibility(self, obj: EventConfiguration) -> str: + """Return value for the visibility""" + return obj.visibility if isinstance(obj.visibility, str) else obj.visibility.value + + def get_event_position(self, obj: EventConfiguration) -> str: + """Return the work state""" + return obj.event_position if isinstance(obj.event_position, str) else obj.event_position.value diff --git a/epictrack-api/src/api/schemas/response/event_template_response.py b/epictrack-api/src/api/schemas/response/event_template_response.py index 7922e8212..dacc23f0c 100644 --- a/epictrack-api/src/api/schemas/response/event_template_response.py +++ b/epictrack-api/src/api/schemas/response/event_template_response.py @@ -1,48 +1,48 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Event Template Response response schema""" -from marshmallow import EXCLUDE, fields - -from api.models import EventTemplate -from api.schemas.base import AutoSchemaBase - - -class EventTemplateResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Task model schema class""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = EventTemplate - include_fk = True - unknown = EXCLUDE - - event_position = fields.Method("get_event_position") - visibility = fields.Method("get_visibility") - - def get_event_position(self, obj: EventTemplate) -> str: - """Return value for the event position""" - return ( - obj.event_position - if isinstance(obj.event_position, str) - else obj.event_position.value - ) - - def get_visibility(self, obj: EventTemplate) -> str: - """Return value for the visibility""" - return ( - obj.visibility if isinstance(obj.visibility, str) else obj.visibility.value - ) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Event Template Response response schema""" +from marshmallow import EXCLUDE, fields + +from api.models import EventTemplate +from api.schemas.base import AutoSchemaBase + + +class EventTemplateResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Task model schema class""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = EventTemplate + include_fk = True + unknown = EXCLUDE + + event_position = fields.Method("get_event_position") + visibility = fields.Method("get_visibility") + + def get_event_position(self, obj: EventTemplate) -> str: + """Return value for the event position""" + return ( + obj.event_position + if isinstance(obj.event_position, str) + else obj.event_position.value + ) + + def get_visibility(self, obj: EventTemplate) -> str: + """Return value for the visibility""" + return ( + obj.visibility if isinstance(obj.visibility, str) else obj.visibility.value + ) diff --git a/epictrack-api/src/api/schemas/response/outcome_configuration_response.py b/epictrack-api/src/api/schemas/response/outcome_configuration_response.py index 3766ad55b..dca8e5680 100644 --- a/epictrack-api/src/api/schemas/response/outcome_configuration_response.py +++ b/epictrack-api/src/api/schemas/response/outcome_configuration_response.py @@ -1,31 +1,31 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Outcome Outcome Response response schema""" -from marshmallow import EXCLUDE - -from api.models import OutcomeConfiguration -from api.schemas.base import AutoSchemaBase - - -class OutcomeConfigurationResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Outcome configuration model schema class""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = OutcomeConfiguration - include_fk = True - unknown = EXCLUDE +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Outcome Outcome Response response schema""" +from marshmallow import EXCLUDE + +from api.models import OutcomeConfiguration +from api.schemas.base import AutoSchemaBase + + +class OutcomeConfigurationResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Outcome configuration model schema class""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = OutcomeConfiguration + include_fk = True + unknown = EXCLUDE diff --git a/epictrack-api/src/api/schemas/response/outcome_template_response.py b/epictrack-api/src/api/schemas/response/outcome_template_response.py index ce6d60d06..944d6fab6 100644 --- a/epictrack-api/src/api/schemas/response/outcome_template_response.py +++ b/epictrack-api/src/api/schemas/response/outcome_template_response.py @@ -1,31 +1,31 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Outcome Template Response response schema""" -from marshmallow import EXCLUDE - -from api.models import OutcomeTemplate -from api.schemas.base import AutoSchemaBase - - -class OutcomeTemplateResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Task model schema class""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = OutcomeTemplate - include_fk = True - unknown = EXCLUDE +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Outcome Template Response response schema""" +from marshmallow import EXCLUDE + +from api.models import OutcomeTemplate +from api.schemas.base import AutoSchemaBase + + +class OutcomeTemplateResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Task model schema class""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = OutcomeTemplate + include_fk = True + unknown = EXCLUDE diff --git a/epictrack-api/src/api/schemas/response/role_response.py b/epictrack-api/src/api/schemas/response/role_response.py index 6f82cdb5e..4925423e8 100644 --- a/epictrack-api/src/api/schemas/response/role_response.py +++ b/epictrack-api/src/api/schemas/response/role_response.py @@ -1,30 +1,30 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Role response""" -from marshmallow import EXCLUDE -from api.schemas.base import AutoSchemaBase -from api.models import Role - - -class RoleResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Staff response schema""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = Role - include_fk = True - unknown = EXCLUDE +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Role response""" +from marshmallow import EXCLUDE +from api.schemas.base import AutoSchemaBase +from api.models import Role + + +class RoleResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Staff response schema""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = Role + include_fk = True + unknown = EXCLUDE diff --git a/epictrack-api/src/api/schemas/response/staff_response.py b/epictrack-api/src/api/schemas/response/staff_response.py index 66cea2551..d59f198b5 100644 --- a/epictrack-api/src/api/schemas/response/staff_response.py +++ b/epictrack-api/src/api/schemas/response/staff_response.py @@ -1,45 +1,45 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Staff response schema""" -from marshmallow import EXCLUDE, fields -from api.schemas.base import AutoSchemaBase -from api.schemas import PositionSchema -from api.models import Staff - - -class StaffResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Staff response schema""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = Staff - include_fk = True - unknown = EXCLUDE - - full_name = fields.Method("get_full_name", dump_only=True) - position = fields.Nested(PositionSchema(), dump_only=True) - last_active_at = fields.Method("get_last_active_at", dump_only=True) - - def get_full_name(self, instance): - """Get the full name""" - return f"{instance.last_name}, {instance.first_name}" - - def get_last_active_at(self, instance): - """Get the last active at""" - if instance.last_active_at is None: - return None - return instance.last_active_at +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Staff response schema""" +from marshmallow import EXCLUDE, fields +from api.schemas.base import AutoSchemaBase +from api.schemas import PositionSchema +from api.models import Staff + + +class StaffResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Staff response schema""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = Staff + include_fk = True + unknown = EXCLUDE + + full_name = fields.Method("get_full_name", dump_only=True) + position = fields.Nested(PositionSchema(), dump_only=True) + last_active_at = fields.Method("get_last_active_at", dump_only=True) + + def get_full_name(self, instance): + """Get the full name""" + return f"{instance.last_name}, {instance.first_name}" + + def get_last_active_at(self, instance): + """Get the last active at""" + if instance.last_active_at is None: + return None + return instance.last_active_at diff --git a/epictrack-api/src/api/schemas/response/staff_work_role_response.py b/epictrack-api/src/api/schemas/response/staff_work_role_response.py index cbfbb4ee8..3564737e6 100644 --- a/epictrack-api/src/api/schemas/response/staff_work_role_response.py +++ b/epictrack-api/src/api/schemas/response/staff_work_role_response.py @@ -1,34 +1,34 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Staff Work Role response""" -from marshmallow import EXCLUDE, fields -from api.schemas.base import AutoSchemaBase -from api.schemas.response.staff_response import StaffResponseSchema -from api.schemas.response.role_response import RoleResponseSchema -from api.models import StaffWorkRole - - -class StaffWorkRoleResponseSchema( - AutoSchemaBase -): # pylint: disable=too-many-ancestors,too-few-public-methods - """Staff response schema""" - - class Meta(AutoSchemaBase.Meta): - """Meta information""" - - model = StaffWorkRole - include_fk = True - unknown = EXCLUDE - staff = fields.Nested(StaffResponseSchema(), dump_only=True) - role = fields.Nested(RoleResponseSchema(), dump_only=True) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Staff Work Role response""" +from marshmallow import EXCLUDE, fields +from api.schemas.base import AutoSchemaBase +from api.schemas.response.staff_response import StaffResponseSchema +from api.schemas.response.role_response import RoleResponseSchema +from api.models import StaffWorkRole + + +class StaffWorkRoleResponseSchema( + AutoSchemaBase +): # pylint: disable=too-many-ancestors,too-few-public-methods + """Staff response schema""" + + class Meta(AutoSchemaBase.Meta): + """Meta information""" + + model = StaffWorkRole + include_fk = True + unknown = EXCLUDE + staff = fields.Nested(StaffResponseSchema(), dump_only=True) + role = fields.Nested(RoleResponseSchema(), dump_only=True) diff --git a/epictrack-api/src/api/schemas/response/user_group_response.py b/epictrack-api/src/api/schemas/response/user_group_response.py index c643b7889..b0620d9ba 100644 --- a/epictrack-api/src/api/schemas/response/user_group_response.py +++ b/epictrack-api/src/api/schemas/response/user_group_response.py @@ -1,57 +1,57 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""User group response schema""" -from marshmallow import Schema, fields - - -class UserGroupResponseSchema(Schema): - """User group response schema""" - - id = fields.Str(metadata={"description": "Id of the group"}) - name = fields.Str(metadata={"description": "Name of the group"}) - path = fields.Method("get_path") - - level = fields.Method("get_level") - display_name = fields.Method("get_display_name") - - def get_level(self, instance): - """ - Retrieve the level attribute from the given instance. - - Args: - instance (dict): A dictionary representing the instance, which is expected to have an "attributes" key. - - Returns: - int: The level value extracted from the instance's attributes. Defaults to 0 if not found. - """ - return int(instance.get("attributes", {}).get("level", [0])[0] or 0) - - def get_display_name(self, instance): - """ - Retrieve the display name of the group from the given instance. - - Args: - instance (dict): A dictionary representing the group instance, - which should contain an "attributes" key. - - Returns: - str: The display name of the group. If the display name is not - found, an empty string is returned. - """ - return instance.get("attributes", {}).get("display_name", [""])[0] or "" - - def get_path(self, instance): - """Format the path of the group from keycloak""" - path = instance["path"] - return path[1:len(path)] +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""User group response schema""" +from marshmallow import Schema, fields + + +class UserGroupResponseSchema(Schema): + """User group response schema""" + + id = fields.Str(metadata={"description": "Id of the group"}) + name = fields.Str(metadata={"description": "Name of the group"}) + path = fields.Method("get_path") + + level = fields.Method("get_level") + display_name = fields.Method("get_display_name") + + def get_level(self, instance): + """ + Retrieve the level attribute from the given instance. + + Args: + instance (dict): A dictionary representing the instance, which is expected to have an "attributes" key. + + Returns: + int: The level value extracted from the instance's attributes. Defaults to 0 if not found. + """ + return int(instance.get("attributes", {}).get("level", [0])[0] or 0) + + def get_display_name(self, instance): + """ + Retrieve the display name of the group from the given instance. + + Args: + instance (dict): A dictionary representing the group instance, + which should contain an "attributes" key. + + Returns: + str: The display name of the group. If the display name is not + found, an empty string is returned. + """ + return instance.get("attributes", {}).get("display_name", [""])[0] or "" + + def get_path(self, instance): + """Format the path of the group from keycloak""" + path = instance["path"] + return path[1:len(path)] diff --git a/epictrack-api/src/api/schemas/response/user_response.py b/epictrack-api/src/api/schemas/response/user_response.py index 02d4f73c1..04eb89969 100644 --- a/epictrack-api/src/api/schemas/response/user_response.py +++ b/epictrack-api/src/api/schemas/response/user_response.py @@ -1,37 +1,37 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""User response schema""" -from marshmallow import Schema, fields - -from .user_group_response import UserGroupResponseSchema - - -class UserResponseSchema(Schema): - """User response schema""" - - id = fields.Str(metadata={"description": "Id of the user"}) - - first_name = fields.Str( - metadata={"description": "First name of the user"}, attribute="firstName" - ) - - last_name = fields.Str( - metadata={"description": "Last name of the user"}, attribute="lastName" - ) - - email = fields.Str( - metadata={"description": "Email of the user"}, - ) - - group = fields.Nested(UserGroupResponseSchema) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""User response schema""" +from marshmallow import Schema, fields + +from .user_group_response import UserGroupResponseSchema + + +class UserResponseSchema(Schema): + """User response schema""" + + id = fields.Str(metadata={"description": "Id of the user"}) + + first_name = fields.Str( + metadata={"description": "First name of the user"}, attribute="firstName" + ) + + last_name = fields.Str( + metadata={"description": "Last name of the user"}, attribute="lastName" + ) + + email = fields.Str( + metadata={"description": "Email of the user"}, + ) + + group = fields.Nested(UserGroupResponseSchema) diff --git a/epictrack-api/src/api/schemas/validators/__init__.py b/epictrack-api/src/api/schemas/validators/__init__.py index 5fee168a7..d4be3bbf3 100644 --- a/epictrack-api/src/api/schemas/validators/__init__.py +++ b/epictrack-api/src/api/schemas/validators/__init__.py @@ -1,23 +1,23 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Exposes custom validators""" -from marshmallow import ValidationError - -from .phone import Phone - - -def is_uppercase(value): - """Validates that the value is in uppercase""" - if not value.isupper(): - raise ValidationError("Value must be in uppercase.") +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Exposes custom validators""" +from marshmallow import ValidationError + +from .phone import Phone + + +def is_uppercase(value): + """Validates that the value is in uppercase""" + if not value.isupper(): + raise ValidationError("Value must be in uppercase.") diff --git a/epictrack-api/src/api/schemas/validators/phone.py b/epictrack-api/src/api/schemas/validators/phone.py index ec63995e2..4ca9e7c92 100644 --- a/epictrack-api/src/api/schemas/validators/phone.py +++ b/epictrack-api/src/api/schemas/validators/phone.py @@ -1,40 +1,40 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Phone validator""" -import re -from typing import Union -from marshmallow import ValidationError, validate - - -class Phone(validate.Validator): # pylint: disable=too-few-public-methods - """Phone number validation""" - - PHONE_REGEX = re.compile( - r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$" - ) - default_message = 'Invalid phone number' - - def __init__(self, *, error: Union[str, None] = None): - """Init""" - self.error = error or self.default_message - - def _format_error(self, value: str) -> str: - """Format the error message with the given value""" - return self.error.format(input=value) - - def __call__(self, value: str) -> str: - """Execute the validation""" - message = self._format_error(value) - if not self.PHONE_REGEX.match(value): - raise ValidationError(message) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Phone validator""" +import re +from typing import Union +from marshmallow import ValidationError, validate + + +class Phone(validate.Validator): # pylint: disable=too-few-public-methods + """Phone number validation""" + + PHONE_REGEX = re.compile( + r"^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$" + ) + default_message = 'Invalid phone number' + + def __init__(self, *, error: Union[str, None] = None): + """Init""" + self.error = error or self.default_message + + def _format_error(self, value: str) -> str: + """Format the error message with the given value""" + return self.error.format(input=value) + + def __call__(self, value: str) -> str: + """Execute the validation""" + message = self._format_error(value) + if not self.PHONE_REGEX.match(value): + raise ValidationError(message) diff --git a/epictrack-api/src/api/services/act_section.py b/epictrack-api/src/api/services/act_section.py index ce74e5f08..82ab9ad3f 100644 --- a/epictrack-api/src/api/services/act_section.py +++ b/epictrack-api/src/api/services/act_section.py @@ -1,28 +1,28 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage ActSection.""" -from flask import current_app - -from api.models import ActSection - - -class ActSectionService: # pylint:disable=too-few-public-methods - """Service to manage sub type related operations""" - - @classmethod - def find_by_ea_act(cls, args: dict): - """Find sub types by type_id""" - current_app.logger.debug(f"find act sections by params {args}") - act_sections = ActSection.find_by_params(args) - return act_sections +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage ActSection.""" +from flask import current_app + +from api.models import ActSection + + +class ActSectionService: # pylint:disable=too-few-public-methods + """Service to manage sub type related operations""" + + @classmethod + def find_by_ea_act(cls, args: dict): + """Find sub types by type_id""" + current_app.logger.debug(f"find act sections by params {args}") + act_sections = ActSection.find_by_params(args) + return act_sections diff --git a/epictrack-api/src/api/services/action_template.py b/epictrack-api/src/api/services/action_template.py index bb21d6cd0..75a2b319c 100644 --- a/epictrack-api/src/api/services/action_template.py +++ b/epictrack-api/src/api/services/action_template.py @@ -1,42 +1,42 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Action Template""" -from api.models.action import ActionEnum -from api.models.phase_code import PhaseCode - - -class ActionTemplateService: # pylint: disable=too-few-public-methods - """Service to manage action templates""" - - @classmethod - def get_action_params(cls, action_type: ActionEnum, request_data: dict): - """Return the action params for the template""" - if action_type == ActionEnum.ADD_EVENT: - return request_data # cls._get_phase_param(request_data) - return request_data - - @classmethod - def _get_phase_param(cls, request_data: dict) -> dict: - """Return the phase_id as dict""" - param = { - "name": request_data.get("phase_name").strip(), - "work_type_id": request_data.get("work_type_id"), - "ea_act_id": request_data.get("ea_act_id") - } - result = PhaseCode.find_by_params(param) - if not result: - return {} - return { - "phase_id": result[0].id - } +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Action Template""" +from api.models.action import ActionEnum +from api.models.phase_code import PhaseCode + + +class ActionTemplateService: # pylint: disable=too-few-public-methods + """Service to manage action templates""" + + @classmethod + def get_action_params(cls, action_type: ActionEnum, request_data: dict): + """Return the action params for the template""" + if action_type == ActionEnum.ADD_EVENT: + return request_data # cls._get_phase_param(request_data) + return request_data + + @classmethod + def _get_phase_param(cls, request_data: dict) -> dict: + """Return the phase_id as dict""" + param = { + "name": request_data.get("phase_name").strip(), + "work_type_id": request_data.get("work_type_id"), + "ea_act_id": request_data.get("ea_act_id") + } + result = PhaseCode.find_by_params(param) + if not result: + return {} + return { + "phase_id": result[0].id + } diff --git a/epictrack-api/src/api/services/eao_team_service.py b/epictrack-api/src/api/services/eao_team_service.py index 0aae8f811..8b8c585c6 100644 --- a/epictrack-api/src/api/services/eao_team_service.py +++ b/epictrack-api/src/api/services/eao_team_service.py @@ -1,30 +1,30 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""EAO Team service""" - -from api.models.eao_team import EAOTeam - - -class EAOTeamService: - """Service to manage EAO Team related operations.""" - - @staticmethod - def find_team_by_id(team_id: int): - """Get team by id.""" - return EAOTeam.find_by_id(team_id) - - @staticmethod - def find_all_teams(): - """Get all teams.""" - return EAOTeam.find_all() +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""EAO Team service""" + +from api.models.eao_team import EAOTeam + + +class EAOTeamService: + """Service to manage EAO Team related operations.""" + + @staticmethod + def find_team_by_id(team_id: int): + """Get team by id.""" + return EAOTeam.find_by_id(team_id) + + @staticmethod + def find_all_teams(): + """Get all teams.""" + return EAOTeam.find_all() diff --git a/epictrack-api/src/api/services/event_configuration.py b/epictrack-api/src/api/services/event_configuration.py index f48184455..304274434 100644 --- a/epictrack-api/src/api/services/event_configuration.py +++ b/epictrack-api/src/api/services/event_configuration.py @@ -1,118 +1,118 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Event Configuration.""" -from typing import Iterable, List -from sqlalchemy import or_ - -from api.models import EventConfiguration, WorkPhase, db -from api.models.event_category import EventCategoryEnum -from api.models.event_template import EventTemplateVisibilityEnum -from api.models.queries.event_configuration_queries import EventConfigurationQuery - - -class EventConfigurationService: # pylint: disable=dangerous-default-value,too-many-arguments - """Service to manage event configurations""" - - @classmethod - def add_configurations(cls, configs: Iterable[EventConfiguration]) -> None: - """Add multiple event configurations""" - db.session.add_all(configs) - db.session.flush() - - @classmethod - def find_configurations( - cls, - work_phase_id: int, - visibility_modes, - event_categories: List[EventCategoryEnum] = [], - _all: bool = False, - ) -> List[EventConfiguration]: - """Get all the mandatory configurations for a given phase""" - configurations = EventConfigurationQuery.find_configurations( - work_phase_id, visibility_modes, event_categories, _all - ) - return configurations - - @classmethod - def find_configurations_for_adding_new_milestone( - cls, - work_phase_id: int, - visibility_modes, - event_categories: List[EventCategoryEnum] = [], - ): - """Return configurations that can be used when adding new milestone""" - configurations = EventConfigurationQuery.find_configurations( - work_phase_id, visibility_modes, event_categories - ) - suggested_active_configurations = ( - EventConfigurationQuery.find_active_suggested_configurations(work_phase_id) - ) - optional_configurations = [ - config - for config in configurations - if config.visibility == EventTemplateVisibilityEnum.OPTIONAL - or ( - config.visibility == EventTemplateVisibilityEnum.SUGGESTED - and config not in suggested_active_configurations - ) - ] - return optional_configurations - - @classmethod - def find_all_configurations_by_work( - cls, work_id: int, event_categories: List[EventCategoryEnum] = [] - ) -> List[EventConfiguration]: - """Find all the configurations based on the work""" - query = ( - db.session.query(EventConfiguration) - .join(WorkPhase, WorkPhase.id == EventConfiguration.work_phase_id) - .filter( - EventConfiguration.is_active.is_(True), - EventConfiguration.is_deleted.is_(False), - WorkPhase.is_active.is_(True), - WorkPhase.is_deleted.is_(False), - WorkPhase.work_id == work_id, - ) - ) - if len(event_categories) > 0: - category_ids = list(map(lambda x: x.value, event_categories)) - query = query.filter(EventConfiguration.event_category_id.in_(category_ids)) - configurations = query.all() - return configurations - - @classmethod - def find_child_configurations(cls, configuration_id: int) -> List[EventConfiguration]: - """Get all the child configurations for a given phase""" - query = db.session.query(EventConfiguration).filter( - EventConfiguration.parent_id == configuration_id, - EventConfiguration.is_active.is_(True), - EventConfiguration.is_deleted.is_(False), - ) - configurations = query.all() - return configurations - - @classmethod - def find_parent_child_configurations( - cls, configuration_id: int - ) -> List[EventConfiguration]: - """Get both parent and child configurations""" - query = db.session.query(EventConfiguration).filter( - or_( - EventConfiguration.id == configuration_id, - EventConfiguration.parent_id == configuration_id, - ), - EventConfiguration.is_active.is_(True), - ) - configurations = query.all() - return configurations +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Event Configuration.""" +from typing import Iterable, List +from sqlalchemy import or_ + +from api.models import EventConfiguration, WorkPhase, db +from api.models.event_category import EventCategoryEnum +from api.models.event_template import EventTemplateVisibilityEnum +from api.models.queries.event_configuration_queries import EventConfigurationQuery + + +class EventConfigurationService: # pylint: disable=dangerous-default-value,too-many-arguments + """Service to manage event configurations""" + + @classmethod + def add_configurations(cls, configs: Iterable[EventConfiguration]) -> None: + """Add multiple event configurations""" + db.session.add_all(configs) + db.session.flush() + + @classmethod + def find_configurations( + cls, + work_phase_id: int, + visibility_modes, + event_categories: List[EventCategoryEnum] = [], + _all: bool = False, + ) -> List[EventConfiguration]: + """Get all the mandatory configurations for a given phase""" + configurations = EventConfigurationQuery.find_configurations( + work_phase_id, visibility_modes, event_categories, _all + ) + return configurations + + @classmethod + def find_configurations_for_adding_new_milestone( + cls, + work_phase_id: int, + visibility_modes, + event_categories: List[EventCategoryEnum] = [], + ): + """Return configurations that can be used when adding new milestone""" + configurations = EventConfigurationQuery.find_configurations( + work_phase_id, visibility_modes, event_categories + ) + suggested_active_configurations = ( + EventConfigurationQuery.find_active_suggested_configurations(work_phase_id) + ) + optional_configurations = [ + config + for config in configurations + if config.visibility == EventTemplateVisibilityEnum.OPTIONAL + or ( + config.visibility == EventTemplateVisibilityEnum.SUGGESTED + and config not in suggested_active_configurations + ) + ] + return optional_configurations + + @classmethod + def find_all_configurations_by_work( + cls, work_id: int, event_categories: List[EventCategoryEnum] = [] + ) -> List[EventConfiguration]: + """Find all the configurations based on the work""" + query = ( + db.session.query(EventConfiguration) + .join(WorkPhase, WorkPhase.id == EventConfiguration.work_phase_id) + .filter( + EventConfiguration.is_active.is_(True), + EventConfiguration.is_deleted.is_(False), + WorkPhase.is_active.is_(True), + WorkPhase.is_deleted.is_(False), + WorkPhase.work_id == work_id, + ) + ) + if len(event_categories) > 0: + category_ids = list(map(lambda x: x.value, event_categories)) + query = query.filter(EventConfiguration.event_category_id.in_(category_ids)) + configurations = query.all() + return configurations + + @classmethod + def find_child_configurations(cls, configuration_id: int) -> List[EventConfiguration]: + """Get all the child configurations for a given phase""" + query = db.session.query(EventConfiguration).filter( + EventConfiguration.parent_id == configuration_id, + EventConfiguration.is_active.is_(True), + EventConfiguration.is_deleted.is_(False), + ) + configurations = query.all() + return configurations + + @classmethod + def find_parent_child_configurations( + cls, configuration_id: int + ) -> List[EventConfiguration]: + """Get both parent and child configurations""" + query = db.session.query(EventConfiguration).filter( + or_( + EventConfiguration.id == configuration_id, + EventConfiguration.parent_id == configuration_id, + ), + EventConfiguration.is_active.is_(True), + ) + configurations = query.all() + return configurations diff --git a/epictrack-api/src/api/services/event_template.py b/epictrack-api/src/api/services/event_template.py index 409864ae8..f0bdccb53 100644 --- a/epictrack-api/src/api/services/event_template.py +++ b/epictrack-api/src/api/services/event_template.py @@ -1,450 +1,450 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Event Template.""" -import copy -import json -from typing import IO, Dict - -import pandas as pd - -from api.exceptions import BadRequestError -from api.models import ( - Action, - ActionTemplate, - EAAct, - EventCategory, - EventTemplate, - EventType, - OutcomeTemplate, - PhaseCode, - WorkType, - db, -) -from api.schemas import request as req -from api.schemas import response as res -from api.services.phaseservice import PhaseService -from api.utils.str import escape_characters - - -class EventTemplateService: - # pylint: disable=too-many-locals, - # pylint: disable=too-few-public-methods, - # pylint: disable=too-many-branches, too-many-statements - """Service to manage configurations""" - - @classmethod - def import_events_template(cls, configuration_file): - """Import event configurations in to database""" - final_result = [] - excel_dict = cls._read_excel(configuration_file=configuration_file) - ( - work_types, - ea_acts, - event_types, - event_categories, - actions, - ) = cls._get_event_configuration_lookup_entities() - event_dict = excel_dict.get("Events") - phase_dict = excel_dict.get("Phases") - outcome_dict = excel_dict.get("Outcomes") - action_dict = excel_dict.get("Actions") - for event_type in event_types: - name = escape_characters(event_type.name, ["(", ")"]) - event_dict = event_dict.replace( - {"event_type_id": rf"^{name}$"}, - {"event_type_id": event_type.id}, - regex=True, - ) - for event_category in event_categories: - name = escape_characters(event_category.name, ["(", ")"]) - event_dict = event_dict.replace( - {"event_category_id": rf"^{name}$"}, - {"event_category_id": event_category.id}, - regex=True, - ) - for work_type in work_types: - name = escape_characters(work_type.name, ["(", ")"]) - phase_dict = phase_dict.replace( - {"work_type_id": rf"^{name}$"}, - {"work_type_id": work_type.id}, - regex=True, - ) - for ea_act in ea_acts: - name = escape_characters(ea_act.name, ["(", ")"]) - phase_dict = phase_dict.replace( - {"ea_act_id": rf"^{name}$"}, {"ea_act_id": ea_act.id}, regex=True - ) - for action in actions: - name = escape_characters(action.name, ["(", ")"]) - action_dict = action_dict.replace( - {"action_id": rf"^{name}$"}, {"action_id": action.id}, regex=True - ) - - event_dict = event_dict.to_dict("records") - phase_dict = phase_dict.to_dict("records") - existing_phases = PhaseService.find_phase_codes_by_ea_act_and_work_type( - phase_dict[0]["ea_act_id"], phase_dict[0]["work_type_id"] - ) - for phase in phase_dict: - selected_phase = next( - (p for p in existing_phases if p.name == phase["name"]), None - ) - phase_obj = req.PhaseBodyParameterSchema().load(phase) - if selected_phase: - phase_result = selected_phase.update(phase_obj, commit=False) - else: - phase_result = PhaseCode(**phase_obj).flush() - phase_result_copy = res.PhaseResponseSchema().dump(phase_result) - phase_result_copy["events"] = [] - parent_events = copy.deepcopy( - list( - filter( - lambda x, _phase_no=phase["no"]: "phase_no" in x - and x["phase_no"] == _phase_no - and not x["parent_id"], - event_dict, - ) - ) - ) - existing_events = EventTemplate.find_by_phase_id(phase_result.id) - template_ids = list(map(lambda x: x.id, existing_events)) - existing_outcomes = OutcomeTemplate.find_by_template_ids(template_ids) - outcome_ids = list(map(lambda x: x.id, existing_outcomes)) - existing_actions = ActionTemplate.find_by_outcome_ids(outcome_ids) - for event in parent_events: - event["phase_id"] = phase_result.id - event["start_at"] = str(event["start_at"]) - event_result = cls._save_event_template( - existing_events, event, phase_result.id - ) - child_events = copy.deepcopy( - list( - filter( - lambda x, _parent_id=event["no"]: "parent_id" in x - and x["parent_id"] == _parent_id, - event_dict, - ) - ) - ) - outcome_dict.loc[ - outcome_dict["template_no"] == event["no"], "event_template_id" - ] = event_result.id - outcome_results = cls._handle_outcomes( - outcome_dict, - existing_outcomes, - existing_actions, - action_dict, - event, - ) - event_result_copy = res.EventTemplateResponseSchema().dump(event_result) - event_result_copy["outcomes"] = outcome_results - (phase_result_copy["events"]).append(event_result_copy) - for child in child_events: - child["phase_id"] = phase_result.id - child["parent_id"] = event_result.id - child["start_at"] = str(child["start_at"]) - child_event_result = cls._save_event_template( - existing_events, child, phase_result.id, event_result.id - ) - outcome_dict.loc[ - outcome_dict["template_no"] == child["no"], "event_template_id" - ] = child_event_result.id - outcome_results = cls._handle_outcomes( - outcome_dict, - existing_outcomes, - existing_actions, - action_dict, - child, - ) - event_result_copy = res.EventTemplateResponseSchema().dump( - child_event_result - ) - event_result_copy["outcomes"] = outcome_results - (phase_result_copy["events"]).append(event_result_copy) - child_events = [] - final_result.append(phase_result_copy) - cls._handle_deletion_templates( - existing_events, - existing_outcomes, - existing_actions, - final_result, - phase_result.id, - ) - # deletion of phases - existing_set = set(list(map(lambda x: x.id, existing_phases))) - incoming_set = set(list(map(lambda x: x["id"], final_result))) - difference = list(existing_set.difference(incoming_set)) - db.session.query(PhaseCode).filter(PhaseCode.id.in_(difference)).update( - {PhaseCode.is_active: False, PhaseCode.is_deleted: True} - ) - # handling deletion of event templates, outcomes and actions of the deleted phases - templates_from_removed_phases = db.session.query(EventTemplate).filter( - EventTemplate.phase_id.in_(difference) - ) - removed_template_ids = list(map(lambda x: x.id, templates_from_removed_phases)) - outcomes_from_removed_templates = db.session.query(OutcomeTemplate).filter( - OutcomeTemplate.event_template_id.in_(removed_template_ids) - ) - removed_outcome_ids = list(map(lambda x: x.id, outcomes_from_removed_templates)) - actions_from_removed_outcomes = db.session.query(ActionTemplate).filter( - ActionTemplate.outcome_id.in_(removed_outcome_ids) - ) - removed_action_ids = list(map(lambda x: x.id, actions_from_removed_outcomes)) - db.session.query(EventTemplate).filter( - EventTemplate.id.in_(removed_template_ids) - ).update({EventTemplate.is_active: False, EventTemplate.is_deleted: True}) - db.session.query(OutcomeTemplate).filter( - OutcomeTemplate.id.in_(removed_outcome_ids) - ).update({OutcomeTemplate.is_active: False, OutcomeTemplate.is_deleted: True}) - db.session.query(ActionTemplate).filter( - ActionTemplate.id.in_(removed_action_ids) - ).update({ActionTemplate.is_active: False, ActionTemplate.is_deleted: True}) - db.session.commit() - return final_result - - @classmethod - def _handle_deletion_templates( - cls, existing_events, existing_outcomes, existing_actions, results, phase_id - ): - # pylint: disable=too-many-arguments - """Handle deletion""" - # events - existing_set = set(list(map(lambda x: x.id, existing_events))) - incoming_set = [] - incoming_outcome_set = [] - incoming_action_set = [] - for phase in results: - event_list = copy.deepcopy( - list(filter(lambda x: x["phase_id"] == phase_id, phase["events"])) - ) - template_ids = list(map(lambda x: x["id"], event_list)) - incoming_set.extend(template_ids) - for event in phase["events"]: - incoming_outcome_set.extend( - list(map(lambda x: x["id"], event["outcomes"])) - ) - for outcome in event["outcomes"]: - incoming_action_set.extend( - list(map(lambda x: x["id"], outcome["actions"])) - ) - difference = list(existing_set.difference(incoming_set)) - db.session.query(EventTemplate).filter(EventTemplate.id.in_(difference)).update( - {EventTemplate.is_active: False, EventTemplate.is_deleted: False} - ) - existing_outcome_set = set(list(map(lambda x: x.id, existing_outcomes))) - difference = list(existing_outcome_set.difference(incoming_outcome_set)) - db.session.query(OutcomeTemplate).filter( - OutcomeTemplate.id.in_(difference) - ).update({OutcomeTemplate.is_active: False, OutcomeTemplate.is_deleted: False}) - existing_action_set = set(list(map(lambda x: x.id, existing_actions))) - difference = list(existing_action_set.difference(incoming_action_set)) - db.session.query(ActionTemplate).filter( - ActionTemplate.id.in_(difference) - ).update({ActionTemplate.is_active: False, ActionTemplate.is_deleted: False}) - - @classmethod - def _handle_outcomes( - cls, outcome_dict, existing_outcomes, existing_actions, action_dict, event - ): - # pylint: disable=too-many-arguments - """Save the outcome""" - outcome_list = copy.deepcopy( - list( - filter( - lambda x: x["template_no"] == event["no"], - outcome_dict.to_dict("records"), - ) - ) - ) - outcome_final = [] - for outcome in outcome_list: - selected_outcome = next( - ( - e - for e in existing_outcomes - if e.name == outcome["name"] - and e.event_template_id == outcome["event_template_id"] - and e.sort_order == outcome["sort_order"] - ), - None, - ) - outcome_obj = req.OutcomeTemplateBodyParameterSchema().load(outcome) - if selected_outcome: - outcome_result = selected_outcome.update(outcome_obj, commit=False) - else: - outcome_result = OutcomeTemplate(**outcome_obj).flush() - outcome_result_copy = res.OutcomeTemplateResponseSchema().dump( - outcome_result - ) - (outcome_result_copy["actions"]) = [] - action_dict.loc[ - action_dict["outcome_no"] == outcome["no"], "outcome_id" - ] = outcome_result.id - actions_list = copy.deepcopy( - list( - filter( - lambda x, _outcome_no=outcome["no"]: x["outcome_no"] - == _outcome_no and x["action_id"] != "NONE", - action_dict.to_dict("records"), - ) - ) - ) - for action in actions_list: - selected_action = next( - ( - e - for e in existing_actions - if e.action_id == action["action_id"] - and e.outcome_id == action["outcome_id"] - and e.sort_order == action["sort_order"] - ), - None, - ) - action["additional_params"] = json.loads(action["additional_params"]) - action_obj = req.ActionTemplateBodyParameterSchema().load(action) - if selected_action: - action_result = selected_action.update(action_obj, commit=False) - else: - action_result = ActionTemplate(**action_obj).flush() - (outcome_result_copy["actions"]).append( - res.ActionTemplateResponseSchema().dump(action_result) - ) - outcome_final.append(outcome_result_copy) - return outcome_final - - @classmethod - def _save_event_template( - cls, existing_events, event, phase_id, parent_id=None - ) -> EventTemplate: - """Save the event templateI""" - selected_event = next( - ( - e - for e in existing_events - if e.name == event["name"] - and e.phase_id == phase_id - and (e.parent_id == parent_id) - and e.event_type_id == event["event_type_id"] - and e.event_category_id == event["event_category_id"] - ), - None, - ) - event_obj = req.EventTemplateBodyParameterSchema().load(event) - if selected_event: - event_result = selected_event.update(event_obj, commit=False) - else: - event_result = EventTemplate(**event_obj).flush() - return event_result - - @classmethod - def _read_excel(cls, configuration_file: IO) -> Dict[str, pd.DataFrame]: - """Read the excel and return the data frame""" - result = {} - sheets = ["Phases", "Events", "Outcomes", "Actions"] - sheet_obj_map = { - "phases": { - "No": "no", - "Name": "name", - "WorkType": "work_type_id", - "EAAct": "ea_act_id", - "NumberOfDays": "number_of_days", - "Color": "color", - "SortOrder": "sort_order", - "Legislated": "legislated", - "Visibility": "visibility", - }, - "events": { - "No": "no", - "Parent": "parent_id", - "PhaseNo": "phase_no", - "EventName": "name", - "Phase": "phase_id", - "EventType": "event_type_id", - "EventCategory": "event_category_id", - "EventPosition": "event_position", - "MultipleDays": "multiple_days", - "NumberOfDays": "number_of_days", - "StartAt": "start_at", - "Visibility": "visibility", - "SortOrder": "sort_order", - }, - "outcomes": { - "No": "no", - "TemplateNo": "template_no", - "TemplateName": "event_template_id", - "OutcomeName": "name", - "SortOrder": "sort_order", - }, - "actions": { - "No": "no", - "OutcomeNo": "outcome_no", - "OutcomeName": "outcome_id", - "ActionName": "action_id", - "ActionDescription": "description", - "AdditionalParams": "additional_params", - "SortOrder": "sort_order", - }, - } - try: - excel_dict = pd.read_excel(configuration_file, sheets, na_filter=False) - phase_dict = pd.DataFrame(excel_dict.get("Phases")) - phase_dict.rename(sheet_obj_map["phases"], axis="columns", inplace=True) - event_dict = pd.DataFrame(excel_dict.get("Events")) - event_dict.rename(sheet_obj_map["events"], axis="columns", inplace=True) - outcome_dict = pd.DataFrame(excel_dict.get("Outcomes")) - outcome_dict.rename(sheet_obj_map["outcomes"], axis="columns", inplace=True) - action_dict = pd.DataFrame(excel_dict.get("Actions")) - action_dict.rename(sheet_obj_map["actions"], axis="columns", inplace=True) - result["Phases"] = phase_dict - result["Events"] = event_dict - result["Outcomes"] = outcome_dict - result["Actions"] = action_dict - except ValueError as exc: - raise BadRequestError( - "Sheets missing in the imported excel.\ - Required sheets are [" - + ",".join(sheets) - + "]" - ) from exc - return result - - @classmethod - def _get_event_configuration_lookup_entities(cls): - """Returns the look up entities required to create the event configurations""" - work_types = WorkType.find_all() - ea_acts = EAAct.find_all() - event_types = EventType.find_all() - event_categories = EventCategory.find_all() - actions = Action.find_all() - return work_types, ea_acts, event_types, event_categories, actions - - @classmethod - def find_mandatory_event_templates(cls, phase_id: int): - """Get all the mandatory event templates""" - templates = EventTemplate.query.filter_by( - EventTemplate.phase_id == phase_id - ).all() - return templates - - @classmethod - def find_by_phase_id(cls, phase_id: int) -> [EventTemplate]: - """Get event templates under given phase""" - templates = EventTemplate.find_by_phase_id(phase_id) - return templates - - @classmethod - def find_by_phase_ids(cls, phase_ids: [int]) -> [EventTemplate]: - """Get event templates under given phases""" - templates = EventTemplate.find_by_phase_ids(phase_ids) - return templates +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Event Template.""" +import copy +import json +from typing import IO, Dict + +import pandas as pd + +from api.exceptions import BadRequestError +from api.models import ( + Action, + ActionTemplate, + EAAct, + EventCategory, + EventTemplate, + EventType, + OutcomeTemplate, + PhaseCode, + WorkType, + db, +) +from api.schemas import request as req +from api.schemas import response as res +from api.services.phaseservice import PhaseService +from api.utils.str import escape_characters + + +class EventTemplateService: + # pylint: disable=too-many-locals, + # pylint: disable=too-few-public-methods, + # pylint: disable=too-many-branches, too-many-statements + """Service to manage configurations""" + + @classmethod + def import_events_template(cls, configuration_file): + """Import event configurations in to database""" + final_result = [] + excel_dict = cls._read_excel(configuration_file=configuration_file) + ( + work_types, + ea_acts, + event_types, + event_categories, + actions, + ) = cls._get_event_configuration_lookup_entities() + event_dict = excel_dict.get("Events") + phase_dict = excel_dict.get("Phases") + outcome_dict = excel_dict.get("Outcomes") + action_dict = excel_dict.get("Actions") + for event_type in event_types: + name = escape_characters(event_type.name, ["(", ")"]) + event_dict = event_dict.replace( + {"event_type_id": rf"^{name}$"}, + {"event_type_id": event_type.id}, + regex=True, + ) + for event_category in event_categories: + name = escape_characters(event_category.name, ["(", ")"]) + event_dict = event_dict.replace( + {"event_category_id": rf"^{name}$"}, + {"event_category_id": event_category.id}, + regex=True, + ) + for work_type in work_types: + name = escape_characters(work_type.name, ["(", ")"]) + phase_dict = phase_dict.replace( + {"work_type_id": rf"^{name}$"}, + {"work_type_id": work_type.id}, + regex=True, + ) + for ea_act in ea_acts: + name = escape_characters(ea_act.name, ["(", ")"]) + phase_dict = phase_dict.replace( + {"ea_act_id": rf"^{name}$"}, {"ea_act_id": ea_act.id}, regex=True + ) + for action in actions: + name = escape_characters(action.name, ["(", ")"]) + action_dict = action_dict.replace( + {"action_id": rf"^{name}$"}, {"action_id": action.id}, regex=True + ) + + event_dict = event_dict.to_dict("records") + phase_dict = phase_dict.to_dict("records") + existing_phases = PhaseService.find_phase_codes_by_ea_act_and_work_type( + phase_dict[0]["ea_act_id"], phase_dict[0]["work_type_id"] + ) + for phase in phase_dict: + selected_phase = next( + (p for p in existing_phases if p.name == phase["name"]), None + ) + phase_obj = req.PhaseBodyParameterSchema().load(phase) + if selected_phase: + phase_result = selected_phase.update(phase_obj, commit=False) + else: + phase_result = PhaseCode(**phase_obj).flush() + phase_result_copy = res.PhaseResponseSchema().dump(phase_result) + phase_result_copy["events"] = [] + parent_events = copy.deepcopy( + list( + filter( + lambda x, _phase_no=phase["no"]: "phase_no" in x + and x["phase_no"] == _phase_no + and not x["parent_id"], + event_dict, + ) + ) + ) + existing_events = EventTemplate.find_by_phase_id(phase_result.id) + template_ids = list(map(lambda x: x.id, existing_events)) + existing_outcomes = OutcomeTemplate.find_by_template_ids(template_ids) + outcome_ids = list(map(lambda x: x.id, existing_outcomes)) + existing_actions = ActionTemplate.find_by_outcome_ids(outcome_ids) + for event in parent_events: + event["phase_id"] = phase_result.id + event["start_at"] = str(event["start_at"]) + event_result = cls._save_event_template( + existing_events, event, phase_result.id + ) + child_events = copy.deepcopy( + list( + filter( + lambda x, _parent_id=event["no"]: "parent_id" in x + and x["parent_id"] == _parent_id, + event_dict, + ) + ) + ) + outcome_dict.loc[ + outcome_dict["template_no"] == event["no"], "event_template_id" + ] = event_result.id + outcome_results = cls._handle_outcomes( + outcome_dict, + existing_outcomes, + existing_actions, + action_dict, + event, + ) + event_result_copy = res.EventTemplateResponseSchema().dump(event_result) + event_result_copy["outcomes"] = outcome_results + (phase_result_copy["events"]).append(event_result_copy) + for child in child_events: + child["phase_id"] = phase_result.id + child["parent_id"] = event_result.id + child["start_at"] = str(child["start_at"]) + child_event_result = cls._save_event_template( + existing_events, child, phase_result.id, event_result.id + ) + outcome_dict.loc[ + outcome_dict["template_no"] == child["no"], "event_template_id" + ] = child_event_result.id + outcome_results = cls._handle_outcomes( + outcome_dict, + existing_outcomes, + existing_actions, + action_dict, + child, + ) + event_result_copy = res.EventTemplateResponseSchema().dump( + child_event_result + ) + event_result_copy["outcomes"] = outcome_results + (phase_result_copy["events"]).append(event_result_copy) + child_events = [] + final_result.append(phase_result_copy) + cls._handle_deletion_templates( + existing_events, + existing_outcomes, + existing_actions, + final_result, + phase_result.id, + ) + # deletion of phases + existing_set = set(list(map(lambda x: x.id, existing_phases))) + incoming_set = set(list(map(lambda x: x["id"], final_result))) + difference = list(existing_set.difference(incoming_set)) + db.session.query(PhaseCode).filter(PhaseCode.id.in_(difference)).update( + {PhaseCode.is_active: False, PhaseCode.is_deleted: True} + ) + # handling deletion of event templates, outcomes and actions of the deleted phases + templates_from_removed_phases = db.session.query(EventTemplate).filter( + EventTemplate.phase_id.in_(difference) + ) + removed_template_ids = list(map(lambda x: x.id, templates_from_removed_phases)) + outcomes_from_removed_templates = db.session.query(OutcomeTemplate).filter( + OutcomeTemplate.event_template_id.in_(removed_template_ids) + ) + removed_outcome_ids = list(map(lambda x: x.id, outcomes_from_removed_templates)) + actions_from_removed_outcomes = db.session.query(ActionTemplate).filter( + ActionTemplate.outcome_id.in_(removed_outcome_ids) + ) + removed_action_ids = list(map(lambda x: x.id, actions_from_removed_outcomes)) + db.session.query(EventTemplate).filter( + EventTemplate.id.in_(removed_template_ids) + ).update({EventTemplate.is_active: False, EventTemplate.is_deleted: True}) + db.session.query(OutcomeTemplate).filter( + OutcomeTemplate.id.in_(removed_outcome_ids) + ).update({OutcomeTemplate.is_active: False, OutcomeTemplate.is_deleted: True}) + db.session.query(ActionTemplate).filter( + ActionTemplate.id.in_(removed_action_ids) + ).update({ActionTemplate.is_active: False, ActionTemplate.is_deleted: True}) + db.session.commit() + return final_result + + @classmethod + def _handle_deletion_templates( + cls, existing_events, existing_outcomes, existing_actions, results, phase_id + ): + # pylint: disable=too-many-arguments + """Handle deletion""" + # events + existing_set = set(list(map(lambda x: x.id, existing_events))) + incoming_set = [] + incoming_outcome_set = [] + incoming_action_set = [] + for phase in results: + event_list = copy.deepcopy( + list(filter(lambda x: x["phase_id"] == phase_id, phase["events"])) + ) + template_ids = list(map(lambda x: x["id"], event_list)) + incoming_set.extend(template_ids) + for event in phase["events"]: + incoming_outcome_set.extend( + list(map(lambda x: x["id"], event["outcomes"])) + ) + for outcome in event["outcomes"]: + incoming_action_set.extend( + list(map(lambda x: x["id"], outcome["actions"])) + ) + difference = list(existing_set.difference(incoming_set)) + db.session.query(EventTemplate).filter(EventTemplate.id.in_(difference)).update( + {EventTemplate.is_active: False, EventTemplate.is_deleted: False} + ) + existing_outcome_set = set(list(map(lambda x: x.id, existing_outcomes))) + difference = list(existing_outcome_set.difference(incoming_outcome_set)) + db.session.query(OutcomeTemplate).filter( + OutcomeTemplate.id.in_(difference) + ).update({OutcomeTemplate.is_active: False, OutcomeTemplate.is_deleted: False}) + existing_action_set = set(list(map(lambda x: x.id, existing_actions))) + difference = list(existing_action_set.difference(incoming_action_set)) + db.session.query(ActionTemplate).filter( + ActionTemplate.id.in_(difference) + ).update({ActionTemplate.is_active: False, ActionTemplate.is_deleted: False}) + + @classmethod + def _handle_outcomes( + cls, outcome_dict, existing_outcomes, existing_actions, action_dict, event + ): + # pylint: disable=too-many-arguments + """Save the outcome""" + outcome_list = copy.deepcopy( + list( + filter( + lambda x: x["template_no"] == event["no"], + outcome_dict.to_dict("records"), + ) + ) + ) + outcome_final = [] + for outcome in outcome_list: + selected_outcome = next( + ( + e + for e in existing_outcomes + if e.name == outcome["name"] + and e.event_template_id == outcome["event_template_id"] + and e.sort_order == outcome["sort_order"] + ), + None, + ) + outcome_obj = req.OutcomeTemplateBodyParameterSchema().load(outcome) + if selected_outcome: + outcome_result = selected_outcome.update(outcome_obj, commit=False) + else: + outcome_result = OutcomeTemplate(**outcome_obj).flush() + outcome_result_copy = res.OutcomeTemplateResponseSchema().dump( + outcome_result + ) + (outcome_result_copy["actions"]) = [] + action_dict.loc[ + action_dict["outcome_no"] == outcome["no"], "outcome_id" + ] = outcome_result.id + actions_list = copy.deepcopy( + list( + filter( + lambda x, _outcome_no=outcome["no"]: x["outcome_no"] + == _outcome_no and x["action_id"] != "NONE", + action_dict.to_dict("records"), + ) + ) + ) + for action in actions_list: + selected_action = next( + ( + e + for e in existing_actions + if e.action_id == action["action_id"] + and e.outcome_id == action["outcome_id"] + and e.sort_order == action["sort_order"] + ), + None, + ) + action["additional_params"] = json.loads(action["additional_params"]) + action_obj = req.ActionTemplateBodyParameterSchema().load(action) + if selected_action: + action_result = selected_action.update(action_obj, commit=False) + else: + action_result = ActionTemplate(**action_obj).flush() + (outcome_result_copy["actions"]).append( + res.ActionTemplateResponseSchema().dump(action_result) + ) + outcome_final.append(outcome_result_copy) + return outcome_final + + @classmethod + def _save_event_template( + cls, existing_events, event, phase_id, parent_id=None + ) -> EventTemplate: + """Save the event templateI""" + selected_event = next( + ( + e + for e in existing_events + if e.name == event["name"] + and e.phase_id == phase_id + and (e.parent_id == parent_id) + and e.event_type_id == event["event_type_id"] + and e.event_category_id == event["event_category_id"] + ), + None, + ) + event_obj = req.EventTemplateBodyParameterSchema().load(event) + if selected_event: + event_result = selected_event.update(event_obj, commit=False) + else: + event_result = EventTemplate(**event_obj).flush() + return event_result + + @classmethod + def _read_excel(cls, configuration_file: IO) -> Dict[str, pd.DataFrame]: + """Read the excel and return the data frame""" + result = {} + sheets = ["Phases", "Events", "Outcomes", "Actions"] + sheet_obj_map = { + "phases": { + "No": "no", + "Name": "name", + "WorkType": "work_type_id", + "EAAct": "ea_act_id", + "NumberOfDays": "number_of_days", + "Color": "color", + "SortOrder": "sort_order", + "Legislated": "legislated", + "Visibility": "visibility", + }, + "events": { + "No": "no", + "Parent": "parent_id", + "PhaseNo": "phase_no", + "EventName": "name", + "Phase": "phase_id", + "EventType": "event_type_id", + "EventCategory": "event_category_id", + "EventPosition": "event_position", + "MultipleDays": "multiple_days", + "NumberOfDays": "number_of_days", + "StartAt": "start_at", + "Visibility": "visibility", + "SortOrder": "sort_order", + }, + "outcomes": { + "No": "no", + "TemplateNo": "template_no", + "TemplateName": "event_template_id", + "OutcomeName": "name", + "SortOrder": "sort_order", + }, + "actions": { + "No": "no", + "OutcomeNo": "outcome_no", + "OutcomeName": "outcome_id", + "ActionName": "action_id", + "ActionDescription": "description", + "AdditionalParams": "additional_params", + "SortOrder": "sort_order", + }, + } + try: + excel_dict = pd.read_excel(configuration_file, sheets, na_filter=False) + phase_dict = pd.DataFrame(excel_dict.get("Phases")) + phase_dict.rename(sheet_obj_map["phases"], axis="columns", inplace=True) + event_dict = pd.DataFrame(excel_dict.get("Events")) + event_dict.rename(sheet_obj_map["events"], axis="columns", inplace=True) + outcome_dict = pd.DataFrame(excel_dict.get("Outcomes")) + outcome_dict.rename(sheet_obj_map["outcomes"], axis="columns", inplace=True) + action_dict = pd.DataFrame(excel_dict.get("Actions")) + action_dict.rename(sheet_obj_map["actions"], axis="columns", inplace=True) + result["Phases"] = phase_dict + result["Events"] = event_dict + result["Outcomes"] = outcome_dict + result["Actions"] = action_dict + except ValueError as exc: + raise BadRequestError( + "Sheets missing in the imported excel.\ + Required sheets are [" + + ",".join(sheets) + + "]" + ) from exc + return result + + @classmethod + def _get_event_configuration_lookup_entities(cls): + """Returns the look up entities required to create the event configurations""" + work_types = WorkType.find_all() + ea_acts = EAAct.find_all() + event_types = EventType.find_all() + event_categories = EventCategory.find_all() + actions = Action.find_all() + return work_types, ea_acts, event_types, event_categories, actions + + @classmethod + def find_mandatory_event_templates(cls, phase_id: int): + """Get all the mandatory event templates""" + templates = EventTemplate.query.filter_by( + EventTemplate.phase_id == phase_id + ).all() + return templates + + @classmethod + def find_by_phase_id(cls, phase_id: int) -> [EventTemplate]: + """Get event templates under given phase""" + templates = EventTemplate.find_by_phase_id(phase_id) + return templates + + @classmethod + def find_by_phase_ids(cls, phase_ids: [int]) -> [EventTemplate]: + """Get event templates under given phases""" + templates = EventTemplate.find_by_phase_ids(phase_ids) + return templates diff --git a/epictrack-api/src/api/services/keycloak.py b/epictrack-api/src/api/services/keycloak.py index 687eb1658..d06e8cac1 100644 --- a/epictrack-api/src/api/services/keycloak.py +++ b/epictrack-api/src/api/services/keycloak.py @@ -1,126 +1,126 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Keycloak admin functions""" -import requests -from flask import current_app -from api.utils.enums import HttpMethod - - -class KeycloakService: - """Keycloak services""" - - @staticmethod - def get_groups(brief_representation: bool = False): - """Get all the groups""" - response = KeycloakService._request_keycloak(f'groups?briefRepresentation={brief_representation}') - return response.json() - - @staticmethod - def get_sub_groups(group_id): - """ - Return the subgroups of a given group. - - Args: - group_id (str): The ID of the group for which to retrieve subgroups. - - Returns: - list: A list of subgroups for the given group. - """ - response = KeycloakService._request_keycloak(f"groups/{group_id}/children") - return response.json() - - @staticmethod - def get_users(): - """Get users""" - response = KeycloakService._request_keycloak('users?max=2000') - return response.json() - - @staticmethod - def get_user_by_email(email: str): - """Get a user by their email address. If the user does not exist, throw an error.""" - encoded_email = requests.utils.quote(email) # URL encode the email to handle special characters - response = KeycloakService._request_keycloak(f'users?email={encoded_email}') - users = response.json() - if not users: - raise ValueError(f"No user found with email: {email}") - print(users) - return users - - @staticmethod - def get_group_members(group_id): - """Get the members of a group""" - response = KeycloakService._request_keycloak(f'groups/{group_id}/members') - return response.json() - - @staticmethod - def update_user_group(user_id, group_id): - """Update the group of user""" - return KeycloakService._request_keycloak(f'users/{user_id}/groups/{group_id}', HttpMethod.PUT) - - @staticmethod - def delete_user_group(user_id, group_id): - """Delete user-group mapping""" - return KeycloakService._request_keycloak(f'users/{user_id}/groups/{group_id}', HttpMethod.DELETE) - - @staticmethod - def get_user_groups(user_id): - """Get groups of a user by user ID""" - response = KeycloakService._request_keycloak(f'users/{user_id}/groups') - return response.json() - - @staticmethod - def _request_keycloak(relative_url, http_method: HttpMethod = HttpMethod.GET, data=None): - """Common method to request keycloak""" - base_url = current_app.config.get('KEYCLOAK_BASE_URL') - realm = current_app.config.get('KEYCLOAK_REALM_NAME') - timeout = int(current_app.config.get('CONNECT_TIMEOUT', 60)) - admin_token = KeycloakService._get_admin_token() - headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {admin_token}' - } - - url = f'{base_url}/auth/admin/realms/{realm}/{relative_url}' - - if http_method == HttpMethod.GET: - response = requests.get(url, headers=headers, timeout=timeout) - elif http_method == HttpMethod.PUT: - response = requests.put(url, headers=headers, data=data, timeout=timeout) - elif http_method == HttpMethod.DELETE: - response = requests.delete(url, headers=headers, timeout=timeout) - else: - raise ValueError('Invalid HTTP method') - response.raise_for_status() - return response - - @staticmethod - def _get_admin_token(): - """Create an admin token.""" - config = current_app.config - base_url = config.get('KEYCLOAK_BASE_URL') - realm = config.get('KEYCLOAK_REALM_NAME') - admin_client_id = config.get( - 'KEYCLOAK_ADMIN_CLIENT') - admin_secret = config.get('KEYCLOAK_ADMIN_SECRET') - timeout = int(config.get('CONNECT_TIMEOUT', 60)) - headers = { - 'Content-Type': 'application/x-www-form-urlencoded' - } - token_url = f'{base_url}/auth/realms/{realm}/protocol/openid-connect/token' - - response = requests.post(token_url, - data=f'client_id={admin_client_id}&grant_type=client_credentials' - f'&client_secret={admin_secret}', headers=headers, - timeout=timeout) - return response.json().get('access_token') +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Keycloak admin functions""" +import requests +from flask import current_app +from api.utils.enums import HttpMethod + + +class KeycloakService: + """Keycloak services""" + + @staticmethod + def get_groups(brief_representation: bool = False): + """Get all the groups""" + response = KeycloakService._request_keycloak(f'groups?briefRepresentation={brief_representation}') + return response.json() + + @staticmethod + def get_sub_groups(group_id): + """ + Return the subgroups of a given group. + + Args: + group_id (str): The ID of the group for which to retrieve subgroups. + + Returns: + list: A list of subgroups for the given group. + """ + response = KeycloakService._request_keycloak(f"groups/{group_id}/children") + return response.json() + + @staticmethod + def get_users(): + """Get users""" + response = KeycloakService._request_keycloak('users?max=2000') + return response.json() + + @staticmethod + def get_user_by_email(email: str): + """Get a user by their email address. If the user does not exist, throw an error.""" + encoded_email = requests.utils.quote(email) # URL encode the email to handle special characters + response = KeycloakService._request_keycloak(f'users?email={encoded_email}') + users = response.json() + if not users: + raise ValueError(f"No user found with email: {email}") + print(users) + return users + + @staticmethod + def get_group_members(group_id): + """Get the members of a group""" + response = KeycloakService._request_keycloak(f'groups/{group_id}/members') + return response.json() + + @staticmethod + def update_user_group(user_id, group_id): + """Update the group of user""" + return KeycloakService._request_keycloak(f'users/{user_id}/groups/{group_id}', HttpMethod.PUT) + + @staticmethod + def delete_user_group(user_id, group_id): + """Delete user-group mapping""" + return KeycloakService._request_keycloak(f'users/{user_id}/groups/{group_id}', HttpMethod.DELETE) + + @staticmethod + def get_user_groups(user_id): + """Get groups of a user by user ID""" + response = KeycloakService._request_keycloak(f'users/{user_id}/groups') + return response.json() + + @staticmethod + def _request_keycloak(relative_url, http_method: HttpMethod = HttpMethod.GET, data=None): + """Common method to request keycloak""" + base_url = current_app.config.get('KEYCLOAK_BASE_URL') + realm = current_app.config.get('KEYCLOAK_REALM_NAME') + timeout = int(current_app.config.get('CONNECT_TIMEOUT', 60)) + admin_token = KeycloakService._get_admin_token() + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {admin_token}' + } + + url = f'{base_url}/auth/admin/realms/{realm}/{relative_url}' + + if http_method == HttpMethod.GET: + response = requests.get(url, headers=headers, timeout=timeout) + elif http_method == HttpMethod.PUT: + response = requests.put(url, headers=headers, data=data, timeout=timeout) + elif http_method == HttpMethod.DELETE: + response = requests.delete(url, headers=headers, timeout=timeout) + else: + raise ValueError('Invalid HTTP method') + response.raise_for_status() + return response + + @staticmethod + def _get_admin_token(): + """Create an admin token.""" + config = current_app.config + base_url = config.get('KEYCLOAK_BASE_URL') + realm = config.get('KEYCLOAK_REALM_NAME') + admin_client_id = config.get( + 'KEYCLOAK_ADMIN_CLIENT') + admin_secret = config.get('KEYCLOAK_ADMIN_SECRET') + timeout = int(config.get('CONNECT_TIMEOUT', 60)) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + token_url = f'{base_url}/auth/realms/{realm}/protocol/openid-connect/token' + + response = requests.post(token_url, + data=f'client_id={admin_client_id}&grant_type=client_credentials' + f'&client_secret={admin_secret}', headers=headers, + timeout=timeout) + return response.json().get('access_token') diff --git a/epictrack-api/src/api/services/outcome_configuration.py b/epictrack-api/src/api/services/outcome_configuration.py index 7a877ac64..f7330f664 100644 --- a/epictrack-api/src/api/services/outcome_configuration.py +++ b/epictrack-api/src/api/services/outcome_configuration.py @@ -1,44 +1,44 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Outcome Configuration.""" -from typing import List - -from flask import current_app - -from api.models import OutcomeConfiguration - - -class OutcomeConfigurationService: # pylint: disable=too-few-public-methods - """Service to manage outcome related operations""" - - @classmethod - def find_by_configuration_id( - cls, configuration_id: int - ) -> List[OutcomeConfiguration]: - """Find outcomes by configuration_id""" - current_app.logger.debug( - f"Find outcomes by configuration id {configuration_id}" - ) - outcome_configurations = OutcomeConfiguration.find_by_params( - {"event_configuration_id": configuration_id} - ) - return outcome_configurations - - @classmethod - def find_all_outcomes(cls, params: dict) -> [OutcomeConfiguration]: - """Return all active outcomes""" - outcomes = OutcomeConfiguration.find_by_params( - {"event_configuration_id": params.get("event_configuration_id")} - ) - return outcomes +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Outcome Configuration.""" +from typing import List + +from flask import current_app + +from api.models import OutcomeConfiguration + + +class OutcomeConfigurationService: # pylint: disable=too-few-public-methods + """Service to manage outcome related operations""" + + @classmethod + def find_by_configuration_id( + cls, configuration_id: int + ) -> List[OutcomeConfiguration]: + """Find outcomes by configuration_id""" + current_app.logger.debug( + f"Find outcomes by configuration id {configuration_id}" + ) + outcome_configurations = OutcomeConfiguration.find_by_params( + {"event_configuration_id": configuration_id} + ) + return outcome_configurations + + @classmethod + def find_all_outcomes(cls, params: dict) -> [OutcomeConfiguration]: + """Return all active outcomes""" + outcomes = OutcomeConfiguration.find_by_params( + {"event_configuration_id": params.get("event_configuration_id")} + ) + return outcomes diff --git a/epictrack-api/src/api/services/project.py b/epictrack-api/src/api/services/project.py index 6e60e62b5..cab4e0426 100644 --- a/epictrack-api/src/api/services/project.py +++ b/epictrack-api/src/api/services/project.py @@ -1,428 +1,428 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Project.""" -from datetime import datetime -from typing import IO, List - -import numpy as np -import pandas as pd -from flask import current_app -from psycopg2.extras import DateTimeTZRange -from sqlalchemy import and_ - -from api.exceptions import BadRequestError, ResourceExistsError, ResourceNotFoundError -from api.models import Project, db -from api.models.indigenous_nation import IndigenousNation -from api.models.indigenous_work import IndigenousWork -from api.models.project import ProjectStateEnum -from api.models.proponent import Proponent -from api.models.region import Region -from api.models.special_field import EntityEnum, SpecialField -from api.models.sub_types import SubType -from api.models.types import Type -from api.models.work import Work -from api.models.work_type import WorkType -from api.schemas.types import TypeSchema -from api.services.special_field import SpecialFieldService -from api.utils.constants import PROJECT_STATE_ENUM_MAPS -from api.utils.enums import ProjectCodeMethod -from api.utils.token_info import TokenInfo - - -class ProjectService: - """Service to manage project related operations.""" - - @classmethod - def find(cls, project_id, exclude_deleted=False): - """Find by project id.""" - query = db.session.query(Project).filter(Project.id == project_id) - if exclude_deleted: - query = query.filter(Project.is_deleted.is_(False)) - project = query.one_or_none() - if project: - return project - raise ResourceNotFoundError(f"Project with id '{project_id}' not found.") - - @classmethod - def find_all(cls, with_works=False, is_active=None): - """Find all projects""" - return Project.find_all_projects(with_works, is_active) - - @classmethod - def create_project(cls, payload: dict): - """Create a new project.""" - exists = cls.check_existence(payload["name"]) - if exists: - raise ResourceExistsError("Project with same name exists") - project = Project(**payload) - project.project_state = ProjectStateEnum.PRE_WORK - current_app.logger.info(f"Project obj {dir(project)}") - project.flush() - proponent_special_field_data = { - "entity": EntityEnum.PROJECT, - "entity_id": project.id, - "field_name": "proponent_id", - "field_value": project.proponent_id, - "active_from": project.created_at, - } - SpecialFieldService.create_special_field_entry(proponent_special_field_data) - project_name_special_field_data = { - "entity": EntityEnum.PROJECT, - "entity_id": project.id, - "field_name": "name", - "field_value": project.name, - "active_from": project.created_at, - } - SpecialFieldService.create_special_field_entry(project_name_special_field_data) - project.save() - return project - - @classmethod - def update_project(cls, project_id: int, payload: dict): - """Update existing project.""" - exists = cls.check_existence(payload["name"], project_id) - if exists: - raise ResourceExistsError("Project with same name exists") - project = Project.find_by_id(project_id) - if not project: - raise ResourceNotFoundError(f"Project with id '{project_id}' not found.") - project = project.update(payload) - return project - - @classmethod - def delete_project(cls, project_id: int): - """Delete project by id.""" - project = Project.find_by_id(project_id) - project.is_deleted = True - project.save() - return True - - @classmethod - def check_existence(cls, name, project_id=None): - """Checks if a project exists with given name""" - return Project.check_existence(name, project_id) - - @classmethod - def find_project_work_types(cls, project_id: int, work_id: int) -> [WorkType]: - """Find all work types associated with the project""" - return ( - db.session.query(WorkType) - .join( - Work, - and_( - Work.work_type_id == WorkType.id, - Work.project_id == project_id, - Work.id != work_id, - ), - ) - .filter( - WorkType.is_active.is_(True), - WorkType.is_deleted.is_(False), - ) - .all() - ) - - @classmethod - def find_first_nations( - cls, project_id: int, work_id: int, work_type_id: int = None - ) -> [IndigenousNation]: - """Find all first nations associated with the project""" - qry = ( - db.session.query(IndigenousNation) - .join( - IndigenousWork, - IndigenousWork.indigenous_nation_id == IndigenousNation.id, - ) - .join( - Work, - and_(Work.id == IndigenousWork.work_id, Work.project_id == project_id), - ) - .filter( - IndigenousNation.is_active.is_(True), - IndigenousNation.is_deleted.is_(False), - Work.id != work_id, - ) - ) - if work_type_id: - qry.filter(Work.work_type_id == work_type_id) - return qry.all() - - @classmethod - def check_first_nation_available(cls, project_id: int, work_id: int) -> bool: - """Checks if any first nation exists for given project""" - result = ( - db.session.query(Project) - .join( - Work, - and_(Work.project_id == project_id, Work.id != work_id), - ) - .join( - IndigenousWork, - Work.id == IndigenousWork.work_id, - ) - .join( - IndigenousNation, - IndigenousNation.id == IndigenousWork.indigenous_nation_id, - ) - .filter( - Project.id == Work.project_id, - IndigenousWork.is_active.is_(True), - IndigenousWork.is_deleted.is_(False), - IndigenousNation.is_active.is_(True), - IndigenousNation.is_deleted.is_(False), - Work.is_active.is_(True), - Work.is_deleted.is_(False), - ) - ) - result = result.count() > 0 - return {"first_nation_available": result} - - @classmethod - def import_projects(cls, file: IO): # pylint: disable=too-many-locals - """Import proponents""" - data = cls._read_excel(file) - proponent_names = set(data["proponent_id"].to_list()) - type_names = set(data["type_id"].to_list()) - sub_type_names = set(data["sub_type_id"].to_list()) - env_region_names = set(data["region_id_env"].to_list()) - flnro_region_names = set(data["region_id_flnro"].to_list()) - - proponents, types, sub_types, regions = cls._get_master_data( - proponent_names, - type_names, - sub_type_names, - env_region_names.union(flnro_region_names), - ) - - data["proponent_id"] = data.apply( - lambda x: cls._find_proponent_id(x["proponent_id"], proponents), axis=1 - ) - data["type_id"] = data.apply( - lambda x: cls._find_type_id(x["type_id"], types), axis=1 - ) - data["sub_type_id"] = data.apply( - lambda x: cls._find_sub_type_id(x["sub_type_id"], sub_types), axis=1 - ) - data["region_id_env"] = data.apply( - lambda x: cls._find_region_id(x["region_id_env"], regions, "ENV"), axis=1 - ) - data["region_id_flnro"] = data.apply( - lambda x: cls._find_region_id(x["region_id_flnro"], regions, "FLNR"), axis=1 - ) - data["project_state"] = data.apply( - lambda x: PROJECT_STATE_ENUM_MAPS[x["project_state"]], axis=1 - ) - - username = TokenInfo.get_username() - data["created_by"] = username - data = cls._update_or_delete_old_projects(data) - data = data.to_dict("records") - db.session.bulk_insert_mappings(Project, data) - special_history_mappings = [] - time_range = DateTimeTZRange( - datetime.now(), None, bounds="[)" - ) - for project_data in data: - project = db.session.query(Project).filter( - Project.name == project_data["name"], - Project.proponent_id == project_data["proponent_id"], - ).first() - special_history_mappings.append( - { - "entity": EntityEnum.PROJECT, - "entity_id": project.id, - "field_name": "name", - "field_value": project.name, - "time_range": time_range - } - ) - db.session.bulk_insert_mappings(SpecialField, special_history_mappings) - db.session.commit() - return "Created successfully" - - @classmethod - def _read_excel(cls, file: IO) -> pd.DataFrame: - """Read the template excel file""" - column_map = { - "Name": "name", - "Proponent": "proponent_id", - "Type": "type_id", - "SubType": "sub_type_id", - "Description": "description", - "Address": "address", - "Latitude": "latitude", - "Longitude": "longitude", - "ENVRegion": "region_id_env", - "FLNRORegion": "region_id_flnro", - "Capital Investment": "capital_investment", - "EPIC Guid": "epic_guid", - "Abbreviation": "abbreviation", - "EACertificate": "ea_certificate", - "Project Closed": "is_project_closed", - "FTE Positions Construction": "fte_positions_construction", - "FTE Positions Operation": "fte_positions_operation", - "Project State": "project_state", - } - data_frame = pd.read_excel(file) - data_frame.rename(column_map, axis="columns", inplace=True) - data_frame = data_frame.infer_objects() - data_frame = data_frame.apply( - lambda x: x.str.strip() if x.dtype == "object" else x - ) - data_frame = data_frame.replace({np.nan: None}) - data_frame = data_frame.replace({np.NaN: None}) - return data_frame - - @classmethod - def _find_proponent_id(cls, name: str, proponents: List[Proponent]) -> int: - """Find and return the id of proponent from given list""" - if name is None: - return None - proponent = next((x for x in proponents if x.name == name), None) - if proponent is None: - print(f"Proponent with name {name} does not exist") - raise ResourceNotFoundError(f"Proponent with name {name} does not exist") - return proponent.id - - @classmethod - def _find_type_id(cls, name: str, types: List[Type]) -> int: - """Find and return the id of type from given list""" - if name is None: - return None - type_obj = next((x for x in types if x.name == name), None) - if type_obj is None: - raise ResourceNotFoundError(f"Type with name {name} does not exist") - return type_obj.id - - @classmethod - def _find_sub_type_id(cls, name: str, sub_types: List[SubType]) -> int: - """Find and return the id of SubType from given list""" - if name is None: - return None - sub_type = next((x for x in sub_types if x.name == name), None) - if sub_type is None: - raise ResourceNotFoundError(f"SubType with name {name} does not exist") - return sub_type.id - - @classmethod - def _find_region_id(cls, name: str, regions: List[Region], entity: str) -> int: - """Find and return the id of region from given list""" - if name is None: - return None - region = next( - (x for x in regions if x.name == name and x.entity == entity), None - ) - if region is None: - raise ResourceNotFoundError(f"Region with name {name} does not exist") - return region.id - - @classmethod - def _generate_project_abbreviation( - cls, project_name: str, method: ProjectCodeMethod - ): - words = project_name.split() - - # Method 1: 1st 3 LETTERS OF FIRST WORD IN NAME + FIRST 3 LETTERS OF 2nd WORD IN NAME - if method == ProjectCodeMethod.METHOD_1 and len(words) >= 2: - return f"{words[0][:3]}{words[1][:3]}".upper() - - # Method 2: 1st LETTER OF FIRST WORD IN NAME - # + 1st LETTER OF 2nd WORD IN NAME + 1st FOUR LETTERS OF THIRD WORD IN NAME - if method == ProjectCodeMethod.METHOD_2 and len(words) >= 3: - return f"{words[0][0]}{words[1][0]}{words[2][:4]}".upper() - - # Method 3: 1st 6 LETTERS OF FIRST WORD IN NAME - if method == ProjectCodeMethod.METHOD_3 and len(words[0]) >= 6: - return words[0][:6].upper() - - return None - - @classmethod - def create_project_abbreviation(cls, project_name: str): - """Return a project code based on the project name""" - for method in ProjectCodeMethod: - project_abbreviation = cls._generate_project_abbreviation( - project_name, method - ) - - if project_abbreviation is not None: - # Check if project abbreviation already exists - project = Project.get_by_abbreviation(project_abbreviation) - if not project: - return project_abbreviation - - raise BadRequestError("Could not generate a unique project abbreviation") - - @classmethod - def find_all_project_types(cls): - """Get all project types""" - project_types = Type.find_all(default_filters=False) - return TypeSchema(many=True).dump(project_types) - - @classmethod - def _get_master_data( - cls, proponent_names, type_names, sub_type_names, region_names - ): - proponents = ( - db.session.query(Proponent) - .filter(Proponent.name.in_(proponent_names), Proponent.is_active.is_(True)) - .all() - ) - types = ( - db.session.query(Type) - .filter(Type.name.in_(type_names), Type.is_active.is_(True)) - .all() - ) - sub_types = ( - db.session.query(SubType) - .filter(SubType.name.in_(sub_type_names), SubType.is_active.is_(True)) - .all() - ) - regions = ( - db.session.query(Region) - .filter( - Region.name.in_(region_names), - Region.is_active.is_(True), - ) - .all() - ) - return proponents, types, sub_types, regions - - @classmethod - def _update_or_delete_old_projects(cls, data) -> pd.DataFrame: - """Marks old entries as deleted or active depending on their existence in input data. - - Returns the DataFrame after filtering out updated entries. - """ - project_names = set(data["name"].to_list()) - existing_projects_qry = db.session.query(Project).filter() - - existing_projects = existing_projects_qry.all() - # Create set of existing project names - existing_projects = {x.name for x in existing_projects} - # Mark removed entries as inactive - to_delete = existing_projects - project_names - disabled_count = existing_projects_qry.filter( - Project.name.in_(to_delete), - ).update({"is_active": False, "is_deleted": True}) - current_app.logger.info(f"Disabled {disabled_count} Projects") - - # Update existing entries to be active - to_update = existing_projects & project_names - enabled_count = existing_projects_qry.filter( - Project.name.in_(to_update) - ).update({"is_active": True, "is_deleted": False}) - current_app.logger.info(f"Enabled {enabled_count} Projects") - # Remove updated projects to avoid creating duplicates - return data[~data["name"].isin(to_update)] +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Project.""" +from datetime import datetime +from typing import IO, List + +import numpy as np +import pandas as pd +from flask import current_app +from psycopg2.extras import DateTimeTZRange +from sqlalchemy import and_ + +from api.exceptions import BadRequestError, ResourceExistsError, ResourceNotFoundError +from api.models import Project, db +from api.models.indigenous_nation import IndigenousNation +from api.models.indigenous_work import IndigenousWork +from api.models.project import ProjectStateEnum +from api.models.proponent import Proponent +from api.models.region import Region +from api.models.special_field import EntityEnum, SpecialField +from api.models.sub_types import SubType +from api.models.types import Type +from api.models.work import Work +from api.models.work_type import WorkType +from api.schemas.types import TypeSchema +from api.services.special_field import SpecialFieldService +from api.utils.constants import PROJECT_STATE_ENUM_MAPS +from api.utils.enums import ProjectCodeMethod +from api.utils.token_info import TokenInfo + + +class ProjectService: + """Service to manage project related operations.""" + + @classmethod + def find(cls, project_id, exclude_deleted=False): + """Find by project id.""" + query = db.session.query(Project).filter(Project.id == project_id) + if exclude_deleted: + query = query.filter(Project.is_deleted.is_(False)) + project = query.one_or_none() + if project: + return project + raise ResourceNotFoundError(f"Project with id '{project_id}' not found.") + + @classmethod + def find_all(cls, with_works=False, is_active=None): + """Find all projects""" + return Project.find_all_projects(with_works, is_active) + + @classmethod + def create_project(cls, payload: dict): + """Create a new project.""" + exists = cls.check_existence(payload["name"]) + if exists: + raise ResourceExistsError("Project with same name exists") + project = Project(**payload) + project.project_state = ProjectStateEnum.PRE_WORK + current_app.logger.info(f"Project obj {dir(project)}") + project.flush() + proponent_special_field_data = { + "entity": EntityEnum.PROJECT, + "entity_id": project.id, + "field_name": "proponent_id", + "field_value": project.proponent_id, + "active_from": project.created_at, + } + SpecialFieldService.create_special_field_entry(proponent_special_field_data) + project_name_special_field_data = { + "entity": EntityEnum.PROJECT, + "entity_id": project.id, + "field_name": "name", + "field_value": project.name, + "active_from": project.created_at, + } + SpecialFieldService.create_special_field_entry(project_name_special_field_data) + project.save() + return project + + @classmethod + def update_project(cls, project_id: int, payload: dict): + """Update existing project.""" + exists = cls.check_existence(payload["name"], project_id) + if exists: + raise ResourceExistsError("Project with same name exists") + project = Project.find_by_id(project_id) + if not project: + raise ResourceNotFoundError(f"Project with id '{project_id}' not found.") + project = project.update(payload) + return project + + @classmethod + def delete_project(cls, project_id: int): + """Delete project by id.""" + project = Project.find_by_id(project_id) + project.is_deleted = True + project.save() + return True + + @classmethod + def check_existence(cls, name, project_id=None): + """Checks if a project exists with given name""" + return Project.check_existence(name, project_id) + + @classmethod + def find_project_work_types(cls, project_id: int, work_id: int) -> [WorkType]: + """Find all work types associated with the project""" + return ( + db.session.query(WorkType) + .join( + Work, + and_( + Work.work_type_id == WorkType.id, + Work.project_id == project_id, + Work.id != work_id, + ), + ) + .filter( + WorkType.is_active.is_(True), + WorkType.is_deleted.is_(False), + ) + .all() + ) + + @classmethod + def find_first_nations( + cls, project_id: int, work_id: int, work_type_id: int = None + ) -> [IndigenousNation]: + """Find all first nations associated with the project""" + qry = ( + db.session.query(IndigenousNation) + .join( + IndigenousWork, + IndigenousWork.indigenous_nation_id == IndigenousNation.id, + ) + .join( + Work, + and_(Work.id == IndigenousWork.work_id, Work.project_id == project_id), + ) + .filter( + IndigenousNation.is_active.is_(True), + IndigenousNation.is_deleted.is_(False), + Work.id != work_id, + ) + ) + if work_type_id: + qry.filter(Work.work_type_id == work_type_id) + return qry.all() + + @classmethod + def check_first_nation_available(cls, project_id: int, work_id: int) -> bool: + """Checks if any first nation exists for given project""" + result = ( + db.session.query(Project) + .join( + Work, + and_(Work.project_id == project_id, Work.id != work_id), + ) + .join( + IndigenousWork, + Work.id == IndigenousWork.work_id, + ) + .join( + IndigenousNation, + IndigenousNation.id == IndigenousWork.indigenous_nation_id, + ) + .filter( + Project.id == Work.project_id, + IndigenousWork.is_active.is_(True), + IndigenousWork.is_deleted.is_(False), + IndigenousNation.is_active.is_(True), + IndigenousNation.is_deleted.is_(False), + Work.is_active.is_(True), + Work.is_deleted.is_(False), + ) + ) + result = result.count() > 0 + return {"first_nation_available": result} + + @classmethod + def import_projects(cls, file: IO): # pylint: disable=too-many-locals + """Import proponents""" + data = cls._read_excel(file) + proponent_names = set(data["proponent_id"].to_list()) + type_names = set(data["type_id"].to_list()) + sub_type_names = set(data["sub_type_id"].to_list()) + env_region_names = set(data["region_id_env"].to_list()) + flnro_region_names = set(data["region_id_flnro"].to_list()) + + proponents, types, sub_types, regions = cls._get_master_data( + proponent_names, + type_names, + sub_type_names, + env_region_names.union(flnro_region_names), + ) + + data["proponent_id"] = data.apply( + lambda x: cls._find_proponent_id(x["proponent_id"], proponents), axis=1 + ) + data["type_id"] = data.apply( + lambda x: cls._find_type_id(x["type_id"], types), axis=1 + ) + data["sub_type_id"] = data.apply( + lambda x: cls._find_sub_type_id(x["sub_type_id"], sub_types), axis=1 + ) + data["region_id_env"] = data.apply( + lambda x: cls._find_region_id(x["region_id_env"], regions, "ENV"), axis=1 + ) + data["region_id_flnro"] = data.apply( + lambda x: cls._find_region_id(x["region_id_flnro"], regions, "FLNR"), axis=1 + ) + data["project_state"] = data.apply( + lambda x: PROJECT_STATE_ENUM_MAPS[x["project_state"]], axis=1 + ) + + username = TokenInfo.get_username() + data["created_by"] = username + data = cls._update_or_delete_old_projects(data) + data = data.to_dict("records") + db.session.bulk_insert_mappings(Project, data) + special_history_mappings = [] + time_range = DateTimeTZRange( + datetime.now(), None, bounds="[)" + ) + for project_data in data: + project = db.session.query(Project).filter( + Project.name == project_data["name"], + Project.proponent_id == project_data["proponent_id"], + ).first() + special_history_mappings.append( + { + "entity": EntityEnum.PROJECT, + "entity_id": project.id, + "field_name": "name", + "field_value": project.name, + "time_range": time_range + } + ) + db.session.bulk_insert_mappings(SpecialField, special_history_mappings) + db.session.commit() + return "Created successfully" + + @classmethod + def _read_excel(cls, file: IO) -> pd.DataFrame: + """Read the template excel file""" + column_map = { + "Name": "name", + "Proponent": "proponent_id", + "Type": "type_id", + "SubType": "sub_type_id", + "Description": "description", + "Address": "address", + "Latitude": "latitude", + "Longitude": "longitude", + "ENVRegion": "region_id_env", + "FLNRORegion": "region_id_flnro", + "Capital Investment": "capital_investment", + "EPIC Guid": "epic_guid", + "Abbreviation": "abbreviation", + "EACertificate": "ea_certificate", + "Project Closed": "is_project_closed", + "FTE Positions Construction": "fte_positions_construction", + "FTE Positions Operation": "fte_positions_operation", + "Project State": "project_state", + } + data_frame = pd.read_excel(file) + data_frame.rename(column_map, axis="columns", inplace=True) + data_frame = data_frame.infer_objects() + data_frame = data_frame.apply( + lambda x: x.str.strip() if x.dtype == "object" else x + ) + data_frame = data_frame.replace({np.nan: None}) + data_frame = data_frame.replace({np.NaN: None}) + return data_frame + + @classmethod + def _find_proponent_id(cls, name: str, proponents: List[Proponent]) -> int: + """Find and return the id of proponent from given list""" + if name is None: + return None + proponent = next((x for x in proponents if x.name == name), None) + if proponent is None: + print(f"Proponent with name {name} does not exist") + raise ResourceNotFoundError(f"Proponent with name {name} does not exist") + return proponent.id + + @classmethod + def _find_type_id(cls, name: str, types: List[Type]) -> int: + """Find and return the id of type from given list""" + if name is None: + return None + type_obj = next((x for x in types if x.name == name), None) + if type_obj is None: + raise ResourceNotFoundError(f"Type with name {name} does not exist") + return type_obj.id + + @classmethod + def _find_sub_type_id(cls, name: str, sub_types: List[SubType]) -> int: + """Find and return the id of SubType from given list""" + if name is None: + return None + sub_type = next((x for x in sub_types if x.name == name), None) + if sub_type is None: + raise ResourceNotFoundError(f"SubType with name {name} does not exist") + return sub_type.id + + @classmethod + def _find_region_id(cls, name: str, regions: List[Region], entity: str) -> int: + """Find and return the id of region from given list""" + if name is None: + return None + region = next( + (x for x in regions if x.name == name and x.entity == entity), None + ) + if region is None: + raise ResourceNotFoundError(f"Region with name {name} does not exist") + return region.id + + @classmethod + def _generate_project_abbreviation( + cls, project_name: str, method: ProjectCodeMethod + ): + words = project_name.split() + + # Method 1: 1st 3 LETTERS OF FIRST WORD IN NAME + FIRST 3 LETTERS OF 2nd WORD IN NAME + if method == ProjectCodeMethod.METHOD_1 and len(words) >= 2: + return f"{words[0][:3]}{words[1][:3]}".upper() + + # Method 2: 1st LETTER OF FIRST WORD IN NAME + # + 1st LETTER OF 2nd WORD IN NAME + 1st FOUR LETTERS OF THIRD WORD IN NAME + if method == ProjectCodeMethod.METHOD_2 and len(words) >= 3: + return f"{words[0][0]}{words[1][0]}{words[2][:4]}".upper() + + # Method 3: 1st 6 LETTERS OF FIRST WORD IN NAME + if method == ProjectCodeMethod.METHOD_3 and len(words[0]) >= 6: + return words[0][:6].upper() + + return None + + @classmethod + def create_project_abbreviation(cls, project_name: str): + """Return a project code based on the project name""" + for method in ProjectCodeMethod: + project_abbreviation = cls._generate_project_abbreviation( + project_name, method + ) + + if project_abbreviation is not None: + # Check if project abbreviation already exists + project = Project.get_by_abbreviation(project_abbreviation) + if not project: + return project_abbreviation + + raise BadRequestError("Could not generate a unique project abbreviation") + + @classmethod + def find_all_project_types(cls): + """Get all project types""" + project_types = Type.find_all(default_filters=False) + return TypeSchema(many=True).dump(project_types) + + @classmethod + def _get_master_data( + cls, proponent_names, type_names, sub_type_names, region_names + ): + proponents = ( + db.session.query(Proponent) + .filter(Proponent.name.in_(proponent_names), Proponent.is_active.is_(True)) + .all() + ) + types = ( + db.session.query(Type) + .filter(Type.name.in_(type_names), Type.is_active.is_(True)) + .all() + ) + sub_types = ( + db.session.query(SubType) + .filter(SubType.name.in_(sub_type_names), SubType.is_active.is_(True)) + .all() + ) + regions = ( + db.session.query(Region) + .filter( + Region.name.in_(region_names), + Region.is_active.is_(True), + ) + .all() + ) + return proponents, types, sub_types, regions + + @classmethod + def _update_or_delete_old_projects(cls, data) -> pd.DataFrame: + """Marks old entries as deleted or active depending on their existence in input data. + + Returns the DataFrame after filtering out updated entries. + """ + project_names = set(data["name"].to_list()) + existing_projects_qry = db.session.query(Project).filter() + + existing_projects = existing_projects_qry.all() + # Create set of existing project names + existing_projects = {x.name for x in existing_projects} + # Mark removed entries as inactive + to_delete = existing_projects - project_names + disabled_count = existing_projects_qry.filter( + Project.name.in_(to_delete), + ).update({"is_active": False, "is_deleted": True}) + current_app.logger.info(f"Disabled {disabled_count} Projects") + + # Update existing entries to be active + to_update = existing_projects & project_names + enabled_count = existing_projects_qry.filter( + Project.name.in_(to_update) + ).update({"is_active": True, "is_deleted": False}) + current_app.logger.info(f"Enabled {enabled_count} Projects") + # Remove updated projects to avoid creating duplicates + return data[~data["name"].isin(to_update)] diff --git a/epictrack-api/src/api/services/region.py b/epictrack-api/src/api/services/region.py index c63de7516..58fc40e56 100644 --- a/epictrack-api/src/api/services/region.py +++ b/epictrack-api/src/api/services/region.py @@ -1,30 +1,30 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Region service""" - -from api.models.region import Region - - -class RegionService: - """Service to manage Region related operations.""" - - @staticmethod - def find_all_regions(): - """Get all regions.""" - return Region.find_all() - - @staticmethod - def find_regions_by_type(region_type: str): - """Get all regions by region type.""" - return Region.find_all_by_region_type(region_type) +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Region service""" + +from api.models.region import Region + + +class RegionService: + """Service to manage Region related operations.""" + + @staticmethod + def find_all_regions(): + """Get all regions.""" + return Region.find_all() + + @staticmethod + def find_regions_by_type(region_type: str): + """Get all regions by region type.""" + return Region.find_all_by_region_type(region_type) diff --git a/epictrack-api/src/api/services/task.py b/epictrack-api/src/api/services/task.py index 3b72db464..24f769f3c 100644 --- a/epictrack-api/src/api/services/task.py +++ b/epictrack-api/src/api/services/task.py @@ -1,483 +1,483 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Service to manage Tasks""" -from datetime import timedelta -from itertools import product -from typing import List, IO - -import numpy as np -import pandas as pd -from dateutil.parser import parse -from flask import current_app -from sqlalchemy import and_, tuple_ -from sqlalchemy.orm import contains_eager, lazyload - -from api.exceptions import ResourceNotFoundError, UnprocessableEntityError -from api.models import ( - StaffWorkRole, - StatusEnum, - TaskEvent, - TaskEventAssignee, - WorkPhase, - db, -) -from api.models.task_event_responsibility import TaskEventResponsibility -from ..models.queries.task_event_queries import find_by_staff_work_role_staff_id -from ..utils.constants import CANADA_TIMEZONE -from ..utils.datetime_helper import get_start_of_day - -from ..utils.roles import Membership -from ..utils.roles import Role as KeycloakRole -from . import authorisation -from .task_template import TaskTemplateService - - -class TaskService: - """Service to manage task related operations""" - - @classmethod - def create_task_events_bulk(cls, data: list, work_phase_id: int): - """Create task events in bulk""" - work_phase = WorkPhase.find_by_id(work_phase_id) - - one_of_roles = ( - Membership.TEAM_MEMBER.name, - KeycloakRole.EXTENDED_EDIT.value, - ) - authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) - - if cls._get_task_count(work_phase_id) > 0: - raise UnprocessableEntityError("Tasks already added for the phase") - task_events = [] - for task in data: - task_data = { - **task, - "work_phase_id": work_phase_id, - } - task_event = TaskEvent(**task_data) - task_event.flush() - task_events.append(task_event) - db.session.commit() - return task_events - - @classmethod - def _read_excel(cls, template_file: IO) -> list: - """Read the template excel file""" - column_map = { - "Name": "name", - "Days": "number_of_days", - "Start Date": "start_date", - "Type": 'type' - } - data_frame = pd.read_excel(template_file) - data_frame.rename(column_map, axis="columns", inplace=True) - data_frame = data_frame.replace({np.nan: None}) - data_frame = data_frame.replace({np.NaN: None}) - return data_frame.to_dict('records') - - @classmethod - def _prepare_task_from_import(cls, data: list) -> list: - """Prepare a task event object""" - tasks = [] - for task in data: - if task.get('type', None) != 'Task': - continue - start_date = parse(task.get("start_date")).astimezone(CANADA_TIMEZONE) - task_data = { - "name": task.get("name", ''), - "start_date": get_start_of_day(start_date) - if task.get("start_date") else None, - "number_of_days": task.get("number_of_days", 0), - } - tasks.append(task_data) - return tasks - - @classmethod - def create_task_events_from_sheet(cls, work_phase_id: int, sheet: IO): - """Create task events from excel sheet""" - read_tasks_data = cls._read_excel(sheet) - task_events_data = cls._prepare_task_from_import(read_tasks_data) - task_events = cls.create_task_events_bulk(task_events_data, work_phase_id) - return task_events - - @classmethod - def create_task_event(cls, data: dict, commit: bool = True) -> TaskEvent: - """Create task event""" - task_event = TaskEvent(**cls._prepare_task_event_object(data)) - work_phase = WorkPhase.find_by_id(data.get("work_phase_id")) - - one_of_roles = ( - Membership.TEAM_MEMBER.name, - KeycloakRole.CREATE.value, - ) - authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) - - if data.get("assignee_ids") and not cls._validate_assignees( - data.get("assignee_ids"), work_phase.work_id - ): - raise UnprocessableEntityError( - "Only team members can be assigned to a task" - ) - task_event = task_event.flush() - if data.get("assignee_ids"): - cls._handle_assignees(data.get("assignee_ids"), [task_event.id]) - if data.get("responsibility_ids"): - cls._handle_responsibilities( - data.get("responsibility_ids"), [task_event.id] - ) - work_phase.task_added = True - if commit: - db.session.commit() - return task_event - - @classmethod - def update_task_event(cls, data: dict, task_event_id) -> TaskEvent: - """Update task event""" - task_event = TaskEvent.find_by_id(task_event_id) - work_phase = WorkPhase.find_by_id(data.get("work_phase_id")) - - one_of_roles = ( - Membership.TEAM_MEMBER.name, - KeycloakRole.EDIT.value, - ) - authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) - - if data.get("assignee_ids") and not cls._validate_assignees( - data.get("assignee_ids"), work_phase.work_id - ): - raise UnprocessableEntityError( - "Only team members can be assigned to a task" - ) - task_event.update(data, commit=False) - cls._handle_assignees(data.get("assignee_ids"), [task_event.id]) - cls._handle_responsibilities(data.get("responsibility_ids"), [task_event.id]) - db.session.commit() - return task_event - - @classmethod - def find_task_events(cls, work_phase_id: int) -> [TaskEvent]: - """Get all task events per work_phase_id""" - return ( - db.session.query(TaskEvent) - .filter( - TaskEvent.is_active.is_(True), - TaskEvent.is_deleted.is_(False), - TaskEvent.work_phase_id == work_phase_id, - ) - .options( - lazyload(TaskEvent.assignees).joinedload(TaskEventAssignee.assignee) - ) - .all() - ) - - @classmethod - def find_by_staff_work_role_staff_id(cls, staff_id: int, is_active: bool = None) -> [TaskEvent]: - """Get all task events per assignee_id""" - tasks = find_by_staff_work_role_staff_id(staff_id, is_active) - return tasks - - @classmethod - def find_task_event(cls, event_id: int, exclude_deleted: bool = False) -> TaskEvent: - """Get the task event""" - query = ( - db.session.query(TaskEvent) - .outerjoin( - TaskEventAssignee, - and_( - TaskEventAssignee.task_event_id == TaskEvent.id, - TaskEventAssignee.is_active.is_(True), - ), - ) - .filter(TaskEvent.id == event_id) - ) - if exclude_deleted: - query = query.filter(TaskEvent.is_deleted.is_(False)) - task_event = query.options(contains_eager(TaskEvent.assignees)).scalar() - print(f"Task event in service {task_event}") - if task_event: - return task_event - raise ResourceNotFoundError(f"TaskEvent with id '{event_id}' not found.") - - @classmethod - def create_task_events_from_template( - cls, params: dict, template_id: int - ) -> List[TaskEvent]: - """Create a list of task events from the given task template""" - work_phase = WorkPhase.find_by_id(params.get("work_phase_id")) - - one_of_roles = ( - Membership.TEAM_MEMBER.name, - KeycloakRole.CREATE.value, - ) - authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) - - if not work_phase: - raise UnprocessableEntityError("No data found for the given work and phase") - if work_phase.task_added: - raise UnprocessableEntityError( - "Template can be uploaded only once for a phase" - ) - template = TaskTemplateService.find_by_id(template_id) - if not template.is_active: - raise UnprocessableEntityError("In-Active templates cannot be processed") - tasks = template.tasks - if not tasks or len(tasks) == 0: - raise UnprocessableEntityError("No tasks found to import") - result_events = [] - for task in tasks: - task_event_dic = { - "name": task.name, - "work_phase_id": params.get("work_phase_id"), - "start_date": work_phase.start_date + timedelta(days=task.start_at), - "number_of_days": task.number_of_days, - "tips": task.tips, - "status": StatusEnum.NOT_STARTED, - } - result_events.append(cls.create_task_event(task_event_dic, commit=False)) - work_phase.task_added = True - db.session.commit() - return result_events - - @classmethod - def _prepare_task_event_object(cls, data: dict) -> dict: - """Prepare a task event object""" - exclude = ["responsibility_ids", "assignee_ids"] - return {key: data[key] for key in data.keys() if key not in exclude} - - @classmethod - def _handle_assignees(cls, assignees: list, task_event_ids: List[int]) -> None: - """Handles the assignees for the task event""" - existing_assignees_qry = db.session.query(TaskEventAssignee).filter( - TaskEventAssignee.is_deleted.is_(False), - TaskEventAssignee.task_event_id.in_(task_event_ids), - ) - - existing_assignees = list( - map( - lambda x: { - "task_event_id": x.task_event_id, - "assignee_id": x.assignee_id, - }, - existing_assignees_qry.all(), - ) - ) - - # Mark removed entries as inactive - disabled_count = existing_assignees_qry.filter( - TaskEventAssignee.is_active.is_(True), - TaskEventAssignee.assignee_id.notin_(assignees), - ).update({"is_active": False}) - current_app.logger.info(f"Disabled {disabled_count} TaskEventAssignees") - - # Update existing entries to be active - enabled_count = existing_assignees_qry.filter( - tuple_(TaskEventAssignee.assignee_id, TaskEventAssignee.task_event_id).in_( - [ - (x["assignee_id"], x["task_event_id"]) - for x in existing_assignees - if x["assignee_id"] in assignees - ] - ) - ).update({"is_active": True}) - current_app.logger.info(f"Enabled {enabled_count} TaskEventAssignees") - - keys = ("task_event_id", "assignee_id") - # Create mappings for new entries - # dict(zip(keys, (i, j))) below creates a dict of the form - # {"task_event_id": i, "assignee_id": j - task_event_assignees = [ - task_assignee - for i, j in product(task_event_ids, assignees) - if (task_assignee := dict(zip(keys, (i, j)))) not in existing_assignees - ] - db.session.bulk_insert_mappings( - TaskEventAssignee, mappings=task_event_assignees - ) - - @classmethod - def _validate_assignees(cls, assignees: list, work_id: int) -> bool: - """Database validation""" - work_staff = [ - r - for (r,) in db.session.query(StaffWorkRole.staff_id) - .filter( - StaffWorkRole.work_id == work_id, - StaffWorkRole.is_deleted.is_(False), - StaffWorkRole.is_active.is_(True), - ) - .all() - ] - return all(assigne in work_staff for assigne in assignees) - - @classmethod - def _get_task_count(cls, work_phase_id: int): - """Task should be inserted only once.""" - return ( - db.session.query(TaskEvent.id) - .filter( - TaskEvent.is_active.is_(True), - TaskEvent.is_deleted.is_(False), - TaskEvent.work_phase_id == work_phase_id, - ) - .count() - ) - - @classmethod - def bulk_update(cls, data: dict): - """Bulk update task events""" - work_id = data.get("work_id") - one_of_roles = ( - Membership.TEAM_MEMBER.name, - KeycloakRole.EDIT.value, - ) - authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_id) - - task_ids = data.pop("task_ids") - if "assignee_ids" in data: - cls._handle_assignees(data["assignee_ids"], task_ids) - elif "responsibility_ids" in data: - cls._handle_responsibilities(data["responsibility_ids"], task_ids) - else: - field = list(data.keys())[0] - keys = ("id", field) - task_event_mappings = [ - dict(zip(keys, (i, j))) for i, j in product(task_ids, [data[field]]) - ] - db.session.bulk_update_mappings(TaskEvent, mappings=task_event_mappings) - db.session.commit() - return "Updated successfully" - - @classmethod - def bulk_delete_tasks(cls, task_ids: List, work_id: int): - """Mark tasks as deleted""" - one_of_roles = ( - Membership.TEAM_MEMBER.name, - KeycloakRole.CREATE.value, - ) - authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_id) - - db.session.query(TaskEvent).filter(TaskEvent.id.in_(task_ids)).update( - {"is_active": False, "is_deleted": True} - ) - db.session.commit() - return "Deleted successfully" - - @classmethod - def _handle_responsibilities( - cls, responsibilities: list, task_event_ids: List[int] - ) -> None: - """Handles the responsibilities for the task event""" - existing_responsibilities_qry = db.session.query( - TaskEventResponsibility - ).filter( - TaskEventResponsibility.is_deleted.is_(False), - TaskEventResponsibility.task_event_id.in_(task_event_ids), - ) - - existing_responsibilities = list( - map( - lambda x: { - "task_event_id": x.task_event_id, - "responsibility_id": x.responsibility_id, - }, - existing_responsibilities_qry.all(), - ) - ) - - # Mark removed entries as inactive - disabled_count = existing_responsibilities_qry.filter( - TaskEventResponsibility.is_active.is_(True), - TaskEventResponsibility.responsibility_id.notin_(responsibilities), - ).update({"is_active": False}) - current_app.logger.info(f"Disabled {disabled_count} TaskEventResponsibilities") - - # Update existing entries to be active - enabled_count = existing_responsibilities_qry.filter( - tuple_( - TaskEventResponsibility.responsibility_id, - TaskEventResponsibility.task_event_id, - ).in_( - [ - (x["responsibility_id"], x["task_event_id"]) - for x in existing_responsibilities - if x["responsibility_id"] in responsibilities - ] - ) - ).update({"is_active": True}) - current_app.logger.info(f"Enabled {enabled_count} TaskEventResponsibilities") - - keys = ("task_event_id", "responsibility_id") - # Create mappings for new entries - # dict(zip(keys, (i, j))) below creates a dict of the form - # {"task_event_id": i, "responsibility_id": j - task_event_responsibilities = [ - task_responsibility - for i, j in product(task_event_ids, responsibilities) - if (task_responsibility := dict(zip(keys, (i, j)))) - not in existing_responsibilities - ] - db.session.bulk_insert_mappings( - TaskEventResponsibility, mappings=task_event_responsibilities - ) - - @classmethod - def copy_task_events(cls, data: dict, commit=True): - """Copy works from source work to target work""" - source_work_id = data.get("source_work_id", None) - target_work_id = data.get("target_work_id") - source_events = ( - db.session.query(TaskEvent) - .join( - WorkPhase, - and_( - WorkPhase.id == TaskEvent.work_phase_id, - WorkPhase.work_id == source_work_id, - ), - ) - .all() - ) - target_work_phases = WorkPhase.find_by_params({"work_id": target_work_id}) - task_events = [] - for work_phase in target_work_phases: - # skip the iteration if the target workphase already has tasks added - if work_phase.task_added: - pass - source_events_per_wp = [ - event - for event in source_events - if event.work_phase.phase_id == work_phase.phase_id - and event.work_phase.sort_order == work_phase.sort_order - ] - task_added = False - for sv in source_events_per_wp: - task_event = TaskEvent( - **{ - "name": sv.name, - "work_phase_id": work_phase.id, - "start_date": work_phase.start_date, - "number_of_days": sv.number_of_days, - "tips": sv.tips, - "notes": sv.notes, - "status": StatusEnum.NOT_STARTED.value, - } - ) - task_event.flush() - task_events.append(task_event) - task_added = True - if task_added: - work_phase.task_added = True - work_phase.update(work_phase.as_dict(recursive=False), commit=False) - if commit: - db.session.commit() - return task_events +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Service to manage Tasks""" +from datetime import timedelta +from itertools import product +from typing import List, IO + +import numpy as np +import pandas as pd +from dateutil.parser import parse +from flask import current_app +from sqlalchemy import and_, tuple_ +from sqlalchemy.orm import contains_eager, lazyload + +from api.exceptions import ResourceNotFoundError, UnprocessableEntityError +from api.models import ( + StaffWorkRole, + StatusEnum, + TaskEvent, + TaskEventAssignee, + WorkPhase, + db, +) +from api.models.task_event_responsibility import TaskEventResponsibility +from ..models.queries.task_event_queries import find_by_staff_work_role_staff_id +from ..utils.constants import CANADA_TIMEZONE +from ..utils.datetime_helper import get_start_of_day + +from ..utils.roles import Membership +from ..utils.roles import Role as KeycloakRole +from . import authorisation +from .task_template import TaskTemplateService + + +class TaskService: + """Service to manage task related operations""" + + @classmethod + def create_task_events_bulk(cls, data: list, work_phase_id: int): + """Create task events in bulk""" + work_phase = WorkPhase.find_by_id(work_phase_id) + + one_of_roles = ( + Membership.TEAM_MEMBER.name, + KeycloakRole.EXTENDED_EDIT.value, + ) + authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) + + if cls._get_task_count(work_phase_id) > 0: + raise UnprocessableEntityError("Tasks already added for the phase") + task_events = [] + for task in data: + task_data = { + **task, + "work_phase_id": work_phase_id, + } + task_event = TaskEvent(**task_data) + task_event.flush() + task_events.append(task_event) + db.session.commit() + return task_events + + @classmethod + def _read_excel(cls, template_file: IO) -> list: + """Read the template excel file""" + column_map = { + "Name": "name", + "Days": "number_of_days", + "Start Date": "start_date", + "Type": 'type' + } + data_frame = pd.read_excel(template_file) + data_frame.rename(column_map, axis="columns", inplace=True) + data_frame = data_frame.replace({np.nan: None}) + data_frame = data_frame.replace({np.NaN: None}) + return data_frame.to_dict('records') + + @classmethod + def _prepare_task_from_import(cls, data: list) -> list: + """Prepare a task event object""" + tasks = [] + for task in data: + if task.get('type', None) != 'Task': + continue + start_date = parse(task.get("start_date")).astimezone(CANADA_TIMEZONE) + task_data = { + "name": task.get("name", ''), + "start_date": get_start_of_day(start_date) + if task.get("start_date") else None, + "number_of_days": task.get("number_of_days", 0), + } + tasks.append(task_data) + return tasks + + @classmethod + def create_task_events_from_sheet(cls, work_phase_id: int, sheet: IO): + """Create task events from excel sheet""" + read_tasks_data = cls._read_excel(sheet) + task_events_data = cls._prepare_task_from_import(read_tasks_data) + task_events = cls.create_task_events_bulk(task_events_data, work_phase_id) + return task_events + + @classmethod + def create_task_event(cls, data: dict, commit: bool = True) -> TaskEvent: + """Create task event""" + task_event = TaskEvent(**cls._prepare_task_event_object(data)) + work_phase = WorkPhase.find_by_id(data.get("work_phase_id")) + + one_of_roles = ( + Membership.TEAM_MEMBER.name, + KeycloakRole.CREATE.value, + ) + authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) + + if data.get("assignee_ids") and not cls._validate_assignees( + data.get("assignee_ids"), work_phase.work_id + ): + raise UnprocessableEntityError( + "Only team members can be assigned to a task" + ) + task_event = task_event.flush() + if data.get("assignee_ids"): + cls._handle_assignees(data.get("assignee_ids"), [task_event.id]) + if data.get("responsibility_ids"): + cls._handle_responsibilities( + data.get("responsibility_ids"), [task_event.id] + ) + work_phase.task_added = True + if commit: + db.session.commit() + return task_event + + @classmethod + def update_task_event(cls, data: dict, task_event_id) -> TaskEvent: + """Update task event""" + task_event = TaskEvent.find_by_id(task_event_id) + work_phase = WorkPhase.find_by_id(data.get("work_phase_id")) + + one_of_roles = ( + Membership.TEAM_MEMBER.name, + KeycloakRole.EDIT.value, + ) + authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) + + if data.get("assignee_ids") and not cls._validate_assignees( + data.get("assignee_ids"), work_phase.work_id + ): + raise UnprocessableEntityError( + "Only team members can be assigned to a task" + ) + task_event.update(data, commit=False) + cls._handle_assignees(data.get("assignee_ids"), [task_event.id]) + cls._handle_responsibilities(data.get("responsibility_ids"), [task_event.id]) + db.session.commit() + return task_event + + @classmethod + def find_task_events(cls, work_phase_id: int) -> [TaskEvent]: + """Get all task events per work_phase_id""" + return ( + db.session.query(TaskEvent) + .filter( + TaskEvent.is_active.is_(True), + TaskEvent.is_deleted.is_(False), + TaskEvent.work_phase_id == work_phase_id, + ) + .options( + lazyload(TaskEvent.assignees).joinedload(TaskEventAssignee.assignee) + ) + .all() + ) + + @classmethod + def find_by_staff_work_role_staff_id(cls, staff_id: int, is_active: bool = None) -> [TaskEvent]: + """Get all task events per assignee_id""" + tasks = find_by_staff_work_role_staff_id(staff_id, is_active) + return tasks + + @classmethod + def find_task_event(cls, event_id: int, exclude_deleted: bool = False) -> TaskEvent: + """Get the task event""" + query = ( + db.session.query(TaskEvent) + .outerjoin( + TaskEventAssignee, + and_( + TaskEventAssignee.task_event_id == TaskEvent.id, + TaskEventAssignee.is_active.is_(True), + ), + ) + .filter(TaskEvent.id == event_id) + ) + if exclude_deleted: + query = query.filter(TaskEvent.is_deleted.is_(False)) + task_event = query.options(contains_eager(TaskEvent.assignees)).scalar() + print(f"Task event in service {task_event}") + if task_event: + return task_event + raise ResourceNotFoundError(f"TaskEvent with id '{event_id}' not found.") + + @classmethod + def create_task_events_from_template( + cls, params: dict, template_id: int + ) -> List[TaskEvent]: + """Create a list of task events from the given task template""" + work_phase = WorkPhase.find_by_id(params.get("work_phase_id")) + + one_of_roles = ( + Membership.TEAM_MEMBER.name, + KeycloakRole.CREATE.value, + ) + authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_phase.work_id) + + if not work_phase: + raise UnprocessableEntityError("No data found for the given work and phase") + if work_phase.task_added: + raise UnprocessableEntityError( + "Template can be uploaded only once for a phase" + ) + template = TaskTemplateService.find_by_id(template_id) + if not template.is_active: + raise UnprocessableEntityError("In-Active templates cannot be processed") + tasks = template.tasks + if not tasks or len(tasks) == 0: + raise UnprocessableEntityError("No tasks found to import") + result_events = [] + for task in tasks: + task_event_dic = { + "name": task.name, + "work_phase_id": params.get("work_phase_id"), + "start_date": work_phase.start_date + timedelta(days=task.start_at), + "number_of_days": task.number_of_days, + "tips": task.tips, + "status": StatusEnum.NOT_STARTED, + } + result_events.append(cls.create_task_event(task_event_dic, commit=False)) + work_phase.task_added = True + db.session.commit() + return result_events + + @classmethod + def _prepare_task_event_object(cls, data: dict) -> dict: + """Prepare a task event object""" + exclude = ["responsibility_ids", "assignee_ids"] + return {key: data[key] for key in data.keys() if key not in exclude} + + @classmethod + def _handle_assignees(cls, assignees: list, task_event_ids: List[int]) -> None: + """Handles the assignees for the task event""" + existing_assignees_qry = db.session.query(TaskEventAssignee).filter( + TaskEventAssignee.is_deleted.is_(False), + TaskEventAssignee.task_event_id.in_(task_event_ids), + ) + + existing_assignees = list( + map( + lambda x: { + "task_event_id": x.task_event_id, + "assignee_id": x.assignee_id, + }, + existing_assignees_qry.all(), + ) + ) + + # Mark removed entries as inactive + disabled_count = existing_assignees_qry.filter( + TaskEventAssignee.is_active.is_(True), + TaskEventAssignee.assignee_id.notin_(assignees), + ).update({"is_active": False}) + current_app.logger.info(f"Disabled {disabled_count} TaskEventAssignees") + + # Update existing entries to be active + enabled_count = existing_assignees_qry.filter( + tuple_(TaskEventAssignee.assignee_id, TaskEventAssignee.task_event_id).in_( + [ + (x["assignee_id"], x["task_event_id"]) + for x in existing_assignees + if x["assignee_id"] in assignees + ] + ) + ).update({"is_active": True}) + current_app.logger.info(f"Enabled {enabled_count} TaskEventAssignees") + + keys = ("task_event_id", "assignee_id") + # Create mappings for new entries + # dict(zip(keys, (i, j))) below creates a dict of the form + # {"task_event_id": i, "assignee_id": j + task_event_assignees = [ + task_assignee + for i, j in product(task_event_ids, assignees) + if (task_assignee := dict(zip(keys, (i, j)))) not in existing_assignees + ] + db.session.bulk_insert_mappings( + TaskEventAssignee, mappings=task_event_assignees + ) + + @classmethod + def _validate_assignees(cls, assignees: list, work_id: int) -> bool: + """Database validation""" + work_staff = [ + r + for (r,) in db.session.query(StaffWorkRole.staff_id) + .filter( + StaffWorkRole.work_id == work_id, + StaffWorkRole.is_deleted.is_(False), + StaffWorkRole.is_active.is_(True), + ) + .all() + ] + return all(assigne in work_staff for assigne in assignees) + + @classmethod + def _get_task_count(cls, work_phase_id: int): + """Task should be inserted only once.""" + return ( + db.session.query(TaskEvent.id) + .filter( + TaskEvent.is_active.is_(True), + TaskEvent.is_deleted.is_(False), + TaskEvent.work_phase_id == work_phase_id, + ) + .count() + ) + + @classmethod + def bulk_update(cls, data: dict): + """Bulk update task events""" + work_id = data.get("work_id") + one_of_roles = ( + Membership.TEAM_MEMBER.name, + KeycloakRole.EDIT.value, + ) + authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_id) + + task_ids = data.pop("task_ids") + if "assignee_ids" in data: + cls._handle_assignees(data["assignee_ids"], task_ids) + elif "responsibility_ids" in data: + cls._handle_responsibilities(data["responsibility_ids"], task_ids) + else: + field = list(data.keys())[0] + keys = ("id", field) + task_event_mappings = [ + dict(zip(keys, (i, j))) for i, j in product(task_ids, [data[field]]) + ] + db.session.bulk_update_mappings(TaskEvent, mappings=task_event_mappings) + db.session.commit() + return "Updated successfully" + + @classmethod + def bulk_delete_tasks(cls, task_ids: List, work_id: int): + """Mark tasks as deleted""" + one_of_roles = ( + Membership.TEAM_MEMBER.name, + KeycloakRole.CREATE.value, + ) + authorisation.check_auth(one_of_roles=one_of_roles, work_id=work_id) + + db.session.query(TaskEvent).filter(TaskEvent.id.in_(task_ids)).update( + {"is_active": False, "is_deleted": True} + ) + db.session.commit() + return "Deleted successfully" + + @classmethod + def _handle_responsibilities( + cls, responsibilities: list, task_event_ids: List[int] + ) -> None: + """Handles the responsibilities for the task event""" + existing_responsibilities_qry = db.session.query( + TaskEventResponsibility + ).filter( + TaskEventResponsibility.is_deleted.is_(False), + TaskEventResponsibility.task_event_id.in_(task_event_ids), + ) + + existing_responsibilities = list( + map( + lambda x: { + "task_event_id": x.task_event_id, + "responsibility_id": x.responsibility_id, + }, + existing_responsibilities_qry.all(), + ) + ) + + # Mark removed entries as inactive + disabled_count = existing_responsibilities_qry.filter( + TaskEventResponsibility.is_active.is_(True), + TaskEventResponsibility.responsibility_id.notin_(responsibilities), + ).update({"is_active": False}) + current_app.logger.info(f"Disabled {disabled_count} TaskEventResponsibilities") + + # Update existing entries to be active + enabled_count = existing_responsibilities_qry.filter( + tuple_( + TaskEventResponsibility.responsibility_id, + TaskEventResponsibility.task_event_id, + ).in_( + [ + (x["responsibility_id"], x["task_event_id"]) + for x in existing_responsibilities + if x["responsibility_id"] in responsibilities + ] + ) + ).update({"is_active": True}) + current_app.logger.info(f"Enabled {enabled_count} TaskEventResponsibilities") + + keys = ("task_event_id", "responsibility_id") + # Create mappings for new entries + # dict(zip(keys, (i, j))) below creates a dict of the form + # {"task_event_id": i, "responsibility_id": j + task_event_responsibilities = [ + task_responsibility + for i, j in product(task_event_ids, responsibilities) + if (task_responsibility := dict(zip(keys, (i, j)))) + not in existing_responsibilities + ] + db.session.bulk_insert_mappings( + TaskEventResponsibility, mappings=task_event_responsibilities + ) + + @classmethod + def copy_task_events(cls, data: dict, commit=True): + """Copy works from source work to target work""" + source_work_id = data.get("source_work_id", None) + target_work_id = data.get("target_work_id") + source_events = ( + db.session.query(TaskEvent) + .join( + WorkPhase, + and_( + WorkPhase.id == TaskEvent.work_phase_id, + WorkPhase.work_id == source_work_id, + ), + ) + .all() + ) + target_work_phases = WorkPhase.find_by_params({"work_id": target_work_id}) + task_events = [] + for work_phase in target_work_phases: + # skip the iteration if the target workphase already has tasks added + if work_phase.task_added: + pass + source_events_per_wp = [ + event + for event in source_events + if event.work_phase.phase_id == work_phase.phase_id + and event.work_phase.sort_order == work_phase.sort_order + ] + task_added = False + for sv in source_events_per_wp: + task_event = TaskEvent( + **{ + "name": sv.name, + "work_phase_id": work_phase.id, + "start_date": work_phase.start_date, + "number_of_days": sv.number_of_days, + "tips": sv.tips, + "notes": sv.notes, + "status": StatusEnum.NOT_STARTED.value, + } + ) + task_event.flush() + task_events.append(task_event) + task_added = True + if task_added: + work_phase.task_added = True + work_phase.update(work_phase.as_dict(recursive=False), commit=False) + if commit: + db.session.commit() + return task_events diff --git a/epictrack-api/src/api/services/user.py b/epictrack-api/src/api/services/user.py index 232bd3afa..539c29398 100644 --- a/epictrack-api/src/api/services/user.py +++ b/epictrack-api/src/api/services/user.py @@ -1,166 +1,166 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""User service""" -from flask import current_app -from api.exceptions import BusinessError, PermissionDeniedError -from api.utils import TokenInfo - -from .keycloak import KeycloakService - - -class UserService: - """User Service""" - - @classmethod - def get_all_users(cls): - """Get all users""" - users = KeycloakService.get_users() - for user in users: - user["group"] = None - groups = UserService.get_groups() - groups = sorted(groups, key=UserService._get_level) - for group in groups: - members = KeycloakService.get_group_members(group["id"]) - member_ids = [member["id"] for member in members] - filtered_users = [user for user in users if user["id"] in member_ids] - for user in filtered_users: - user["group"] = group - return users - - @classmethod - def get_groups(cls): - """ - Retrieve groups that have the "level" attribute set up. - - This method fetches all groups from the Keycloak service and filters them - to include only those groups that have sub-groups. It logs the groups and - sub-groups at various stages for debugging purposes. - - Returns: - list: A list of filtered groups that have sub-groups. - """ - # Fetch all groups from the Keycloak service - groups = KeycloakService.get_groups() - current_app.logger.debug(f"Groups: {groups}") - filtered_groups = [] - - for group in groups: - current_app.logger.info(f"group: {group}") - - # Check if the group has sub-groups by looking at the "subGroupCount" attribute - if group.get("subGroupCount", 0) > 0: - - # Fetch the sub-groups for the current group - sub_groups = KeycloakService.get_sub_groups(group["id"]) - current_app.logger.debug(f"sub_groups: {sub_groups}") - filtered_groups.extend(sub_groups) - - current_app.logger.debug(f"filtered_groups: {filtered_groups}") - return filtered_groups - - @classmethod - def update_user_group(cls, user_id, user_group_request): - """ - Updates the user's group based on the provided user group request. - - Args: - cls: The class instance. - user_id (str): The ID of the user to update. - user_group_request (dict): A dictionary containing the group update request details. - Expected keys: - - "group_id_to_update" (str): The ID of the group to update. - Raises: - PermissionDeniedError: If the requester does not have permission to update the group. - Returns: - dict: The result of the group update operation from KeycloakService. - """ - token_groups = TokenInfo.get_user_data()["groups"] - groups = cls.get_groups() - requesters_group = next( - (group for group in groups if group["name"] in token_groups), None - ) - updating_group = next( - ( - group - for group in groups - if group["id"] == user_group_request.get("group_id_to_update") - ), - None, - ) - if ( - not requesters_group - and not updating_group - and int(UserService._get_level(requesters_group)) - < int(UserService._get_level(updating_group)) - ): - raise PermissionDeniedError("Permission denied") - - # if a group has exclusive flag , user can only be present exclusively in that group - # All other group access has to be removed before assigning to exclusive group - requires_all_group_removal = updating_group.get("attributes", {}).get("exclusive", ['false'])[ - 0].lower() == 'true' - - if requires_all_group_removal: - UserService._delete_from_all_epictrack_subgroups(user_id) - else: - UserService._delete_from_current_group(user_group_request, user_id) - - result = KeycloakService.update_user_group( - user_id, user_group_request["group_id_to_update"] - ) - return result - - @classmethod - def _delete_from_current_group(cls, user_group_request, user_id): - existing_group_id = user_group_request.get("existing_group_id") - if existing_group_id: - result = KeycloakService.delete_user_group( - user_id, user_group_request.get("existing_group_id") - ) - if result.status_code != 204: - raise BusinessError("Error removing group", 500) - - @staticmethod - def _delete_from_all_epictrack_subgroups(user_id): - """Delete all subgroups of 'epictrack' for a user""" - groups = KeycloakService.get_user_groups(user_id) - - # Find the main group 'epictrack' and get its subgroups - track_subgroups = [group for group in groups if 'track' in group['path'].lower()] - - for subgroup in track_subgroups: - result = KeycloakService.delete_user_group(user_id, subgroup['id']) - - if result.status_code != 204: - raise BusinessError("Error removing group", 500) - - @classmethod - def _get_level(cls, group): - """ - Retrieves the level from the given group. - - Args: - group (dict): A dictionary representing the group, which should contain - an "attributes" key with a nested "level" key. - - Returns: - int: The level extracted from the group. If the level is not found or - cannot be converted to an integer, returns 0. - """ - level_str = group["attributes"].get("level", [0])[0] - try: - return int(level_str) - except (KeyError, IndexError, TypeError) as e: - current_app.logger.error(f"Error getting level from group: {e}. Returning 0.") - return 0 +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""User service""" +from flask import current_app +from api.exceptions import BusinessError, PermissionDeniedError +from api.utils import TokenInfo + +from .keycloak import KeycloakService + + +class UserService: + """User Service""" + + @classmethod + def get_all_users(cls): + """Get all users""" + users = KeycloakService.get_users() + for user in users: + user["group"] = None + groups = UserService.get_groups() + groups = sorted(groups, key=UserService._get_level) + for group in groups: + members = KeycloakService.get_group_members(group["id"]) + member_ids = [member["id"] for member in members] + filtered_users = [user for user in users if user["id"] in member_ids] + for user in filtered_users: + user["group"] = group + return users + + @classmethod + def get_groups(cls): + """ + Retrieve groups that have the "level" attribute set up. + + This method fetches all groups from the Keycloak service and filters them + to include only those groups that have sub-groups. It logs the groups and + sub-groups at various stages for debugging purposes. + + Returns: + list: A list of filtered groups that have sub-groups. + """ + # Fetch all groups from the Keycloak service + groups = KeycloakService.get_groups() + current_app.logger.debug(f"Groups: {groups}") + filtered_groups = [] + + for group in groups: + current_app.logger.info(f"group: {group}") + + # Check if the group has sub-groups by looking at the "subGroupCount" attribute + if group.get("subGroupCount", 0) > 0: + + # Fetch the sub-groups for the current group + sub_groups = KeycloakService.get_sub_groups(group["id"]) + current_app.logger.debug(f"sub_groups: {sub_groups}") + filtered_groups.extend(sub_groups) + + current_app.logger.debug(f"filtered_groups: {filtered_groups}") + return filtered_groups + + @classmethod + def update_user_group(cls, user_id, user_group_request): + """ + Updates the user's group based on the provided user group request. + + Args: + cls: The class instance. + user_id (str): The ID of the user to update. + user_group_request (dict): A dictionary containing the group update request details. + Expected keys: + - "group_id_to_update" (str): The ID of the group to update. + Raises: + PermissionDeniedError: If the requester does not have permission to update the group. + Returns: + dict: The result of the group update operation from KeycloakService. + """ + token_groups = TokenInfo.get_user_data()["groups"] + groups = cls.get_groups() + requesters_group = next( + (group for group in groups if group["name"] in token_groups), None + ) + updating_group = next( + ( + group + for group in groups + if group["id"] == user_group_request.get("group_id_to_update") + ), + None, + ) + if ( + not requesters_group + and not updating_group + and int(UserService._get_level(requesters_group)) + < int(UserService._get_level(updating_group)) + ): + raise PermissionDeniedError("Permission denied") + + # if a group has exclusive flag , user can only be present exclusively in that group + # All other group access has to be removed before assigning to exclusive group + requires_all_group_removal = updating_group.get("attributes", {}).get("exclusive", ['false'])[ + 0].lower() == 'true' + + if requires_all_group_removal: + UserService._delete_from_all_epictrack_subgroups(user_id) + else: + UserService._delete_from_current_group(user_group_request, user_id) + + result = KeycloakService.update_user_group( + user_id, user_group_request["group_id_to_update"] + ) + return result + + @classmethod + def _delete_from_current_group(cls, user_group_request, user_id): + existing_group_id = user_group_request.get("existing_group_id") + if existing_group_id: + result = KeycloakService.delete_user_group( + user_id, user_group_request.get("existing_group_id") + ) + if result.status_code != 204: + raise BusinessError("Error removing group", 500) + + @staticmethod + def _delete_from_all_epictrack_subgroups(user_id): + """Delete all subgroups of 'epictrack' for a user""" + groups = KeycloakService.get_user_groups(user_id) + + # Find the main group 'epictrack' and get its subgroups + track_subgroups = [group for group in groups if 'track' in group['path'].lower()] + + for subgroup in track_subgroups: + result = KeycloakService.delete_user_group(user_id, subgroup['id']) + + if result.status_code != 204: + raise BusinessError("Error removing group", 500) + + @classmethod + def _get_level(cls, group): + """ + Retrieves the level from the given group. + + Args: + group (dict): A dictionary representing the group, which should contain + an "attributes" key with a nested "level" key. + + Returns: + int: The level extracted from the group. If the level is not found or + cannot be converted to an integer, returns 0. + """ + level_str = group["attributes"].get("level", [0])[0] + try: + return int(level_str) + except (KeyError, IndexError, TypeError) as e: + current_app.logger.error(f"Error getting level from group: {e}. Returning 0.") + return 0 diff --git a/epictrack-api/src/api/utils/caching.py b/epictrack-api/src/api/utils/caching.py index aa7bdcc72..b8a8c0abc 100644 --- a/epictrack-api/src/api/utils/caching.py +++ b/epictrack-api/src/api/utils/caching.py @@ -1,22 +1,22 @@ -"""Cache configurations.""" -import pydoc - -from flask_caching import Cache -from api import config - - -class AppCache: # pylint: disable=too-few-public-methods - """Caching""" - - cache = None - - @staticmethod - def configure_cache(runmode, app): - """Configure Caching""" - conf = pydoc.locate(config.CONFIGURATION[runmode]) - - AppCache.cache = Cache(config={ - "CACHE_TYPE": conf.CACHE_TYPE, - "CACHE_DEFAULT_TIMEOUT": conf.CACHE_DEFAULT_TIMEOUT - }) - AppCache.cache.init_app(app) +"""Cache configurations.""" +import pydoc + +from flask_caching import Cache +from api import config + + +class AppCache: # pylint: disable=too-few-public-methods + """Caching""" + + cache = None + + @staticmethod + def configure_cache(runmode, app): + """Configure Caching""" + conf = pydoc.locate(config.CONFIGURATION[runmode]) + + AppCache.cache = Cache(config={ + "CACHE_TYPE": conf.CACHE_TYPE, + "CACHE_DEFAULT_TIMEOUT": conf.CACHE_DEFAULT_TIMEOUT + }) + AppCache.cache.init_app(app) diff --git a/epictrack-api/src/api/utils/datetime_helper.py b/epictrack-api/src/api/utils/datetime_helper.py index 695a8b922..1c79efbca 100644 --- a/epictrack-api/src/api/utils/datetime_helper.py +++ b/epictrack-api/src/api/utils/datetime_helper.py @@ -1,24 +1,24 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Helper for datetime.""" -from datetime import datetime -import pytz - - -def get_start_of_day(date: datetime): - """Returns the date object with hours, minutes, seconds set as zero""" - if date: - date = date.replace(hour=2, minute=0, second=0, microsecond=0) - date = date.astimezone(pytz.utc) - return date +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Helper for datetime.""" +from datetime import datetime +import pytz + + +def get_start_of_day(date: datetime): + """Returns the date object with hours, minutes, seconds set as zero""" + if date: + date = date.replace(hour=2, minute=0, second=0, microsecond=0) + date = date.astimezone(pytz.utc) + return date diff --git a/epictrack-api/src/api/utils/enums.py b/epictrack-api/src/api/utils/enums.py index cc8d67e41..8b75f5549 100644 --- a/epictrack-api/src/api/utils/enums.py +++ b/epictrack-api/src/api/utils/enums.py @@ -1,48 +1,48 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Enum definitions.""" -from enum import Enum - - -class HttpMethod(Enum): - """Http methods""" - - GET = 'GET' - PUT = 'PUT' - POST = 'POST' - PATCH = 'PATCH' - DELETE = 'DELETE' - - -class ProjectCodeMethod(Enum): - """Project abbreviation code generation methods""" - - METHOD_1 = 1 - METHOD_2 = 2 - METHOD_3 = 3 - - -class RegionEntityType(Enum): - """Region entity types""" - - ENV = "ENV" - FLNR = "FLNR" - - -class StalenessEnum(Enum): - """Status update staleness level ENUM""" - - CRITICAL = "CRITICAL" - WARN = "WARN" - GOOD = "GOOD" +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Enum definitions.""" +from enum import Enum + + +class HttpMethod(Enum): + """Http methods""" + + GET = 'GET' + PUT = 'PUT' + POST = 'POST' + PATCH = 'PATCH' + DELETE = 'DELETE' + + +class ProjectCodeMethod(Enum): + """Project abbreviation code generation methods""" + + METHOD_1 = 1 + METHOD_2 = 2 + METHOD_3 = 3 + + +class RegionEntityType(Enum): + """Region entity types""" + + ENV = "ENV" + FLNR = "FLNR" + + +class StalenessEnum(Enum): + """Status update staleness level ENUM""" + + CRITICAL = "CRITICAL" + WARN = "WARN" + GOOD = "GOOD" diff --git a/epictrack-api/src/api/utils/str.py b/epictrack-api/src/api/utils/str.py index 6f3485451..460cf2b6e 100644 --- a/epictrack-api/src/api/utils/str.py +++ b/epictrack-api/src/api/utils/str.py @@ -1,41 +1,41 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""String helpers""" -import locale -from http import HTTPStatus -from typing import List, Text - -from natsort import humansorted, ns - -from api.exceptions import BusinessError - - -def escape_characters(source_string: Text, characters_to_escape: List[Text]) -> Text: - """Returns character escaped string""" - for character in characters_to_escape: - source_string = source_string.replace(character, fr"\{character}") - return source_string - - -def natural_sort(data: List, key: str = None) -> List[dict]: - """Sort the data based on natural sort algorithms.""" - locale.setlocale(locale.LC_ALL, "") - if isinstance(data[0], dict): - if key: - data = humansorted(data, key=lambda x: fr"{x[key]}", alg=ns.LOCALE) - else: - raise BusinessError("Key not provided to 'natural_sort'", HTTPStatus.FAILED_DEPENDENCY) - else: - data = humansorted(data) - return data +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""String helpers""" +import locale +from http import HTTPStatus +from typing import List, Text + +from natsort import humansorted, ns + +from api.exceptions import BusinessError + + +def escape_characters(source_string: Text, characters_to_escape: List[Text]) -> Text: + """Returns character escaped string""" + for character in characters_to_escape: + source_string = source_string.replace(character, fr"\{character}") + return source_string + + +def natural_sort(data: List, key: str = None) -> List[dict]: + """Sort the data based on natural sort algorithms.""" + locale.setlocale(locale.LC_ALL, "") + if isinstance(data[0], dict): + if key: + data = humansorted(data, key=lambda x: fr"{x[key]}", alg=ns.LOCALE) + else: + raise BusinessError("Key not provided to 'natural_sort'", HTTPStatus.FAILED_DEPENDENCY) + else: + data = humansorted(data) + return data diff --git a/epictrack-api/src/api/utils/token_info.py b/epictrack-api/src/api/utils/token_info.py index fbb988d3f..9902a7783 100644 --- a/epictrack-api/src/api/utils/token_info.py +++ b/epictrack-api/src/api/utils/token_info.py @@ -1,69 +1,69 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Helper for token decoding""" -from flask import g, current_app - - -class TokenInfo: - """Token info.""" - - @staticmethod - def get_id(): - """Get the user identifier.""" - try: - token_info = g.token_info - return token_info.get('sub', None) - except AttributeError: - return None - - @staticmethod - def get_user_data(): - """Get the user data.""" - token_info = g.token_info - user_data = { - 'id': token_info.get('sub', None), - 'first_name': token_info.get('given_name', None), - 'last_name': token_info.get('family_name', None), - 'email_id': token_info.get('email', None), - 'username': token_info.get('preferred_username', None), - 'identity_provider': token_info.get('identity_provider', ''), - 'groups': [group[1:len(group)] for group in token_info.get('groups')] - } - return user_data - - @staticmethod - def get_username(): - """Return the user name.""" - username = g.token_info.get("preferred_username", None) - if username is None: - username = g.jwt_oidc_token_info.get("email") - return username - - @staticmethod - def is_super_user() -> bool: - """Return True if the user is staff user.""" - # TODO Implement this method - return True - - @staticmethod - def get_roles(): - """Return roles of a user from the token.""" - token_info = g.token_info - realm_access = token_info.get('realm_access', {}) - realm_roles = realm_access.get('roles', []) - client_name = current_app.config.get('JWT_OIDC_AUDIENCE') - resource_access = token_info.get('resource_access', {}) - client_roles = resource_access.get(client_name, {}).get('roles', []) - - return realm_roles + client_roles +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Helper for token decoding""" +from flask import g, current_app + + +class TokenInfo: + """Token info.""" + + @staticmethod + def get_id(): + """Get the user identifier.""" + try: + token_info = g.token_info + return token_info.get('sub', None) + except AttributeError: + return None + + @staticmethod + def get_user_data(): + """Get the user data.""" + token_info = g.token_info + user_data = { + 'id': token_info.get('sub', None), + 'first_name': token_info.get('given_name', None), + 'last_name': token_info.get('family_name', None), + 'email_id': token_info.get('email', None), + 'username': token_info.get('preferred_username', None), + 'identity_provider': token_info.get('identity_provider', ''), + 'groups': [group[1:len(group)] for group in token_info.get('groups')] + } + return user_data + + @staticmethod + def get_username(): + """Return the user name.""" + username = g.token_info.get("preferred_username", None) + if username is None: + username = g.jwt_oidc_token_info.get("email") + return username + + @staticmethod + def is_super_user() -> bool: + """Return True if the user is staff user.""" + # TODO Implement this method + return True + + @staticmethod + def get_roles(): + """Return roles of a user from the token.""" + token_info = g.token_info + realm_access = token_info.get('realm_access', {}) + realm_roles = realm_access.get('roles', []) + client_name = current_app.config.get('JWT_OIDC_AUDIENCE') + resource_access = token_info.get('resource_access', {}) + client_roles = resource_access.get(client_name, {}).get('roles', []) + + return realm_roles + client_roles diff --git a/epictrack-api/tests/unit/apis/test_projects.py b/epictrack-api/tests/unit/apis/test_projects.py index ca00b7d5c..959016882 100644 --- a/epictrack-api/tests/unit/apis/test_projects.py +++ b/epictrack-api/tests/unit/apis/test_projects.py @@ -1,123 +1,123 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Test suite for projects.""" - -from decimal import Decimal -from http import HTTPStatus -from pathlib import Path -from urllib.parse import urljoin - -from werkzeug.datastructures import FileStorage - -from tests.utilities.factory_scenarios import TestProjectInfo -from tests.utilities.factory_utils import factory_project_model - - -API_BASE_URL = "/api/v1/" - - -def test_create_project(client, auth_header): - """Test create new project.""" - url = urljoin(API_BASE_URL, "projects") - response = client.post(url, json=TestProjectInfo.project1.value, headers=auth_header) - assert response.status_code == HTTPStatus.CREATED - assert "id" in response.json - - -def test_get_projects(client, auth_header): - """Test get projects.""" - url = urljoin(API_BASE_URL, "projects") - response = client.get(url, headers=auth_header) - assert response.status_code == HTTPStatus.OK - - -def test_update_project(client, auth_header): - """Test update project.""" - project = factory_project_model() - # Update the project - payload = TestProjectInfo.project1.value - payload["name"] = "New Project Updated" - url = urljoin(API_BASE_URL, f'projects/{project.id}') - response = client.put(url, json=payload, headers=auth_header) - - assert response.status_code == HTTPStatus.OK - assert response.json["name"] == "New Project Updated" - - -def test_delete_project(client, auth_header): - """Test delete project.""" - project = factory_project_model() - url = urljoin(API_BASE_URL, f'projects/{project.id}') - client.delete(url, headers=auth_header) - response = client.get(url, headers=auth_header) - assert response.status_code == HTTPStatus.NOT_FOUND - - -def test_project_detail(client, auth_header): - """Test project details.""" - project_payload = TestProjectInfo.project1.value - project = factory_project_model() - url = urljoin(API_BASE_URL, f'projects/{project.id}') - response = client.get(url, headers=auth_header) - assert response.status_code == HTTPStatus.OK - assert "id" in response.json - for key, expected_value in project_payload.items(): - response_value = response.json.get(key) - assert response_value is not None, f"Key {key} not found in response" - if isinstance(expected_value, Decimal): # some of the values are decimal - response_value = Decimal(response_value) - assert expected_value == response_value, \ - f"Value mismatch for key {key}: expected {expected_value}, got {response_value}" - - -def test_import_project(client, auth_header): - """Test import project""" - url = urljoin(API_BASE_URL, "projects/import") - file_path = Path("./src/api/templates/master_templates/Projects.xlsx") - file_path = file_path.resolve() - file = FileStorage( - stream=open(file_path, "rb"), - filename="projects.xlsx", - ) - response = client.post( - url, - data={"file": file}, - content_type="multipart/form-data", - headers=auth_header - ) - assert response.status_code == HTTPStatus.CREATED - - -def test_validate_project(client, auth_header): - """Test validate project""" - url = urljoin(API_BASE_URL, "projects/exists") - - # Scenario 1: Updating an existing project - project = factory_project_model() - payload = {"name": project.name, "project_id": project.id} - response = client.get(url, query_string=payload, headers=auth_header) - assert response.status_code == HTTPStatus.OK - assert not response.json["exists"] - - # Scenario 2: Creating new project with existing name - del payload["project_id"] - response = client.get(url, query_string=payload, headers=auth_header) - assert response.status_code == HTTPStatus.OK - assert response.json["exists"] - - # Scenario 3: Creating new project with new name - payload = TestProjectInfo.project2.value - response = client.get(url, query_string=payload, headers=auth_header) - assert response.status_code == HTTPStatus.OK - assert not response.json["exists"] +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test suite for projects.""" + +from decimal import Decimal +from http import HTTPStatus +from pathlib import Path +from urllib.parse import urljoin + +from werkzeug.datastructures import FileStorage + +from tests.utilities.factory_scenarios import TestProjectInfo +from tests.utilities.factory_utils import factory_project_model + + +API_BASE_URL = "/api/v1/" + + +def test_create_project(client, auth_header): + """Test create new project.""" + url = urljoin(API_BASE_URL, "projects") + response = client.post(url, json=TestProjectInfo.project1.value, headers=auth_header) + assert response.status_code == HTTPStatus.CREATED + assert "id" in response.json + + +def test_get_projects(client, auth_header): + """Test get projects.""" + url = urljoin(API_BASE_URL, "projects") + response = client.get(url, headers=auth_header) + assert response.status_code == HTTPStatus.OK + + +def test_update_project(client, auth_header): + """Test update project.""" + project = factory_project_model() + # Update the project + payload = TestProjectInfo.project1.value + payload["name"] = "New Project Updated" + url = urljoin(API_BASE_URL, f'projects/{project.id}') + response = client.put(url, json=payload, headers=auth_header) + + assert response.status_code == HTTPStatus.OK + assert response.json["name"] == "New Project Updated" + + +def test_delete_project(client, auth_header): + """Test delete project.""" + project = factory_project_model() + url = urljoin(API_BASE_URL, f'projects/{project.id}') + client.delete(url, headers=auth_header) + response = client.get(url, headers=auth_header) + assert response.status_code == HTTPStatus.NOT_FOUND + + +def test_project_detail(client, auth_header): + """Test project details.""" + project_payload = TestProjectInfo.project1.value + project = factory_project_model() + url = urljoin(API_BASE_URL, f'projects/{project.id}') + response = client.get(url, headers=auth_header) + assert response.status_code == HTTPStatus.OK + assert "id" in response.json + for key, expected_value in project_payload.items(): + response_value = response.json.get(key) + assert response_value is not None, f"Key {key} not found in response" + if isinstance(expected_value, Decimal): # some of the values are decimal + response_value = Decimal(response_value) + assert expected_value == response_value, \ + f"Value mismatch for key {key}: expected {expected_value}, got {response_value}" + + +def test_import_project(client, auth_header): + """Test import project""" + url = urljoin(API_BASE_URL, "projects/import") + file_path = Path("./src/api/templates/master_templates/Projects.xlsx") + file_path = file_path.resolve() + file = FileStorage( + stream=open(file_path, "rb"), + filename="projects.xlsx", + ) + response = client.post( + url, + data={"file": file}, + content_type="multipart/form-data", + headers=auth_header + ) + assert response.status_code == HTTPStatus.CREATED + + +def test_validate_project(client, auth_header): + """Test validate project""" + url = urljoin(API_BASE_URL, "projects/exists") + + # Scenario 1: Updating an existing project + project = factory_project_model() + payload = {"name": project.name, "project_id": project.id} + response = client.get(url, query_string=payload, headers=auth_header) + assert response.status_code == HTTPStatus.OK + assert not response.json["exists"] + + # Scenario 2: Creating new project with existing name + del payload["project_id"] + response = client.get(url, query_string=payload, headers=auth_header) + assert response.status_code == HTTPStatus.OK + assert response.json["exists"] + + # Scenario 3: Creating new project with new name + payload = TestProjectInfo.project2.value + response = client.get(url, query_string=payload, headers=auth_header) + assert response.status_code == HTTPStatus.OK + assert not response.json["exists"]