diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index afc5581..1f4db7c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,4 @@ +#GUSINFO:NPC Asteroids, SFDO Volunteers for Salesforce # Python *.py @SalesforceFoundation/release-engineering-reviewers diff --git a/.gitignore b/.gitignore index d68d7b2..685e389 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ src/profiles/Partner Community User.profile src/workflows/User.workflow **/.sfdx/ **/.vscode/ -.cci \ No newline at end of file +.cci +test_results.* \ No newline at end of file diff --git a/cumulusci.yml b/cumulusci.yml index a9b98d0..9f7b960 100644 --- a/cumulusci.yml +++ b/cumulusci.yml @@ -1,306 +1,223 @@ -minimum_cumulusci_version: 2.5.0 +minimum_cumulusci_version: 3.23.0 project: - name: Volunteers-for-Salesforce - package: - name: Volunteers for Salesforce - namespace: GW_Volunteers - api_version: '40.0' - install_class: InstallScript - git: - prefix_release: rel/ + name: Volunteers-for-Salesforce + package: + name: Volunteers for Salesforce + namespace: GW_Volunteers + api_version: "40.0" + install_class: InstallScript + git: + prefix_release: rel/ + prefix_beta: beta/ + repo_url: https://github.com/SalesforceFoundation/Volunteers-for-Salesforce + dependency_resolutions: + preproduction: include_beta + +sources: + npsp: + github: https://github.com/SalesforceFoundation/NPSP tasks: - deploy_dev_config: - description: Deploys the post install configuration for an unmanaged DE org - class_path: cumulusci.tasks.salesforce.Deploy - options: - path: unpackaged/config/dev - - deploy_qa_config: - description: Deploys the post install configuration for a regression or QA scratch org - class_path: cumulusci.tasks.salesforce.Deploy - options: - path: unpackaged/config/qa - - deploy_v4s_only_page_layouts: - description: Deploys the page layouts for an org with Volunteers for Salesforce, but not NPSP - class_path: cumulusci.tasks.salesforce.Deploy - options: - path: unpackaged/config/v4s_only_layouts - namespace_inject: $project_config.project__package__namespace - - deploy_npsp_v4s_page_layouts: - description: Deploys the page layouts for an org with Volunteers for Salesforce and NPSP - class_path: cumulusci.tasks.salesforce.Deploy - options: - path: unpackaged/config/npsp_v4s_layouts - namespace_inject: $project_config.project__package__namespace - - deploy_delete_config: - description: Deploys the metadata deletions for the post install DE org config - class_path: cumulusci.tasks.salesforce.Deploy - options: - path: unpackaged/config/delete - - deploy_package_settings: - description: Configure the default Volunteers Package Settings - class_path: cumulusci.tasks.apex.anon.AnonymousApexTask - options: - path: scripts/DeployScript.cls - apex: insertPackageSettings(); - - assign_pset: - description: Runs anonymous apex to assign pset for guest user. - class_path: cumulusci.tasks.apex.anon.AnonymousApexTask - options: - apex: > - Id psetId = [SELECT ID From PermissionSet WHERE Name = 'V4S_Site_Minimum' LIMIT 1][0].id; - Id guestId = [SELECT ID From User WHERE Name = 'Volunteers Site Guest User' LIMIT 1][0].id; - insert new PermissionSetAssignment(PermissionSetId=psetId, AssigneeId=guestId); - - deploy_test_data: - description: 'Loads a test data set for Volunteers for Salesforce' - class_path: cumulusci.tasks.bulkdata.LoadData - options: - sql_path: 'datasets/regression/test_data.sql' - mapping: 'datasets/regression/mapping.yml' - - delete_test_data: - description: 'WARNING: deletes all data in sObjects used for testing' - class_path: cumulusci.tasks.bulkdata.DeleteData - options: - objects: - - Volunteer_Hours__c - - Volunteer_Shift__c - - Job_Recurrence_Schedule__c - - Volunteer_Recurrence_Schedule__c - - Volunteer_Job__c - - Contact - - Account - - Campaign - - deploy_test_data_managed: - description: 'Loads a test data set for Volunteers for Salesforce to a managed install' - class_path: cumulusci.tasks.bulkdata.LoadData - options: - sql_path: 'datasets/regression/test_data.sql' - mapping: 'datasets/regression/mapping_managed.yml' - - delete_test_data_managed: - description: 'WARNING: deletes all data in sObjects used for testing' - class_path: cumulusci.tasks.bulkdata.DeleteData - options: - objects: - - GW_Volunteers__Volunteer_Hours__c - - GW_Volunteers__Volunteer_Shift__c - - GW_Volunteers__Job_Recurrence_Schedule__c - - GW_Volunteers__Volunteer_Recurrence_Schedule__c - - GW_Volunteers__Volunteer_Job__c - - Contact - - Account - - Campaign - - capture_test_data: - description: 'Capture test data from an org' - class_path: cumulusci.tasks.bulkdata.ExtractData - options: - sql_path: 'datasets/regression/test_data.sql' - mapping: 'datasets/regression/mapping.yml' - - ensure_record_type: - name: Ensure Campaign Record Types - description: This will ensure Record Types are enabled for the Campaign object in your org before installing Volunteers for Salesforce. If there are no Campaign record types yet, it will create one called Default. - class_path: cumulusci.tasks.salesforce.EnsureRecordTypes - options: - record_type_label: Default - record_type_developer_name: Default - sobject: Campaign - - install_npsp_with_unpackaged_config: - description: Install NPSP with dependencies and unpackaged configuration - class_path: cumulusci.tasks.salesforce.UpdateDependencies - options: - dependencies: - - github: 'https://github.com/SalesforceFoundation/Cumulus' - - repo_name: Cumulus - repo_owner: SalesforceFoundation - subfolder: unpackaged/config/trial - namespace_inject: 'npsp' - unmanaged: False - - repo_name: Cumulus - repo_owner: SalesforceFoundation - subfolder: unpackaged/config/reports - namespace_inject: 'npsp' - unmanaged: False - - insert_npsp_relationships: - description: 'Runs execute anonymous to insert the default NPSP relationships. Copied from Cumulus on 2019-07-09.' - class_path: cumulusci.tasks.apex.anon.AnonymousApexTask - options: - apex: > - List defaultRelationships = new List{ - new npe4__Relationship_Lookup__c(Name = 'Father',npe4__Male__c = 'Son', npe4__Female__c = 'Daughter', npe4__Neutral__c = 'Child'), - new npe4__Relationship_Lookup__c(Name = 'Mother',npe4__Male__c = 'Son', npe4__Female__c = 'Daughter', npe4__Neutral__c = 'Child'), - new npe4__Relationship_Lookup__c(Name = 'Parent',npe4__Male__c = 'Son', npe4__Female__c = 'Daughter', npe4__Neutral__c = 'Child'), - new npe4__Relationship_Lookup__c(Name = 'Son',npe4__Male__c = 'Father', npe4__Female__c = 'Mother', npe4__Neutral__c = 'Parent'), - new npe4__Relationship_Lookup__c(Name = 'Daughter',npe4__Male__c = 'Father', npe4__Female__c = 'Mother', npe4__Neutral__c = 'Parent'), - new npe4__Relationship_Lookup__c(Name = 'Child',npe4__Male__c = 'Father', npe4__Female__c = 'Mother', npe4__Neutral__c = 'Parent'), - new npe4__Relationship_Lookup__c(Name = 'Aunt',npe4__Male__c = 'Nephew', npe4__Female__c = 'Niece', npe4__Neutral__c = 'Sibling\'s Child'), - new npe4__Relationship_Lookup__c(Name = 'Uncle',npe4__Male__c = 'Nephew', npe4__Female__c = 'Niece', npe4__Neutral__c = 'Sibling\'s Child'), - new npe4__Relationship_Lookup__c(Name = 'Husband',npe4__Male__c = 'Husband', npe4__Female__c = 'Wife', npe4__Neutral__c = 'Spouse'), - new npe4__Relationship_Lookup__c(Name = 'Wife',npe4__Male__c = 'Husband', npe4__Female__c = 'Wife', npe4__Neutral__c = 'Spouse'), - new npe4__Relationship_Lookup__c(Name = 'Partner',npe4__Male__c = 'Partner', npe4__Female__c = 'Partner', npe4__Neutral__c = 'Partner'), - new npe4__Relationship_Lookup__c(Name = 'Cousin',npe4__Male__c = 'Cousin', npe4__Female__c = 'Cousin', npe4__Neutral__c = 'Cousin'), - new npe4__Relationship_Lookup__c(Name = 'Grandmother',npe4__Male__c = 'Grandson', npe4__Female__c = 'Granddaughter', npe4__Neutral__c = 'Grandchild'), - new npe4__Relationship_Lookup__c(Name = 'Grandfather',npe4__Male__c = 'Grandson', npe4__Female__c = 'Granddaughter', npe4__Neutral__c = 'Grandchild'), - new npe4__Relationship_Lookup__c(Name = 'Grandparent',npe4__Male__c = 'Grandson', npe4__Female__c = 'Granddaughter', npe4__Neutral__c = 'Grandchild'), - new npe4__Relationship_Lookup__c(Name = 'Grandson',npe4__Male__c = 'Grandfather', npe4__Female__c = 'Grandmother', npe4__Neutral__c = 'Grandparent'), - new npe4__Relationship_Lookup__c(Name = 'Granddaughter',npe4__Male__c = 'Grandfather', npe4__Female__c = 'Grandmother', npe4__Neutral__c = 'Grandparent'), - new npe4__Relationship_Lookup__c(Name = 'Grandchild',npe4__Male__c = 'Grandfather', npe4__Female__c = 'Grandmother', npe4__Neutral__c = 'Grandparent'), - new npe4__Relationship_Lookup__c(Name = 'Employer',npe4__Male__c = 'Employee', npe4__Female__c = 'Employee', npe4__Neutral__c = 'Employee'), - new npe4__Relationship_Lookup__c(Name = 'Employee',npe4__Male__c = 'Employer', npe4__Female__c = 'Employer', npe4__Neutral__c = 'Employer') - }; - insert defaultRelationships; - - insert_npsp_test_data: - description: 'Loads a NPSP test data set for most NPSP objects based on 100 Contacts that should fit into a scratch org or DE org' - class_path: cumulusci.tasks.bulkdata.LoadData - options: - database_url: 'sqlite:///Cumulus/datasets/dev_org/test_data.db' - mapping: 'Cumulus/datasets/mapping_managed.yml' + deploy_dev_config: + description: Deploys the post install configuration for an unmanaged DE org + class_path: cumulusci.tasks.salesforce.Deploy + options: + path: unpackaged/config/dev + + deploy_qa_config: + description: Deploys the post install configuration for a regression or QA scratch org + class_path: cumulusci.tasks.salesforce.Deploy + options: + path: unpackaged/config/qa + + deploy_tab_config: + description: Deploys the post install configuration for custom tabs + class_path: cumulusci.tasks.salesforce.Deploy + options: + path: unpackaged/config/tabs + + deploy_v4s_only_page_layouts: + description: Deploys the page layouts for an org with Volunteers for Salesforce, but not NPSP + class_path: cumulusci.tasks.salesforce.Deploy + options: + path: unpackaged/config/v4s_only_layouts + + deploy_npsp_v4s_page_layouts: + description: Deploys the page layouts for an org with Volunteers for Salesforce and NPSP + class_path: cumulusci.tasks.salesforce.Deploy + options: + path: unpackaged/config/npsp_v4s_layouts + + deploy_delete_config: + description: Deploys the metadata deletions for the post install DE org config + class_path: cumulusci.tasks.salesforce.Deploy + options: + path: unpackaged/config/delete + + deploy_package_settings: + description: Configure the default Volunteers Package Settings + class_path: cumulusci.tasks.apex.anon.AnonymousApexTask + options: + path: scripts/DeployScript.cls + apex: insertPackageSettings(); + + assign_pset: + description: Runs anonymous apex to assign pset for guest user. + class_path: cumulusci.tasks.apex.anon.AnonymousApexTask + options: + apex: > + Id psetId = [SELECT ID From PermissionSet WHERE Name = 'V4S_Site_Minimum' LIMIT 1][0].id; + Id guestId = [SELECT ID From User WHERE Name = 'Volunteers Site Guest User' LIMIT 1][0].id; + insert new PermissionSetAssignment(PermissionSetId=psetId, AssigneeId=guestId); + + delete_data: + options: + objects: + - Volunteer_Hours__c + - Volunteer_Shift__c + - Job_Recurrence_Schedule__c + - Volunteer_Recurrence_Schedule__c + - Volunteer_Job__c + - Contact + - Account + - Campaign + + ensure_record_type: + name: Ensure Campaign Record Types + description: This will ensure Record Types are enabled for the Campaign object in your org before installing Volunteers for Salesforce. If there are no Campaign record types yet, it will create one called Default. + class_path: cumulusci.tasks.salesforce.EnsureRecordTypes + options: + record_type_label: Default + record_type_developer_name: Default + sobject: Campaign + + run_tests: + options: + retry_failures: + - "unable to obtain exclusive access to this record" + - "UNABLE_TO_LOCK_ROW" + - "connection was cancelled here" + retry_always: True flows: - ci_feature: - description: Deploys the unmanaged package metadata and all dependencies to the target org and runs tests - tasks: - 5: - task: None - - config_dev: - steps: - 3: - task: deploy_delete_config - 4: - task: deploy_dev_config - options: - unmanaged: True - namespace_inject: $project_config.project__package__namespace - 5: - task: deploy_package_settings - 6: - task: assign_pset - ignore_failure: True - 7: - task: deploy_test_data - - config_qa: - steps: - 3: - task: deploy_delete_config - 4: - task: deploy_dev_config - options: - unmanaged: True - namespace_inject: $project_config.project__package__namespace - 5: - task: deploy_qa_config - options: - unmanaged: True - namespace_inject: $project_config.project__package__namespace - 6: - task: deploy_package_settings - 7: - task: assign_pset - ignore_failure: True - 8: - task: deploy_test_data - - config_regression: - steps: - 2: - task: deploy_delete_config - 3: - task: deploy_dev_config - options: - unmanaged: False - namespace_inject: $project_config.project__package__namespace - 4: - task: deploy_qa_config - options: - unmanaged: False - namespace_inject: $project_config.project__package__namespace - 5: - task: assign_pset - 6: - task: deploy_test_data_managed - - install_npsp: - description: 'Installs NPSP with unmanaged metadata and inserts NPSP test data' - steps: - 1: - task: install_npsp_with_unpackaged_config - 2: - task: insert_npsp_relationships - - customer_org_npsp: - steps: - 1: - flow: install_npsp - 2: - task: ensure_record_type - 3: - task: install_managed - 4: - task: deploy_npsp_v4s_page_layouts - options: - unmanaged: False - - customer_org: - steps: - 1: - task: ensure_record_type - 2: - task: install_managed - 3: - task: deploy_v4s_only_page_layouts - options: - unmanaged: False + config_unmanaged: + steps: + 1: + task: deploy_delete_config + 2: + task: deploy_dev_config + 3: + task: deploy_tab_config + 4: + task: deploy_package_settings + when: '"GW_Volunteers" not in org_config.installed_packages' + 5: + task: assign_pset + ignore_failure: True + 6: + task: load_dataset + + config_managed: + steps: + 3: + task: deploy_tab_config + config_dev: + steps: + 3: + flow: config_unmanaged + + config_qa: + steps: + 3: + flow: config_unmanaged + 4: + task: deploy_qa_config + + config_regression: + steps: + 3: + flow: config_unmanaged + 4: + task: deploy_qa_config + + install_npsp: + steps: + 1: + flow: npsp:install_prod + + customer_org: + steps: + 1: + task: ensure_record_type + ui_options: + name: Default Campaign Record Type + 2: + task: install_managed + 3: + task: deploy_tab_config + checks: + - when: "'GW_Volunteers' in tasks.get_installed_packages()" # Cached at start + action: "hide" + options: + unmanaged: False + ui_options: + name: "Install Volunteers for Salesforce Tabs" + 4: + task: deploy_npsp_v4s_page_layouts + checks: + - when: "'npsp' not in tasks.get_installed_packages()" + action: hide + options: + unmanaged: False + ui_options: + name: "Install Page Layouts (NPSP)" + is_required: False + when: "org_config.has_minimum_package_version('npsp', '1.0')" + 5: + task: deploy_v4s_only_page_layouts + checks: + - when: "'npsp' in tasks.get_installed_packages()" + action: hide + options: + unmanaged: False + ui_options: + name: "Install Page Layouts (V4S)" + is_required: False + when: "not org_config.has_minimum_package_version('npsp', '1.0')" orgs: - scratch: - dev_namespaced: - config_file: orgs/dev.json - namespaced: True - days: 7 - prerelease: - config_file: orgs/prerelease.json + scratch: + dev_namespaced: + config_file: orgs/dev.json + namespaced: True + days: 7 + prerelease: + config_file: orgs/prerelease.json plans: - install: - slug: install - title: Install Volunteers for Salesforce - tier: primary - is_listed: False - steps: - 1: - task: ensure_record_type - ui_options: - name: Default Campaign Record Type - 2: - task: install_managed - ui_options: - name: Install Volunteers for Salesforce - 3: - task: deploy_npsp_v4s_page_layouts - checks: - - when: "'npsp' not in tasks.get_installed_packages()" - action: hide - ui_options: - is_required: False - 4: - task: deploy_v4s_only_page_layouts - checks: - - when: "'npsp' in tasks.get_installed_packages()" - action: hide - ui_options: - is_required: False + install: + slug: install + title: Install Volunteers for Salesforce + tier: primary + is_listed: True + steps: + 1: + flow: customer_org + + upgrade: + slug: upgrade + title: Product Upgrade + tier: additional + is_listed: False + preflight_message: "This installer upgrades this package and any required dependencies to the latest version in your org. This installer isn't supported and has risks. Please don't run this installer unless you're aware of its specific use cases and considerations." + post_install_message: "Installation complete and package is on the latest version." + steps: + 1: + task: update_dependencies + options: + security_type: PUSH + packages_only: True + 2: + task: install_managed + options: + security_type: PUSH diff --git a/datasets/mapping.yml b/datasets/mapping.yml new file mode 100644 index 0000000..152b774 --- /dev/null +++ b/datasets/mapping.yml @@ -0,0 +1,120 @@ +Insert Campaign: + sf_object: Campaign + fields: + - Id + - IsActive + - Name + - Status + - StartDate + - EndDate + - Volunteer_Website_Time_Zone__c +Insert Account: + sf_object: Account + fields: + - Id + - Name +Insert Contact: + sf_object: Contact + fields: + - Id + - FirstName + - LastName + - Email + - Volunteer_Auto_Reminder_Email_Opt_Out__c + - Volunteer_Availability__c + - Volunteer_Last_Web_Signup_Date__c + - Volunteer_Manager_Notes__c + - Volunteer_Notes__c + - Volunteer_Organization__c + - Volunteer_Skills__c + - Volunteer_Status__c + lookups: + AccountId: + table: Account +Insert Volunteer_Job__c: + sf_object: Volunteer_Job__c + fields: + - Description__c + - Display_on_Website__c + - External_Signup_Url__c + - Id + - Inactive__c + - Location_City__c + - Location_Information__c + - Location_Street__c + - Location_Zip_Postal_Code__c + - Location__c + - Name + - Ongoing__c + - Skills_Needed__c + - Volunteer_Website_Time_Zone__c + lookups: + Campaign__c: + table: Campaign +Insert Volunteer_Recurrence_Schedule__c: + sf_object: Volunteer_Recurrence_Schedule__c + fields: + - Comments__c + - Days_of_Week__c + - Duration__c + - Id + - Number_of_Volunteers__c + - Schedule_End_Date__c + - Schedule_Start_Date_Time__c + - Volunteer_Hours_Status__c + - Weekly_Occurrence__c + lookups: + Contact__c: + table: Contact + Volunteer_Job__c: + table: Volunteer_Job__c +Insert Job_Recurrence_Schedule__c: + sf_object: Job_Recurrence_Schedule__c + fields: + - Days_of_Week__c + - Description__c + - Desired_Number_of_Volunteers__c + - Duration__c + - Id + - Schedule_End_Date__c + - Schedule_Start_Date_Time__c + - Weekly_Occurrence__c + lookups: + Volunteer_Job__c: + table: Volunteer_Job__c +Insert Volunteer_Shift__c: + sf_object: Volunteer_Shift__c + fields: + - Description__c + - Desired_Number_of_Volunteers__c + - Duration__c + - Id + - Start_Date_Time__c + - System_Note__c + - Total_Volunteers__c + lookups: + Job_Recurrence_Schedule__c: + table: Job_Recurrence_Schedule__c + Volunteer_Job__c: + table: Volunteer_Job__c +Insert Volunteer_Hours__c: + sf_object: Volunteer_Hours__c + fields: + - Comments__c + - End_Date__c + - Hours_Worked__c + - Id + - Number_of_Volunteers__c + - Planned_Start_Date_Time__c + - Start_Date__c + - Status__c + - System_Note__c + lookups: + Contact__c: + table: Contact + Volunteer_Job__c: + table: Volunteer_Job__c + Volunteer_Recurrence_Schedule__c: + table: Volunteer_Recurrence_Schedule__c + Volunteer_Shift__c: + table: Volunteer_Shift__c diff --git a/datasets/regression/mapping.yml b/datasets/regression/mapping.yml deleted file mode 100644 index 7affc85..0000000 --- a/datasets/regression/mapping.yml +++ /dev/null @@ -1,128 +0,0 @@ -Insert Campaign: - sf_object: Campaign - table: campaign - fields: - Id: sf_id - IsActive: isactive - Name: name - Status: status - StartDate: startdate - EndDate: enddate - Volunteer_Website_Time_Zone__c: volunteer_website_time_zone -Insert Account: - sf_object: Account - table: account - fields: - Id: sf_id - Name: name -Insert Contact: - sf_object: Contact - table: contact - fields: - Id: sf_id - FirstName: firstname - LastName: lastname - Email: email - Volunteer_Auto_Reminder_Email_Opt_Out__c: volunteer_auto_reminder_email_opt_out - Volunteer_Availability__c: volunteer_availability - Volunteer_Last_Web_Signup_Date__c: volunteer_last_web_signup_date - Volunteer_Manager_Notes__c: volunteer_manager_notes - Volunteer_Notes__c: volunteer_notes - Volunteer_Organization__c: volunteer_organization - Volunteer_Skills__c: volunteer_skills - Volunteer_Status__c: volunteer_status - lookups: - AccountId: - table: account -Insert Volunteer_Job__c: - sf_object: Volunteer_Job__c - table: volunteer_job__c - fields: - Description__c: description - Display_on_Website__c: display_on_website - External_Signup_Url__c: external_signup_url - Id: sf_id - Inactive__c: inactive - Location_City__c: location_city - Location_Information__c: location_information - Location_Street__c: location_street - Location_Zip_Postal_Code__c: location_zip_postal_code - Location__c: location - Name: name - Ongoing__c: ongoing - Skills_Needed__c: skills_needed - Volunteer_Website_Time_Zone__c: volunteer_website_time_zone - lookups: - Campaign__c: - table: campaign -Insert Volunteer_Recurrence_Schedule__c: - sf_object: Volunteer_Recurrence_Schedule__c - table: volunteer_recurrence_schedule__c - fields: - Comments__c: comments - Days_of_Week__c: days_of_week - Duration__c: duration - Id: sf_id - Number_of_Volunteers__c: number_of_volunteers - Schedule_End_Date__c: schedule_end_date - Schedule_Start_Date_Time__c: schedule_start_date_time - Volunteer_Hours_Status__c: volunteer_hours_status - Weekly_Occurrence__c: weekly_occurrence - lookups: - Contact__c: - table: contact - Volunteer_Job__c: - table: volunteer_job__c -Insert Job_Recurrence_Schedule__c: - sf_object: Job_Recurrence_Schedule__c - table: job_recurrence_schedule__c - fields: - Days_of_Week__c: days_of_week - Description__c: description - Desired_Number_of_Volunteers__c: desired_number_of_volunteers - Duration__c: duration - Id: sf_id - Schedule_End_Date__c: schedule_end_date - Schedule_Start_Date_Time__c: schedule_start_date_time - Weekly_Occurrence__c: weekly_occurrence - lookups: - Volunteer_Job__c: - table: volunteer_job__c -Insert Volunteer_Shift__c: - sf_object: Volunteer_Shift__c - table: volunteer_shift__c - fields: - Description__c: description - Desired_Number_of_Volunteers__c: desired_number_of_volunteers - Duration__c: duration - Id: sf_id - Start_Date_Time__c: start_date_time - System_Note__c: system_note - Total_Volunteers__c: total_volunteers - lookups: - Job_Recurrence_Schedule__c: - table: job_recurrence_schedule__c - Volunteer_Job__c: - table: volunteer_job__c -Insert Volunteer_Hours__c: - sf_object: Volunteer_Hours__c - table: volunteer_hours__c - fields: - Comments__c: comments - End_Date__c: end_date - Hours_Worked__c: hours_worked - Id: sf_id - Number_of_Volunteers__c: number_of_volunteers - Planned_Start_Date_Time__c: planned_start_date_time - Start_Date__c: start_date - Status__c: status - System_Note__c: system_note - lookups: - Contact__c: - table: contact - Volunteer_Job__c: - table: volunteer_job__c - Volunteer_Recurrence_Schedule__c: - table: volunteer_recurrence_schedule__c - Volunteer_Shift__c: - table: volunteer_shift__c diff --git a/datasets/regression/mapping_managed.yml b/datasets/regression/mapping_managed.yml deleted file mode 100644 index 3a72749..0000000 --- a/datasets/regression/mapping_managed.yml +++ /dev/null @@ -1,138 +0,0 @@ -Insert Campaign: - sf_object: Campaign - table: campaign - fields: - Id: sf_id - IsActive: isactive - Name: name - Status: status - StartDate: startdate - EndDate: enddate - GW_Volunteers__Volunteer_Website_Time_Zone__c: volunteer_website_time_zone -Insert Account: - sf_object: Account - table: account - fields: - Id: sf_id - Name: name -Insert Contact: - sf_object: Contact - table: contact - fields: - Id: sf_id - FirstName: firstname - LastName: lastname - Email: email - GW_Volunteers__Volunteer_Auto_Reminder_Email_Opt_Out__c: volunteer_auto_reminder_email_opt_out - GW_Volunteers__Volunteer_Availability__c: volunteer_availability - GW_Volunteers__Volunteer_Last_Web_Signup_Date__c: volunteer_last_web_signup_date - GW_Volunteers__Volunteer_Manager_Notes__c: volunteer_manager_notes - GW_Volunteers__Volunteer_Notes__c: volunteer_notes - GW_Volunteers__Volunteer_Organization__c: volunteer_organization - GW_Volunteers__Volunteer_Skills__c: volunteer_skills - GW_Volunteers__Volunteer_Status__c: volunteer_status - lookups: - AccountId: - table: account -Insert GW_Volunteers__Volunteer_Job__c: - sf_object: GW_Volunteers__Volunteer_Job__c - table: volunteer_job__c - fields: - GW_Volunteers__Description__c: description - GW_Volunteers__Display_on_Website__c: display_on_website - GW_Volunteers__External_Signup_Url__c: external_signup_url - Id: sf_id - GW_Volunteers__Inactive__c: inactive - GW_Volunteers__Location_City__c: location_city - GW_Volunteers__Location_Information__c: location_information - GW_Volunteers__Location_Street__c: location_street - GW_Volunteers__Location_Zip_Postal_Code__c: location_zip_postal_code - GW_Volunteers__Location__c: location - Name: name - GW_Volunteers__Ongoing__c: ongoing - GW_Volunteers__Skills_Needed__c: skills_needed - GW_Volunteers__Volunteer_Website_Time_Zone__c: volunteer_website_time_zone - lookups: - GW_Volunteers__Campaign__c: - table: campaign - key_field: campaign__c -Insert GW_Volunteers__Volunteer_Recurrence_Schedule__c: - sf_object: GW_Volunteers__Volunteer_Recurrence_Schedule__c - table: volunteer_recurrence_schedule__c - fields: - GW_Volunteers__Comments__c: comments - GW_Volunteers__Days_of_Week__c: days_of_week - GW_Volunteers__Duration__c: duration - Id: sf_id - GW_Volunteers__Number_of_Volunteers__c: number_of_volunteers - GW_Volunteers__Schedule_End_Date__c: schedule_end_date - GW_Volunteers__Schedule_Start_Date_Time__c: schedule_start_date_time - GW_Volunteers__Volunteer_Hours_Status__c: volunteer_hours_status - GW_Volunteers__Weekly_Occurrence__c: weekly_occurrence - lookups: - GW_Volunteers__Contact__c: - table: contact - key_field: contact__c - GW_Volunteers__Volunteer_Job__c: - table: volunteer_job__c - key_field: volunteer_job__c -Insert GW_Volunteers__Job_Recurrence_Schedule__c: - sf_object: GW_Volunteers__Job_Recurrence_Schedule__c - table: job_recurrence_schedule__c - fields: - GW_Volunteers__Days_of_Week__c: days_of_week - GW_Volunteers__Description__c: description - GW_Volunteers__Desired_Number_of_Volunteers__c: desired_number_of_volunteers - GW_Volunteers__Duration__c: duration - Id: sf_id - GW_Volunteers__Schedule_End_Date__c: schedule_end_date - GW_Volunteers__Schedule_Start_Date_Time__c: schedule_start_date_time - GW_Volunteers__Weekly_Occurrence__c: weekly_occurrence - lookups: - GW_Volunteers__Volunteer_Job__c: - table: volunteer_job__c - key_field: volunteer_job__c -Insert GW_Volunteers__Volunteer_Shift__c: - sf_object: GW_Volunteers__Volunteer_Shift__c - table: volunteer_shift__c - fields: - GW_Volunteers__Description__c: description - GW_Volunteers__Desired_Number_of_Volunteers__c: desired_number_of_volunteers - GW_Volunteers__Duration__c: duration - Id: sf_id - GW_Volunteers__Start_Date_Time__c: start_date_time - GW_Volunteers__System_Note__c: system_note - GW_Volunteers__Total_Volunteers__c: total_volunteers - lookups: - GW_Volunteers__Job_Recurrence_Schedule__c: - table: job_recurrence_schedule__c - key_field: job_recurrence_schedule__c - GW_Volunteers__Volunteer_Job__c: - table: volunteer_job__c - key_field: volunteer_job__c -Insert GW_Volunteers__Volunteer_Hours__c: - sf_object: GW_Volunteers__Volunteer_Hours__c - table: volunteer_hours__c - fields: - GW_Volunteers__Comments__c: comments - GW_Volunteers__End_Date__c: end_date - GW_Volunteers__Hours_Worked__c: hours_worked - Id: sf_id - GW_Volunteers__Number_of_Volunteers__c: number_of_volunteers - GW_Volunteers__Planned_Start_Date_Time__c: planned_start_date_time - GW_Volunteers__Start_Date__c: start_date - GW_Volunteers__Status__c: status - GW_Volunteers__System_Note__c: system_note - lookups: - GW_Volunteers__Contact__c: - table: contact - key_field: contact__c - GW_Volunteers__Volunteer_Job__c: - table: volunteer_job__c - key_field: volunteer_job__c - GW_Volunteers__Volunteer_Recurrence_Schedule__c: - table: volunteer_recurrence_schedule__c - key_field: volunteer_recurrence_schedule__c - GW_Volunteers__Volunteer_Shift__c: - table: volunteer_shift__c - key_field: volunteer_shift__c diff --git a/datasets/regression/test_data.sql b/datasets/regression/test_data.sql deleted file mode 100644 index b6f6bac..0000000 --- a/datasets/regression/test_data.sql +++ /dev/null @@ -1,164 +0,0 @@ -BEGIN TRANSACTION; -CREATE TABLE account ( - sf_id VARCHAR(255) NOT NULL, - name VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "account" VALUES('0011k00000QIzaAAAT','Al Jamil Household'); -INSERT INTO "account" VALUES('0011k00000QIza5AAD','Mendoza Household'); -INSERT INTO "account" VALUES('0011k00000QJ0iNAAT','Anagonye Household'); -INSERT INTO "account" VALUES('0011k00000QIzZvAAL','Shellstrop Household'); -INSERT INTO "account" VALUES('0011k00000QIzaFAAT','Scoop Household'); -CREATE TABLE campaign ( - sf_id VARCHAR(255) NOT NULL, - isactive VARCHAR(255), - name VARCHAR(255), - status VARCHAR(255), - startdate VARCHAR(255), - enddate VARCHAR(255), - volunteer_website_time_zone VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "campaign" VALUES('7011k000000gzcYAAQ','true','Dog Care at The Good Place for Pets','Planned','','',''); -INSERT INTO "campaign" VALUES('7011k000000gz5hAAA','true','GC Product Webinar - Jan 7, 2002','Completed','2018-12-25','2018-12-25',''); -INSERT INTO "campaign" VALUES('7011k000000gz5iAAA','true','User Conference - Jun 17-19, 2002','Planned','2019-06-04','2019-06-06',''); -INSERT INTO "campaign" VALUES('7011k000000gz5jAAA','true','DM Campaign to Top Customers - Nov 12-23, 2001','Completed','2018-10-30','2018-11-10',''); -INSERT INTO "campaign" VALUES('7011k000000gz5kAAA','true','International Electrical Engineers Association Trade Show - Mar 4-5, 2002','Planned','2019-02-19','2019-02-20',''); -INSERT INTO "campaign" VALUES('7011k000000gzcnAAA','true','2nd Annual Chidi''s Chili Cookoff','Planned','2019-08-24','2019-08-24',''); -CREATE TABLE contact ( - sf_id VARCHAR(255) NOT NULL, - firstname VARCHAR(255), - lastname VARCHAR(255), - email VARCHAR(255), - volunteer_auto_reminder_email_opt_out VARCHAR(255), - volunteer_availability VARCHAR(255), - volunteer_last_web_signup_date VARCHAR(255), - volunteer_manager_notes VARCHAR(255), - volunteer_notes VARCHAR(255), - volunteer_organization VARCHAR(255), - volunteer_skills VARCHAR(255), - volunteer_status VARCHAR(255), - account_id VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "contact" VALUES('0031k00000MPhYGAA1','Camila','Al Jamil','kselvocki+camila@salesforce.com','false','','','','','','','Active','0011k00000QIzaAAAT'); -INSERT INTO "contact" VALUES('0031k00000MPhY1AAL','Eleanor','Shellstrop','kselvocki+eleanor@salesforce.com','false','','','','','','','Active','0011k00000QIzZvAAL'); -INSERT INTO "contact" VALUES('0031k00000MPhY6AAL','Jason','Mendoza','kselvocki+jason@salesforce.com','false','','','','','','','Active','0011k00000QIza5AAD'); -INSERT INTO "contact" VALUES('0031k00000MPhYBAA1','Tahani','Al Jamil','kselvocki+tahani@salesforce.com','false','','','','','','Fundraising;Event Planning','Active','0011k00000QIzaAAAT'); -INSERT INTO "contact" VALUES('0031k00000MPixMAAT','Chidi','Anagonye','kselvocki+chidi@salesforce.com','false','','','','','','','Active','0011k00000QJ0iNAAT'); -INSERT INTO "contact" VALUES('0031k00000MPhYQAA1','Janet','Scoop','kselvocki+janet@salesforce.com','false','','','','','','','Active','0011k00000QIzaFAAT'); -INSERT INTO "contact" VALUES('0031k00000MPhYLAA1','Michael','Scoop','kselvocki+michael@salesforce.com','false','','','','','','','Active','0011k00000QIzaFAAT'); -CREATE TABLE job_recurrence_schedule__c ( - sf_id VARCHAR(255) NOT NULL, - days_of_week VARCHAR(255), - description VARCHAR(255), - desired_number_of_volunteers VARCHAR(255), - duration VARCHAR(255), - schedule_end_date VARCHAR(255), - schedule_start_date_time VARCHAR(255), - weekly_occurrence VARCHAR(255), - volunteer_job__c VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "job_recurrence_schedule__c" VALUES('a001k000002B1jLAAS','Monday;Tuesday;Wednesday;Thursday;Friday','Take the puppers of The Good Place for Pets for a midday walk!','5.0','1.0','2020-07-31','2019-07-17T19:00:00.000Z','Every','a031k000001OMGqAAO'); -INSERT INTO "job_recurrence_schedule__c" VALUES('a001k000002B1jQAAS','Saturday','Keep the kennels of The Good Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-08-01','2019-07-06T15:00:00.000Z','Every','a031k000001OMGrAAO'); -CREATE TABLE volunteer_hours__c ( - sf_id VARCHAR(255) NOT NULL, - comments VARCHAR(255), - end_date VARCHAR(255), - hours_worked VARCHAR(255), - number_of_volunteers VARCHAR(255), - planned_start_date_time VARCHAR(255), - start_date VARCHAR(255), - status VARCHAR(255), - system_note VARCHAR(255), - contact__c VARCHAR(255), - volunteer_job__c VARCHAR(255), - volunteer_recurrence_schedule__c VARCHAR(255), - volunteer_shift__c VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "volunteer_hours__c" VALUES('a021k000002YotkAAC','','2019-08-24','2.0','1.0','2019-08-24T17:00:00.000Z','2019-08-24','Confirmed','','0031k00000MPhYQAA1','a031k000001OMHFAA4','','a051k000001HaSVAA0'); -INSERT INTO "volunteer_hours__c" VALUES('a021k000002YotlAAC','','2019-08-24','2.0','1.0','2019-08-24T17:00:00.000Z','2019-08-24','Confirmed','','0031k00000MPhY6AAL','a031k000001OMHFAA4','','a051k000001HaSVAA0'); -INSERT INTO "volunteer_hours__c" VALUES('a021k000002YotmAAC','','2019-08-24','2.0','1.0','2019-08-24T17:00:00.000Z','2019-08-24','Confirmed','','0031k00000MPhYLAA1','a031k000001OMHFAA4','','a051k000001HaSVAA0'); -INSERT INTO "volunteer_hours__c" VALUES('a021k000002YostAAC','','2019-08-24','5.0','1.0','2019-08-24T19:00:00.000Z','2019-08-24','Confirmed','','0031k00000MPixMAAT','a031k000001OMHAAA4','','a051k000001HaSkAAK'); -CREATE TABLE volunteer_job__c ( - sf_id VARCHAR(255) NOT NULL, - description VARCHAR(255), - display_on_website VARCHAR(255), - external_signup_url VARCHAR(255), - inactive VARCHAR(255), - location_city VARCHAR(255), - location_information VARCHAR(255), - location_street VARCHAR(255), - location_zip_postal_code VARCHAR(255), - location VARCHAR(255), - name VARCHAR(255), - ongoing VARCHAR(255), - skills_needed VARCHAR(255), - volunteer_website_time_zone VARCHAR(255), - campaign__c VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMH5AAO','

Spread the word about the 2nd Annual Chidi's Chili Cookoff, and help us make this year's event our biggest and best yet!

','true','','false','','','','','','Distribute Flyers for the Chili Cookoff','false','Marketing','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMHAAA4','

Feed all the animal lovers at our 2nd Annual Chidi's Chili Cookoff, and get the chance to win chili glory!

','false','','false','','','','','','Chili Cooks','false','','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMHFAA4','

Help prepare for our chili cookoff and get free entry to the day!

','true','','false','','','','','','Setup Crew','false','Manual Labor','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMHPAA4','

Who made the best chili of the 2nd Annual Chidi's Chili Cookoff? You get to help us decide as one of our esteemed judges!

','false','','false','','','','','','Chili Judges','false','','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMHKAA4','

Enjoy fun treats while helping us break down after a rockin' chili cookoff!

','true','','false','','','','','','Cleanup Crew','false','Manual Labor','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGqAAO','

Take the puppers of The Good Place for Pets for a midday walk!

','true','','false','','','','','','Lunchtime Dog Walking','false','','','7011k000000gzcYAAQ'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGrAAO','

Keep the kennels of The Good Place for Pets clean and cozy for our adoptable dogs!

','true','','false','','','','','','Weekly Kennel Cleaning','false','Manual Labor','','7011k000000gzcYAAQ'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGsAAO','

Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.

','false','','false','','','','','','Puppy Playtime','false','','','7011k000000gzcYAAQ'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGtAAO','','true','','false','','','','','','Recruit Chili Cooks','false','','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGuAAO','','true','','false','','','','','','Quality Entertainment','false','','','7011k000000gzcnAAA'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGvAAO','','true','','false','','','','','','Foster Care for Small Dogs','false','','','7011k000000gzcYAAQ'); -INSERT INTO "volunteer_job__c" VALUES('a031k000001OMGwAAO','','true','','false','','','','','','Foster Care for Large Dogs','false','','','7011k000000gzcYAAQ'); - -CREATE TABLE volunteer_recurrence_schedule__c ( - sf_id VARCHAR(255) NOT NULL, - comments VARCHAR(255), - days_of_week VARCHAR(255), - duration VARCHAR(255), - number_of_volunteers VARCHAR(255), - schedule_end_date VARCHAR(255), - schedule_start_date_time VARCHAR(255), - volunteer_hours_status VARCHAR(255), - weekly_occurrence VARCHAR(255), - contact__c VARCHAR(255), - volunteer_job__c VARCHAR(255), - PRIMARY KEY (sf_id) -); -CREATE TABLE volunteer_shift__c ( - sf_id VARCHAR(255) NOT NULL, - description VARCHAR(255), - desired_number_of_volunteers VARCHAR(255), - duration VARCHAR(255), - start_date_time VARCHAR(255), - system_note VARCHAR(255), - total_volunteers VARCHAR(255), - job_recurrence_schedule__c VARCHAR(255), - volunteer_job__c VARCHAR(255), - PRIMARY KEY (sf_id) -); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxyAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-03-15T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxoAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-12-15T18:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSfAAK','','5.0','2.0','2019-08-24T23:00:00.000Z','','','','a031k000001OMHPAA4'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSkAAK','','30.0','5.0','2019-08-24T19:00:00.000Z','','1.0','','a031k000001OMHAAA4'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxjAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-10-20T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSBAA0','','4.0','1.0','2019-07-27T22:00:00.000Z','','','','a031k000001OMH5AAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaS6AAK','','4.0','1.0','2019-07-20T22:00:00.000Z','','','','a031k000001OMH5AAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZy8AAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-05-17T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSLAA0','','4.0','1.0','2019-08-03T22:00:00.000Z','','','','a031k000001OMH5AAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSMAA0','','4.0','1.0','2019-08-10T22:00:00.000Z','','','','a031k000001OMH5AAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxtAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-02-16T18:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZy3AAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-04-19T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxeAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-09-15T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxfAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-11-17T18:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxZAAW','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-08-18T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxaAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-01-19T18:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxbAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-06-21T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZxcAAG','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-07-19T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSVAA0','','10.0','2.0','2019-08-24T17:00:00.000Z','','','','a031k000001OMHFAA4'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HZsCAAW','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-07-21T17:00:00.000Z','','','','a031k000001OMGsAAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSQAA0','','4.0','1.0','2019-08-17T22:00:00.000Z','','','','a031k000001OMH5AAO'); -INSERT INTO "volunteer_shift__c" VALUES('a051k000001HaSaAAK','','12.0','2.0','2019-08-25T01:00:00.000Z','','','','a031k000001OMHKAA4'); -COMMIT; diff --git a/datasets/sample.sql b/datasets/sample.sql new file mode 100644 index 0000000..61a40c7 --- /dev/null +++ b/datasets/sample.sql @@ -0,0 +1,656 @@ +BEGIN TRANSACTION; +CREATE TABLE "Account" ( + "Id" VARCHAR(255) NOT NULL, + "Name" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Account" VALUES('0015600000Sl5b1AAB','Harrison Household'); +INSERT INTO "Account" VALUES('0015600000Sl5b2AAB','van Hoop Household'); +INSERT INTO "Account" VALUES('0015600000Sl5eqAAB','Regenstein Household'); +INSERT INTO "Account" VALUES('0015600000Sl5erAAB','Mendoza Household'); +INSERT INTO "Account" VALUES('0015600000Sl5esAAB','Fernandez Household'); +CREATE TABLE "Campaign" ( + "Id" VARCHAR(255) NOT NULL, + "IsActive" VARCHAR(255), + "Name" VARCHAR(255), + "Status" VARCHAR(255), + "StartDate" VARCHAR(255), + "EndDate" VARCHAR(255), + "Volunteer_Website_Time_Zone__c" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Campaign" VALUES('70156000002hjt6AAA','true','Dog Care at The Place for Pets','Planned','','',''); +INSERT INTO "Campaign" VALUES('70156000002hjt7AAA','true','GC Product Webinar - Jan 7, 2002','Completed','2018-12-25','2018-12-25',''); +INSERT INTO "Campaign" VALUES('70156000002hjt8AAA','true','User Conference - Jun 17-19, 2002','Planned','2019-06-04','2019-06-06',''); +INSERT INTO "Campaign" VALUES('70156000002hjt9AAA','true','DM Campaign to Top Customers - Nov 12-23, 2001','Completed','2018-10-30','2018-11-10',''); +INSERT INTO "Campaign" VALUES('70156000002hjtAAAQ','true','International Electrical Engineers Association Trade Show - Mar 4-5, 2002','Planned','2019-02-19','2019-02-20',''); +INSERT INTO "Campaign" VALUES('70156000002hjtBAAQ','true','2nd Annual Aziz''s Chili Cookoff','Planned','2020-08-29','2020-08-29',''); +CREATE TABLE "Contact" ( + "Id" VARCHAR(255) NOT NULL, + "FirstName" VARCHAR(255), + "LastName" VARCHAR(255), + "Email" VARCHAR(255), + "Volunteer_Auto_Reminder_Email_Opt_Out__c" VARCHAR(255), + "Volunteer_Availability__c" VARCHAR(255), + "Volunteer_Last_Web_Signup_Date__c" VARCHAR(255), + "Volunteer_Manager_Notes__c" VARCHAR(255), + "Volunteer_Notes__c" VARCHAR(255), + "Volunteer_Organization__c" VARCHAR(255), + "Volunteer_Skills__c" VARCHAR(255), + "Volunteer_Status__c" VARCHAR(255), + "AccountId" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Contact" VALUES('0035600000TclMxAAJ','Elizabeth','Mendoza','elizabeth@example.com','false','','','','','','','Active','0015600000Sl5erAAB'); +INSERT INTO "Contact" VALUES('0035600000TclMyAAJ','Jacob','van Hoop','jacob@example.com','false','','','','','','','Active','0015600000Sl5b2AAB'); +INSERT INTO "Contact" VALUES('0035600000TclMzAAJ','Catherine','Harrison','catherine@example.com','false','','','','','','','Active','0015600000Sl5b1AAB'); +INSERT INTO "Contact" VALUES('0035600000TclN0AAJ','Tamara','Harrison','tamara@example.com','false','','','','','','Fundraising;Event Planning','Active','0015600000Sl5b1AAB'); +INSERT INTO "Contact" VALUES('0035600000TclN1AAJ','Hiroko','Fernandez','hiroko@example.com','false','','','','','','','Active','0015600000Sl5esAAB'); +INSERT INTO "Contact" VALUES('0035600000TclN2AAJ','Michael','Fernandez','michael@example.com','false','','','','','','','Active','0015600000Sl5esAAB'); +INSERT INTO "Contact" VALUES('0035600000TclN3AAJ','Aziz','Regenstein','aziz@example.com','false','','','','','','','Active','0015600000Sl5eqAAB'); +CREATE TABLE "Job_Recurrence_Schedule__c" ( + "Id" VARCHAR(255) NOT NULL, + "Days_of_Week__c" VARCHAR(255), + "Description__c" VARCHAR(255), + "Desired_Number_of_Volunteers__c" VARCHAR(255), + "Duration__c" VARCHAR(255), + "Schedule_End_Date__c" VARCHAR(255), + "Schedule_Start_Date_Time__c" VARCHAR(255), + "Weekly_Occurrence__c" VARCHAR(255), + "Volunteer_Job__c" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Job_Recurrence_Schedule__c" VALUES('a0056000004bMNSAA2','Monday;Tuesday;Wednesday;Thursday;Friday','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-31','2020-01-06T19:00:00.000Z','Every','a0356000003Krp9AAC'); +INSERT INTO "Job_Recurrence_Schedule__c" VALUES('a0056000004bMNTAA2','Saturday','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-08-01','2020-01-04T15:00:00.000Z','Every','a0356000003KrpAAAS'); +CREATE TABLE "Volunteer_Hours__c" ( + "Id" VARCHAR(255) NOT NULL, + "Comments__c" VARCHAR(255), + "End_Date__c" VARCHAR(255), + "Hours_Worked__c" VARCHAR(255), + "Number_of_Volunteers__c" VARCHAR(255), + "Planned_Start_Date_Time__c" VARCHAR(255), + "Start_Date__c" VARCHAR(255), + "Status__c" VARCHAR(255), + "System_Note__c" VARCHAR(255), + "Contact__c" VARCHAR(255), + "Volunteer_Job__c" VARCHAR(255), + "Volunteer_Recurrence_Schedule__c" VARCHAR(255), + "Volunteer_Shift__c" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Volunteer_Hours__c" VALUES('a0256000004UmHKAA0','','2019-08-24','2.0','1.0','2019-08-24T17:00:00.000Z','2019-08-24','Confirmed','','0035600000TclMyAAJ','a0356000003KrpGAAS','','a055600000A06dDAAR'); +INSERT INTO "Volunteer_Hours__c" VALUES('a0256000004UmHLAA0','','2019-08-24','2.0','1.0','2019-08-24T17:00:00.000Z','2019-08-24','Confirmed','','0035600000TclN2AAJ','a0356000003KrpGAAS','','a055600000A06dDAAR'); +INSERT INTO "Volunteer_Hours__c" VALUES('a0256000004UmHMAA0','','2019-08-24','2.0','1.0','2019-08-24T17:00:00.000Z','2019-08-24','Confirmed','','0035600000TclN1AAJ','a0356000003KrpGAAS','','a055600000A06dDAAR'); +INSERT INTO "Volunteer_Hours__c" VALUES('a0256000004UmHNAA0','','2019-08-24','5.0','1.0','2019-08-24T19:00:00.000Z','2019-08-24','Confirmed','','0035600000TclN3AAJ','a0356000003KrpFAAS','','a055600000A06dCAAR'); +CREATE TABLE "Volunteer_Job__c" ( + "Id" VARCHAR(255) NOT NULL, + "Description__c" VARCHAR(255), + "Display_on_Website__c" VARCHAR(255), + "External_Signup_Url__c" VARCHAR(255), + "Inactive__c" VARCHAR(255), + "Location_City__c" VARCHAR(255), + "Location_Information__c" VARCHAR(255), + "Location_Street__c" VARCHAR(255), + "Location_Zip_Postal_Code__c" VARCHAR(255), + "Location__c" VARCHAR(255), + "Name" VARCHAR(255), + "Ongoing__c" VARCHAR(255), + "Skills_Needed__c" VARCHAR(255), + "Volunteer_Website_Time_Zone__c" VARCHAR(255), + "Campaign__c" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003Krp9AAC','

Take the puppers of The Place for Pets for a midday walk!

','true','','false','','','','','','Lunchtime Dog Walking','false','','','70156000002hjt6AAA'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpAAAS','

Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!

','true','','false','','','','','','Weekly Kennel Cleaning','false','Manual Labor','','70156000002hjt6AAA'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpBAAS','

Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.

','false','','false','','','','','','Puppy Playtime','false','','','70156000002hjt6AAA'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpCAAS','','true','','false','','','','','','Foster Care for Small Dogs','false','','','70156000002hjt6AAA'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpDAAS','','true','','false','','','','','','Foster Care for Large Dogs','false','','','70156000002hjt6AAA'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpEAAS','

Spread the word about the 2nd Annual Aziz's Chili Cookoff, and help us make this year's event our biggest and best yet!

','true','','false','','','','','','Distribute Flyers for the Chili Cookoff','false','Marketing','','70156000002hjtBAAQ'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpFAAS','

Feed all the animal lovers at our 2nd Annual Aziz's Chili Cookoff, and get the chance to win chili glory!

','false','','false','','','','','','Chili Cooks','false','','','70156000002hjtBAAQ'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpGAAS','

Help prepare for our chili cookoff and get free entry to the day!

','true','','false','','','','','','Setup Crew','false','Manual Labor','','70156000002hjtBAAQ'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpHAAS','

Who made the best chili of the 2nd Annual Aziz's Chili Cookoff? You get to help us decide as one of our esteemed judges!

','false','','false','','','','','','Chili Judges','false','','','70156000002hjtBAAQ'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpIAAS','

Enjoy fun treats while helping us break down after a rockin' chili cookoff!

','true','','false','','','','','','Cleanup Crew','false','Manual Labor','','70156000002hjtBAAQ'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpJAAS','','true','','false','','','','','','Recruit Chili Cooks','false','','','70156000002hjtBAAQ'); +INSERT INTO "Volunteer_Job__c" VALUES('a0356000003KrpKAAS','','true','','false','','','','','','Quality Entertainment','false','','','70156000002hjtBAAQ'); +CREATE TABLE "Volunteer_Recurrence_Schedule__c" ( + "Id" VARCHAR(255) NOT NULL, + "Comments__c" VARCHAR(255), + "Days_of_Week__c" VARCHAR(255), + "Duration__c" VARCHAR(255), + "Number_of_Volunteers__c" VARCHAR(255), + "Schedule_End_Date__c" VARCHAR(255), + "Schedule_Start_Date_Time__c" VARCHAR(255), + "Volunteer_Hours_Status__c" VARCHAR(255), + "Weekly_Occurrence__c" VARCHAR(255), + "Contact__c" VARCHAR(255), + "Volunteer_Job__c" VARCHAR(255), + PRIMARY KEY ("Id") +); +CREATE TABLE "Volunteer_Shift__c" ( + "Id" VARCHAR(255) NOT NULL, + "Description__c" VARCHAR(255), + "Desired_Number_of_Volunteers__c" VARCHAR(255), + "Duration__c" VARCHAR(255), + "Start_Date_Time__c" VARCHAR(255), + "System_Note__c" VARCHAR(255), + "Total_Volunteers__c" VARCHAR(255), + "Job_Recurrence_Schedule__c" VARCHAR(255), + "Volunteer_Job__c" VARCHAR(255), + PRIMARY KEY ("Id") +); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WgAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WhAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-04T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WiAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WjAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WkAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WlAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WmAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WnAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WoAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WpAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WqAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WrAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WsAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WtAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WuAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WvAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WwAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WxAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WyAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WzAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X0AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X1AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bgAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-02-22T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bhAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-02-29T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06biAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-03-07T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bjAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-03-14T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bkAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-03-21T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06blAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-03-28T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bmAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-04-04T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bnAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-04-11T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06boAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-04-18T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bpAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-04-25T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bqAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-05-02T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06brAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-05-09T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bsAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-05-16T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06btAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-05-23T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06buAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-05-30T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bvAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-06-06T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bwAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-06-13T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bxAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-06-20T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06byAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-06-27T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bzAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-07-04T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c0AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-07-11T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X2AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X3AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X4AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X5AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X6AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X7AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X8AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06X9AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XAAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c1AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-07-18T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c2AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-07-25T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c3AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-08-01T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c4AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-08-08T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c5AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-08-15T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c6AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-08-22T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c7AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-08-29T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c8AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-09-05T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06c9AAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-09-12T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cAAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-09-19T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cBAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-09-26T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cCAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-10-03T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cDAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-10-10T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cEAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-10-17T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cFAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-10-24T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cGAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-10-31T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cHAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-11-07T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cIAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-11-14T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cJAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-11-21T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cKAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-11-28T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cLAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-12-05T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cMAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-12-12T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cNAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-12-19T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cOAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-12-26T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cPAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-01-02T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cQAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-01-09T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cRAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-01-16T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cSAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-01-23T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cTAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-01-30T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cUAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-02-06T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cVAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-02-13T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cWAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-02-20T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cXAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-02-27T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cYAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-03-06T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cZAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-03-13T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06caAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-03-20T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cbAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-03-27T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ccAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-04-03T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cdAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-04-10T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ceAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-04-17T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cfAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-04-24T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cgAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-05-01T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XBAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XCAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XDAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XEAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XFAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XGAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XHAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XIAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XJAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XKAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06chAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-05-08T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ciAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-05-15T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cjAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-05-22T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ckAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-05-29T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06clAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-06-05T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cmAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-06-12T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cnAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-06-19T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06coAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-06-26T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cpAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-07-03T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cqAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-07-10T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06crAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-07-17T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06csAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-07-24T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ctAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2021-07-31T14:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XLAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XMAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-07-31T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XNAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XOAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-04T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XPAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XQAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XRAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XSAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XTAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XUAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XVAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XWAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XXAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XYAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XZAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XaAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XbAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XcAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XdAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XeAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XfAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XgAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XhAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-08-31T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XiAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XjAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XkAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XlAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-04T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XmAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XnAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XoAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XpAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XqAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XrAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XsAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XtAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XuAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XvAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XwAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XxAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XyAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06XzAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y0AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y1AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y2AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y3AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-09-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y4AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y5AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y6AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y7AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y8AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Y9AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YAAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YBAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YCAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YDAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YEAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YFAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YGAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YHAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YIAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YJAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YKAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YLAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YMAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YNAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YOAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YPAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-10-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YQAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-02T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YRAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-03T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YSAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YTAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-05T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YUAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-06T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YVAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-09T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YWAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-10T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YXAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-11T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YYAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-12T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YZAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-13T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YaAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-16T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YbAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-17T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YcAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-18T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YdAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-19T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YeAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-20T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YfAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-23T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YgAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-24T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YhAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-25T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YiAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-26T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YjAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-27T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YkAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-11-30T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YlAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-01T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YmAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-02T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YnAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-03T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YoAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YpAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-07T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YqAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-08T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YrAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-09T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YsAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-10T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YtAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-11T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YuAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-14T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YvAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-15T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YwAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-16T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YxAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-17T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YyAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-18T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06YzAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-21T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z0AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-22T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z1AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-23T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z2AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-24T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z3AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-25T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z4AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-28T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z5AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-29T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z6AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-30T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z7AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-12-31T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z8AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-01T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06Z9AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZAAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-05T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZBAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-06T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZCAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-07T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZDAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-08T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZEAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-11T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZFAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-12T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZGAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-13T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZHAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-14T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZIAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-15T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZJAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-18T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZKAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-19T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZLAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-20T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZMAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-21T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZNAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-22T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZOAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-25T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZPAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-26T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZQAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-27T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZRAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-28T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZSAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-01-29T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZTAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-01T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZUAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-02T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZVAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-03T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZWAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZXAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-05T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZYAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-08T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZZAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-09T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZaAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-10T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZbAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-11T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZcAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-12T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZdAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-15T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZeAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-16T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZfAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-17T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZgAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-18T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZhAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-19T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZiAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-22T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZjAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-23T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZkAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-24T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZlAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-25T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZmAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-02-26T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZnAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-01T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZoAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-02T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZpAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-03T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZqAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06UxAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-06T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06UyAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-07T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06UzAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-08T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V0AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-09T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V1AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-10T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V2AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-13T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V3AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-14T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V4AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-15T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V5AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-16T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V6AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-17T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V7AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-20T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V8AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-21T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06V9AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-22T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VAAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-23T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VBAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-24T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VCAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-27T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VDAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-28T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VEAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-29T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VFAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-30T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZrAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-05T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZsAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-08T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZtAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-09T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZuAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-10T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZvAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-11T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZwAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-12T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZxAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZyAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ZzAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a0AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a1AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a2AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a3AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a4AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a5AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a6AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a7AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a8AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06a9AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-03-31T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aAAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aBAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aCAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aDAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VGAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-01-31T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VHAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-03T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VIAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VJAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-05T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VKAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-06T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VLAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-07T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VMAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-10T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VNAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-11T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VOAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-12T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VPAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-13T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VQAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-14T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VRAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-17T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VSAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-18T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VTAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-19T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VUAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-20T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VVAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-21T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VWAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-24T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VXAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-25T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VYAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-26T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VZAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-27T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VaAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-02-28T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VbAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-02T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aEAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aFAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aGAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aHAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aIAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aJAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aKAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aLAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aMAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aNAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aOAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aPAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aQAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aRAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aSAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aTAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aUAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aVAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-04-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aWAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aXAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-04T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aYAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aZAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VfAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-06T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VgAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VhAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ViAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VjAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VkAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VlAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VmAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VnAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VoAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VpAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VqAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VrAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VsAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VtAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VcAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-03T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VdAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-04T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VeAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-05T19:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VuAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VvAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VwAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-03-31T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VxAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aaAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06abAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06acAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06adAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aeAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06afAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06agAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ahAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aiAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ajAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06akAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06alAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06amAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06anAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aoAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06apAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06aqAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-05-31T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06arAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06asAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06atAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06auAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-04T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06avAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06awAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VyAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06VzAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-03T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W0AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W1AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W2AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W3AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W4AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W5AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W6AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W7AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W8AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06W9AAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WAAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WBAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WCAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WDAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WEAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WFAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WGAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WHAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WIAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-04-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WJAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06axAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06ayAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-10T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06azAAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b0AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b1AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b2AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b3AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-17T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b4AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b5AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b6AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b7AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b8AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-24T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06b9AAB','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bAAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bBAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bCAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-06-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bDAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bEAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bFAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bGAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bHAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bIAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bJAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-09T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WKAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-04T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WLAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-05T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WMAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-06T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WNAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-07T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WOAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-08T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WPAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-11T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WQAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WRAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WSAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WTAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WUAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-18T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WVAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WWAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WXAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WYAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WZAAZ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-25T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WaAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WbAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WcAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WdAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-05-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WeAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-01T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06WfAAJ','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2020-06-02T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bKAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-12T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bLAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-13T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bMAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-14T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bNAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-15T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bOAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-16T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bPAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-19T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bQAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-20T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bRAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-21T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bSAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-22T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bTAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-23T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bUAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-26T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bVAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-27T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bWAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-28T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bXAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-29T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bYAAR','Take the puppers of The Place for Pets for a midday walk!','5.0','1.0','2021-07-30T18:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00000.','','a0056000004bMNSAA2','a0356000003Krp9AAC'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bZAAR','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-01-04T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06baAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-01-11T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bbAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-01-18T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bcAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-01-25T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bdAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-02-01T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06beAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-02-08T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06bfAAB','Keep the kennels of The Place for Pets clean and cozy for our adoptable dogs!','10.0','2.0','2020-02-15T15:00:00.000Z','Auto-created for Job Recurrence Schedule: JRS-00001.','','a0056000004bMNTAA2','a0356000003KrpAAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d9AAB','','4.0','1.0','2019-08-03T22:00:00.000Z','','','','a0356000003KrpEAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06dAAAR','','4.0','1.0','2019-08-10T22:00:00.000Z','','','','a0356000003KrpEAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06dBAAR','','4.0','1.0','2019-08-17T22:00:00.000Z','','','','a0356000003KrpEAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06dCAAR','','30.0','5.0','2019-08-24T19:00:00.000Z','','2.0','','a0356000003KrpFAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06dDAAR','','10.0','2.0','2019-08-24T17:00:00.000Z','','3.0','','a0356000003KrpGAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06dEAAR','','12.0','2.0','2019-08-25T01:00:00.000Z','','','','a0356000003KrpIAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06dFAAR','','5.0','2.0','2019-08-24T23:00:00.000Z','','','','a0356000003KrpHAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cuAAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-03-15T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cvAAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-12-15T18:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cwAAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-10-20T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cxAAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-05-17T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d2AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-08-18T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d3AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-01-19T18:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d4AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-06-21T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06cyAAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-02-16T18:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06czAAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-04-19T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d0AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-09-15T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d1AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-11-17T18:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d5AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2020-07-19T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d6AAB','Help us get our puppies ready for their new forever homes with socialization, snuggles, and positive reinforcement.','3.0','1.0','2019-07-21T17:00:00.000Z','','','','a0356000003KrpBAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d7AAB','','4.0','1.0','2019-07-27T22:00:00.000Z','','','','a0356000003KrpEAAS'); +INSERT INTO "Volunteer_Shift__c" VALUES('a055600000A06d8AAB','','4.0','1.0','2019-07-20T22:00:00.000Z','','','','a0356000003KrpEAAS'); +COMMIT; diff --git a/documentation/automation.md b/documentation/automation.md new file mode 100644 index 0000000..70c0d42 --- /dev/null +++ b/documentation/automation.md @@ -0,0 +1,50 @@ +# Volunteers for Salesforce Automation Inventory + +## Key Workflows + +This section describes the key workflows for developers, QEs, and technical +writers. + +| Workflow | Flow | Org Type | Managed | Namespace | NPSP | +| -------------------------- | -------------------- | -------------- | ------- | --------------- | ---- | +| Development | `dev_org` | dev | | | | +| Development (Namespaced) | `dev_org_namespaced` | dev_namespaced | | `GW_Volunteers` | | +| QA | `qa_org` | dev | | | | +| Regression | `regression_org` | regression | ✔ | | | +| Customer Install | `customer_org` | beta | ✔ | | | +| Customer Install with NPSP | `customer_org_npsp` | beta | ✔ | | ✔ | + +## Utility Tasks and Flows + +| Name | Type | Purpose | +| -------------- | ---- | ---------------------------------------------------------------- | +| `install_npsp` | Flow | Install NPSP with relationship configuration and trial metadata. | + + +## Unpackaged Metadata + +Unpackaged directory structure: + +``` +unpackaged +└── config + ├── delete + ├── dev + ├── npsp_v4s_layouts + ├── qa + └── v4s_only_layouts +``` + +Each directory is used as follows: + +| Directory | Purpose | Deploy task | Retrieve task | +| ------------------- | --------------------------------------------- | ------------------------------ | ------------- | +| `dev/` | Profiles, Permission Sets, and Sites | `deploy_dev_config` | | +| `qa/` | Contact layout for QA | `deploy_qa_config` | | +| `delete/` | remove stock layouts | `deploy_delete_config` | | +| `npsp_v4s_layouts/` | Page layouts for an org with NPSP and V4S | `deploy_npsp_v4s_page_layouts` | | +| `v4s_only_layouts/` | Page layouts for an org with V4S but not NPSP | `deploy_v4s_only_page_layouts` | | + +# Test Data + +V4S includes a comprehensive data set intended for QA and regression testing. Test data is automatically deployed in dev, QA, and regression orgs. diff --git a/metadeploy/labels_en.json b/metadeploy/labels_en.json new file mode 100644 index 0000000..7eb621a --- /dev/null +++ b/metadeploy/labels_en.json @@ -0,0 +1,54 @@ +{ + "product": { + "title": { + "message": "Volunteers for Salesforce (V4S)", + "description": "name of product" + }, + "short_description": { + "message": "Volunteer management for your organization.", + "description": "tagline of product" + }, + "description": { + "message": "Volunteer management for your organization.\r\n\r\nVolunteers for Salesforce helps manage your volunteer events and people. It manages volunteers, jobs, shifts, and hours, allowing you to track and report on all of the data as well as allow public signups via Sites pages on your website.\r\n\r\nOnce you've installed Volunteers for Salesforce, check out these great resources:\r\n\r\n* [Manage Volunteers for Nonprofits on Trailhead](https://trailhead.salesforce.com/content/learn/trails/nonprofit_volunteer)\r\n\r\n* [Salesforce.org Hub](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHOwSAM?tab=discussion&sort=LAST_MODIFIED_DATE_DESC)\r\n\r\n* [All Volunteers for Salesforce Documentation](https://help.salesforce.com/s/articleView?id=sfdo.Volunteers_for_Salesforce.htm&type=5)", + "description": "shown on product detail page (markdown)" + }, + "click_through_agreement": { + "message": "Copyright (c) 2017, Salesforce.org \r\nAll rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are met:\r\n\r\n* Redistributions of source code must retain the above copyright\r\nnotice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\nnotice, this list of conditions and the following disclaimer in the\r\ndocumentation and/or other materials provided with the distribution.\r\n* Neither the name of Salesforce.org nor the names of\r\nits contributors may be used to endorse or promote products derived\r\nfrom this software without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\r\nFOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\r\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\r\nBUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\r\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\r\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\r\nLIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\r\nANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r\nPOSSIBILITY OF SUCH DAMAGE.", + "description": "legal text shown in modal dialog" + } + }, + "plan:install": { + "title": { + "message": "Install Volunteers for Salesforce", + "description": "title of installation plan" + } + }, + "steps": { + "Default Campaign Record Type": { + "message": "Default Campaign Record Type", + "description": "title of installation step" + }, + "Install {product} {version}": { + "message": "Install {product} {version}", + "description": "title of installation step" + }, + "Install Volunteers for Salesforce Tabs": { + "message": "Install Volunteers for Salesforce Tabs", + "description": "title of installation step" + }, + "Install Page Layouts (NPSP)": { + "message": "Install Page Layouts (NPSP)", + "description": "title of installation step" + }, + "Install Page Layouts (V4S)": { + "message": "Install Page Layouts (V4S)", + "description": "title of installation step" + } + }, + "checks": { + "Please enable Chatter in your org prior to installing.": { + "message": "Please enable Chatter in your org prior to installing.", + "description": "shown if validation fails" + } + } +} diff --git a/orgs/beta.json b/orgs/beta.json index 0cc74a9..4abd5a9 100644 --- a/orgs/beta.json +++ b/orgs/beta.json @@ -4,10 +4,11 @@ "hasSampleData": "false", "features": "Sites", "settings": { - "orgPreferenceSettings": { - "chatterEnabled": true, - "disableParallelApexTesting": true, - "s1DesktopEnabled": true + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "chatterSettings": { + "enableChatter": true } } } \ No newline at end of file diff --git a/orgs/beta_prerelease.json b/orgs/beta_prerelease.json index 4064070..e22d6f2 100644 --- a/orgs/beta_prerelease.json +++ b/orgs/beta_prerelease.json @@ -5,10 +5,11 @@ "hasSampleData": "false", "features": "Sites", "settings": { - "orgPreferenceSettings": { - "chatterEnabled": true, - "disableParallelApexTesting": true, - "s1DesktopEnabled": true + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "chatterSettings": { + "enableChatter": true } } } \ No newline at end of file diff --git a/orgs/dev.json b/orgs/dev.json index dcaa44b..b2b24d1 100644 --- a/orgs/dev.json +++ b/orgs/dev.json @@ -4,12 +4,17 @@ "hasSampleData": "false", "features": "Sites", "settings": { - "orgPreferenceSettings": { - "chatterEnabled": true, - "disableParallelApexTesting": true, - "s1DesktopEnabled": true, - "translation": true, - "s1EncryptedStoragePref2": false + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "chatterSettings": { + "enableChatter": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + }, + "languageSettings": { + "enableTranslationWorkbench": true } } } \ No newline at end of file diff --git a/orgs/feature.json b/orgs/feature.json index 8348df2..59db6ce 100644 --- a/orgs/feature.json +++ b/orgs/feature.json @@ -4,10 +4,11 @@ "hasSampleData": "false", "features": "Sites", "settings": { - "orgPreferenceSettings": { - "chatterEnabled": true, - "disableParallelApexTesting": true, - "s1DesktopEnabled": true + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "chatterSettings": { + "enableChatter": true } } } diff --git a/orgs/prerelease.json b/orgs/prerelease.json index 755bdba..3dbd1a0 100644 --- a/orgs/prerelease.json +++ b/orgs/prerelease.json @@ -5,12 +5,17 @@ "hasSampleData": "false", "features": "Sites", "settings": { - "orgPreferenceSettings": { - "chatterEnabled": true, - "disableParallelApexTesting": true, - "s1DesktopEnabled": true, - "translation": true, - "s1EncryptedStoragePref2": false + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "chatterSettings": { + "enableChatter": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + }, + "languageSettings": { + "enableTranslationWorkbench": true } } } \ No newline at end of file diff --git a/orgs/release.json b/orgs/release.json index eb1ac09..bdbd04f 100644 --- a/orgs/release.json +++ b/orgs/release.json @@ -4,11 +4,14 @@ "hasSampleData": "false", "features": "Sites", "settings": { - "orgPreferenceSettings": { - "chatterEnabled": true, - "disableParallelApexTesting": true, - "s1DesktopEnabled": true, - "s1EncryptedStoragePref2": false + "lightningExperienceSettings": { + "enableS1DesktopEnabled": true + }, + "chatterSettings": { + "enableChatter": true + }, + "mobileSettings": { + "enableS1EncryptedStoragePref2": false } } } \ No newline at end of file diff --git a/src/classes/DatabaseDml.cls b/src/classes/DatabaseDml.cls new file mode 100644 index 0000000..5dbf972 --- /dev/null +++ b/src/classes/DatabaseDml.cls @@ -0,0 +1,81 @@ +/* + Copyright (c) 2019, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/* + This class was created for situations where we need to run in system + mode. +*/ +/* sfge-disable ApexFlsViolationRule */ +public without sharing virtual class DatabaseDml { + + protected DatabaseDml() {} + + @TestVisible private static DatabaseDml instance = null; + + public static DatabaseDml getInstance() { + if (instance == null) { + instance = new DatabaseDml(); + } + return instance; + } + + /******************************************************************************************************* + * @description Commits a list of updated SObjects + * @param List of SObjects to update + ********************************************************************************************************/ + public void updateRecords(List sObjects) { + update sObjects; + } + + /******************************************************************************************************* + * @description Commits a list of updated SObjects + * @param List of SObjects to update + * @param DML Options for handling duplicates + ********************************************************************************************************/ + public void updateRecords(List sObjects, Database.DMLOptions dmlDuplicateOptions) { + Database.update(sObjects, dmlDuplicateOptions); + } + + /******************************************************************************************************* + * @description Commits a list of SObjects + * @param List of SObjects to insert + ********************************************************************************************************/ + public void insertRecords(List sObjects) { + insert sObjects; + } + + /******************************************************************************************************* + * @description Commits a list of SObjects + * @param List of SObjects to insert + * @param DML Options for handling duplicates + ********************************************************************************************************/ + public void insertRecords(List sObjects, Database.DMLOptions dmlDuplicateOptions) { + Database.insert(sObjects, dmlDuplicateOptions); + } +} \ No newline at end of file diff --git a/src/classes/VOL_SharedCodeAPI25.cls-meta.xml b/src/classes/DatabaseDml.cls-meta.xml similarity index 80% rename from src/classes/VOL_SharedCodeAPI25.cls-meta.xml rename to src/classes/DatabaseDml.cls-meta.xml index 307ce73..91b23b8 100644 --- a/src/classes/VOL_SharedCodeAPI25.cls-meta.xml +++ b/src/classes/DatabaseDml.cls-meta.xml @@ -1,5 +1,5 @@ - 25.0 + 46.0 Active diff --git a/src/classes/DatabaseDml_TEST.cls b/src/classes/DatabaseDml_TEST.cls new file mode 100644 index 0000000..5f81edc --- /dev/null +++ b/src/classes/DatabaseDml_TEST.cls @@ -0,0 +1,245 @@ +/* + Copyright (c) 2019, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +@IsTest +public with sharing class DatabaseDml_TEST { + private static List setupSObjects = new List(); + + /** + * Creates base data used throughout the different test scenarios + */ + @TestSetup + private static void generateData() { + UTIL_UnitTest.generateData(); + } + + @IsTest + private static void guestUserCanUpdateUnownedRecords() { + if (UTIL_UnitTest.guestUser == null) { + return; + } + + List records = new List(); + String name = 'Guest Updated Me ' + System.now(); + setSetupSObjects(); + + System.runAs(UTIL_UnitTest.guestUser) { + for (SObject record : setupSObjects) { + if (record.getSObjectType() == Contact.SObjectType) { + record.put('LastName', name); + } else { + record.put('Name', name); + } + records.add(record); + } + + DatabaseDml.getInstance().updateRecords(records); + } + + System.assertEquals(name, getCampaigns()[0].Name, 'The campaign name should have been updated.'); + System.assertEquals(name, getJobs()[0].Name, 'The job name should have been updated.'); + System.assertEquals(name, getContacts()[0].LastName, 'The contact name should have been updated.'); + } + + @IsTest + private static void guestUserCanInsertRecords() { + if (UTIL_UnitTest.guestUser == null) { + return; + } + + List records = new List(); + String name = 'Guest Created Me ' + System.now(); + + System.runAs(UTIL_UnitTest.guestUser) { + records.add(UTIL_UnitTest.createCampaign(name)); + records.add(UTIL_UnitTest.createContact(name)); + + DatabaseDml.getInstance().insertRecords(records); + } + + for (SObject record : records) { + System.assertNotEquals(null, record.Id, 'Expected the records to have been inserted.'); + } + } + + @IsTest + private static void updateRecordsShouldFailAllRecordsWhenAllOrNone() { + List records = new List(); + String name = 'Guest Updated Me ' + System.now(); + Database.DMLOptions allOrNoneOption = new Database.DMLOptions(); + allOrNoneOption.OptAllOrNone = true; + setSetupSObjects(); + + for (SObject record : setupSObjects) { + if (record.getSObjectType() == Contact.SObjectType) { + record.put('FirstName', null); + record.put('LastName', null); + } else { + record.put('Name', name); + } + + records.add(record); + } + + try { + DatabaseDml.getInstance().updateRecords(records, allOrNoneOption); + System.assert(false, 'An exception should have been thrown.'); + + } catch (Exception ex) { + System.assert(ex.getMessage().contains('REQUIRED_FIELD_MISSING'), 'Expected the missing required field to cause an exception.'); + } + + System.assertNotEquals(name, getCampaigns()[0].Name, 'The campaign name should not have been updated.'); + System.assertNotEquals(name, getJobs()[0].Name, 'The job name should not have been updated.'); + System.assertNotEquals(null, getContacts()[0].LastName, 'The contact name should not have been updated.'); + } + + @IsTest + private static void updateRecordsShouldFailOneRecordWhenNotAllOrNone() { + List records = new List(); + String name = 'Guest Updated Me ' + System.now(); + Database.DMLOptions allOrNoneOption = new Database.DMLOptions(); + allOrNoneOption.OptAllOrNone = false; + setSetupSObjects(); + + for (SObject record : setupSObjects) { + if (record.getSObjectType() == Contact.SObjectType) { + record.put('FirstName', null); + record.put('LastName', null); + } else { + record.put('Name', name); + } + + records.add(record); + } + + try { + DatabaseDml.getInstance().updateRecords(records, allOrNoneOption); + + } catch (Exception ex) { + System.assert(false, 'An exception should not have been thrown. ' + ex.getMessage()); + } + + System.assertEquals(name, getCampaigns()[0].Name, 'The campaign name should have been updated.'); + System.assertEquals(name, getJobs()[0].Name, 'The job name should have been updated.'); + System.assertNotEquals(null, getContacts()[0].LastName, 'The contact name should not have been updated.'); + } + + @IsTest + private static void insertRecordsShouldFailAllRecordsWhenAllOrNone() { + List records = new List(); + String name = 'Guest Created Me ' + System.now(); + Database.DMLOptions allOrNoneOption = new Database.DMLOptions(); + allOrNoneOption.OptAllOrNone = true; + records.add(UTIL_UnitTest.createCampaign(name)); + records.add(UTIL_UnitTest.createContact(name)); + records.add(UTIL_UnitTest.createJob(name, null)); + + try { + DatabaseDml.getInstance().insertRecords(records, allOrNoneOption); + System.assert(false, 'An exception should have been thrown.'); + + } catch (Exception ex) { + System.assert(ex.getMessage().contains('REQUIRED_FIELD_MISSING'), 'Expected the missing required field to cause an exception.'); + } + + System.assertEquals(null, records[0].get('Id'), 'The campaign should not have been created.'); + System.assertEquals(null, records[1].get('Id'), 'The contact should not have been created.'); + System.assertEquals(null, records[2].get('Id'), 'The job should not have been created.'); + } + + @IsTest + private static void insertRecordsShouldFailOneRecordWhenNotAllOrNone() { + List records = new List(); + String name = 'Guest Created Me ' + System.now(); + Database.DMLOptions allOrNoneOption = new Database.DMLOptions(); + allOrNoneOption.OptAllOrNone = false; + records.add(UTIL_UnitTest.createCampaign(name)); + records.add(UTIL_UnitTest.createContact(name)); + records.add(UTIL_UnitTest.createJob(name, null)); + + try { + DatabaseDml.getInstance().insertRecords(records, allOrNoneOption); + + } catch (Exception ex) { + System.assert(false, 'An exception should not have been thrown. ' + ex.getMessage()); + } + + System.assertNotEquals(null, records[0].get('Id'), 'The campaign should have been created.'); + System.assertNotEquals(null, records[1].get('Id'), 'The contact should have been created.'); + System.assertEquals(null, records[2].get('Id'), 'The job should not have been created.'); + } + + + ///////////////// + /// Helpers + ///////////////// + + + private static void setSetupSObjects() { + setupSObjects.addAll(getCampaigns()); + setupSObjects.addAll(getJobs()); + setupSObjects.addAll(getContacts()); + } + + private static List getCampaigns() { + return [SELECT Id, Name FROM Campaign]; + } + + private static List getJobs() { + return [SELECT Id, Name FROM Volunteer_Job__c]; + } + + private static List getContacts() { + return [SELECT Id, FirstName, LastName FROM Contact]; + } + + + /******************************************************************************************************************* + * @description Stub for DatabaseDml instance + */ + public class Stub implements System.StubProvider { + public Map countByMethodName = new Map{ + 'updateRecords' => 0, + 'insertRecords' => 0 + }; + + public Object handleMethodCall( + Object stubbedObject, + String stubbedMethodName, + Type returnType, + List listOfParamTypes, + List listOfParamNames, + List listOfArgs + ) { + countByMethodName.put(stubbedMethodName, countByMethodName.get(stubbedMethodName) + 1); + return null; + } + } +} \ No newline at end of file diff --git a/src/classes/DatabaseDml_TEST.cls-meta.xml b/src/classes/DatabaseDml_TEST.cls-meta.xml new file mode 100644 index 0000000..91b23b8 --- /dev/null +++ b/src/classes/DatabaseDml_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 46.0 + Active + diff --git a/src/classes/SoqlListView.cls b/src/classes/SoqlListView.cls index 3879778..349d97d 100644 --- a/src/classes/SoqlListView.cls +++ b/src/classes/SoqlListView.cls @@ -146,7 +146,7 @@ public with sharing class SoqlListView extends ComponentControllerBase { // action method to delete an item from the database. public PageReference DeleteItem() { - if (idDeleteItem != null) { + if (idDeleteItem != null && idDeleteItem.getSObjectType().getDescribe().isDeletable()) { database.delete(idDeleteItem); idDeleteItem = null; setCon = null; diff --git a/src/classes/Telemetry.cls b/src/classes/Telemetry.cls index d249514..baadc8e 100644 --- a/src/classes/Telemetry.cls +++ b/src/classes/Telemetry.cls @@ -37,13 +37,17 @@ public without sharing class Telemetry { VOLUNTEER_JOBS_DISPLAY_ON_WEBSITE, VOLUNTEER_SHIFTS, VOLUNTEER_SHIFTS_CREATED_LAST_YEAR, - JOB_RECURRENCE_SCHEDULES + JOB_RECURRENCE_SCHEDULES, + GRANT_GUEST_USERS_UPDATE_ACCESS } + private static final String TELEMETRY_PARAM_ERROR = + 'The telemetry param type {0} is already in use, only one type can be sent at a time.'; @TestVisible private SObjectType sObjType; @TestVisible private DeveloperName developerName; private String criteria; + private Boolean booleanValue; @TestVisible private QueryBuilder queryBuilder { @@ -62,13 +66,44 @@ public without sharing class Telemetry { this.developerName = developerName; } + public Telemetry(DeveloperName developerName) { + this.developerName = developerName; + } + public Telemetry withCriteria(String criteria) { + if (booleanValue != null) { + throw new TelemetryException(String.format(TELEMETRY_PARAM_ERROR, new List{ 'boolean' })); + } + this.criteria = criteria; return this; } + public Telemetry withBooleanValue(Boolean booleanValue) { + if (criteria != null) { + throw new TelemetryException(String.format(TELEMETRY_PARAM_ERROR, new List{ 'criteria' })); + } + + this.booleanValue = booleanValue; + return this; + } + public void send() { - send(getDeveloperName(), getCountQuery()); + if (criteria != null) { + send(getDeveloperName(), getCountQuery()); + } else if (booleanValue != null) { + send(getDeveloperName(), booleanValue); + } + } + + @Future + private static void send(String developerName, Boolean booleanValue) { + try { + FeatureManagement.setPackageBooleanValue(developerName, booleanValue); + + } catch (Exception ex) { + throw new TelemetryException(System.Label.TelemetryException); + } } @Future diff --git a/src/classes/TelemetryService.cls b/src/classes/TelemetryService.cls index 71974ff..7f664ce 100644 --- a/src/classes/TelemetryService.cls +++ b/src/classes/TelemetryService.cls @@ -67,6 +67,7 @@ public without sharing class TelemetryService { telemetries.addAll(createJobRecurrenceScheduleTelemetries()); telemetries.addAll(createCampaignTelemetries()); telemetries.addAll(createContactTelemetries()); + telemetries.addAll(createVolunteerSettingsTelemetries()); return telemetries; } @@ -110,6 +111,13 @@ public without sharing class TelemetryService { new Telemetry(Job_Recurrence_Schedule__c.SObjectType, Telemetry.DeveloperName.JOB_RECURRENCE_SCHEDULES) }; } + + private List createVolunteerSettingsTelemetries() { + return new List{ + new Telemetry(Telemetry.DeveloperName.GRANT_GUEST_USERS_UPDATE_ACCESS) + .withBooleanValue(VOL_SharedCode.VolunteersSettings.Grant_Guest_Users_Update_Access__c) + }; + } } diff --git a/src/classes/Telemetry_TEST.cls b/src/classes/Telemetry_TEST.cls index 938e0af..4d4f52c 100644 --- a/src/classes/Telemetry_TEST.cls +++ b/src/classes/Telemetry_TEST.cls @@ -35,12 +35,18 @@ private class Telemetry_TEST { private static final SObjectType SOBJECT_TYPE = Account.SObjectType; @IsTest - private static void constructorSetsPropertiesWithValuesProvided() { + private static void constructorSetsPropertiesWithSObjectTypeAndDevName() { accountTelemetry = new Telemetry(SOBJECT_TYPE, DEV_NAME); System.assertEquals(SOBJECT_TYPE, accountTelemetry.sObjType, 'Expected the sObject Type to be set.'); System.assertEquals(DEV_NAME.name(), accountTelemetry.developerName.name(), 'Expected the developer name to be set.'); } + @IsTest + private static void constructorSetsPropertiesWithDevName() { + accountTelemetry = new Telemetry(DEV_NAME); + System.assertEquals(DEV_NAME.name(), accountTelemetry.developerName.name(), 'Expected the developer name to be set.'); + } + @IsTest private static void getDeveloperNameRemovesUnderscores() { accountTelemetry = new Telemetry(SOBJECT_TYPE, DEV_NAME); diff --git a/src/classes/UTIL_HtmlOutput_CTRL.cls b/src/classes/UTIL_HtmlOutput_CTRL.cls new file mode 100644 index 0000000..5d7c28f --- /dev/null +++ b/src/classes/UTIL_HtmlOutput_CTRL.cls @@ -0,0 +1,156 @@ +/* + Copyright (c) 2020, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2020 +* @group Utilities +* @description The UTIL_HtmlOutput_CTRL is used to safely unescape allowlisted tags and urls +*/ +public with sharing class UTIL_HtmlOutput_CTRL { + + /** @description The map of allowed tags and their temporary substitution values */ + private static final Map SUBSTITUTION_BY_ALLOWED_TAG = new Map { + '/>' => '|backClose|', + ' '|closeFront|', + '>' => '|emptyClose|', + ' '|bold|', + ' '|break|', + ' '|head1|', + ' '|head2|', + ' '|head3|', + ' '|lineItem|', + ' '|orderList|', + ' '|para|', + ' '|unorderedList|' + }; + + /** @description The map of allowed urls and their temporary substitution values */ + private static final Map SUBSTITUTION_BY_ALLOWED_URL = new Map { + ' '|schedBatch|', + ' '|schedJobs|', + 'target="_blank"' => '|blankTarget|', + 'target="_new"' => '|newTarget|' + }; + + /** @description The unescaped html passed in by the page. */ + public String unsafeHtml { + get; + set { + if (value != null) { + unsafeHtml = value; + } + } + } + + /** @description True when the html contains a url that should be safely escaped. */ + public Boolean hasURL { + get; + set { + if (value != null) { + hasURL = value; + } else { + hasURL = false; + } + } + } + + /******************************************************************************************************************* + * @description Returns the escaped html with the unescaped allowlisted tags and url + */ + public String getSafeHtml() { + if (String.isBlank(unsafeHtml)) { + return ''; + } + + String safeHtml = replaceAllowlist(unsafeHtml); + safeHtml = safeHtml.escapeHtml4(); + safeHtml = resetAllowlist(safeHtml); + + return safeHtml; + } + + /******************************************************************************************************************* + * @description Returns the unescaped html with the allowlisted tags and url replaced with temporary values + */ + private String replaceAllowlist(String unescapedHtml) { + String replacedHtml = unescapedHtml.normalizeSpace(); + + for (String allowedTag : SUBSTITUTION_BY_ALLOWED_TAG.keySet()) { + replacedHtml = replacedHtml.replace(allowedTag, SUBSTITUTION_BY_ALLOWED_TAG.get(allowedTag)); + } + + if (hasURL) { + replacedHtml = replacedHtml.trim(); + for (String allowedUrl : SUBSTITUTION_BY_ALLOWED_URL.keySet()) { + String subbedValue = SUBSTITUTION_BY_ALLOWED_URL.get(allowedUrl); + replacedHtml = replacedHtml.replace(allowedUrl, subbedValue); + } + + replacedHtml = cleanupUrl(replacedHtml, SUBSTITUTION_BY_ALLOWED_URL.values()[0]); + replacedHtml = cleanupUrl(replacedHtml, SUBSTITUTION_BY_ALLOWED_URL.values()[1]); + } + + return replacedHtml; + } + + private String cleanupUrl(String replacedHtml, String urlSubstitution) { + String urlAttributes = replacedHtml.substringBetween(urlSubstitution, '|emptyClose|'); + if (urlAttributes == null) { + return replacedHtml; + } + if (urlAttributes.contains(SUBSTITUTION_BY_ALLOWED_URL.values()[2])) { + urlAttributes = urlAttributes.substringAfter(SUBSTITUTION_BY_ALLOWED_URL.values()[2]); + } else if (urlAttributes.contains(SUBSTITUTION_BY_ALLOWED_URL.values()[3])) { + urlAttributes = urlAttributes.substringAfter(SUBSTITUTION_BY_ALLOWED_URL.values()[3]); + } + + return replacedHtml.replace(urlAttributes, ''); + } + + /******************************************************************************************************************* + * @description Returns the escaped html with the temporary tags and url to their original allowlisted values + */ + private String resetAllowlist(String escapedHtml) { + String html = escapedHtml; + + for (String allowedTag : SUBSTITUTION_BY_ALLOWED_TAG.keySet()) { + html = html.replace(SUBSTITUTION_BY_ALLOWED_TAG.get(allowedTag), allowedTag); + } + + if (hasURL) { + for (String allowedUrl : SUBSTITUTION_BY_ALLOWED_URL.keySet()) { + html = html.replace(SUBSTITUTION_BY_ALLOWED_URL.get(allowedUrl), allowedUrl); + } + } + + return html; + } + +} \ No newline at end of file diff --git a/src/classes/UTIL_HtmlOutput_CTRL.cls-meta.xml b/src/classes/UTIL_HtmlOutput_CTRL.cls-meta.xml new file mode 100644 index 0000000..5591d32 --- /dev/null +++ b/src/classes/UTIL_HtmlOutput_CTRL.cls-meta.xml @@ -0,0 +1,5 @@ + + + 47.0 + Active + diff --git a/src/classes/UTIL_HtmlOutput_TEST.cls b/src/classes/UTIL_HtmlOutput_TEST.cls new file mode 100644 index 0000000..710c31e --- /dev/null +++ b/src/classes/UTIL_HtmlOutput_TEST.cls @@ -0,0 +1,158 @@ +/* + Copyright (c) 2020 Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2020 +* @group Utilities +* @description Unit tests for UTIL_HtmlOutput class +*/ +@isTest(IsParallel=true) +public with sharing class UTIL_HtmlOutput_TEST { + /******************************************************************************************************* + * @description Verifies a string with allowlisted tag with extra space is returned without escaping + */ + @isTest + private static void shouldReturnOriginalTagWithSpace() { + String html = ' Bold is allowed '; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = false; + + System.assertEquals(html, controller.getSafeHtml(),'Expected the bold tags with a space to be allowed.'); + } + + /******************************************************************************************************* + * @description Verifies a string with allowlisted tag is returned without escaping + */ + @isTest + private static void shouldReturnOriginalTag() { + String html = ' Bold is allowed '; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = false; + + System.assertEquals(html, controller.getSafeHtml(), 'Expected the bold tags to be allowed.'); + } + + /******************************************************************************************************* + * @description Verifies a string with a tag that is not allowlisted is returned escaped + */ + @isTest + private static void shouldReturnEscapedTag() { + String html = ''; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = false; + String actualHtml = controller.getSafeHtml(); + + System.assert(actualHtml.startsWith('<'), 'Expected the img tag to be escaped.'); + System.assertNotEquals(html, actualHtml, 'Expected the html to have been escaped.'); + } + + /******************************************************************************************************* + * @description Verifies a string with allowlisted url is returned without escaping + */ + @isTest + private static void shouldReturnOriginalUrl() { + String html = ''; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = true; + + System.assertEquals(html, controller.getSafeHtml(), 'Expected the url to be allowed.'); + } + + /******************************************************************************************************* + * @description Verifies a string with allowlisted url with extra space is returned without the space + */ + @isTest + private static void shouldRemoveWhiteSpaceFromUrl() { + String html = ''; + String expectedHtml = ''; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = true; + + System.assertEquals(expectedHtml, controller.getSafeHtml(), 'Expected only the space to be removed from the url.'); + } + + /******************************************************************************************************* + * @description Verifies a string with a url that is not allowlisted is returned escaped + */ + @isTest + private static void shouldReturnEscapedUrl() { + String html = ''; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = true; + String actualHtml = controller.getSafeHtml(); + + System.assert(actualHtml.startsWith('<'), 'Expected the url to be escaped.'); + System.assertNotEquals(html, actualHtml, 'Expected the html to be espaced.'); + } + + /******************************************************************************************************* + * @description Verifies unallowed attributes within the url are removed + */ + @isTest + private static void shouldReturnCleanUrl() { + String html = ''; + String expectedHtml = ''; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = true; + String actualHtml = controller.getSafeHtml(); + + System.assertEquals(expectedHtml, controller.getSafeHtml(), 'Expected the unallowed attributes to be removed from the url.'); + } + + /******************************************************************************************************* + * @description Verifies target remains in url. + */ + @isTest + private static void shouldAllowTargetInUrl() { + String html = ''; + String expectedHtml = ''; + UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); + + controller.unsafeHtml = html; + controller.hasUrl = true; + String actualHtml = controller.getSafeHtml(); + + System.assertEquals(expectedHtml, controller.getSafeHtml(), 'Expected the target to remain in the url.'); + } +} diff --git a/src/classes/UTIL_HtmlOutput_TEST.cls-meta.xml b/src/classes/UTIL_HtmlOutput_TEST.cls-meta.xml new file mode 100644 index 0000000..c20c185 --- /dev/null +++ b/src/classes/UTIL_HtmlOutput_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 47.0 + Active + diff --git a/src/classes/UTIL_UnitTest.cls b/src/classes/UTIL_UnitTest.cls new file mode 100644 index 0000000..c6ed0a0 --- /dev/null +++ b/src/classes/UTIL_UnitTest.cls @@ -0,0 +1,254 @@ +/* + Copyright (c) 2019, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +@IsTest +public class UTIL_UnitTest { + private static Map> sObjectByIdByType = new Map>(); + /** + * @description: The guest site user when found otherwise creates a default guest user + */ + public static User guestUser { + get { + if (guestUser == null) { + guestUser = findGuestSiteUser(); + } + + if (guestUser == null) { + guestUser = createGuestUser(); + } + + return guestUser; + } + private set; + } + + public static void enableElevateGuestUserAccess() { + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + } + + /////// Users /////// + + public static User createGuestUser() { + String email = 'guest' + '@testorg.com'; + // The OOB guest user profile depends on the type of org + // packaging org has Standard Guest and Dev orgs have Guest License User + // scratch orgs have both + List profiles = getProfiles(new Set { 'Standard Guest', 'Guest License User' }); + if (profiles.isEmpty()) { + return null; + } + + return createUser(email, profiles[0]); + } + + public static User createStandardUser() { + String email = 'guest' + '@testorg.com'; + + return createUser(email, getProfileRecord('Standard User')); + } + + public static User createAdminUser() { + String email = 'admin' + '@testorg.com'; + + return createUser(email, getProfileRecord('System Administrator')); + } + + private static Profile getProfileRecord(String profileName) { + List profiles = getProfiles(new Set{ profileName }); + return profiles.isEmpty() ? null : profiles[0]; + } + + private static List getProfiles(Set profileNames) { + return [SELECT Id FROM Profile WHERE Name IN :profileNames]; + } + + private static User createUser(String email, Profile profileRecord) { + return new User( + Alias = 'jdoe', + Email = email, + EmailEncodingKey = 'UTF-8', + LanguageLocaleKey = 'en_US', + LastName = 'Doe', + LocaleSidKey = 'en_US', + ProfileId = profileRecord.Id, + TimeZoneSidKey = 'America/Los_Angeles', + Username = email + Datetime.now().getTime() + ); + } + + @TestVisible + private static User findGuestSiteUser() { + List guestUsers = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + Name like '%Site Guest User%']; + + if (guestUsers.isEmpty()) { + return null; + } + + return guestUsers[0]; + } + + /////// Data /////// + + public static Map> generateData() { + sObjectByIdByType = new Map>(); + DateTime now = System.now(); + + Campaign campaignRecord = createCampaign('Spring Garden Party ' + now); + insert campaignRecord; + sObjectByIdByType.put(Campaign.SObjectType, new Map{ campaignRecord.Id => campaignRecord }); + + Volunteer_Job__c volunteerJob = createJob('Classroom Helper ' + now, campaignRecord.Id); + insert volunteerJob; + sObjectByIdByType.put(Volunteer_Job__c.SObjectType, new Map{ volunteerJob.Id => volunteerJob }); + + Account accountRecord = createAccount('TestAccount' + now); + insert accountRecord; + sObjectByIdByType.put(Account.SObjectType, new Map{ accountRecord.Id => accountRecord }); + + Contact contactRecord = createContact('Helper ' + now); + contactRecord.AccountId = accountRecord.Id; + insert contactRecord; + sObjectByIdByType.put(Contact.SObjectType, new Map{ contactRecord.Id => contactRecord }); + + return sObjectByIdByType; + } + + public static Map> generateDataWithRecurrenceSchedules() { + Contact contactRecord; + Volunteer_Job__c jobRecord; + generateData(); + + contactRecord = (Contact) sObjectByIdByType.get(Contact.SObjectType).values()[0]; + jobRecord = (Volunteer_Job__c) sObjectByIdByType.get(Volunteer_Job__c.SObjectType).values()[0]; + + Job_Recurrence_Schedule__c jrsRecord = createJobRecurrence(jobRecord.Id); + insert jrsRecord; + sObjectByIdByType.put(Job_Recurrence_Schedule__c.SObjectType, new Map{ jrsRecord.Id => jrsRecord }); + + Volunteer_Recurrence_Schedule__c vrsRecord = createVolunteerRecurrence(contactRecord.Id, jobRecord.Id); + insert vrsRecord; + sObjectByIdByType.put(Volunteer_Recurrence_Schedule__c.SObjectType, new Map{ vrsRecord.Id => vrsRecord }); + + return sObjectByIdByType; + } + + public static Map> generateDataWithShift() { + generateData(); + Volunteer_Shift__c shift = createShift(getId(Volunteer_Job__c.SObjectType)); + insert shift; + sObjectByIdByType.put(Volunteer_Shift__c.SObjectType, new Map{ shift.Id => shift }); + + return sObjectByIdByType; + } + + public static Id getId(SObjectType sObjType) { + SObject sObj = getSObject(sObjType); + + return sObj == null ? null : (Id) sObj.get('Id'); + } + + public static SObject getSObject(SObjectType sObjType) { + Map sObjectById = sObjectByIdByType.get(sObjType); + + return sObjectById == null ? null : sObjectById.values()[0]; + } + + public static Account createAccount(String name) { + return new Account(Name = name); + } + + public static Contact createContact() { + return createContact('Testerton ' + Datetime.now().getTime()); + } + + public static Contact createContact(String lastName) { + return new Contact( + FirstName = 'Andy', + LastName = lastName, + Email = 'andy@test.com'); + } + + public static Campaign createCampaign(String name) { + return new Campaign( + RecordTypeId = VOL_SharedCode.recordtypeIdVolunteersCampaign, + Name = name, + IsActive = true + ); + } + + public static Volunteer_Job__c createJob(String name, Id campaignId) { + return new Volunteer_Job__c( + Name = name, + Campaign__c = campaignId, + Display_on_Website__c = true + ); + } + + public static Volunteer_Hours__c createHours(Id contactId, Id jobId, Id shiftId) { + return new Volunteer_Hours__c( + Contact__c = contactId, + Volunteer_Job__c = jobId, + Volunteer_Shift__c = shiftId, + Status__c = 'Web Sign Up', + Start_Date__c = System.today(), + End_Date__c = System.today(), + Hours_Worked__c = 1, + Number_of_Volunteers__c = 1 + ); + } + + public static Volunteer_Shift__c createShift(Id jobId) { + return new Volunteer_Shift__c( + Volunteer_Job__c = jobId, + Duration__c = 1, + Start_Date_Time__c = System.now() + ); + } + + public static Job_Recurrence_Schedule__c createJobRecurrence(Id jobId){ + return new Job_Recurrence_Schedule__c( + Volunteer_Job__c = jobId, + Days_of_Week__c = 'Monday;Friday', + Duration__c = 1.5, + Schedule_Start_Date_Time__c = datetime.now(), + Weekly_Occurrence__c = '1st'); + } + + public static Volunteer_Recurrence_Schedule__c createVolunteerRecurrence(Id contactId, Id jobid){ + return new Volunteer_Recurrence_Schedule__c( + Contact__c = contactId, + Volunteer_Job__c = jobId, + Days_of_Week__c = 'Monday;Friday', + Duration__c = 1.5, + Schedule_Start_Date_Time__c = datetime.now(), + Weekly_Occurrence__c = '1st'); + } + +} \ No newline at end of file diff --git a/src/classes/UTIL_UnitTest.cls-meta.xml b/src/classes/UTIL_UnitTest.cls-meta.xml new file mode 100644 index 0000000..91b23b8 --- /dev/null +++ b/src/classes/UTIL_UnitTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 46.0 + Active + diff --git a/src/classes/VOL_Access.cls b/src/classes/VOL_Access.cls new file mode 100644 index 0000000..937dc86 --- /dev/null +++ b/src/classes/VOL_Access.cls @@ -0,0 +1,164 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/* + This class checks to see if the current user is a guest site user and + whether or not the admin has elevated the guest site user access. All + permission checks are expected to be done by the caller. +*/ +/* sfge-disable ApexFlsViolationRule */ +public with sharing virtual class VOL_Access { + protected VOL_Access() {} + + @TestVisible private static VOL_Access instance = null; + + public static VOL_Access getInstance() { + if (instance == null) { + instance = new VOL_Access(); + } + return instance; + } + + /******************************************************************************************************* + * @description Calls UserInfo to determine whether the current user is the Sites Guest User + * @return boolean value indicating whether current user is a guest user + ********************************************************************************************************/ + @TestVisible + private Boolean isGuestUser { + get { + if (isGuestUser == null) { + isGuestUser = UserInfo.getUserType() == 'Guest'; + } + return isGuestUser; + } + private set; + } + + /** + * @description Custom setting that determines whether or not V4S should grant guest user update access + * based on their create access. + */ + @TestVisible + private Boolean elevateGuestUserAccessSetting { + get { + if (elevateGuestUserAccessSetting == null) { + elevateGuestUserAccessSetting = VOL_SharedCode.VolunteersSettings.Grant_Guest_Users_Update_Access__c; + } + return elevateGuestUserAccessSetting; + } + set; + } + + /** + * @description Determines whether the users update access should match their create access and + * allows dml to be elevated to system context + */ + private Boolean isElevated { + get { + if (isElevated == null) { + isElevated = isGuestUser && elevateGuestUserAccessSetting; + } + return isElevated; + } + private set; + } + + /** + * @description Calls the create check to verify the users create access + */ + public void checkCreateAccess(String objectName, Set fieldNames) { + UTIL_Describe.checkCreateAccess(objectName, fieldNames); + } + + /** + * @description Elevates update to create access checks for elevated users + */ + public void checkUpdateAccess(String objectName, Set fieldNames) { + if (isElevated) { + UTIL_Describe.checkCreateAccess(objectName, fieldNames); + return; + } + + UTIL_Describe.checkUpdateAccess(objectName, fieldNames); + } + + /******************************************************************************************************* + * @description Routes the DML to the DatabaseDml class for elevated users + * @param List of SObjects to update + ********************************************************************************************************/ + public void updateRecords(List sObjects) { + if (isElevated) { + DatabaseDml.getInstance().updateRecords(sObjects); + return; + } + + update sObjects; + } + + /******************************************************************************************************* + * @description Routes the DML to the DatabaseDml class for elevated users + * @param List of SObjects to update + * @param DML Options for handling duplicates + ********************************************************************************************************/ + public void updateRecords(List sObjects, Database.DMLOptions dmlDuplicateOptions) { + if (isElevated) { + DatabaseDml.getInstance().updateRecords(sObjects, dmlDuplicateOptions); + return; + } + + Database.update(sObjects, dmlDuplicateOptions); + } + + /******************************************************************************************************* + * @description Routes the DML to the DatabaseDml class for elevated users + * @param List of SObjects to insert + ********************************************************************************************************/ + public void insertRecords(List sObjects) { + if (isElevated) { + DatabaseDml.getInstance().insertRecords(sObjects); + return; + } + + insert sObjects; + } + + /******************************************************************************************************* + * @description Routes the DML to the DatabaseDml class for elevated users + * @param List of SObjects to insert + * @param DML Options for handling duplicates + ********************************************************************************************************/ + public void insertRecords(List sObjects, Database.DMLOptions dmlDuplicateOptions) { + if (isElevated) { + DatabaseDml.getInstance().insertRecords(sObjects, dmlDuplicateOptions); + return; + } + + Database.insert(sObjects, dmlDuplicateOptions); + } +} diff --git a/src/classes/VOL_Access.cls-meta.xml b/src/classes/VOL_Access.cls-meta.xml new file mode 100644 index 0000000..bdc05b7 --- /dev/null +++ b/src/classes/VOL_Access.cls-meta.xml @@ -0,0 +1,5 @@ + + + 46.0 + Active + diff --git a/src/classes/VOL_Access_TEST.cls b/src/classes/VOL_Access_TEST.cls new file mode 100644 index 0000000..3868591 --- /dev/null +++ b/src/classes/VOL_Access_TEST.cls @@ -0,0 +1,393 @@ +@IsTest +public with sharing class VOL_Access_TEST { + private static DatabaseDml_TEST.Stub dmlMock = new DatabaseDml_TEST.Stub(); + private static Contact contactRecord = UTIL_UnitTest.createContact('Name ' + DateTime.now().getTime()); + private static List contacts = new List{ contactRecord }; + + @IsTest + private static void isGuestUserReturnsTrueForExistingGuestSiteUser() { + User guestSiteUser = UTIL_UnitTest.findGuestSiteUser(); + if (guestSiteUser == null) { + return; // bail if no guest site users are found + } + + System.runAs(guestSiteUser) { + System.assert(VOL_Access.getInstance().isGuestUser, 'Expected isGuestUser to be true for a guest site users.'); + } + } + + @IsTest + private static void isGuestUserReturnsTrueForStandardGuestUsers() { + User standardGuest = UTIL_UnitTest.createGuestUser(); + + System.runAs(standardGuest) { + System.assert(VOL_Access.getInstance().isGuestUser, 'Expected isGuestUser to be true for a standard guest user.'); + } + } + + @IsTest + private static void isGuestUserReturnsFalseForStandardUser() { + User standardUser = UTIL_UnitTest.createStandardUser(); + + System.runAs(standardUser) { + System.assert(!VOL_Access.getInstance().isGuestUser, 'Expected isGuestUser to be false for a standard users.'); + } + } + + @IsTest + private static void createAccessThrowsExceptionOnUpdateForGuestUserWithElevateSettingChecked() { + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + String actualException; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + try { + VOL_Access.getInstance().checkUpdateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualException = ex.getMessage(); + } + } + System.assertEquals(String.format(Label.PermissionCreateException, new List{ 'Contact.LastName' }), actualException, 'Expected the create permission exception to be thrown.'); + } + + @IsTest + private static void updateAccessThrowsExceptionOnUpdateForGuestUserWithElevateSettingUnchecked() { + VOL_Access.getInstance().elevateGuestUserAccessSetting = false; + String actualException; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + try { + VOL_Access.getInstance().checkUpdateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualException = ex.getMessage(); + } + } + + System.assertEquals(String.format(Label.PermissionUpdateException, new List{ 'Contact.LastName' }), actualException, 'Expected the update permission exception to be thrown.'); + } + + @IsTest + private static void createAccessThrowsExceptionOnCreateForGuestUser() { + List actualExceptions = new List(); + + System.runAs(UTIL_UnitTest.createGuestUser()) { + try { + VOL_Access.getInstance().checkCreateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualExceptions.add(ex.getMessage()); + } + + VOL_Access.getInstance().elevateGuestUserAccessSetting = !VOL_Access.getInstance().elevateGuestUserAccessSetting; + try { + VOL_Access.getInstance().checkCreateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualExceptions.add(ex.getMessage()); + } + } + + for (String actualException : actualExceptions) { + System.assertEquals(String.format(Label.PermissionCreateException, new List{ 'Contact.LastName' }), actualException, 'Expected the create permission exception to be thrown.'); + } + } + + @IsTest + private static void updateAccessThrowsExceptionOnUpdateForStandardUser() { + List actualExceptions = new List(); + + System.runAs(UTIL_UnitTest.createStandardUser()) { + try { + VOL_Access.getInstance().checkUpdateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualExceptions.add(ex.getMessage()); + } + + VOL_Access.getInstance().elevateGuestUserAccessSetting = !VOL_Access.getInstance().elevateGuestUserAccessSetting; + try { + VOL_Access.getInstance().checkUpdateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualExceptions.add(ex.getMessage()); + } + } + + for (String actualException : actualExceptions) { + System.assertEquals(String.format(Label.PermissionUpdateException, new List{ 'Contact.LastName' }), actualException, 'Expected the update permission exception to be thrown.'); + } + } + + @IsTest + private static void createAccessThrowsExceptionOnCreateForStandardUser() { + List actualExceptions = new List(); + + System.runAs(UTIL_UnitTest.createStandardUser()) { + try { + VOL_Access.getInstance().checkCreateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualExceptions.add(ex.getMessage()); + } + + VOL_Access.getInstance().elevateGuestUserAccessSetting = !VOL_Access.getInstance().elevateGuestUserAccessSetting; + try { + VOL_Access.getInstance().checkCreateAccess('Contact', new Set{ 'LastName' }); + } catch (Exception ex) { + actualExceptions.add(ex.getMessage()); + } + } + + for (String actualException : actualExceptions) { + System.assertEquals(String.format(Label.PermissionCreateException, new List{ 'Contact.LastName' }), actualException, 'Expected the create permission exception to be thrown.'); + } + } + + @IsTest + private static void indirectUpdateForGuestUserWithElevateSettingChecked() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + VOL_Access.getInstance().updateRecords(contacts); + } + + System.assertEquals(1, dmlMock.countByMethodName.get('updateRecords'), 'Expected update records in database dml to have been called.'); + } + + @IsTest + private static void indirectUpdateWithOptionsForGuestUserWithElevateSettingChecked() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + VOL_Access.getInstance().updateRecords(contacts, new Database.DMLOptions()); + } + + System.assertEquals(1, dmlMock.countByMethodName.get('updateRecords'), 'Expected update records in database dml to have been called.'); + } + + @IsTest + private static void indirectInsertForGuestUserWithElevateSettingChecked() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + VOL_Access.getInstance().insertRecords(contacts); + } + + System.assertEquals(1, dmlMock.countByMethodName.get('insertRecords'), 'Expected insert records in database dml to have been called.'); + } + + @IsTest + private static void indirectInsertWithOptionsForGuestUserWithElevateSettingChecked() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + VOL_Access.getInstance().insertRecords(contacts, new Database.DMLOptions()); + } + + System.assertEquals(1, dmlMock.countByMethodName.get('insertRecords'), 'Expected insert records in database dml to have been called.'); + } + + @IsTest + private static void directUpdateForAdminUsers() { + contactRecord.LastName = 'Before Update'; + insert contactRecord; + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createAdminUser()) { + contactRecord.LastName = 'After Update'; + VOL_Access.getInstance().updateRecords(contacts); + } + + System.assertEquals(0, dmlMock.countByMethodName.get('updateRecords'), 'Did not expect update records in database dml to have been called.'); + System.assertEquals(1, [SELECT Id FROM Contact WHERE LastName = 'After Update'].size(), 'Expected the last name to have been updated on the contact record.'); + } + + @IsTest + private static void directUpdateWithOptionsForAdminUsers() { + contactRecord.LastName = 'Before Update With Options'; + insert contactRecord; + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createAdminUser()) { + contactRecord.LastName = 'After Update With Options'; + VOL_Access.getInstance().updateRecords(contacts); + } + + System.assertEquals(0, dmlMock.countByMethodName.get('updateRecords'), 'Did not expect update records in database dml to have been called.'); + System.assertEquals(1, [SELECT Id FROM Contact WHERE LastName = 'After Update With Options'].size(), 'Expected the last name to have been updated on the contact record.'); + } + + @IsTest + private static void directInsertForAdminUsers() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createAdminUser()) { + contactRecord.LastName = 'After Insert'; + VOL_Access.getInstance().insertRecords(contacts); + } + + System.assertEquals(0, dmlMock.countByMethodName.get('insertRecords'), 'Did not expect insert records in database dml to have been called.'); + System.assertNotEquals(null, contactRecord.Id, 'Expected the insert to be performed directly.'); + } + + @IsTest + private static void directInsertWithOptionsForAdminUsers() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = true; + + System.runAs(UTIL_UnitTest.createAdminUser()) { + contactRecord.LastName = 'After Insert With Options'; + VOL_Access.getInstance().insertRecords(contacts); + } + + System.assertEquals(0, dmlMock.countByMethodName.get('insertRecords'), 'Did not expect insert records in database dml to have been called.'); + System.assertNotEquals(null, contactRecord.Id, 'Expected the insert with options to be performed directly.'); + } + + + @IsTest + private static void updateThrowsExceptionForGuestUserWithElevateSettingUnchecked() { + contactRecord.LastName = 'Before Update'; + insert contactRecord; + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = false; + String actualException; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + contactRecord.LastName = 'After Update'; + try { + VOL_Access.getInstance().updateRecords(contacts); + } catch (Exception ex) { + actualException = ex.getMessage(); + } + } + + System.assertEquals(0, dmlMock.countByMethodName.get('updateRecords'), 'Did not expect update records in database dml to have been called.'); + System.assert(actualException.contains('INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY'), 'Expected a cross reference exception to be thrown. ' + actualException); + } + + @IsTest + private static void updateWithOptionsThrowsExceptionForGuestUserWithElevateSettingUnchecked() { + contactRecord.LastName = 'Before Update With Options'; + insert contactRecord; + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = false; + String actualException; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + contactRecord.LastName = 'After Update With Options'; + try { + VOL_Access.getInstance().updateRecords(contacts); + } catch (Exception ex) { + actualException = ex.getMessage(); + } + } + + System.assertEquals(0, dmlMock.countByMethodName.get('updateRecords'), 'Did not expect update records in database dml to have been called.'); + System.assert(actualException.contains('INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY'), 'Expected a cross reference exception to be thrown. ' + actualException); + } + + @IsTest + private static void directInsertForGuestUserWithElevateSettingUnchecked() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = false; + String actualException; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + contactRecord.LastName = 'After Insert'; + VOL_Access.getInstance().insertRecords(contacts); + } + + System.assertEquals(0, dmlMock.countByMethodName.get('insertRecords'), 'Did not expect insert records in database dml to have been called.'); + System.assertNotEquals(null, contactRecord.Id, 'Expected the insert to be performed directly.'); + } + + @IsTest + private static void directInsertWithOptionsForGuestUserWithElevateSettingUnchecked() { + setDatabaseDmlMock(); + VOL_Access.getInstance().elevateGuestUserAccessSetting = false; + String actualException; + + System.runAs(UTIL_UnitTest.createGuestUser()) { + contactRecord.LastName = 'After Insert With Options'; + VOL_Access.getInstance().insertRecords(contacts); + } + + System.assertEquals(0, dmlMock.countByMethodName.get('insertRecords'), 'Did not expect insert records in database dml to have been called.'); + System.assertNotEquals(null, contactRecord.Id, 'Expected the insert to be performed directly.'); + } + + ///////////////// + /// Helpers + ///////////////// + + private static void setDatabaseDmlMock() { + DatabaseDml.instance = (DatabaseDml) Test.createStub(DatabaseDml.class, dmlMock); + } + + /******************************************************************************************************************* + * @description Stub for DatabaseDml instance + */ + public class Stub implements System.StubProvider { + public Map> sObjectTypesByMethodName = new Map>(); + + public Object handleMethodCall( + Object stubbedObject, + String stubbedMethodName, + Type returnType, + List listOfParamTypes, + List listOfParamNames, + List listOfArgs + ) { + addSObjectType(stubbedMethodName, listOfArgs); + + return null; + } + + public void assertMethodCalled(String stubbedMethodName, SObjectType sObjType) { + if (String.isEmpty(stubbedMethodName) || sObjType == null) { + return; + } + + System.assert(sObjectTypesByMethodName.containsKey(stubbedMethodName), 'Expected the method, ' + stubbedMethodName + ', to have been called with ' + sObjType + ' records.'); + System.assert(sObjectTypesByMethodName.get(stubbedMethodName).contains(sObjType), 'Expected ' + sObjType + ' records to have been passed in. ' + sObjectTypesByMethodName.get(stubbedMethodName)); + } + + private void addSObjectType(String stubbedMethodName, List listOfArgs) { + if (listOfArgs == null || listOfArgs.isEmpty()) { + return; + } + + Set sObjectTypes = sObjectTypesByMethodName.get(stubbedMethodName); + if (sObjectTypes == null) { + sObjectTypes = new Set(); + } + + for (Object arg : listOfArgs) { + addSObjectType(stubbedMethodName, arg, sObjectTypes); + } + } + + private void addSObjectType(String stubbedMethodName, Object arg, Set sObjectTypes) { + if (arg instanceof List) { + List records = (List) arg; + if (records.isEmpty()) { + return; // getSObjectType returns null for the list, need to request it form the first record. + } + + SObjectType sObjType = records[0].getSObjectType(); + sObjectTypes.add(sObjType); + + sObjectTypesByMethodName.put(stubbedMethodName, sObjectTypes); + + } else if (arg instanceof String) { + SObjectType sObjType = ((SObject)(Type.forName('Schema.' + arg).newInstance())).getSObjectType(); + sObjectTypes.add(sObjType); + + sObjectTypesByMethodName.put(stubbedMethodName, sObjectTypes); + } + } + } + +} diff --git a/src/classes/VOL_Access_TEST.cls-meta.xml b/src/classes/VOL_Access_TEST.cls-meta.xml new file mode 100644 index 0000000..e907da6 --- /dev/null +++ b/src/classes/VOL_Access_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 46.0 + Active + diff --git a/src/classes/VOL_CTRL_JobCalendar.cls b/src/classes/VOL_CTRL_JobCalendar.cls index 5fb1b72..b817aaf 100644 --- a/src/classes/VOL_CTRL_JobCalendar.cls +++ b/src/classes/VOL_CTRL_JobCalendar.cls @@ -49,7 +49,8 @@ global with sharing class VOL_CTRL_JobCalendar { if (p != null && p != '') { // Ensure the user has access to the object and fields before querying UTIL_Describe.checkReadAccess('Campaign', new Set{'Id', 'StartDate'}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listCampaign = [select Id, StartDate from Campaign where Id = :p]; if (listCampaign.size() > 0) { initialDate = Date.valueOf(listCampaign[0].StartDate); @@ -70,7 +71,8 @@ global with sharing class VOL_CTRL_JobCalendar { new Set{'Id', UTIL_Describe.StrTokenNSPrefix('First_Shift__c'), UTIL_Describe.StrTokenNSPrefix('Campaign__c')}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listJob = [select Id, First_Shift__c, Campaign__c from Volunteer_Job__c where Id = :p]; if (listJob.size() > 0) { @@ -91,7 +93,10 @@ global with sharing class VOL_CTRL_JobCalendar { new Set{'Id', UTIL_Describe.StrTokenNSPrefix('Start_Date_Time__c'), UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')}); - + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set{UTIL_Describe.StrTokenNSPrefix('Campaign__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listShift = [select Id, Start_Date_Time__c, Volunteer_Job__c, Volunteer_Job__r.Campaign__c from Volunteer_Shift__c where Id = :p]; @@ -157,6 +162,8 @@ global with sharing class VOL_CTRL_JobCalendar { // only specify the css file if in the web page scenario. if (strURLtoCSSFile == null && fWeb) { + // System query to find the css doc if the admin has added it for custom css + /* sfge-disable-next-line ApexFlsViolationRule */ list listDocs = [SELECT Name, Id From Document WHERE Name = 'JobCalendarCSS.css' LIMIT 1 ]; if (listDocs.size() > 0) { Document doc = listDocs[0]; @@ -193,11 +200,17 @@ global with sharing class VOL_CTRL_JobCalendar { get { list listSO = new list(); listSO.add(new SelectOption('', system.label.labelChoiceAllActiveCampaigns)); - for (Campaign c : [select Name, Id, StartDate from Campaign - where RecordTypeId = :VOL_SharedCode.recordtypeIdVolunteersCampaign - and IsActive = true order by Name asc limit 999]) { - listSO.add(new SelectOption(c.id, c.name)); - } + if (Campaign.SObjectType.getDescribe().isAccessible() && Campaign.Name.getDescribe().isAccessible() && + Campaign.IsActive.getDescribe().isAccessible() && Campaign.StartDate.getDescribe().isAccessible() && + Campaign.RecordTypeId.getDescribe().isAccessible()) { + for (Campaign c : [select Name, Id, StartDate from Campaign + where RecordTypeId = :VOL_SharedCode.recordtypeIdVolunteersCampaign + and IsActive = true order by Name asc limit 999]) { + listSO.add(new SelectOption(c.id, c.name)); + } + } + + // Allow the page to load without options. return listSO; } set; @@ -223,17 +236,25 @@ global with sharing class VOL_CTRL_JobCalendar { get { list listSO = new list(); listSO.add(new SelectOption('', system.label.labelChoiceAllJobs)); - if (campaignId == null) { - for (Volunteer_Job__c vj : [select Name, Id from Volunteer_Job__c - where Campaign__r.IsActive = true order by name limit 999]) { - listSO.add(new SelectOption(vj.id, vj.name)); - } - } else { - for (Volunteer_Job__c vj : [select Name, Id from Volunteer_Job__c - where Campaign__c = :campaignId order by name limit 999]) { - listSO.add(new SelectOption(vj.id, vj.name)); - } - } + if (Volunteer_Job__c.SObjectType.getDescribe().isAccessible() && Volunteer_Job__c.Campaign__c.getDescribe().isAccessible() + && Campaign.SObjectType.getDescribe().isAccessible() && Campaign.IsActive.getDescribe().isAccessible()) { + if (campaignId == null) { + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ + for (Volunteer_Job__c vj : [select Name, Id from Volunteer_Job__c + where Campaign__r.IsActive = true order by name limit 999]) { + listSO.add(new SelectOption(vj.id, vj.name)); + } + } else { + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ + for (Volunteer_Job__c vj : [select Name, Id from Volunteer_Job__c + where Campaign__c = :campaignId order by name limit 999]) { + listSO.add(new SelectOption(vj.id, vj.name)); + } + } + } // Allow the page to load without options + return listSO; } @@ -367,8 +388,15 @@ global with sharing class VOL_CTRL_JobCalendar { UTIL_Describe.StrTokenNSPrefix('Total_Volunteers__c'), UTIL_Describe.StrTokenNSPrefix('Number_of_Volunteers_Still_Needed__c'), UTIL_Describe.StrTokenNSPrefix('Description__c')}); + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set{'Name', + UTIL_Describe.StrTokenNSPrefix('Campaign__c'), + UTIL_Describe.StrTokenNSPrefix('Display_On_Website__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Website_Time_Zone__c')}); + UTIL_Describe.checkReadAccess('Campaign', + new Set{UTIL_Describe.StrTokenNSPrefix('Volunteer_Website_Time_Zone__c')}); - if (!fAllJob) { + if (!fAllJob) { listShifts = [select Id, Name, Volunteer_Job__c, Volunteer_Job__r.Name, Volunteer_Job__r.Volunteer_Website_Time_Zone__c, Volunteer_Job__r.Campaign__r.Volunteer_Website_Time_Zone__c, Volunteer_Job__r.Campaign__c, Start_Date_Time__c, Duration__c, Total_Volunteers__c, Number_of_Volunteers_Still_Needed__c, Description__c @@ -378,6 +406,7 @@ global with sharing class VOL_CTRL_JobCalendar { and (Volunteer_Job__r.Display_On_Website__c = true or Volunteer_Job__r.Display_On_Website__c = :fWeb) order by Start_Date_Time__c asc]; } else if (fAllCampaign && fAllJob) { + UTIL_Describe.checkReadAccess('Campaign', new Set{'IsActive'}); listShifts = [select Id, Name, Volunteer_Job__c, Volunteer_Job__r.Name, Volunteer_Job__r.Volunteer_Website_Time_Zone__c,Volunteer_Job__r.Campaign__r.Volunteer_Website_Time_Zone__c, Volunteer_Job__r.Campaign__c, Start_Date_Time__c, Duration__c, Total_Volunteers__c, Number_of_Volunteers_Still_Needed__c, Description__c @@ -391,7 +420,6 @@ global with sharing class VOL_CTRL_JobCalendar { if (fShowCampaignHierarchy) { listCampaignIds = VOL_SharedCode.listIdsCampaignsInHierarchy(strCampaignId); } - listShifts = [select Id, Name, Volunteer_Job__c, Volunteer_Job__r.Name, Volunteer_Job__r.Volunteer_Website_Time_Zone__c,Volunteer_Job__r.Campaign__r.Volunteer_Website_Time_Zone__c, Volunteer_Job__r.Campaign__c, Start_Date_Time__c, Duration__c, Total_Volunteers__c, Number_of_Volunteers_Still_Needed__c, Description__c diff --git a/src/classes/VOL_CTRL_NewAndEditVRS.cls b/src/classes/VOL_CTRL_NewAndEditVRS.cls index bad1060..23ba2d0 100644 --- a/src/classes/VOL_CTRL_NewAndEditVRS.cls +++ b/src/classes/VOL_CTRL_NewAndEditVRS.cls @@ -51,6 +51,20 @@ public with sharing class VOL_CTRL_NewAndEditVRS { list listSO = new list(); listSO.add(new SelectOption('', '')); ID jobId = vrs.Volunteer_Job__c; + + // Ensure the user has access to the object before querying + try { + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Job_Recurrence_Schedule__c'), + new Set{ + UTIL_Describe.StrTokenNSPrefix('Days_of_Week__c'), + UTIL_Describe.StrTokenNSPrefix('Duration__c'), + UTIL_Describe.StrTokenNSPrefix('Schedule_Start_Date_Time__c'), + UTIL_Describe.StrTokenNSPrefix('Schedule_End_Date__c'), + UTIL_Describe.StrTokenNSPrefix('Weekly_Occurrence__c')}); + } catch (Exception ex) { + // we will return an empty list vs throwing an error + return listSO; + } for (Job_Recurrence_Schedule__c jrs : [select Id, Name, Days_of_Week__c, Duration__c, Schedule_Start_Date_Time__c, Schedule_End_Date__c, Weekly_Occurrence__c from Job_Recurrence_Schedule__c diff --git a/src/classes/VOL_CTRL_PersonalSiteContactInfo.cls b/src/classes/VOL_CTRL_PersonalSiteContactInfo.cls index 24bcae4..0656205 100644 --- a/src/classes/VOL_CTRL_PersonalSiteContactInfo.cls +++ b/src/classes/VOL_CTRL_PersonalSiteContactInfo.cls @@ -29,7 +29,9 @@ */ global with sharing class VOL_CTRL_PersonalSiteContactInfo { - + @TestVisible + private static VOL_Access access = VOL_Access.getInstance(); + global VOL_CTRL_PersonalSiteContactInfo() { // set default property values @@ -90,8 +92,7 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { setStrFields.addAll(listStrFieldsExtraFS); setStrFields.add('Firstname'); setStrFields.add('Lastname'); - contactReadOnly = (Contact)VOL_SharedCodeAPI25.LoadAndCopyObject(contactId, contactEdit, new List(setStrFields)); - + contactReadOnly = (Contact)VOL_SharedCode.LoadAndCopyObject(contactId, contactEdit, new List(setStrFields)); // output error page messages for any field that doesn't have visibility correctly set. VOL_SharedCode.testObjectFieldVisibility('Contact', listStrFieldsPanel1FS); @@ -178,7 +179,9 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { strSoql += ' and Status__c = \'Completed\' '; strSoql += ' order by Start_Date__c DESC '; strSoql += ' limit ' + cRowsCompleted; - listCompletedVolunteerHours = Database.Query(strSoql); + SObjectAccessDecision accessDecision = Security.stripInaccessible(AccessType.READABLE, Database.Query(strSoql)); + listCompletedVolunteerHours = (List) accessDecision.getRecords(); + // store friendly datetime string in system field for display only dateTimeFixup(listCompletedVolunteerHours); } @@ -203,7 +206,15 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), new Set{ UTIL_Describe.StrTokenNSPrefix('Status__c'), - UTIL_Describe.StrTokenNSPrefix('Shift_Start_Date_Time__c')}); + UTIL_Describe.StrTokenNSPrefix('Shift_Start_Date_Time__c'), + UTIL_Describe.StrTokenNSPrefix('Contact__c')}); + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set{'Name', + UTIL_Describe.StrTokenNSPrefix('Volunteer_Website_Time_Zone__c')}); + UTIL_Describe.checkReadAccess('Campaign', + new Set{UTIL_Describe.StrTokenNSPrefix('Volunteer_Website_Time_Zone__c')}); + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), + new Set{UTIL_Describe.StrTokenNSPrefix('Duration__c')}); if (listUpcomingVolunteerHours == null) { string strSoql = 'select Volunteer_Job__r.Name, Volunteer_Job__r.Volunteer_Website_Time_Zone__c, ' + @@ -219,6 +230,8 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { strSoql += ' and Shift_Start_Date_Time__c >= :dtToday '; strSoql += ' order by Shift_Start_Date_Time__c ASC '; strSoql += ' limit ' + cRowsUpcoming; + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ listUpcomingVolunteerHours = Database.Query(strSoql); // store friendly datetime string in system field for display only dateTimeFixup(listUpcomingVolunteerHours); @@ -273,7 +286,7 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { } } } - + global PageReference edit() { isEditing = true; return null; @@ -298,9 +311,8 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { } } } - VOL_SharedCode.checkUpdateAccessSites('Contact', contactFieldsToUpdate); - update contactReadOnly; + access.updateRecords(new List{ contactReadOnly }); } isEditing = false; } catch (Exception ex) { @@ -313,18 +325,23 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { isEditing = false; return null; } - + global PageReference cancelShift() { try { - if (hoursId != null) { + if (hoursId != null) { + // We are dynamically check update access below, the query result is not being returned to the user. + /* sfge-disable-next-line ApexFlsViolationRule */ Volunteer_Hours__c hr = [select Id, Status__c, Hours_Worked__c from Volunteer_Hours__c where Id = :hoursId]; hr.Status__c = 'Canceled'; - hr.Hours_Worked__c = 0; - UTIL_Describe.checkUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), + hr.Hours_Worked__c = 0; + access.checkUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), new Set{ UTIL_Describe.StrTokenNSPrefix('Status__c'), UTIL_Describe.StrTokenNSPrefix('Hours_Worked__c')}); - update hr; + // We are dynamically check update access above. + /* sfge-disable-next-line ApexFlsViolationRule */ + access.updateRecords(new List{hr}); + hoursId = null; listUpcomingVolunteerHours = null; // to force it to be refreshed. } @@ -351,29 +368,36 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { global list getChartData() { integer cMonths = 12; Date dtStart = date.today().addMonths(-cMonths + 1).toStartOfMonth(); - - list listAG = [select CALENDAR_YEAR(Start_Date__c) theYear, CALENDAR_MONTH(Start_Date__c) theMonth, SUM(Hours_Worked__c) sumHours + list listCD = new list(); + + if (Volunteer_Hours__c.SObjectType.getDescribe().isAccessible() + && Volunteer_Hours__c.Start_Date__c.getDescribe().isAccessible() + && Volunteer_Hours__c.Hours_Worked__c.getDescribe().isAccessible() + && Volunteer_Hours__c.Contact__c.getDescribe().isAccessible() + && Volunteer_Hours__c.Status__c.getDescribe().isAccessible()) { + + list listAG = [select CALENDAR_YEAR(Start_Date__c) theYear, CALENDAR_MONTH(Start_Date__c) theMonth, SUM(Hours_Worked__c) sumHours from Volunteer_Hours__c where Contact__c = :contactId and Status__c = 'Completed' and Start_Date__c >= :dtStart group by CALENDAR_YEAR(Start_Date__c), CALENDAR_MONTH(Start_Date__c) order by CALENDAR_YEAR(Start_Date__c), CALENDAR_MONTH(Start_Date__c) ]; - list listCD = new list(); - - Date dtNext = dtStart; - Time timeT = Time.newInstance(1, 0, 0, 0); - for (AggregateResult ag : listAG) { - Date dt = date.newInstance(integer.valueOf(ag.get('theYear')), integer.valueOf(ag.get('theMonth')), 1); - - // handle months with no data - while (dtNext < dt) { - listCD.add(new ChartData(datetime.newInstance(dtNext,timeT).format(strChartDateFormat), 0)); - dtNext = dtNext.addMonths(1); + Date dtNext = dtStart; + Time timeT = Time.newInstance(1, 0, 0, 0); + for (AggregateResult ag : listAG) { + Date dt = date.newInstance(integer.valueOf(ag.get('theYear')), integer.valueOf(ag.get('theMonth')), 1); + + // handle months with no data + while (dtNext < dt) { + listCD.add(new ChartData(datetime.newInstance(dtNext,timeT).format(strChartDateFormat), 0)); + dtNext = dtNext.addMonths(1); + } + + listCD.add(new ChartData(datetime.newInstance(dt,timeT).format(strChartDateFormat), integer.valueOf(ag.get('sumHours')))); + dtNext = dt.addMonths(1); } - - listCD.add(new ChartData(datetime.newInstance(dt,timeT).format(strChartDateFormat), integer.valueOf(ag.get('sumHours')))); - dtNext = dt.addMonths(1); - } + } // Allow the page to load without the chart data. + return listCD; } @@ -407,6 +431,13 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { integer iVol = 0; integer iCurrent = 0; + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), + new Set{'Id', + UTIL_Describe.StrTokenNSPrefix('Start_Date__c'), + UTIL_Describe.StrTokenNSPrefix('Hours_Worked__c'), + UTIL_Describe.StrTokenNSPrefix('Contact__c'), + UTIL_Describe.StrTokenNSPrefix('Status__c')}); + for (list listAG : [select Contact__c cId, SUM(Hours_Worked__c) sumHours from Volunteer_Hours__c where Status__c = 'Completed' and Start_Date__c >= :dtStart @@ -429,26 +460,48 @@ global with sharing class VOL_CTRL_PersonalSiteContactInfo { return ''; } } - - private string strRankLifetime() { - integer iVol = 0; - integer iCurrent = 0; - for (list listCon : [select Id, Volunteer_Hours__c - from Contact where Volunteer_Hours__c > 0 order by Volunteer_Hours__c desc]) { - - for (Contact con : listCon) { - if (con.Id == contactId) { - iCurrent = iVol; - } - iVol++; - } + + @TestVisible + private String strRankLifetime() { + try { + UTIL_Describe.checkReadAccess('Contact', + new Set{'Id', UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ + Decimal contactTotalHours = [SELECT Id, Volunteer_Hours__c FROM Contact WHERE Id = :contactId LIMIT 1].Volunteer_Hours__c; + + if (contactTotalHours == null || contactTotalHours == 0) { + return ''; + } + + String hoursField = String.valueOf(Contact.Volunteer_Hours__c); + String totalVolunteersQuery = 'SELECT count() FROM Contact WHERE ' + hoursField + ' > 0'; + String totalVolunteersWithMoreHoursQuery = totalVolunteersQuery + ' AND ' + hoursField + ' > ' + contactTotalHours; + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ + Integer totalVolunteers = Database.countQuery(totalVolunteersQuery); + + if (totalVolunteers <= 2) { + return ''; + } + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ + Integer totalVolunteersWithMoreHours = Database.countQuery(totalVolunteersWithMoreHoursQuery); + + Integer rank = Integer.valueOf( + 100 * ( + Decimal.valueOf(totalVolunteersWithMoreHours) + / Decimal.valueOf(totalVolunteers - 1) + ) + ); + + if (rank == 0) { + rank = 1; + } + + return String.format(Label.labelContactInfoRankTextLifetime, new List{ rank + '%' }); + } catch(Exception ex) { + return ''; // allow the page to load without the ranking } - if (iVol > 2) { - integer irank = integer.valueOf(100 * (decimal.valueOf(iCurrent)/decimal.valueOf(iVol - 1))); - if (irank == 0) irank = 1; - return string.format(label.labelContactInfoRankTextLifetime, new string[]{irank + '%'}); - } else { - return ''; - } - } + } } \ No newline at end of file diff --git a/src/classes/VOL_CTRL_PersonalSiteContactInfo_TEST.cls b/src/classes/VOL_CTRL_PersonalSiteContactInfo_TEST.cls index 0054366..aeb617e 100644 --- a/src/classes/VOL_CTRL_PersonalSiteContactInfo_TEST.cls +++ b/src/classes/VOL_CTRL_PersonalSiteContactInfo_TEST.cls @@ -30,60 +30,167 @@ @isTest private with sharing class VOL_CTRL_PersonalSiteContactInfo_TEST { + private static VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); + private static final VOL_CTRL_PersonalSiteContactInfo CTRL_INSTANCE = new VOL_CTRL_PersonalSiteContactInfo(); //****************************************************************************************************** // Test Code + private static void setAccessMock() { + VOL_CTRL_PersonalSiteContactInfo.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + } + + @IsTest + private static void shouldCheckCreateAccessOnSave() { + VOL_SharedCode_TEST.setAccessMock(); + setAccessMock(); + Util_UnitTest.generateDataWithRecurrenceSchedules(); + + Contact contactRecord = [SELECT Id FROM Contact LIMIT 1]; + System.assertNotEquals(null, contactRecord); + + // setup page + PageReference pageRef = Page.PersonalSiteContactInfo; + pageRef.getParameters().put('contactId', contactRecord.Id); + system.assertNotEquals(null, contactRecord.Id); + Test.setCurrentPage(pageRef); + + VOL_CTRL_PersonalSiteContactInfo ctrl = new VOL_CTRL_PersonalSiteContactInfo(); + ctrl.contactEdit.LastName = 'ChangedLastName'; + ctrl.save(); + + VOL_SharedCode_TEST.accessMock.assertMethodCalled('checkCreateAccess', Contact.SObjectType); + accessMock.assertMethodCalled('updateRecords', Contact.SObjectType); + } + + @IsTest + private static void shouldReturnEmptyLifeTimeRankWhenHoursAreEmpty() { + Contact contactRecord = new Contact(FirstName = 'Fred', LastName = 'Test'); + insert contactRecord; + + CTRL_INSTANCE.contactId = contactRecord.Id; + String rank = CTRL_INSTANCE.strRankLifetime(); + + System.assertEquals('', rank, 'Did not expect a value to be returned.'); + } + + @IsTest + private static void shouldReturnEmptyLifeTimeRankWhenTotalVolunteersIsOne() { + UTIL_UnitTest.generateData(); + Contact contactRecord; + Id jobId; + + contactRecord = (Contact) UTIL_UnitTest.getSObject(Contact.SObjectType); + jobId = UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType); + Volunteer_Hours__c hours = UTIL_UnitTest.createHours(contactRecord.Id, jobId, null); + hours.Status__c = 'Completed'; + insert hours; + + Test.startTest(); + contactRecord = [SELECT Id, Volunteer_Hours__c FROM Contact]; + CTRL_INSTANCE.contactId = contactRecord.Id; + String rank = CTRL_INSTANCE.strRankLifetime(); + Test.stopTest(); + + System.assertEquals(hours.Hours_Worked__c, contactRecord.Volunteer_Hours__c, 'Expected the total completed hours to roll up to the contact.'); + System.assertEquals('', rank, 'Did not expect a value to be returned.'); + } + + @IsTest + private static void shouldReturnRankWhenThreeTotalVolunteersExist() { + UTIL_UnitTest.generateData(); + Integer expectedRank = Integer.valueOf(100 * (2 / (3 - 1))); + expectedRank = expectedRank == 0 ? 1 : expectedRank; + + Contact contactRecord; + Id jobId; + + contactRecord = (Contact) UTIL_UnitTest.getSObject(Contact.SObjectType); + jobId = UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType); + Volunteer_Hours__c hours = UTIL_UnitTest.createHours(contactRecord.Id, jobId, null); + hours.Status__c = 'Completed'; + hours.Hours_Worked__c = 1; + + Contact contact2 = contactRecord.clone(); + contact2.LastName = 'Two'; + Contact contact3 = contactRecord.clone(); + contact3.LastName = 'Three'; + insert new List{ contact2, contact3 }; + + Volunteer_Hours__c hours2 = UTIL_UnitTest.createHours(contact2.Id, jobId, null); + hours2.Hours_Worked__c = 2; + hours2.Status__c = 'Completed'; + Volunteer_Hours__c hours3 = UTIL_UnitTest.createHours(contact3.Id, jobId, null); + hours3.Hours_Worked__c = 3; + hours3.Status__c = 'Completed'; + insert new List{ hours, hours2, hours3 }; + + List contacts = [SELECT Id, Volunteer_Hours__c FROM Contact ORDER BY Id]; + CTRL_INSTANCE.contactId = contacts[0].Id; + String actualRank = CTRL_INSTANCE.strRankLifetime(); + + System.assertEquals(hours.Hours_Worked__c, contacts[0].Volunteer_Hours__c, 'Expected the total completed hours to roll up to the first contact.'); + System.assertEquals(hours2.Hours_Worked__c, contacts[1].Volunteer_Hours__c, 'Expected the total completed hours to roll up to the second contact.'); + System.assertEquals(hours3.Hours_Worked__c, contacts[2].Volunteer_Hours__c, 'Expected the total completed hours to roll up to the third contact.'); + System.assert(actualRank.contains(String.valueOf(expectedRank)), 'The rank value was not as expected. Expected: ' + expectedRank + '; Actual: ' + actualRank); + } + + @IsTest + private static void shouldReturnSameRankWhenHoursMatch() { + UTIL_UnitTest.generateData(); + Integer expectedRank = Integer.valueOf(100 * (0 / (3 - 1))); + expectedRank = expectedRank == 0 ? 1 : expectedRank; + Contact contactRecord; + Id jobId; + + contactRecord = (Contact) UTIL_UnitTest.getSObject(Contact.SObjectType); + jobId = UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType); + Volunteer_Hours__c hours = UTIL_UnitTest.createHours(contactRecord.Id, jobId, null); + hours.Status__c = 'Completed'; + hours.Hours_Worked__c = 1; + + Contact contact2 = contactRecord.clone(); + contact2.LastName = 'Two'; + Contact contact3 = contactRecord.clone(); + contact3.LastName = 'Three'; + insert new List{ contact2, contact3 }; + + Volunteer_Hours__c hours2 = UTIL_UnitTest.createHours(contact2.Id, jobId, null); + hours2.Hours_Worked__c = 2; + hours2.Status__c = 'Completed'; + Volunteer_Hours__c hours3 = UTIL_UnitTest.createHours(contact3.Id, jobId, null); + hours3.Hours_Worked__c = 2; + hours3.Status__c = 'Completed'; + insert new List{ hours, hours2, hours3 }; + + List contacts = [SELECT Id, Volunteer_Hours__c FROM Contact ORDER BY Id]; + CTRL_INSTANCE.contactId = contacts[1].Id; + String actualRank = CTRL_INSTANCE.strRankLifetime(); + CTRL_INSTANCE.contactId = contacts[2].Id; + String actualRank2 = CTRL_INSTANCE.strRankLifetime(); + + System.assert(actualRank.contains(String.valueOf(expectedRank)), 'The rank value was not as expected. Expected: ' + expectedRank + '; Actual: ' + actualRank); + System.assert(actualRank2.contains(String.valueOf(expectedRank)), 'The rank value was not as expected. Expected: ' + expectedRank + '; Actual: ' + actualRank); + } + /******************************************************************************************************* * @description test the visualforce page controller, running as the Sites Guest User, if such as user * exists. if not, will run under the current user. * @return void ********************************************************************************************************/ - private static testMethod void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and - Name like '%Site Guest User%']; - if (listU.size() > 0) { - system.debug('Running test as Sites Guest User: ' + listU[0]); - system.runAs(listU[0]) { - testPSC(); - } - } else { - system.debug('Running test as system.'); - testPSC(); - } + @IsTest + private static void testPageFunctionality() { + Util_UnitTest.generateDataWithRecurrenceSchedules(); + testPSC(); } - + private static void testPSC() { - // create test data - Campaign cmp = new Campaign(recordtypeid=VOL_SharedCode.recordtypeIdVolunteersCampaign, - name='Volunteer Personal Site Test Campaign', IsActive=true); - insert cmp; - Volunteer_Job__c job = new Volunteer_Job__c(name='Job1', campaign__c=cmp.Id); - insert job; - Contact contact = new Contact(firstname='test', lastname='test'); - insert contact; - - Job_Recurrence_Schedule__c jrs = new Job_Recurrence_Schedule__c( - Volunteer_Job__c = job.Id, - Days_of_Week__c = 'Monday;Friday', - Duration__c = 1.5, - Schedule_Start_Date_Time__c = datetime.now(), - Weekly_Occurrence__c = '1st'); - insert jrs; - system.assertNotEquals(null, jrs.Id); - - Volunteer_Recurrence_Schedule__c vrs = new Volunteer_Recurrence_Schedule__c( - Contact__c = contact.Id, - Volunteer_Job__c = job.Id, - Days_of_Week__c = 'Monday;Friday', - Duration__c = 1.5, - Schedule_Start_Date_Time__c = datetime.now(), - Weekly_Occurrence__c = '1st'); - insert vrs; - system.assertNotEquals(null, vrs.Id); + Contact contact = [SELECT Id FROM Contact LIMIT 1]; + System.AssertNotEquals(null, contact); + // setup page PageReference pageRef = Page.PersonalSiteContactInfo; - pageRef.getParameters().put('contactId', contact.Id); + pageRef.getParameters().put('contactId', contact.Id); system.assertNotEquals(null, contact.Id); Test.setCurrentPage(pageRef); @@ -149,7 +256,8 @@ private with sharing class VOL_CTRL_PersonalSiteContactInfo_TEST { * @description test timezone handling for Shifts under LA timezone * @return void */ - private static testMethod void testTimeZoneHandlingFromLA() { + @IsTest + private static void testTimeZoneHandlingFromLA() { testTimeZoneHandling('America/Los_Angeles'); } @@ -157,7 +265,8 @@ private with sharing class VOL_CTRL_PersonalSiteContactInfo_TEST { * @description test timezone handling for Shifts under Sydney timezone * @return void */ - private static testMethod void testTimeZoneHandlingFromSydney() { + @IsTest + private static void testTimeZoneHandlingFromSydney() { testTimeZoneHandling('Australia/Sydney'); } diff --git a/src/classes/VOL_CTRL_PersonalSiteContactLookup.cls b/src/classes/VOL_CTRL_PersonalSiteContactLookup.cls index 969554d..edb8712 100644 --- a/src/classes/VOL_CTRL_PersonalSiteContactLookup.cls +++ b/src/classes/VOL_CTRL_PersonalSiteContactLookup.cls @@ -29,6 +29,8 @@ */ global with sharing class VOL_CTRL_PersonalSiteContactLookup { + @TestVisible + private static VOL_Access access = VOL_Access.getInstance(); // constructor global VOL_CTRL_PersonalSiteContactLookup() { @@ -77,7 +79,7 @@ global with sharing class VOL_CTRL_PersonalSiteContactLookup { list listCon = VOL_SharedCode.LookupContact(contact, null); if (listCon == null || listCon.size() == 0) { - strResult = System.Label.labelContactLookupNotFound; + strResult = System.Label.labelContactLookupAmbiguous; } else { SendEmailToContact(listCon[0]); } @@ -117,7 +119,7 @@ global with sharing class VOL_CTRL_PersonalSiteContactLookup { strResult = null; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setTargetObjectId(con.Id); - mail.setSaveAsActivity(true); + mail.setSaveAsActivity(false); mail.setTemplateID(emailTemplateId); if (orgWideEmailId != null) { mail.setOrgWideEmailAddressId(orgWideEmailId); @@ -125,7 +127,14 @@ global with sharing class VOL_CTRL_PersonalSiteContactLookup { list listSER; listSER = Messaging.sendEmail(new Messaging.Email[] { mail }, false); if (listSER[0].isSuccess()) { - strResult = System.Label.labelContactLookupSuccess; + strResult = System.Label.labelContactLookupAmbiguous; + Task taskRecord = toTask(mail); + + // We are generating a task record as a system user to log for + // the admin when users are requesting their volunteer information + /* sfge-disable-next-line ApexFlsViolationRule */ + access.insertRecords(new List{ taskRecord }); + } else { list listSEE = listSER[0].getErrors(); for (Messaging.SendEmailError see : listSEE) { @@ -141,7 +150,8 @@ global with sharing class VOL_CTRL_PersonalSiteContactLookup { // Ensure the user has access to the object and fields before querying UTIL_Describe.checkReadAccess('Contact', new Set{'Id', 'Name', 'Email'}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listCon = [select Name, Email from Contact where Id =: objId]; string strDetails = ''; if (listCon.size() > 0) @@ -152,6 +162,31 @@ global with sharing class VOL_CTRL_PersonalSiteContactLookup { } catch (Exception e) { ApexPages.addMessages(e); } - } + } + + @TestVisible + private static Task toTask(Messaging.SingleEmailMessage email) { + final String subType = 'Email'; + + Task taskRecord = new Task( + WhoId = email.getTargetObjectId(), + Subject = subType + ': ' + email.getSubject(), + ActivityDate = System.today(), + Status = getClosedTaskStatus(), + Description = email.getPlainTextBody(), + TaskSubtype = subType + ); + + return taskRecord; + } + + @TestVisible + private static String getClosedTaskStatus() { + List closedTaskStatuses = [SELECT ApiName FROM TaskStatus WHERE IsClosed = true]; + + return closedTaskStatuses.isEmpty() ? + null : + closedTaskStatuses[0].ApiName; + } } \ No newline at end of file diff --git a/src/classes/VOL_CTRL_PersonalSiteContactLookup_TEST.cls b/src/classes/VOL_CTRL_PersonalSiteContactLookup_TEST.cls index b9e5b17..70723d7 100644 --- a/src/classes/VOL_CTRL_PersonalSiteContactLookup_TEST.cls +++ b/src/classes/VOL_CTRL_PersonalSiteContactLookup_TEST.cls @@ -32,6 +32,51 @@ private with sharing class VOL_CTRL_PersonalSiteContactLookup_TEST { //==================== TEST METHOD(s) ====================================== + + @IsTest + private static void shouldInsertTaskRecordsWhenEmailSent() { + PageReference personalSiteLookupPage = new PageReference('Page.PersonalSiteContactLookup'); + Test.setCurrentPageReference(personalSiteLookupPage); + VOL_CTRL_PersonalSiteContactLookup personalSiteLookupCtrl = new VOL_CTRL_PersonalSiteContactLookup(); + + VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); + VOL_CTRL_PersonalSiteContactLookup.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + + Contact contactRecord = UTIL_UnitTest.createContact(); + insert contactRecord; + + personalSiteLookupCtrl.SendEmailToContact(contactRecord); + accessMock.assertMethodCalled('insertRecords', Task.SObjectType); + } + @IsTest + private static void shouldConvertEmailToTask() { + String expectedSubject = 'This is the subject'; + String expectedDescription = 'This the description.'; + Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage(); + + email.setSubject(expectedSubject); + email.setPlainTextBody(expectedDescription); + + Task actualTask = VOL_CTRL_PersonalSiteContactLookup.toTask(email); + System.assert(actualTask.Subject.contains(expectedSubject), 'Expected the Task subject to contain the Email\'s subject.'); + System.assert(actualTask.Description.contains(expectedDescription), 'Expected the Task subject to contain the Email\'s subject.'); + } + + @IsTest + private static void shouldReturnDefaultClosedStatus() { + final String defaultClosedStatus = 'Completed'; + + System.assertEquals(defaultClosedStatus, VOL_CTRL_PersonalSiteContactLookup.getClosedTaskStatus(), 'Expected the system to find the default closed task status.'); + } + + @IsTest + private static void shouldReturnDefaultClosedStatusWhenRunAsGuest() { + final String defaultClosedStatus = 'Completed'; + + System.runAs(UTIL_UnitTest.guestUser) { + System.assertEquals(defaultClosedStatus, VOL_CTRL_PersonalSiteContactLookup.getClosedTaskStatus(), 'Expected the system to find the default closed task status when ran as a guest user.'); + } + } @isTest(SeeAllData=true) // SeeAllData so the CSS file is viewable /******************************************************************************************************* @@ -40,17 +85,22 @@ private with sharing class VOL_CTRL_PersonalSiteContactLookup_TEST { * @return void ********************************************************************************************************/ private static void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + /** Without the ability to create sharing records in apex when owd sharing is public read/write or controlled by parent, we can no longer + * run this test as the guest user and will only run it as the admin. Commenting out so that we can reinstate + * if / when the ability to do so becomes available + * list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and Name like '%Site Guest User%']; if (listU.size() > 0) { + UTIL_UnitTest.enableElevateGuestUserAccess(); system.debug('Running test as Sites Guest User: ' + listU[0]); system.runAs(listU[0]) { TestPersonalSiteContactLookup(); } } else { + */ system.debug('Running test as system.'); TestPersonalSiteContactLookup(); - } + //} } private static void TestPersonalSiteContactLookup() { @@ -73,14 +123,14 @@ private with sharing class VOL_CTRL_PersonalSiteContactLookup_TEST { ctrl.contact.Lastname = null; ctrl.contact.Email = null; system.assertEquals(null, ctrl.LookupContact()); - system.assertEquals(System.Label.labelContactLookupNotFound, ctrl.strResult); + system.assertEquals(System.Label.labelContactLookupAmbiguous, ctrl.strResult); // test bogus contact ctrl.contact.Firstname = 'Not There'; ctrl.contact.Lastname = 'Not There'; ctrl.contact.Email = 'NotThere@NotThere.com'; system.assertEquals(null, ctrl.LookupContact()); - system.assertEquals(System.Label.labelContactLookupNotFound, ctrl.strResult); + system.assertEquals(System.Label.labelContactLookupAmbiguous, ctrl.strResult); // create a temp contact Contact con = new Contact(); @@ -90,11 +140,11 @@ private with sharing class VOL_CTRL_PersonalSiteContactLookup_TEST { insert con; // test existing contact - ctrl.contact.FirstName = 'LookupTestFirstName'; + ctrl.contact.FirstName = 'LookupTestFirstName'; ctrl.contact.LastName = 'LookupTestLastName'; ctrl.contact.Email = 'LookupTestEmail@email.com'; system.assertEquals(null, ctrl.LookupContact()); - system.assertEquals(System.Label.labelContactLookupSuccess, ctrl.strResult); + system.assertEquals(System.Label.labelContactLookupAmbiguous, ctrl.strResult); // test error email handling con.Email = null; @@ -105,7 +155,7 @@ private with sharing class VOL_CTRL_PersonalSiteContactLookup_TEST { } update con; ctrl.SendEmailToContact(con); - system.assertNotEquals(System.Label.labelContactLookupSuccess, ctrl.strResult); + system.assertNotEquals(System.Label.labelContactLookupAmbiguous, ctrl.strResult); } } \ No newline at end of file diff --git a/src/classes/VOL_CTRL_SendBulkEmail.cls b/src/classes/VOL_CTRL_SendBulkEmail.cls index 77ab876..7ce09cd 100644 --- a/src/classes/VOL_CTRL_SendBulkEmail.cls +++ b/src/classes/VOL_CTRL_SendBulkEmail.cls @@ -58,8 +58,9 @@ public with sharing class VOL_CTRL_SendBulkEmail { if (shiftId != null) { // Ensure the user has access to the object and fields before querying UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), - new Set{'Id', 'Name'}); - + new Set{'Id', 'Name', UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ Volunteer_Shift__c shift = [select Name, Volunteer_Job__r.Name from Volunteer_Shift__c where Id = :shiftId]; strJobName = shift.Volunteer_Job__r.Name + ' - ' + shift.Name; templateObject = 'Shift'; @@ -67,14 +68,16 @@ public with sharing class VOL_CTRL_SendBulkEmail { // Ensure the user has access to the object and fields before querying UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), new Set{'Id','Name'}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ Volunteer_Job__c job = [select Name from Volunteer_Job__c where Id = :jobId]; strJobName = job.Name; templateObject = 'Job'; } else if (campaignId != null) { // Ensure the user has access to the object and fields before querying UTIL_Describe.checkReadAccess('Campaign', new Set{'Id','Name'}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ Campaign cmp = [select Name from Campaign where Id = :campaignId]; strJobName = cmp.Name; templateObject = 'Campaign'; @@ -89,6 +92,8 @@ public with sharing class VOL_CTRL_SendBulkEmail { UTIL_Describe.checkReadAccess('Folder', new Set{'Id','DeveloperName'}); // get the folderId for our Volunteer email templates + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listf = [select Id from Folder where DeveloperName='Volunteers_Email_Templates']; if (listf.size() > 0) folderId = listf[0].Id; } catch (Exception e) { @@ -101,9 +106,9 @@ public with sharing class VOL_CTRL_SendBulkEmail { list listSO = new list(); listSO.add(new SelectOption('', '')); for (EmailTemplate et : [select Id, Name, Subject, Body from EmailTemplate - where isActive=true and FolderId=:folderId order by name limit 999]) { + where isActive=true and FolderId=:folderId order by name limit 999]) { listSO.add(new SelectOption(et.id, et.name)); - } + } return listSO; } @@ -114,7 +119,7 @@ public with sharing class VOL_CTRL_SendBulkEmail { listSO.add(new SelectOption('', thisUser.Name + ' <' + thisUser.Email + '>')); for (OrgWideEmailAddress owa : [select id, Address, DisplayName from OrgWideEmailAddress]) { listSO.add(new SelectOption(owa.id, owa.DisplayName + ' <' + owa.Address + '>')); - } + } return listSO; } @@ -165,19 +170,39 @@ public with sharing class VOL_CTRL_SendBulkEmail { if (template.HtmlValue != null) { isHtmlTemplate = true; - // Salesforce escapes html type email templates, we must escape when displaying html value from other types - message = template.TemplateType.equalsIgnoreCase('html') - ? template.HtmlValue - : escapeHtml(template.HtmlValue); - + message = parseHtmlBody(template.HtmlValue); } else { message = template.Body; } } + /******************************************************************************************************************* + * @description The HTML classic email template contains each section of the email within cdata tags + * @param html - The html value to parse + * @return String All parts of the html template joined by line breaks + */ + private String parseHtmlBody(String html) { + String htmlBody = html.replaceAll( '', 'CDATA_END' ); + Pattern cdataPattern = Pattern.compile( '(CDATA_START)(.*?)(CDATA_END)' ); + Matcher cdataMatcher = cdataPattern.matcher( htmlBody ); + List bodyParts = new List(); + + while ( cdataMatcher.find() ) { + String bodyPart = cdataMatcher.group().substringAfter('CDATA_START').substringBefore('CDATA_END'); + if (String.isBlank(bodyPart)) { + continue; + } + bodyParts.add(bodyPart); + } + + htmlBody = bodyParts.isEmpty() ? html : String.join(bodyParts, '
'); + + return escapeHtml(htmlBody); + } + /******************************************************************************************************************* * @description Escape the html values by doing a temporary commit to a Rich Text Area field to allow - * Salesforce to strip the non whitelisted tags for us (NOTE: You cannot rerender rich text input fields on + * Salesforce to strip the non allowlisted tags for us (NOTE: You cannot rerender rich text input fields on * Visualforce pages, https://success.salesforce.com/ideaview?id=08730000000BpHiAAK) * @param htmlValue - the value to escape * @return Volunteer_Job__c the job associated with this page @@ -190,7 +215,10 @@ public with sharing class VOL_CTRL_SendBulkEmail { try { job.Description__c = htmlValue; + // We are using Salesforce to sanitize the field then reverting the save. + /* sfge-disable-next-line ApexFlsViolationRule */ upsert job; + /* sfge-disable-next-line ApexFlsViolationRule */ escapedHtml = [SELECT Description__c FROM Volunteer_Job__c WHERE Id = :job.Id LIMIT 1].Description__c; } catch (Exception ex) { @@ -211,6 +239,10 @@ public with sharing class VOL_CTRL_SendBulkEmail { Volunteer_Job__c relatedJob = new Volunteer_Job__c(); if (shiftId != null) { + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), + new Set{'Id', UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ relatedJob.Id = [SELECT Volunteer_Job__c FROM Volunteer_Shift__c WHERE Id = :shiftId LIMIT 1].Volunteer_Job__c; return relatedJob; } @@ -221,6 +253,10 @@ public with sharing class VOL_CTRL_SendBulkEmail { } if (campaignId != null) { + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set{'Id', UTIL_Describe.StrTokenNSPrefix('Campaign__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ List jobs = [SELECT Id FROM Volunteer_Job__c WHERE Campaign__c = :campaignId LIMIT 1]; if (jobs.isEmpty()) { relatedJob.Campaign__c = campaignId; @@ -246,13 +282,19 @@ public with sharing class VOL_CTRL_SendBulkEmail { UTIL_Describe.StrTokenNSPrefix('Status__c'), UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')}); + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set{UTIL_Describe.StrTokenNSPrefix('Campaign__c')}); if (shiftId != null) { + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ listHr = [select contact__c from Volunteer_Hours__c where Status__c = :hourStatus.Status__c and Volunteer_Shift__c = :shiftId]; } else if (jobId != null){ + /* sfge-disable-next-line ApexFlsViolationRule */ listHr = [select contact__c from Volunteer_Hours__c where Status__c = :hourStatus.Status__c and Volunteer_Job__c = :jobId]; } else if (campaignId != null) { // Salesforce failed to match our campaignId against the formula field which is text, so use full reference. + /* sfge-disable-next-line ApexFlsViolationRule */ listHr = [select contact__c from Volunteer_Hours__c where Status__c = :hourStatus.Status__c and Volunteer_Job__r.Campaign__c = :campaignId ]; } @@ -294,7 +336,16 @@ public with sharing class VOL_CTRL_SendBulkEmail { // specific shift // specific job with or without shifts (or mixture) // specific campaign, with or without jobs, with or without shifts - + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), + new Set{ + UTIL_Describe.StrTokenNSPrefix('Contact__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + UTIL_Describe.StrTokenNSPrefix('Status__c') + }); + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set{ UTIL_Describe.StrTokenNSPrefix('Campaign__c') }); + UTIL_Describe.checkReadAccess('Contact', new Set{ 'Email' }); string strSoql = 'select Contact__c, Volunteer_Shift__c, Volunteer_Job__c, Volunteer_Job__r.Campaign__c from Volunteer_Hours__c ' + ' where Status__c = \'' + VOL_SharedCode.StrEscape(hourStatus.Status__c) + '\' and ' + ' Contact__r.Email != null '; @@ -310,7 +361,8 @@ public with sharing class VOL_CTRL_SendBulkEmail { // to keep track of unique contacts set setContactId = new set(); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ for (Volunteer_Hours__c hr : database.query(strSoql)) { if (!fEmailContactsOnlyOnce || setContactId.add(hr.Contact__c)) { Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); @@ -368,7 +420,8 @@ public with sharing class VOL_CTRL_SendBulkEmail { } // Ensure the user has access to the object and fields before querying UTIL_Describe.checkReadAccess('Contact', new Set{'Id', 'Name', 'Email'}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listCon = [select Name, Email from Contact where Id =: objId]; string strDetails = ''; if (listCon.size() > 0) diff --git a/src/classes/VOL_CTRL_SendBulkEmail_TEST.cls b/src/classes/VOL_CTRL_SendBulkEmail_TEST.cls index 8c8230f..798678e 100644 --- a/src/classes/VOL_CTRL_SendBulkEmail_TEST.cls +++ b/src/classes/VOL_CTRL_SendBulkEmail_TEST.cls @@ -40,7 +40,7 @@ private with sharing class VOL_CTRL_SendBulkEmail_TEST { Contact con = new Contact(Lastname='Testy', Email='testy@foo.com', AccountId=acc.Id); insert con; Campaign cmp = new Campaign(recordtypeid=VOL_SharedCode.recordtypeIdVolunteersCampaign, - name='Test Campaign', IsActive=true); + name='Test Job And Shift Campaign', IsActive=true); insert cmp; Volunteer_Job__c job = new Volunteer_Job__c(name='Job1', campaign__c=cmp.Id); insert job; @@ -116,7 +116,7 @@ private with sharing class VOL_CTRL_SendBulkEmail_TEST { Contact con = new Contact(Lastname='Testy', Email='testy@foo.com', AccountId=acc.Id); insert con; Campaign cmp = new Campaign(recordtypeid=VOL_SharedCode.recordtypeIdVolunteersCampaign, - name='Test Campaign', IsActive=true); + name='Test Campaign Job Only', IsActive=true); insert cmp; Volunteer_Job__c job = new Volunteer_Job__c(name='Job1', campaign__c=cmp.Id); insert job; @@ -242,7 +242,7 @@ private with sharing class VOL_CTRL_SendBulkEmail_TEST { */ @isTest private static void returnsJobWhenAnIdIsProvided() { - Campaign campaign = new Campaign(Name = 'Related Job Campaign'); + Campaign campaign = new Campaign(Name = 'Job Id Provided'); insert campaign; Volunteer_Job__c job = new Volunteer_Job__c(Name = 'Job to Return', Campaign__c = campaign.Id); @@ -282,13 +282,13 @@ private with sharing class VOL_CTRL_SendBulkEmail_TEST { } /******************************************************************************************************************* - * @description Verifies tags that are not whitelisted are removed from the template html value + * @description Verifies tags that are not allowlisted are removed from the template html value */ @isTest private static void returnsEscapedHtml() { VOL_CTRL_SendBulkEmail sendBulkEmail = new VOL_CTRL_SendBulkEmail(); EmailTemplate template = new EmailTemplate(TemplateType = 'custom'); - Campaign campaign = new Campaign(Name = 'Related Job Campaign'); + Campaign campaign = new Campaign(Name = 'Escaped Html Campaign'); insert campaign; sendBulkEmail.campaignId = campaign.Id; @@ -297,7 +297,7 @@ private with sharing class VOL_CTRL_SendBulkEmail_TEST { System.assertNotEquals(template.HtmlValue, escapeHtml, 'Html string should be modified: ' + escapeHtml); System.assert(escapeHtml.contains('BAD XSS:'), 'Html string should contain valid text: ' + escapeHtml); - System.assert(!escapeHtml.contains(''), 'Html string should not contain tags that are not whitelisted: ' + escapeHtml); + System.assert(!escapeHtml.contains(''), 'Html string should not contain tags that are not allowlisted: ' + escapeHtml); System.assert([SELECT Id FROM Volunteer_Job__c].isEmpty(), 'The database should be rolled back and the record created to clean the html has been removed.'); } @@ -308,6 +308,10 @@ private with sharing class VOL_CTRL_SendBulkEmail_TEST { private static void loadsEmailDetailsFromTemplate() { VOL_CTRL_SendBulkEmail sendBulkEmail = new VOL_CTRL_SendBulkEmail(); EmailTemplate template = new EmailTemplate(TemplateType = 'text'); + Campaign campaign = new Campaign(Name = 'Load Email Template Campaign'); + insert campaign; + + sendBulkEmail.campaignId = campaign.Id; template.Subject = 'Test subject'; template.Body = 'Test load from template'; diff --git a/src/classes/VOL_CTRL_VolunteersBulkEnterHours.cls b/src/classes/VOL_CTRL_VolunteersBulkEnterHours.cls index 9660645..cf0ff0d 100644 --- a/src/classes/VOL_CTRL_VolunteersBulkEnterHours.cls +++ b/src/classes/VOL_CTRL_VolunteersBulkEnterHours.cls @@ -122,6 +122,10 @@ global virtual with sharing class VOL_CTRL_VolunteersBulkEnterHours { // initial date range +- 1 month around passed in Shift or today (if no shift) Date dt; if (volunteerShiftId != null) { + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), + new Set{'Id', UTIL_Describe.StrTokenNSPrefix('Start_Date_Time__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ Volunteer_Shift__c shift = [select Start_Date_Time__c from Volunteer_Shift__c where Id = :volunteerShiftId]; dt = shift.Start_Date_Time__c.date(); } else { @@ -209,8 +213,9 @@ global virtual with sharing class VOL_CTRL_VolunteersBulkEnterHours { strSoql += ' and Start_Date__c <= :dtEnd '; } strSoql += ' order by Full_Name__c, Start_Date__c '; - - listVolunteerHours = Database.Query(strSoql); + + SObjectAccessDecision accessDecision = Security.stripInaccessible(AccessType.READABLE, Database.Query(strSoql)); + listVolunteerHours = (List) accessDecision.getRecords(); AddMoreEmptyRowsToVolunteerHours(); } @@ -235,6 +240,8 @@ global virtual with sharing class VOL_CTRL_VolunteersBulkEnterHours { UTIL_Describe.StrTokenNSPrefix('Duration__c'), UTIL_Describe.StrTokenNSPrefix('Start_Date_Time__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ Volunteer_Shift__c vs = [select Id, Duration__c, Start_Date_Time__c from Volunteer_Shift__c where Id = :volunteerShiftId]; hoursWorked = vs.Duration__c; dateStart = vs.Start_Date_Time__c.date(); @@ -308,11 +315,14 @@ global virtual with sharing class VOL_CTRL_VolunteersBulkEnterHours { listVolunteerHoursCreate.add(vh); } } - + SObjectAccessDecision updateDecision = Security.stripInaccessible(AccessType.UPDATABLE, listVolunteerHoursUpdate); + listVolunteerHoursUpdate = (List) updateDecision.getRecords(); update listVolunteerHoursUpdate; // Ensure the user can create the object UTIL_Describe.checkObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c')); + SObjectAccessDecision insertDecision = Security.stripInaccessible(AccessType.CREATABLE, listVolunteerHoursCreate); + listVolunteerHoursCreate = (List) insertDecision.getRecords(); insert listVolunteerHoursCreate; strSaveResults = String.valueOf(listVolunteerHoursCreate.size() + listVolunteerHoursUpdate.size()) + ' ' + Label.labelMassEditSaveSuccess; diff --git a/src/classes/VOL_CTRL_VolunteersCampaignWizard.cls b/src/classes/VOL_CTRL_VolunteersCampaignWizard.cls index 83391c7..1c2050e 100644 --- a/src/classes/VOL_CTRL_VolunteersCampaignWizard.cls +++ b/src/classes/VOL_CTRL_VolunteersCampaignWizard.cls @@ -33,6 +33,8 @@ * @description Page Controller class for the Volunteers Wizard visualforce page. ********************************************************************************************************/ public with sharing class VOL_CTRL_VolunteersCampaignWizard { + private static final String LAST_VIEWED_DATE = 'LastViewedDate'; + private static final String LAST_REFERENCED_DATE = 'LastReferencedDate'; // the new campaign we will create public Campaign cmpVols { @@ -104,7 +106,11 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { try { // Ensure the user can create the object - UTIL_Describe.checkObjectCreateAccess('Campaign'); + UTIL_Describe.checkObjectCreateAccess('Campaign'); + SObjectAccessDecision campaignAccessDecision = Security.stripInaccessible(AccessType.CREATABLE, new List{ cmpVols }); + cmpVols = (Campaign) campaignAccessDecision.getRecords()[0]; + // Using strip inaccessible in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ insert cmpVols; if (campaignIdClone != null) { @@ -141,10 +147,20 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { listJobs.add(job); } - // Ensure the user has access to the object and fields before querying - UTIL_Describe.checkObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')); + // Ensure the user has access to create the object and fields + UTIL_Describe.checkCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + new Set {'Name', UTIL_Describe.StrTokenNSPrefix('Campaign__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ insert listJobs; + UTIL_Describe.checkCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), + new Set{ + UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), + UTIL_Describe.StrTokenNSPrefix('Start_Date_Time__c'), + UTIL_Describe.StrTokenNSPrefix('Duration__c'), + UTIL_Describe.StrTokenNSPrefix('Description__c') + }); // create the sample shifts for (Integer iJob = 0; iJob < cSampleJobs; iJob++) { for (Integer iShift = 0; iShift < cSampleShifts; iShift++) { @@ -157,8 +173,8 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { listShifts.add(shift); } } - // Ensure the user has access to the object and fields before querying - UTIL_Describe.checkObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c')); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ insert listShifts; } @@ -223,7 +239,8 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { if (listStrFieldsShift == null) { listStrFieldsShift = new List(); Map mapS = Schema.SObjectType.Volunteer_Shift__c.fields.getMap(); - listStrFieldsShift.addAll(mapS.keySet()); + Set fieldNames = removeTabSpecificFields(mapS.keySet()); + listStrFieldsShift.addAll(fieldNames); } return listStrFieldsShift; } @@ -238,13 +255,24 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { if (listStrFieldsHours == null) { listStrFieldsHours = new List(); Map mapS = Schema.SObjectType.Volunteer_Hours__c.fields.getMap(); - listStrFieldsHours.addAll(mapS.keySet()); + Set fieldNames = removeTabSpecificFields(mapS.keySet()); + listStrFieldsHours.addAll(fieldNames); } return listStrFieldsHours; } set; } + /******************************************************************************************************* + * @description removes fields that only exist when a tab for the object has been created + * https://help.salesforce.com/s/articleView?id=000315500&type=1 + * @param Set Set of fields that might contain the fields to be returned. + */ + private Set removeTabSpecificFields(Set fieldNames) { + fieldNames.removeAll(new Set{ LAST_REFERENCED_DATE.toLowerCase(), LAST_VIEWED_DATE.toLowerCase() }); + return fieldNames; + } + /******************************************************************************************************* * @description queries for all of the Volunteer Jobs for the specified Campaign * @param campaignId the Id of the Campaign @@ -343,6 +371,8 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { } // Ensure the user has access to the object and fields before saving UTIL_Describe.checkObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')); + SObjectAccessDecision jobsAccessDecision = Security.stripInaccessible(AccessType.CREATABLE, listJobs); + listJobs = (List) jobsAccessDecision.getRecords(); insert listJobs; return listJobs; } @@ -400,6 +430,8 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { // Ensure the user has access to the object and fields before saving UTIL_Describe.checkObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c')); + SObjectAccessDecision shiftsAccessDecision = Security.stripInaccessible(AccessType.CREATABLE, listShifts); + listShifts = (List) shiftsAccessDecision.getRecords(); insert listShifts; return listShifts; } @@ -477,6 +509,8 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { // Ensure the user has access to the object and fields before saving UTIL_Describe.checkObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c')); + SObjectAccessDecision hoursAccessDecision = Security.stripInaccessible(AccessType.CREATABLE, listHours); + listHours = (List) hoursAccessDecision.getRecords(); insert listHours; return listHours; } @@ -496,7 +530,9 @@ public with sharing class VOL_CTRL_VolunteersCampaignWizard { new Set{'Id', UTIL_Describe.StrTokenNSPrefix('First_Shift__c'), UTIL_Describe.StrTokenNSPrefix('Campaign__c')}); - + + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listJobs = [select Id, First_Shift__c from Volunteer_Job__c where Campaign__c = :campaignIdClone order by First_Shift__c]; diff --git a/src/classes/VOL_CTRL_VolunteersFind.cls b/src/classes/VOL_CTRL_VolunteersFind.cls index f7f3033..c6c7c22 100644 --- a/src/classes/VOL_CTRL_VolunteersFind.cls +++ b/src/classes/VOL_CTRL_VolunteersFind.cls @@ -273,7 +273,8 @@ public with sharing class VOL_CTRL_VolunteersFind extends PageControllerBase { UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), new Set{'Id', UTIL_Describe.StrTokenNSPrefix('Start_Date_Time__c')}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ Volunteer_Shift__c vs = [select Start_Date_Time__c from Volunteer_Shift__c where Id = :volunteerShiftId]; dtStart = date.valueOf(vs.Start_Date_Time__c); } @@ -286,7 +287,8 @@ public with sharing class VOL_CTRL_VolunteersFind extends PageControllerBase { new Set{ UTIL_Describe.StrTokenNSPrefix('Contact__c'), UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c')}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ listVHExisting = [select Contact__c from Volunteer_Hours__c where Volunteer_Job__c = :volunteerJobId]; } else { // Ensure the user has access to the object and fields before querying @@ -294,7 +296,8 @@ public with sharing class VOL_CTRL_VolunteersFind extends PageControllerBase { new Set{ UTIL_Describe.StrTokenNSPrefix('Contact__c'), UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c')}); - + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ listVHExisting = [select Contact__c from Volunteer_Hours__c where Volunteer_Shift__c = :volunteerShiftId]; } set setContactId = new set(); @@ -334,6 +337,8 @@ public with sharing class VOL_CTRL_VolunteersFind extends PageControllerBase { UTIL_Describe.StrTokenNSPrefix('Status__c'), UTIL_Describe.StrTokenNSPrefix('Hours_Worked__c'), UTIL_Describe.StrTokenNSPrefix('Number_of_Volunteers__c')}); + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ insert listHours; // if shift was specified, force its picklist to update with new numbers diff --git a/src/classes/VOL_CTRL_VolunteersJobListing.cls b/src/classes/VOL_CTRL_VolunteersJobListing.cls index 058e564..6a105d0 100644 --- a/src/classes/VOL_CTRL_VolunteersJobListing.cls +++ b/src/classes/VOL_CTRL_VolunteersJobListing.cls @@ -29,6 +29,8 @@ */ global virtual with sharing class VOL_CTRL_VolunteersJobListing { + @TestVisible + private static VOL_Access access = VOL_Access.getInstance(); // page parameters that can get passed into the page to control its behavior. global ID campaignIdFilter { get; set; } @@ -77,6 +79,8 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListing { global string strURLtoCSSFile { get { if (strURLtoCSSFile == null) { + // System query to find the css doc if the admin has added it for custom css + /* sfge-disable-next-line ApexFlsViolationRule */ list listDocs = [SELECT Name, Id From Document WHERE Name = 'VolunteersJobListingCSS.css' LIMIT 1 ]; if (listDocs.size() > 0) { Document doc = listDocs[0]; @@ -170,6 +174,8 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListing { } } + SObjectAccessDecision accessDecision = Security.stripInaccessible(AccessType.READABLE, listVolunteerJobs); + listVolunteerJobs = (List) accessDecision.getRecords(); return sortVolunteerJobs(listVolunteerJobs); } @@ -251,14 +257,15 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListing { vh.Start_Date__c = system.today(); VolunteerHoursBeforeInsert(vh); - // Ensure the user has access to the object and fields before querying - UTIL_Describe.checkCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), + // Ensure the user has access to the object and fields before inserting + access.checkCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), new Set{ UTIL_Describe.StrTokenNSPrefix('Number_of_Volunteers__c'), UTIL_Describe.StrTokenNSPrefix('Status__c'), UTIL_Describe.StrTokenNSPrefix('Start_Date__c')}); - - insert vh; + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ + access.insertRecords(new List{ vh }); volunteerHoursIdSignUp = vh.Id; } diff --git a/src/classes/VOL_CTRL_VolunteersJobListingFS.cls b/src/classes/VOL_CTRL_VolunteersJobListingFS.cls index 67a46b0..325757a 100644 --- a/src/classes/VOL_CTRL_VolunteersJobListingFS.cls +++ b/src/classes/VOL_CTRL_VolunteersJobListingFS.cls @@ -29,6 +29,8 @@ */ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { + @TestVisible + private static VOL_Access access = VOL_Access.getInstance(); /** * Hard coding the subquery limit to 200 to prevent an error being displayed when * attempting to access the list of child records when more than 200 are present @@ -78,7 +80,7 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { Cookie cId = ApexPages.currentPage().getCookies().get('contactIdPersonalSite'); if (cId != null) contactIdSignUp = cId.getValue(); if (contactIdSignUp != null && contactIdSignUp != '') { - VOL_SharedCodeAPI25.LoadAndCopyObject(contactIdSignUp, contact, listStrFields); + VOL_SharedCode.LoadAndCopyObject(contactIdSignUp, contact, listStrFields); } else { // if state & country picklists enabled, we want to copy in any defaults from state/country Code to // the state & country fields, since the picklists cannot be included in the field set. @@ -119,6 +121,8 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { if (fPersonalSite) return null; // just use whatever CSS the Site Template includes. if (strURLtoCSSFile == null) { + // System query to find the css doc if the admin has added it for custom css + /* sfge-disable-next-line ApexFlsViolationRule */ list listDocs = [SELECT Name, Id From Document WHERE Name = 'VolunteersJobListingCSS.css' LIMIT 1 ]; if (listDocs.size() > 0) { Document doc = listDocs[0]; @@ -133,9 +137,9 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { } global string strGoogleMapAPIKey { - get { - return VOL_SharedCode.VolunteersSettings.Google_Maps_API_Key__c; - } + get { + return VOL_SharedCode.VolunteersSettings.Google_Maps_API_Key__c; + } } // constructor @@ -154,7 +158,7 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { ShowSkills = false; ShowShifts = true; ShowCampaignHierarchy = false; - ShowNumberAvailable = false; + ShowNumberAvailable = false; strLanguage = 'en-us'; strDateFormat = 'EEEE M/d/yyyy'; strTimeFormat = 'h:mm tt'; @@ -226,12 +230,12 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { // output error page messages for any field that doesn't have visibility correctly set. VOL_SharedCode.testObjectFieldVisibility('Contact', listStrFields); VOL_SharedCode.testObjectFieldVisibility(VOL_SharedCode.StrTokenNSPrefix('Volunteer_Job__c'), - new list{'Name','Description__c','Campaign__c','Location_Information__c','Number_of_Shifts__c','Skills_Needed__c', - 'Volunteer_Website_Time_Zone__c','Location_Street__c','First_Shift__c'}); + new list{'Name','Description__c','Campaign__c','Location_Information__c','Number_of_Shifts__c','Skills_Needed__c', + 'Volunteer_Website_Time_Zone__c','Location_Street__c','First_Shift__c'}); VOL_SharedCode.testObjectFieldVisibility(VOL_SharedCode.StrTokenNSPrefix('Volunteer_Shift__c'), - new list{'Start_Date_Time__c','Duration__c','Number_of_Volunteers_Still_Needed__c','Description__c','System_Note__c'}); - VOL_SharedCode.testObjectFieldVisibility(VOL_SharedCode.StrTokenNSPrefix('Volunteer_Hours__c'), - VOL_SharedCode.listStrFieldsFromFieldSet(Schema.SObjectType.Volunteer_Hours__c.FieldSets.VolunteersJobListingFS)); + new list{'Start_Date_Time__c','Duration__c','Number_of_Volunteers_Still_Needed__c','Description__c','System_Note__c'}); + VOL_SharedCode.testObjectFieldVisibility(VOL_SharedCode.StrTokenNSPrefix('Volunteer_Hours__c'), + VOL_SharedCode.listStrFieldsFromFieldSet(Schema.SObjectType.Volunteer_Hours__c.FieldSets.VolunteersJobListingFS)); contactIdSignUp = null; volunteerHoursIdSignUp = null; @@ -245,8 +249,8 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { listVolunteerJobs = new list(); Date dateNow = dtMonthFilter; if (nDaysToShow == 0) { - dateNow = dateNow.toStartOfMonth(); - if (dateNow < system.Today()) dateNow = system.Today(); + dateNow = dateNow.toStartOfMonth(); + if (dateNow < system.Today()) dateNow = system.Today(); } DateTime dtNow = dateNow; @@ -268,9 +272,9 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { // but are in the next month GMT. DateTime dtLast; if (nDaysToShow > 0) - dtLast = VOL_SharedCode.dtGmtFromDtTimeZone(dateNow.addDays(nDaysToShow), tz); + dtLast = VOL_SharedCode.dtGmtFromDtTimeZone(dateNow.addDays(nDaysToShow), tz); else - dtLast = VOL_SharedCode.dtGmtFromDtTimeZone(dateNow.addMonths(nMonthsToShow).toStartOfMonth(), tz); + dtLast = VOL_SharedCode.dtGmtFromDtTimeZone(dateNow.addMonths(nMonthsToShow).toStartOfMonth(), tz); if (shiftIdFilter != null && nMonthsToShow == 0) { listVolunteerJobs = [select Id, Name, Campaign__c, Campaign__r.IsActive, Campaign__r.Name, Campaign__r.StartDate, Campaign__r.Volunteer_Website_Time_Zone__c, @@ -320,29 +324,31 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { // now remove any jobs whose shifts are all past date. // unless the user links directly to the job if (shiftIdFilter == null && jobIdFilter == null) { - for (integer i = listVolunteerJobs.size() - 1; i >= 0; i--) { - Volunteer_Job__c job = listVolunteerJobs[i]; - if (job.Number_of_Shifts__c > 0 && job.Volunteer_Job_Slots__r.size() == 0) { - listVolunteerJobs.remove(i); - } - } + for (integer i = listVolunteerJobs.size() - 1; i >= 0; i--) { + Volunteer_Job__c job = listVolunteerJobs[i]; + if (job.Number_of_Shifts__c > 0 && job.Volunteer_Job_Slots__r.size() == 0) { + listVolunteerJobs.remove(i); + } + } } // if one event was selected; check if the event was in the past jobAllShiftsInThePast = false; if (listVolunteerJobs.size() == 1) { - Volunteer_Job__c job = listVolunteerJobs[0]; - jobAllShiftsInThePast = 0 < job.Number_of_Shifts__c; - for (Volunteer_Shift__c shift : job.Volunteer_Job_Slots__r) { - if (date.today() <= shift.Start_Date_Time__c) { - jobAllShiftsInThePast = false; - break; - } - } + Volunteer_Job__c job = listVolunteerJobs[0]; + jobAllShiftsInThePast = 0 < job.Number_of_Shifts__c; + for (Volunteer_Shift__c shift : job.Volunteer_Job_Slots__r) { + if (date.today() <= shift.Start_Date_Time__c) { + jobAllShiftsInThePast = false; + break; + } + } } } + SObjectAccessDecision accessDecision = Security.stripInaccessible(AccessType.READABLE, listVolunteerJobs); + listVolunteerJobs = (List) accessDecision.getRecords(); VOL_SharedCode.dateTimeFixup(listVolunteerJobs, strDateFormat, strTimeFormat); return sortVolunteerJobs(listVolunteerJobs); } @@ -437,6 +443,8 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { UTIL_Describe.StrTokenNSPrefix('Duration__c')}); // make sure we don't go over the number of volunteers still needed on the shift. + // Using a dynamic describe access check in the method called above. + /* sfge-disable-next-line ApexFlsViolationRule */ list listShift = [select Number_of_Volunteers_Still_Needed__c, Start_Date_Time__c, Duration__c from Volunteer_Shift__c where Id = :shiftIdSignUp]; if (listShift != null) { if (vhours.Number_of_Volunteers__c > listShift[0].Number_of_Volunteers_Still_Needed__c) { @@ -455,7 +463,7 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { if (cId != null) contactIdPersonalSite = cId.getValue(); // save or update the contact - contactIdSignUp = VOL_SharedCode.CreateOrUpdateContactFS(contactIdPersonalSite, contact, contact.Volunteer_Organization__c, listStrFields, true); + contactIdSignUp = VOL_SharedCode.CreateOrUpdateContactFS(contactIdPersonalSite, contact, contact.Volunteer_Organization__c, listStrFields, true); // upload any attachment if (contactIdSignUp != null && attachment != null && attachment.body != null) { @@ -463,11 +471,12 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { attachment.Description = strFileUploadLabel + ' ' + system.label.labelFileUploadDescription; // Ensure the user has access to the object and fields before querying - UTIL_Describe.checkCreateAccess('Attachment', new Set{'ParentId', 'Description'}); - insert attachment; + access.checkCreateAccess('Attachment', new Set{'ParentId', 'Description'}); + access.insertRecords(new List{ attachment }); + attachment = new Attachment(); } - + // then Update or Create hours if it was successful. if (contactIdSignUp != null) { // if signing up for a specific shift, see if they already have an Hours record. @@ -504,14 +513,13 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { } // update the status if not already Confirmed or Completed if (listVHours[0].Status__c != 'Confirmed' && listVHours[0].Status__c != 'Completed') - listVHours[0].Status__c = 'Web Sign Up'; - + listVHours[0].Status__c = 'Web Sign Up'; + VolunteerHoursBeforeInsert(listVHours[0]); // Ensure the user has access to the object and fields before updating - UTIL_Describe.checkUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), vhFieldsToUpdate); - - update listVHours[0]; + access.checkUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), vhFieldsToUpdate); + access.updateRecords(new List{ listVHours[0] }); volunteerHoursIdSignUp = listVHours[0].Id; } else { vhours.Contact__c = contactIdSignUp; @@ -523,7 +531,7 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { VolunteerHoursBeforeInsert(vhours); // Ensure the user has access to the object and fields before querying - UTIL_Describe.checkCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), + access.checkCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), new Set{ UTIL_Describe.StrTokenNSPrefix('Contact__c'), UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), @@ -531,7 +539,7 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { UTIL_Describe.StrTokenNSPrefix('Start_Date__c'), UTIL_Describe.StrTokenNSPrefix('Hours_Worked__c')}); - insert vhours; + access.insertRecords(new List{ vhours }); volunteerHoursIdSignUp = vhours.Id; } @@ -567,8 +575,8 @@ global virtual with sharing class VOL_CTRL_VolunteersJobListingFS { set; } - // this was a testmethod which can't be compiled in this class under api 31. - // but because it was marked global, it could not be completely removed. + // this was a testmethod which can't be compiled in this class under api 31. + // but because it was marked global, it could not be completely removed. global static void UnitTest1() {} } \ No newline at end of file diff --git a/src/classes/VOL_CTRL_VolunteersJobListingFS_TEST.cls b/src/classes/VOL_CTRL_VolunteersJobListingFS_TEST.cls index 73dac06..049dca5 100644 --- a/src/classes/VOL_CTRL_VolunteersJobListingFS_TEST.cls +++ b/src/classes/VOL_CTRL_VolunteersJobListingFS_TEST.cls @@ -28,13 +28,76 @@ POSSIBILITY OF SUCH DAMAGE. */ -@isTest +@IsTest private with sharing class VOL_CTRL_VolunteersJobListingFS_TEST { private static VOL_CTRL_VolunteersJobListingFS jobListingCtrl; + private static VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); private static final Integer QUERY_LIMIT = VOL_CTRL_VolunteersJobListingFS.SUBQUERY_LIMIT; private static final Integer SUBQUERY_ERROR_THRESHOLD = 251; // causes query exception with test data at this number //==================== TEST METHOD(s) ====================================== + @IsTest + private static void shouldCheckCreateAccessAndInsertHours() { + setupPage(new Map()); + setAccessMock(); + jobListingCtrl.contact = UTIL_UnitTest.createContact('Signup Insert ' + DateTime.now().getTime()); + + jobListingCtrl.VolunteerShiftSignUp(); + // The contact creation is completed by a separate class, VOL_SharedCode + accessMock.assertMethodCalled('checkCreateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('insertRecords', Volunteer_Hours__c.SObjectType); + } + + @IsTest + private static void shouldCheckCreateAccessAndInsertAttachment() { + Contact existingContact = UTIL_UnitTest.createContact('Signup Update ' + DateTime.now().getTime()); + insert existingContact; + + setupPage(new Map()); + setAccessMock(); + jobListingCtrl.contact = existingContact; + jobListingCtrl.attachment = new Attachment(Body = Blob.valueOf('Signup attachment insert...')); + + jobListingCtrl.VolunteerShiftSignUp(); + accessMock.assertMethodCalled('checkCreateAccess', Attachment.SObjectType); + accessMock.assertMethodCalled('checkCreateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('insertRecords', Attachment.SObjectType); + accessMock.assertMethodCalled('insertRecords', Volunteer_Hours__c.SObjectType); + } + + @IsTest + private static void shouldCheckUpdateAccessAndUpdateHours() { + generateData(1); + + + Contact existingContact = UTIL_UnitTest.createContact('Signup Update ' + DateTime.now().getTime()); + insert existingContact; + + Id jobId = [SELECT Id FROM Volunteer_Job__c LIMIT 1].Id; + Id shiftId = [SELECT Id FROM Volunteer_Shift__c Where Volunteer_Job__c = :jobId LIMIT 1].Id; + Volunteer_Hours__c hours = new Volunteer_Hours__c( + Contact__c = existingContact.Id, + Volunteer_Job__c = jobId, + Volunteer_Shift__c = shiftId, + Status__c = 'Web Sign Up', + Start_Date__c = System.today(), + Hours_Worked__c = 1, + Number_of_Volunteers__c = 1 + ); + insert hours; + + setupPage(new Map()); + setAccessMock(); + jobListingCtrl.shiftIdSignUp = shiftId; + jobListingCtrl.jobIdSignUp = jobId; + jobListingCtrl.contact = existingContact; + jobListingCtrl.vhours.Number_of_Volunteers__c = 2; + + jobListingCtrl.VolunteerShiftSignUp(); + accessMock.assertMethodCalled('checkUpdateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('updateRecords', Volunteer_Hours__c.SObjectType); + } + @IsTest private static void shouldReturnOneShiftWhenJobIdAndShiftIdAreProvidedWithZeroMonths() { generateData(10); @@ -144,18 +207,24 @@ private with sharing class VOL_CTRL_VolunteersJobListingFS_TEST { * exists. if not, will run under the current user. * @return void ********************************************************************************************************/ - private static testmethod void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + @IsTest + private static void testPageWithSitesGuestUser() { + /** Without the ability to create sharing records in apex when owd sharing is public read/write or controlled by parent, we can no longer + * run this test as the guest user and will only run it as the admin. Commenting out so that we can reinstate + * if / when the ability to do so becomes available + * list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and Name like '%Site Guest User%']; if (listU.size() > 0) { + UTIL_UnitTest.enableElevateGuestUserAccess(); system.debug('Running test as Sites Guest User: ' + listU[0]); system.runAs(listU[0]) { UnitTest1(); } } else { + */ system.debug('Running test as system.'); UnitTest1(); - } + //} } private static void UnitTest1() { @@ -356,4 +425,8 @@ private with sharing class VOL_CTRL_VolunteersJobListingFS_TEST { } } + private static void setAccessMock() { + VOL_CTRL_VolunteersJobListingFS.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + } + } \ No newline at end of file diff --git a/src/classes/VOL_CTRL_VolunteersJobListing_TEST.cls b/src/classes/VOL_CTRL_VolunteersJobListing_TEST.cls index 44f0d3a..c576d8d 100644 --- a/src/classes/VOL_CTRL_VolunteersJobListing_TEST.cls +++ b/src/classes/VOL_CTRL_VolunteersJobListing_TEST.cls @@ -32,23 +32,44 @@ private with sharing class VOL_CTRL_VolunteersJobListing_TEST { //==================== TEST METHOD(s) ====================================== + private static void shouldCheckCreateAccessAndInsertHours() { + VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); + PageReference volunteersJobListPage = Page.VolunteersJobListing; + Test.setCurrentPage(volunteersJobListPage); + VOL_CTRL_VolunteersJobListing.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + VOL_CTRL_VolunteersJobListing jobListingCtrl = new VOL_CTRL_VolunteersJobListing(); + + jobListingCtrl.contact = UTIL_UnitTest.createContact('Signup Insert ' + DateTime.now().getTime()); + + jobListingCtrl.VolunteerShiftSignUp(); + // The contact creation is completed by a separate class, VOL_SharedCode + accessMock.assertMethodCalled('checkCreateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('insertRecords', Volunteer_Hours__c.SObjectType); + } + /******************************************************************************************************* * @description test the visualforce page controller, running as the Sites Guest User, if such as user * exists. if not, will run under the current user. * @return void ********************************************************************************************************/ - private static testmethod void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + @IsTest + private static void testPageWithSitesGuestUser() { + /** Without the ability to create sharing records in apex when owd sharing is public read/write or controlled by parent, we can no longer + * run this test as the guest user and will only run it as the admin. Commenting out so that we can reinstate + * if / when the ability to do so becomes available + * list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and Name like '%Site Guest User%']; if (listU.size() > 0) { + UTIL_UnitTest.enableElevateGuestUserAccess(); system.debug('Running test as Sites Guest User: ' + listU[0]); system.runAs(listU[0]) { UnitTest1(); } } else { + */ system.debug('Running test as system.'); UnitTest1(); - } + //} } private static void UnitTest1() { @@ -186,5 +207,5 @@ private with sharing class VOL_CTRL_VolunteersJobListing_TEST { VOL_CTRL_VolunteersJobListing ctrl2 = new VOL_CTRL_VolunteersJobListing(); system.assertEquals(mapCmp.size(), ctrl2.listVolunteerJobs.size()); } - + } \ No newline at end of file diff --git a/src/classes/VOL_CTRL_VolunteersReportHours.cls b/src/classes/VOL_CTRL_VolunteersReportHours.cls index 7df38c1..4d54ee1 100644 --- a/src/classes/VOL_CTRL_VolunteersReportHours.cls +++ b/src/classes/VOL_CTRL_VolunteersReportHours.cls @@ -29,9 +29,11 @@ */ global virtual with sharing class VOL_CTRL_VolunteersReportHours { - - private VOL_SharedCode volSharedCode; - + private VOL_SharedCode volSharedCode; + + @TestVisible + private static VOL_Access access = VOL_Access.getInstance(); + // constructor global VOL_CTRL_VolunteersReportHours() { try { @@ -79,7 +81,7 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { Cookie cId = ApexPages.currentPage().getCookies().get('contactIdPersonalSite'); if (cId != null) contactIdPersonalSite = cId.getValue(); if (contactIdPersonalSite != null && contactIdPersonalSite != '') { - VOL_SharedCodeAPI25.LoadAndCopyObject(contactIdPersonalSite, contact, listStrFields); + VOL_SharedCode.LoadAndCopyObject(contactIdPersonalSite, contact, listStrFields); } else { // if state & country picklists enabled, we want to copy in any defaults from state/country Code to // the state & country fields, since the picklists cannot be included in the field set. @@ -153,6 +155,15 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { if (listSOVolunteerJobs == null) { listSOVolunteerJobs = new list(); listSOVolunteerJobs.add(new SelectOption('', '')); + + + // Ensure the user has access to the object before querying + try { + UTIL_Describe.checkObjectReadAccess(String.valueOf(Volunteer_Job__c.SObjectType)); + } catch (Exception ex) { + // we will return an empty list vs throwing an error + return listSOVolunteerJobs; + } Boolean filterByContact = VOL_SharedCode.VolunteersSettings.Personal_Site_Report_Hours_Filtered__c; List volunteerJobs = new List(); @@ -233,6 +244,12 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { // set date and hours from shift if (volunteerShiftId != null) { + UTIL_Describe.checkReadAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Shift__c'), + new Set { + 'Id', + UTIL_Describe.StrTokenNSPrefix('Start_Date_Time__c'), + UTIL_Describe.StrTokenNSPrefix('Duration__c') + }); Volunteer_Shift__c shift = [select Start_Date_Time__c, Duration__c from Volunteer_Shift__c where Id = :volunteerShiftId]; vhours.Start_Date__c = shift.Start_Date_Time__c.Date(); @@ -333,7 +350,7 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { } } - update listHours[0]; + access.updateRecords(new List{ listHours[0] }); } else { checkCreateAccess(); @@ -343,7 +360,7 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { vhours.Status__c = 'Completed'; vhours.Contact__c = contactId; vhours.Number_of_Volunteers__c = 1; - insert vhours; + access.insertRecords(new List{ vhours }); } // clear current job & shift info @@ -374,7 +391,7 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { * @return void. ********************************************************************************************************/ private void checkCreateAccess() { - UTIL_Describe.checkCreateAccess( + access.checkCreateAccess( UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), new Set{ UTIL_Describe.StrTokenNSPrefix('Volunteer_Job__c'), @@ -405,7 +422,7 @@ global virtual with sharing class VOL_CTRL_VolunteersReportHours { } } - UTIL_Describe.checkUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), flsCheckFields); + access.checkUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteer_Hours__c'), flsCheckFields); } // this was a testmethod which can't be compiled in this class under api 31. diff --git a/src/classes/VOL_CTRL_VolunteersReportHours_TEST.cls b/src/classes/VOL_CTRL_VolunteersReportHours_TEST.cls index 7a98b7c..a222406 100644 --- a/src/classes/VOL_CTRL_VolunteersReportHours_TEST.cls +++ b/src/classes/VOL_CTRL_VolunteersReportHours_TEST.cls @@ -30,26 +30,91 @@ @isTest private with sharing class VOL_CTRL_VolunteersReportHours_TEST { + private static VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); + private static PageReference reportHoursPage = Page.VolunteersReportHours; //==================== TEST METHOD(s) ====================================== + @IsTest + private static void shouldCheckAccessOnPageLoad() { + Test.setCurrentPage(reportHoursPage); + setAccessMock(); + VOL_CTRL_VolunteersReportHours reportHoursCtrl = new VOL_CTRL_VolunteersReportHours(); + + accessMock.assertMethodCalled('checkCreateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('checkUpdateAccess', Volunteer_Hours__c.SObjectType); + } + + @IsTest + private static void shouldCheckCreateAccessAndInsertHoursOnSave() { + UTIL_UnitTest.generateData(); + Contact contactRecord; + Id jobId; + + contactRecord = (Contact) UTIL_UnitTest.getSObject(Contact.SObjectType); + jobId = UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType); + Volunteer_Hours__c hours = UTIL_UnitTest.createHours(contactRecord.Id, jobId, null); + + Test.setCurrentPage(reportHoursPage); + VOL_CTRL_VolunteersReportHours reportHoursCtrl = new VOL_CTRL_VolunteersReportHours(); + setAccessMock(); + reportHoursCtrl.contact = contactRecord; + reportHoursCtrl.volunteerJobId = jobId; + reportHoursCtrl.vhours = hours; + reportHoursCtrl.Save(); + + accessMock.assertMethodCalled('checkCreateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('insertRecords', Volunteer_Hours__c.SObjectType); + } + + @IsTest + private static void shouldCheckUpdateAccessAndUpdateHoursOnSave() { + UTIL_UnitTest.generateData(); + Contact contactRecord; + Id jobId; + + contactRecord = (Contact) UTIL_UnitTest.getSObject(Contact.SObjectType); + jobId = UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType); + Volunteer_Hours__c hours = UTIL_UnitTest.createHours(contactRecord.Id, jobId, null); + insert hours; + + Test.setCurrentPage(reportHoursPage); + VOL_CTRL_VolunteersReportHours reportHoursCtrl = new VOL_CTRL_VolunteersReportHours(); + setAccessMock(); + reportHoursCtrl.contact = contactRecord; + reportHoursCtrl.volunteerJobId = jobId; + reportHoursCtrl.vhours = hours; + reportHoursCtrl.Save(); + + accessMock.assertMethodCalled('checkUpdateAccess', Volunteer_Hours__c.SObjectType); + accessMock.assertMethodCalled('updateRecords', Volunteer_Hours__c.SObjectType); + } /******************************************************************************************************* * @description test the visualforce page controller, running as the Sites Guest User, if such as user * exists. if not, will run under the current user. * @return void ********************************************************************************************************/ - private static testmethod void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + @IsTest + private static void testPageWithSitesGuestUser() { + /** Without the ability to create sharing records in apex when owd sharing is public read/write or controlled by parent, we can no longer + * run this test as the guest user and will only run it as the admin. Commenting out so that we can reinstate + * if / when the ability to do so becomes available + * list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and Name like '%Site Guest User%']; if (listU.size() > 0) { + UTIL_UnitTest.enableElevateGuestUserAccess(); system.debug('Running test as Sites Guest User: ' + listU[0]); system.runAs(listU[0]) { CodeCoverageTests(); } } else { + */ system.debug('Running test as system.'); CodeCoverageTests(); - } + //} + } + private static void setAccessMock() { + VOL_CTRL_VolunteersReportHours.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); } private static void CodeCoverageTests() { diff --git a/src/classes/VOL_CTRL_VolunteersSignupFS_TEST.cls b/src/classes/VOL_CTRL_VolunteersSignupFS_TEST.cls index efa1ce8..3fd203c 100644 --- a/src/classes/VOL_CTRL_VolunteersSignupFS_TEST.cls +++ b/src/classes/VOL_CTRL_VolunteersSignupFS_TEST.cls @@ -39,18 +39,24 @@ private with sharing class VOL_CTRL_VolunteersSignupFS_TEST { * exists. if not, will run under the current user. * @return void ********************************************************************************************************/ - private static testmethod void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + @IsTest + private static void testPageWithSitesGuestUser() { + /** Without the ability to create sharing records in apex when owd sharing is public read/write or controlled by parent, we can no longer + * run this test as the guest user and will only run it as the admin. Commenting out so that we can reinstate + * if / when the ability to do so becomes available + * list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and Name like '%Site Guest User%']; if (listU.size() > 0) { + UTIL_UnitTest.enableElevateGuestUserAccess(); system.debug('Running test as Sites Guest User: ' + listU[0]); system.runAs(listU[0]) { CodeCoverageTests(); } } else { + */ system.debug('Running test as system.'); CodeCoverageTests(); - } + //} } private static void CodeCoverageTests() { diff --git a/src/classes/VOL_CTRL_VolunteersSignup_TEST.cls b/src/classes/VOL_CTRL_VolunteersSignup_TEST.cls index d45cb3a..db8f8b3 100644 --- a/src/classes/VOL_CTRL_VolunteersSignup_TEST.cls +++ b/src/classes/VOL_CTRL_VolunteersSignup_TEST.cls @@ -38,18 +38,24 @@ private with sharing class VOL_CTRL_VolunteersSignup_TEST { * exists. if not, will run under the current user. * @return void ********************************************************************************************************/ - private static testmethod void testPageWithSitesGuestUser() { - list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and + @IsTest + private static void testPageWithSitesGuestUser() { + /** Without the ability to create sharing records in apex when owd sharing is public read/write or controlled by parent, we can no longer + * run this test as the guest user and will only run it as the admin. Commenting out so that we can reinstate + * if / when the ability to do so becomes available + * list listU = [Select Username, UserType, Name, IsActive, Id From User where IsActive = true and UserType = 'Guest' and Name like '%Site Guest User%']; if (listU.size() > 0) { + UTIL_UnitTest.enableElevateGuestUserAccess(); system.debug('Running test as Sites Guest User: ' + listU[0]); system.runAs(listU[0]) { CodeCoverageTests(); } } else { + */ system.debug('Running test as system.'); CodeCoverageTests(); - } + //} } private static void CodeCoverageTests() { diff --git a/src/classes/VOL_SharedCode.cls b/src/classes/VOL_SharedCode.cls index 3732c7c..280cf5c 100644 --- a/src/classes/VOL_SharedCode.cls +++ b/src/classes/VOL_SharedCode.cls @@ -1,877 +1,973 @@ -/* - Copyright (c) 2016, Salesforce.org - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Salesforce.org nor the names of - its contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. -*/ - -global with sharing class VOL_SharedCode { - - // the list of Campaigns that have Volunteer Jobs - global list listSOCampaignsWithJobs { - get { - list listSO = new list(); - listSO.add(new SelectOption('', '')); - for (Campaign c : [select Name, Id, StartDate from Campaign where RecordTypeId = :recordtypeIdVolunteersCampaign - and IsActive = true order by StartDate desc, Name asc limit 999]) { - listSO.add(new SelectOption(c.id, c.name)); - } - return listSO; - } - } - - // the list of Volunteer Jobs for the specified Campaign - global list listSOVolunteerJobsOfCampaignId(ID campaignId) { - list listSO = new list(); - listSO.add(new SelectOption('', '')); - for (Volunteer_Job__c vj : [select Name, Id from Volunteer_Job__c where Campaign__c = :campaignId order by name limit 999]) { - listSO.add(new SelectOption(vj.id, vj.name)); - } - return listSO; - } - - // the list of Volunteer Job Shifts for the specified Job - global list listSOVolunteerShiftsOfVolunteerJobId(ID volunteerJobId, Date dtStart, Date dtEnd, boolean fIncludeShiftName, boolean fIncludeNumberNeeded) { - return listSOVolunteerShiftsOfVolunteerJobIdFormat(volunteerJobId, dtStart, dtEnd, fIncludeShiftName, fIncludeNumberNeeded, null, null); - } - - // list of select options of Shifts for the specified job, using the date & time format strings for the shifts - public static list listSOVolunteerShiftsOfVolunteerJobIdFormat(ID volunteerJobId, Date dtStart, Date dtEnd, - boolean fIncludeShiftName, boolean fIncludeNumberNeeded, string strDateFormat, string strTimeFormat) { - - list listVolunteerJobs = new List(); - - list listSO = new list(); - listSO.add(new SelectOption('', '')); - - // ensure valid date ranges - if (dtStart == null) - dtStart = system.today(); - if (dtEnd == null) - dtEnd = system.today().addMonths(12); - dtEnd = dtEnd.addDays(1); - - // get our shifts in a Job query, so we can use our common date/time formatting routine. - listVolunteerJobs = [select Id, Campaign__r.IsActive, Campaign__r.Volunteer_Website_Time_Zone__c, - Volunteer_Website_Time_Zone__c, - (Select Id, Name, Start_Date_Time__c, Duration__c, Number_of_Volunteers_Still_Needed__c, - Description__c, System_Note__c From Volunteer_Job_Slots__r - where Start_Date_Time__c >= :dtStart and Start_Date_Time__c < :dtEnd - order by Start_Date_Time__c LIMIT 999) - from Volunteer_Job__c where Id = :volunteerJobId]; - - // bail out if no jobs found - if (listVolunteerJobs.size() == 0) - return listSO; - - // whether to use our datetime formatting, or salesforce default for the current user - boolean useDateTimeFixup = (strDateFormat != null && strTimeFormat != null); - - // put correct date/time format with appropriate timezone in system note field (in memory only) - if (useDateTimeFixup) - dateTimeFixup(listVolunteerJobs, strDateFormat, strTimeFormat); - - for (Volunteer_Shift__c vs : listVolunteerJobs[0].Volunteer_Job_Slots__r) { - SelectOption so = new SelectOption(vs.id, - (useDateTimeFixup ? vs.System_Note__c : vs.Start_Date_Time__c.format()) + - (fIncludeShiftName ? '    (' + vs.name + ')' : '' ) + - (fIncludeNumberNeeded ? '  ' + - (vs.Number_of_Volunteers_Still_Needed__c > 0 ? - system.label.labelCalendarStillNeeded + vs.Number_of_Volunteers_Still_Needed__c : system.label.labelCalendarShiftFull) + - ' ' : '' )); - so.setEscapeItem(false); - listSO.add(so); - } - return listSO; - } - - // routine to go through all the shifts, and create the display string - // for the shifts start date & time - end date & time, using the appropriate - // time zone that might be specified on the Job, Campaign, or Site Guest User. - // Note that it stores this string in the Shift's System_Note__c field (in memory only). - public static void dateTimeFixup(list listJob, string strDateFormat, string strTimeFormat) { - // get default time zone for site guest user - User u = [Select TimeZoneSidKey From User where id =: Userinfo.getUserId()]; - - // javascript formatting used 'tt' for am/pm, whereas apex formatting uses 'a'. - string strFormat = strDateFormat + ' ' + strTimeFormat.replace('tt','a'); - string strFormatEndTime = strTimeFormat.replace('tt','a'); - - for (Volunteer_Job__c job : listJob) { - string strTimeZone = job.Volunteer_Website_Time_Zone__c; - if (strTimeZone == null) strTimeZone = job.Campaign__r.Volunteer_Website_Time_Zone__c; - if (strTimeZone == null) strTimeZone = u.TimeZoneSidKey; - - for (Volunteer_Shift__c shift : job.Volunteer_Job_Slots__r) { - - DateTime dtEnd = shift.Start_Date_Time__c.addMinutes(integer.valueOf(shift.Duration__c * 60)); - string strStart = shift.Start_Date_Time__c.format(strFormat, strTimeZone); - - // see if start and end are on the same day - if (shift.Start_Date_Time__c.format('d', strTimeZone) == dtEnd.format('d', strTimeZone)) { - shift.System_Note__c = strStart + ' - ' + dtEnd.format(strFormatEndTime, strTimeZone); - } else { - shift.System_Note__c = strStart + ' - ' + dtEnd.format(strFormat, strTimeZone); - } - } - } - } - - // return the GMT datetime for a datetime in the specified timezone - public static DateTime dtGmtFromDtTimeZone(DateTime dt, TimeZone tz) { - integer offset = tz.getOffset(dt); - return dt.addSeconds(-offset / 1000); - } - - // Volunteer Custom Settings object. Loads an existing, and if not found creates one with default values. - global static Volunteers_Settings__c VolunteersSettings { - get { - if (VolunteersSettings == null) { - VolunteersSettings = Volunteers_Settings__c.getInstance(); - - if (VolunteersSettings.Id == null) { - VolunteersSettings = Volunteers_Settings__c.getOrgDefaults(); - } - - if (VolunteersSettings.Id == null) { - VolunteersSettings.Setupownerid = UserInfo.getOrganizationId(); - - // create reasonable defaults - VolunteersSettings.Signup_Matches_Existing_Contacts__c = false; - VolunteersSettings.Signup_Creates_Contacts_If_No_Match__c = false; - VolunteersSettings.Signup_Bucket_Account_On_Create__c = null; - VolunteersSettings.Recurring_Job_Future_Months__c = 4; - VolunteersSettings.Contact_Match_Email_Fields__c = null; - VolunteersSettings.Contact_Match_First_Name_Fields__c = null; - VolunteersSettings.Personal_Site_Org_Wide_Email_Name__c = null; - VolunteersSettings.Contact_Matching_Rule__c = 'Firstname;Lastname;Email'; - VolunteersSettings.Personal_Site_Report_Hours_Filtered__c = false; - if (UTIL_Describe.hasObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteers_Settings__c'))) { - insert VolunteersSettings; - } - } else if (VolunteersSettings.Contact_Matching_Rule__c == null) { - VolunteersSettings.Contact_Matching_Rule__c = 'Firstname;Lastname;Email'; - if (UTIL_Describe.hasObjectUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteers_Settings__c'))) { - update VolunteersSettings; - } - } - } - return VolunteersSettings; - } - - set; - } - - // helper to get the AccoutId of the Bucket Account specified in Custom Settings. - global static ID SettingsBucketAccountId { - get { - if (SettingsBucketAccountId == null) { - if (VolunteersSettings.Signup_Bucket_Account_On_Create__c != null) { - Account[] acc = [select Id from Account where name = :VolunteersSettings.Signup_Bucket_Account_On_Create__c limit 1]; - if (acc.size() > 0) SettingsBucketAccountId = acc[0].Id; - } - } - return SettingsBucketAccountId; - } - - set; - } - - // test helper that allows one to override the users's Custom Settings with the settings we want to test with. - global static Volunteers_Settings__c getVolunteersSettingsForTests(Volunteers_Settings__c mySettings) { - //clear out whatever settings exist - delete [select id from Volunteers_Settings__c]; - SettingsBucketAccountId = null; - - //create our own based on what's passed in from the test - VolunteersSettings = new Volunteers_Settings__c ( - Signup_Matches_Existing_Contacts__c = mySettings.Signup_Matches_Existing_Contacts__c, - Signup_Creates_Contacts_If_No_Match__c = mySettings.Signup_Creates_Contacts_If_No_Match__c, - Signup_Bucket_Account_On_Create__c = mySettings.Signup_Bucket_Account_On_Create__c, - Recurring_Job_Future_Months__c = mySettings.Recurring_Job_Future_Months__c, - Contact_Match_Email_Fields__c = mySettings.Contact_Match_Email_Fields__c, - Contact_Match_First_Name_Fields__c = mySettings.Contact_Match_First_Name_Fields__c, - Contact_Matching_Rule__c = mySettings.Contact_Matching_Rule__c, - Personal_Site_Org_Wide_Email_Name__c = mySettings.Personal_Site_Org_Wide_Email_Name__c, - Personal_Site_Report_Hours_Filtered__c = mySettings.Personal_Site_Report_Hours_Filtered__c - ); - - insert VolunteersSettings; - return VolunteersSettings; - } - - // global helper to get the Volunteers Campaign recordtype. - private class MyException extends Exception {} - global static Id recordtypeIdVolunteersCampaign { - get { - if (recordtypeIdVolunteersCampaign == null) { - list listRT = [SELECT Id FROM RecordType WHERE DeveloperName='Volunteers_Campaign']; - if (listRT.size() == 0) { - throw (new MyException('The Volunteers Campaign Record Type is missing and must be restored.')); - } - recordtypeIdVolunteersCampaign = listRT[0].Id; - } - return recordtypeIdVolunteersCampaign; - } - set; - } - - // shared routine to get all Fields names from the specified Field Set on Contact - // also explicitly adds additional Contact fields that we will always use in our Sites pages. - global static list listStrFieldsFromContactFieldSet(Schema.FieldSet fs) { - set setStrFields = new set(); - for (Schema.FieldSetMember f : fs.getFields()) { - setStrFields.add(f.getFieldPath().toLowerCase()); - } - // also add the fields we explicitly refer to in CreateOrUpdateContactFS() - // we use a set (with lowercase) to avoid creating duplicates. - setStrFields.add('firstname'); - setStrFields.add('lastname'); - setStrFields.add('email'); - //setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_status__c').tolowerCase()); - //setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_notes__c').toLowerCase()); - list listStrFields = new list(); - listStrFields.addAll(setStrFields); - return listStrFields; - } - - // shared routine to get all Fields names form the specified Field Set - global static list listStrFieldsFromFieldSet(Schema.FieldSet fs) { - list listStrFields = new list(); - for (Schema.FieldSetMember f : fs.getFields()) { - listStrFields.add(f.getFieldPath()); - } - return listStrFields; - } - - // global code to create a new lead or contact for web volunteer signup. - // this code is used by both the VolunteersSignup page, and the VolunteersJobListing page. - // it uses the custom setting for the bucket account, but takes parameters for - // matching existing contacts, and create contacts vs. leads. this is because the two pages have different use cases. - // it also assumes that the contact that is passed in is the dummy record from the web page, and thus isn't real, and - // uses the Department field to track the user's company name. - global static ID CreateContactOrLead(Contact contact, boolean fMatchExistingContacts, boolean fCreateContacts) { - // update the date before we start - contact.Volunteer_Last_Web_Signup_Date__c = system.today(); - - // let's see if we can find any matching Contacts. - list listCon = [select Id, Lastname, Firstname, Email, Phone, HomePhone, - Volunteer_Availability__c, Volunteer_Notes__c, Volunteer_Last_Web_Signup_Date__c, - Volunteer_Status__c, Volunteer_Skills__c, Volunteer_Organization__c from Contact - where Lastname=:contact.Lastname and Firstname=:contact.Firstname and Email=:contact.Email]; - - Set setFlds = new Set{'Lastname', 'Firstname', 'Email', 'Phone', 'HomePhone', - UTIL_Describe.StrTokenNSPrefix('Volunteer_Availability__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Notes__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Last_Web_Signup_Date__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Status__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Skills__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Organization__c')}; - - // if we can match existing contacts, and we found a match, update them. - if (fMatchExistingContacts && listCon.size() > 0) { - for (Contact con : listCon) { - con.Volunteer_Last_Web_Signup_Date__c = contact.Volunteer_Last_Web_Signup_Date__c; - con.Volunteer_Availability__c = contact.Volunteer_Availability__c; - string strNotes = con.Volunteer_Notes__c; - if (strNotes != '') strNotes += ' '; - if (contact.Volunteer_Notes__c != null) { - con.Volunteer_Notes__c = strNotes + '[' + string.valueof(System.today()) + ']: ' + contact.Volunteer_Notes__c; - } - con.Volunteer_Skills__c = contact.Volunteer_Skills__c; - if (con.Volunteer_Status__c == null) con.Volunteer_Status__c = 'New Sign Up'; - if (contact.Phone != null) con.Phone = contact.Phone; - if (contact.HomePhone != null) con.HomePhone = contact.HomePhone; - // NOTE: if we find existing contact(s), we don't worry about doing anything with Company. - // but we can at least put it in the new Volunteer_Organization__c field. - if (contact.Department != null) con.Volunteer_Organization__c = contact.Department; - } - checkUpdateAccessSites('Contact', setFlds); - Database.update(listCon, dmlDuplicateOptions); - return listCon[0].Id; - } else if (fCreateContacts) { // No Match found, create a Contact - contact.LeadSource = 'Web Volunteer Signup'; - contact.Volunteer_Status__c = 'New Sign Up'; - - Account accToUse = null; - - // see if we can find their company (which we assume the form used Department to record.) - if (contact.Department != null) { - list listAccount = [select Id, Name from Account where Name = :contact.Department limit 1]; - if (listAccount.size() > 0) accToUse = listAccount.get(0); - contact.Volunteer_Organization__c = contact.Department; - } - - // if company found, use it - if (accToUse != null) { - contact.AccountId = accToUse.Id; - } else { // otherwise use the bucket account (which may be null and imply the 1:1 model in NPSP) - contact.AccountId = VOL_SharedCode.SettingsBucketAccountId; - } - UTIL_Describe.checkCreateAccess('Contact', setFlds); - Database.insert(contact, dmlDuplicateOptions); - return contact.Id; - } else { // No Match found, create a Lead - Lead lead = new lead(); - lead.FirstName = contact.FirstName; - lead.LastName = contact.LastName; - lead.Company = (contact.Department == null ? '[not provided]' : contact.Department); - lead.Email = contact.Email; - lead.Phone = contact.Phone; - lead.MobilePhone = contact.HomePhone; // leads don't have a home phone! - lead.Volunteer_Availability__c = contact.Volunteer_Availability__c; - lead.Volunteer_Notes__c = contact.Volunteer_Notes__c; - lead.Volunteer_Skills__c = contact.Volunteer_Skills__c; - lead.Volunteer_Status__c = 'New Sign Up'; - lead.LeadSource = 'Web Volunteer Signup'; - UTIL_Describe.checkCreateAccess('Lead', - new Set{'FirstName','LastName','Company','Email','Phone','MobilePhone','LeadSource', - UTIL_Describe.StrTokenNSPrefix('Volunteer_Availability__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Notes__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Skills__c'), - UTIL_Describe.StrTokenNSPrefix('Volunteer_Status__c')}); - Database.insert(lead, dmlDuplicateOptions); - return lead.Id; - } - } - - // global code to verify the passed in ContactId is valid, as well as the email - // exists on the Contact record. - global static boolean isValidContactIdAndEmail(ID contactId, string strEmail) { - string strSoql = 'select Id from Contact where Id = :contactId '; - if (VolunteersSettings.Personal_Site_Requires_URL_Email_Match__c) { - if (strEmail == null || strEmail == '') - return false; - strSoql += 'and (Email= :strEmail'; - // any additional email fields to check - if (VolunteersSettings.Contact_Match_Email_Fields__c != null) { - list listStrEmail = new list(); - listStrEmail = VolunteersSettings.Contact_Match_Email_Fields__c.split(';'); - for (string str : listStrEmail) { - strSoql += ' or ' + str + ' = :strEmail '; - } - } - // handle NPSP email fields - if (IsNPSPInstalled) { - strSoql += ' or npe01__AlternateEmail__c = :strEmail '; - strSoql += ' or npe01__HomeEmail__c = :strEmail '; - strSoql += ' or npe01__WorkEmail__c = :strEmail '; - } - strSoql += ') '; - } - list listCon = Database.Query(strSoql); - return listCon.size() > 0; - } - - // global code to lookup an existing contact - // listStrFields are optional fields to include in the soql call - global static list LookupContact(Contact contactRecord, list listStrFields) { - // let's see if we can find any matching Contacts. - // we need to use dynamic soql, since we allow the user to modify the FieldSet of fields to edit. - string strSoql = 'select '; - string strComma = ''; - if (listStrFields == null) { - strSoql += 'Id'; - } else { - for (string strF : listStrFields) { - strSoql += strComma + strF; - strComma = ', '; - } - } - strSoql += ' from Contact '; - string strAnd = ' where '; - - // make sure their settings haven't been completely cleared - if (VolunteersSettings.Contact_Matching_Rule__c == null || VolunteersSettings.Contact_Matching_Rule__c == '') { - VolunteersSettings.Contact_Matching_Rule__c = 'Firstname;Lastname;Email'; - } - - final String rule = VolunteersSettings.Contact_Matching_Rule__c; - if (rule.containsIgnoreCase(String.valueOf(Contact.LastName))) { - strSoql += strAnd + ' (Lastname=\'' + StrEscape(contactRecord.LastName) + '\') '; - strAnd = ' and '; - } - if (rule.containsIgnoreCase(String.valueOf(Contact.FirstName))) { - strSoql += strAnd + ' (Firstname=\'' + StrEscape(contactRecord.FirstName) + '\''; - strAnd = ' and '; - - // any additional firstname fields to check - if (VolunteersSettings.Contact_Match_First_Name_Fields__c != null) { - list listStrFname = new list(); - listStrFname = VolunteersSettings.Contact_Match_First_Name_Fields__c.split(';'); - for (string str : listStrFname) { - strSoql += ' or ' + str + '=\'' + StrEscape(contactRecord.FirstName) + '\''; - } - } - strSoql += ') '; - } - if (rule.containsIgnoreCase(String.valueOf(Contact.Email))) { - strSoql += strAnd + ' (Email=\'' + contactRecord.Email + '\''; - strAnd = ' and '; - // any additional email fields to check - if (VolunteersSettings.Contact_Match_Email_Fields__c != null) { - list listStrEmail = new list(); - listStrEmail = VolunteersSettings.Contact_Match_Email_Fields__c.split(';'); - for (string str : listStrEmail) { - strSoql += ' or ' + str + '=\'' + contactRecord.Email + '\''; - } - } - // handle NPSP email fields - if (IsNPSPInstalled) { - strSoql += ' or npe01__AlternateEmail__c=\'' + contactRecord.Email + '\''; - strSoql += ' or npe01__HomeEmail__c=\'' + contactRecord.Email + '\''; - strSoql += ' or npe01__WorkEmail__c=\'' + contactRecord.Email + '\''; - } - strSoql += ') '; - } - strSoql += ' limit 999 '; - list listCon = Database.Query(strSoql); - return listCon; - } - - // global code to create a new contact, or update an existing contact, for web volunteer signup. - // this code is used by both the VolunteersSignupFS page, and the VolunteersJobListingFS page. - // if creating a new Contact, it uses the custom setting for the bucket account, but takes parameters for - // the account name to try to lookup and match. - // It also takes the list of fields on the contact object to copy over. - global static ID CreateOrUpdateContactFS(string contactIdExisting, Contact contact, string strAccountName, list listStrFields) { - return CreateOrUpdateContactFS(contactIdExisting, contact, strAccountName, listStrFields, true); - } - - global static ID CreateOrUpdateContactFS(string contactIdExisting, Contact contact, string strAccountName, list listStrFields, boolean setLastWebSignup) { - // listStrFields is the specific list of fields on the form's fieldset, which we should assume we want to save. - // we also need to special case several fields we will potentially set, but we also need to know, - // if they are in the fieldset or not. - set setStrFields = new set(); - - // store all fields in lower case - for (string strField : listStrFields) - setStrFields.add(strField.toLowerCase()); - - boolean isStatusInFS = !setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_status__c').tolowerCase()); - boolean isNotesInFS = !setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_notes__c').toLowerCase()); - - // create a new list with all the fields - list listStrFieldsAll = new list(setStrFields); - - // we will check perms on the fields in this set, so remove Id - setStrFields.remove('id'); - - list listCon = LookupContact(contact, listStrFieldsAll); - - // if we found a match - if (listCon.size() > 0) { - Contact conExisting = null; - - // match the one that has the same Id - if (contactIdExisting != null && contactIdExisting != '') { - for (integer i = 0; i < listCon.size(); i++) { - if (listCon[i].Id == contactIdExisting) - conExisting = listCon[i]; - } - } - // use first one if no match found. - if (conExisting == null) { - conExisting = listCon[0]; - } - - // special case appending Volunteer Notes, rather than overwriting. - if (isNotesInFS) { - if (contact.Volunteer_Notes__c != null && contact.Volunteer_Notes__c != conExisting.Volunteer_Notes__c) { - contact.Volunteer_Notes__c = (conExisting.Volunteer_Notes__c != null ? (conExisting.Volunteer_Notes__c + ' ') : '') + - '[' + string.valueof(System.today()) + ']: ' + contact.Volunteer_Notes__c; - } else { - contact.Volunteer_Notes__c = conExisting.Volunteer_Notes__c; - } - } - - // special case setting Volunteer Status, only if not currently set. - if (conExisting.Volunteer_Status__c != null) { - contact.Volunteer_Status__c = null; - } else { - conExisting.Volunteer_Status__c = 'New Sign Up'; - } - - // now copy over all the non-null fields from the form's contact to the existing contact. - // avoid overwriting existing first name or existing email, since we might match it from in a different field. - // special case address fields - boolean hasMailingAddress = false; - boolean hasOtherAddress = false; - for (string strF : listStrFields) { - strF = strF.toLowerCase(); - // we actually still want to copy email if it is currently empty - if (strF == 'email' && conExisting.Email == null) { - conExisting.Email = contact.Email; - continue; - } - if (strF != 'id' && strF != 'firstname' && strF != 'email' && contact.get(strF) != null) { - if (isMailingAddressField(strF)) { - hasMailingAddress = true; - continue; - } - if (isOtherAddressField(strF)) { - hasOtherAddress = true; - continue; - } - conExisting.put(strF, contact.get(strF)); - } - } - if (hasMailingAddress) - VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Mailing', conExisting, 'Mailing'); - if (hasOtherAddress) - VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Other', conExisting, 'Other'); - - if (setLastWebSignup) - conExisting.Volunteer_Last_Web_Signup_Date__c = system.today(); - - checkUpdateAccessSites('Contact', setStrFields); - Database.update(conExisting, dmlDuplicateOptions); - // null out notes, so another update won't append them again! - contact.Volunteer_Notes__c = null; - return conExisting.Id; - } else { // No Match found, create a Contact - - // don't assume the contact object wasn't already used. - // since we can't null out Id for the insert, copy all - // the fields to a new object and use it. - Contact conNew = new Contact(); - // now copy over all the non-null fields from the form's contact to the existing contact. - // special case address fields - boolean hasMailingAddress = false; - boolean hasOtherAddress = false; - for (string strF : listStrFields) { - strF = strF.toLowerCase(); - if (strF != 'id' && contact.get(strF) != null) { - if (isMailingAddressField(strF)) { - hasMailingAddress = true; - continue; - } - if (isOtherAddressField(strF)) { - hasOtherAddress = true; - continue; - } - conNew.put(strF, contact.get(strF)); - } - } - if (hasMailingAddress) - VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Mailing', conNew, 'Mailing'); - if (hasOtherAddress) - VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Other', conNew, 'Other'); - - // see if we can find their company - Account accToUse = null; - if (strAccountName != null) { - list listAccount = [select Id, Name from Account where Name = :strAccountName limit 1]; - if (listAccount.size() > 0) accToUse = listAccount.get(0); - } - - // if company found, use it - if (accToUse != null) { - conNew.AccountId = accToUse.Id; - } else { // otherwise use the bucket account (which may be null and imply the 1:1 model in NPSP) - conNew.AccountId = VOL_SharedCode.SettingsBucketAccountId; - } - - if (setLastWebSignup) - conNew.Volunteer_Last_Web_Signup_Date__c = system.today(); - conNew.LeadSource = 'Web Volunteer Signup'; - conNew.Volunteer_Status__c = 'New Sign Up'; - - UTIL_Describe.checkCreateAccess('Contact', setStrFields); - Database.insert(conNew, dmlDuplicateOptions); - // null out notes, so another update won't append them again! - contact.Volunteer_Notes__c = null; - return conNew.Id; - } - } - - /******************************************************************************************************* - * @description detects standard mailing address fields - * @param strField The field to check against - * @return Boolean True if it is a standard mailing address field, false if not. - */ - public static Boolean isMailingAddressField(String strField) { - return (strField.containsIgnoreCase('mailing') && !strField.endsWith('__c')); - } - - /******************************************************************************************************* - * @description detects standard other address fields - * @param strField The field to check against - * @return Boolean True if it is a standard other address field, false if not. - */ - public static Boolean isOtherAddressField(String strField) { - return (strField.containsIgnoreCase('other') && !strField.endsWith('__c') && !strField.equalsIgnoreCase('otherphone')); - } - - // global utility to escape a string. - global static string StrEscape(string str) { - if (str == null) return null; - return string.escapeSingleQuotes(str); - } - - // global utility to load up an existing object and copy it to the provided object - global static void LoadAndCopyObject(ID id, SObject sobj) { - - // this code moved to VOL_SharedCodeAPI25 to keep it running with api 25 - // behavior, which is that the Sites Guest User Profile can still edit - // this new contact object we created. Under api 31, the contact object is readonly. - // we needed to update the rest of this class to api 31 to handle state & country picklists. - VOL_SharedCodeAPI25.LoadAndCopyObject(id, sobj, null); - } - - public static void VolunteerHoursTrigger(list listHoursOld, list listHoursNew, boolean resetTotals) { - - // consider both newMap and oldMap. - // for each hours object, there are two potential shifts it interacts with. - // within a batch of hours changes (import scenario), multiple hours can affect the same shift. - // thus need to keep track of the shifts to update, their original value, and the sum of their changed values. - - // Insert scenario: status=Confirmed or Completed. Shift <> null. Number of Volunteers <> null. - // Delete scenario: status=Confirmed or Completed. Shift <> null. Number of Volunteers <> null. - // Update scenario: just treat as a delete and an insert, since we already have to handle multiple changes to same job! - - - // WARNING: deleting, undeleting, or merging a Contact, does NOT call any trigger on the Hours! - // thus I've manually called this from the before delete & after undelete trigger on Contacts (VOL_Contact_MaintainHours). - map mpShiftIdDelta = new map(); - - // first we go through the new hours, and add up the number of volunteers per shift - if (listHoursNew != null) { - for (Volunteer_Hours__c hr : listHoursNew) { - if ((hr.Status__c == 'Confirmed' || hr.Status__c == 'Completed') && - (hr.Volunteer_Shift__c <> null && hr.Number_Of_Volunteers__c != null)) { - Double numVols = mpShiftIdDelta.get(hr.Volunteer_Shift__c); - if (numVols == null) numVols = 0; - numVols += hr.Number_of_Volunteers__c; - mpShiftIdDelta.put(hr.Volunteer_Shift__c, numVols); - } - } - } - - // second we go through the old hours, and subtract the number of volunteers per shift - if (listHoursOld != null) { - for (Volunteer_Hours__c hr : listHoursOld) { - if ((hr.Status__c == 'Confirmed' || hr.Status__c == 'Completed') && - (hr.Volunteer_Shift__c <> null && hr.Number_Of_Volunteers__c != null)) { - Double numVols = mpShiftIdDelta.get(hr.Volunteer_Shift__c); - if (numVols == null) numVols = 0; - numVols -= hr.Number_of_Volunteers__c; - mpShiftIdDelta.put(hr.Volunteer_Shift__c, numVols); - } - } - } - - // bail out if nothing found (to avoid runtime error!) - if (mpShiftIdDelta.size() == 0) - return; - - // now that we have the Id's of the shifts, let's get them from the database, update them by the number of volunteers, and then commit. - //list listShifts = new list(); - for (list listShifts : [select Id, Total_Volunteers__c from Volunteer_Shift__c where Id in :mpShiftIdDelta.keySet()]) { - - // loop through and update them - for (Volunteer_Shift__c shift : listShifts) { - Double numVols = shift.Total_Volunteers__c; - if (numVols == null || resetTotals) numVols = 0; - shift.Total_Volunteers__c = numVols + mpShiftIdDelta.get(shift.Id); - } - update listShifts; - } - } - - - // global utility used to detect whether the Non Profit Starter Pack is installed in this instance. - private static boolean fCheckedForNPSP = false; - global static boolean IsNPSPInstalled { - get { - if (!fCheckedForNPSP) { - Schema.SObjectType token = Schema.getGlobalDescribe().get('npe01__OppPayment__c'); - IsNPSPInstalled = (token != null); - fCheckedForNPSP = true; - } - return IsNPSPInstalled; - } - set; - } - - // global utility used to detect whether the Volunteers is running in a managed instance or unmanaged instance - private static boolean fCheckedForVolunteersNamespace = false; - global static boolean IsManagedCode { - get { - if (!fCheckedForVolunteersNamespace) { - // in order for this call to work as expected, we must be API Version 28, but we - // want to stay at version 25, so let's find another way!! - //Schema.SObjectType token = Schema.getGlobalDescribe().get('GW_Volunteers__Volunteer_Job__c'); - //IsManagedCode = (token != null); - - IsManagedCode = (getNamespace() != ''); - - fCheckedForVolunteersNamespace = true; - } - return IsManagedCode; - } - set; - } - - /****************************************************************************************************** - * @description String helper property for getNamespace() method. - *******************************************************************************************************/ - private static string plainNamespace; - - /******************************************************************************************************* - * @description Finds the namespace for the current context. - * @return string The current namespace as a string, or a blank string if we're not in a namespaced context. - ********************************************************************************************************/ - public static string getNamespace() { - if (plainNamespace == null) { - string withDotNotation = VOL_SharedCode.class.getName(); - - if (withDotNotation.contains('.')) { - plainNamespace = withDotNotation.substringBefore('.'); - } else { - plainNamespace = ''; - } - } - return plainNamespace; - } - - /******************************************************************************************************* - * @description Static method that takes a string - * If we are in a managed package, tokens in dynamic SOQL must include the package namespace prefix. - * If you ever deploy this package as unmanaged, this routine will do nothing! - * @param str token name - * @return token name, with namespace prefix, if required. - ********************************************************************************************************/ - global static string StrTokenNSPrefix(string str) { - if (getNamespace() == '') return str; - str = getNamespace() + '__' + str; - return str; - } - - // utility to verify all the specified fields are accessible to the current user. - // fields that are not accessible will have a pageMessage added to the current page - // so the warning is displayed to the user. - global static void testObjectFieldVisibility(string strObj, listlistStrField) { - - Map gd; - Schema.DescribeSObjectResult sobjDescr; - Map mapFieldDesc; - - // Obtaining the field name/token map for the object - gd = Schema.getGlobalDescribe(); - if (gd != null) - sobjDescr = gd.get(strObj).getDescribe(); - if (sobjDescr != null) - mapFieldDesc = sobjDescr.fields.getMap(); - if (mapFieldDesc != null) - for (String strField : listStrField) { - // Check if the user has access on the each field - // note that fields in our own package must not have their prefix for the Describe Field Map - Schema.SObjectField fld = mapFieldDesc.get(strField.replace(StrTokenNSPrefix(''), '')); - if (fld != null && !fld.getDescribe().isAccessible()) { - ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, 'Field ' + strObj + '.' + strField + - ' You need to enable field level security for this field on the Site\'s Guest User profile.')); - } - } - } - - /******************************************************************************************************* - * @description Static method checks if running user has field update access for a set of fields - * @param objectName the name of the object the field belongs to - * @param fieldNames the set of field names to check update access - * @return void - ********************************************************************************************************/ - public static void checkUpdateAccessSites(String objectName, Set fieldNames) { - // for backward compatibility with 1000's of nonprofit customers, we can - // only enforce create permissions on the Sites user for Contacts. - if (objectName == 'Contact') { - UTIL_Describe.checkCreateAccess(objectName, fieldNames); - } else { - UTIL_Describe.checkUpdateAccess(objectName, fieldNames); - } - } - - /** Description: DML options to allow overriding duplicate rules to create contacts, while throwing - * exceptions for validation rules, required fields, etc. - */ - private static Database.DMLOptions dmlDuplicateOptions { - get { - if (dmlDuplicateOptions == null) { - dmlDuplicateOptions = new Database.DMLOptions(); - dmlDuplicateOptions.optAllOrNone = true; - dmlDuplicateOptions.DuplicateRuleHeader.allowSave = true; - } - return dmlDuplicateOptions; - } - } - - // This massively nested SOQL where statement looks hard coded and dumb, but as it turns out - // there's a limit on number of campaign hierarchy levels anyway, so this isn't as dumb as it - // seems. This method will get all campaigns in hierarchy, and keeps the logic in a single query - - /******************************************************************************************************* - * @description Static method that takes an Id - * Return a list of Campaign Ids that are children/grand-children &c of the given Campaign. - * @param Id for any campaign - * @return List of child campaigns - ********************************************************************************************************/ - global static List listIdsCampaignsInHierarchy(Id campaignId) { - Map campaignsInHierarchy = new Map( - [SELECT Id,Name FROM Campaign WHERE IsActive =:true - AND RecordTypeId =: recordtypeIdVolunteersCampaign AND - (Id =: campaignId - OR ParentId =: campaignId - OR Parent.ParentId =: campaignId - OR Parent.Parent.ParentId =: campaignId - OR Parent.Parent.Parent.ParentId =: campaignId - OR Parent.Parent.Parent.Parent.ParentId =: campaignId)] - ); - return new List(campaignsInHierarchy.keySet()); - } - - /******************************************************************************************************* - * @description keeps references to old labels that have been packaged, that we no longer use. - * @return void - ********************************************************************************************************/ - private static void keepUnusedLabelsInPackage() { - string str; - str = Label.labelCalendarConfirmed; - str = Label.labelContactInfoRankText; - str = Label.labelHide; - str = Label.labelMassEmailHelp1; - str = Label.labelMassEmailHelp2; - str = Label.labelMassEmailHelp3; - str = Label.labelMassEmailHelp4; - str = Label.labelMassEmailHelp5; - str = Label.labelMassEmailHelp6; - str = Label.labelMassEmailHelp7; - str = Label.labelShowHelp; - str = Label.labelFindVolunteersCriteria; - str = Label.labelFindVolunteersHelpAssign; - str = Label.labelVolunteersWizardNewCampaignTitle; - } - -} +/* + Copyright (c) 2016, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +global with sharing class VOL_SharedCode { + @TestVisible + private static VOL_Access access = VOL_Access.getInstance(); + + // the list of Campaigns that have Volunteer Jobs + global list listSOCampaignsWithJobs { + get { + list listSO = new list(); + listSO.add(new SelectOption('', '')); + + // Ensure the user has access to the object before querying + if (Campaign.SObjectType.getDescribe().isAccessible() && Campaign.Name.getDescribe().isAccessible() && + Campaign.IsActive.getDescribe().isAccessible() && Campaign.StartDate.getDescribe().isAccessible() && + Campaign.RecordTypeId.getDescribe().isAccessible()) { + for (Campaign c : [select Name, Id from Campaign where RecordTypeId = :recordtypeIdVolunteersCampaign + and IsActive = true order by StartDate desc, Name asc limit 999]) { + listSO.add(new SelectOption(c.id, c.name)); + } + } + + return listSO; + } + } + + // the list of Volunteer Jobs for the specified Campaign + global list listSOVolunteerJobsOfCampaignId(ID campaignId) { + list listSO = new list(); + listSO.add(new SelectOption('', '')); + + // Ensure the user has access to the object before querying + if (Volunteer_Job__c.SObjectType.getDescribe().isAccessible() + && Volunteer_Job__c.Name.getDescribe().isAccessible() + && Volunteer_Job__c.Campaign__c.getDescribe().isAccessible()) { + + for (Volunteer_Job__c vj : [select Name, Id from Volunteer_Job__c where Campaign__c = :campaignId order by name limit 999]) { + listSO.add(new SelectOption(vj.id, vj.name)); + } + } + return listSO; + } + + // the list of Volunteer Job Shifts for the specified Job + global list listSOVolunteerShiftsOfVolunteerJobId(ID volunteerJobId, Date dtStart, Date dtEnd, boolean fIncludeShiftName, boolean fIncludeNumberNeeded) { + return listSOVolunteerShiftsOfVolunteerJobIdFormat(volunteerJobId, dtStart, dtEnd, fIncludeShiftName, fIncludeNumberNeeded, null, null); + } + + // list of select options of Shifts for the specified job, using the date & time format strings for the shifts + public static list listSOVolunteerShiftsOfVolunteerJobIdFormat(ID volunteerJobId, Date dtStart, Date dtEnd, + boolean fIncludeShiftName, boolean fIncludeNumberNeeded, string strDateFormat, string strTimeFormat) { + + list listVolunteerJobs = new List(); + + list listSO = new list(); + listSO.add(new SelectOption('', '')); + + // Ensure the user has access to the object before querying + try { + UTIL_Describe.checkObjectReadAccess(String.valueOf(Volunteer_Shift__c.SObjectType)); + + } catch (Exception ex) { + // we will return an empty list vs throwing an error + return listSO; + } + + Boolean canReadDate = Schema.sObjectType.Volunteer_Shift__c.fields.Start_Date_Time__c.isAccessible(); + Boolean canReadNumberNeeded = Schema.sObjectType.Volunteer_Shift__c.fields.Number_of_Volunteers_Still_Needed__c.isAccessible(); + + // ensure valid date ranges + if (dtStart == null) + dtStart = system.today(); + if (dtEnd == null) + dtEnd = system.today().addMonths(12); + dtEnd = dtEnd.addDays(1); + + // get our shifts in a Job query, so we can use our common date/time formatting routine. + listVolunteerJobs = [select Id, Campaign__r.IsActive, Campaign__r.Volunteer_Website_Time_Zone__c, + Volunteer_Website_Time_Zone__c, + (Select Id, Name, Start_Date_Time__c, Duration__c, Number_of_Volunteers_Still_Needed__c, + Description__c, System_Note__c From Volunteer_Job_Slots__r + where Start_Date_Time__c >= :dtStart and Start_Date_Time__c < :dtEnd + order by Start_Date_Time__c LIMIT 999) + from Volunteer_Job__c where Id = :volunteerJobId]; + + // bail out if no jobs found + if (listVolunteerJobs.size() == 0) + return listSO; + + // whether to use our datetime formatting, or salesforce default for the current user + boolean useDateTimeFixup = (strDateFormat != null && strTimeFormat != null); + + // put correct date/time format with appropriate timezone in system note field (in memory only) + if (useDateTimeFixup) + dateTimeFixup(listVolunteerJobs, strDateFormat, strTimeFormat); + + for (Volunteer_Shift__c vs : listVolunteerJobs[0].Volunteer_Job_Slots__r) { + SelectOption so = new SelectOption(vs.id, + (canReadDate ? (useDateTimeFixup ? vs.System_Note__c : vs.Start_Date_Time__c.format()) : '') + + (fIncludeShiftName ? '    (' + vs.name + ')' : '' ) + + (fIncludeNumberNeeded && canReadNumberNeeded ? '  ' + + (vs.Number_of_Volunteers_Still_Needed__c > 0 ? + system.label.labelCalendarStillNeeded + vs.Number_of_Volunteers_Still_Needed__c : system.label.labelCalendarShiftFull) + + ' ' : '' )); + so.setEscapeItem(false); + listSO.add(so); + } + return listSO; + } + + // routine to go through all the shifts, and create the display string + // for the shifts start date & time - end date & time, using the appropriate + // time zone that might be specified on the Job, Campaign, or Site Guest User. + // Note that it stores this string in the Shift's System_Note__c field (in memory only). + public static void dateTimeFixup(list listJob, string strDateFormat, string strTimeFormat) { + // get default time zone for site guest user + User u = [Select TimeZoneSidKey From User where id =: Userinfo.getUserId()]; + + // javascript formatting used 'tt' for am/pm, whereas apex formatting uses 'a'. + string strFormat = strDateFormat + ' ' + strTimeFormat.replace('tt','a'); + string strFormatEndTime = strTimeFormat.replace('tt','a'); + + for (Volunteer_Job__c job : listJob) { + string strTimeZone = job.Volunteer_Website_Time_Zone__c; + if (strTimeZone == null) strTimeZone = job.Campaign__r.Volunteer_Website_Time_Zone__c; + if (strTimeZone == null) strTimeZone = u.TimeZoneSidKey; + + for (Volunteer_Shift__c shift : job.Volunteer_Job_Slots__r) { + + DateTime dtEnd = shift.Start_Date_Time__c.addMinutes(integer.valueOf(shift.Duration__c * 60)); + string strStart = shift.Start_Date_Time__c.format(strFormat, strTimeZone); + + // see if start and end are on the same day + if (shift.Start_Date_Time__c.format('d', strTimeZone) == dtEnd.format('d', strTimeZone)) { + shift.System_Note__c = strStart + ' - ' + dtEnd.format(strFormatEndTime, strTimeZone); + } else { + shift.System_Note__c = strStart + ' - ' + dtEnd.format(strFormat, strTimeZone); + } + } + } + } + + // return the GMT datetime for a datetime in the specified timezone + public static DateTime dtGmtFromDtTimeZone(DateTime dt, TimeZone tz) { + integer offset = tz.getOffset(dt); + return dt.addSeconds(-offset / 1000); + } + + // Volunteer Custom Settings object. Loads an existing, and if not found creates one with default values. + global static Volunteers_Settings__c VolunteersSettings { + get { + if (VolunteersSettings == null) { + VolunteersSettings = Volunteers_Settings__c.getInstance(); + + if (VolunteersSettings.Id == null) { + VolunteersSettings = Volunteers_Settings__c.getOrgDefaults(); + } + + if (VolunteersSettings.Id == null) { + VolunteersSettings.Setupownerid = UserInfo.getOrganizationId(); + + // create reasonable defaults + VolunteersSettings.Signup_Matches_Existing_Contacts__c = false; + VolunteersSettings.Signup_Creates_Contacts_If_No_Match__c = false; + VolunteersSettings.Signup_Bucket_Account_On_Create__c = null; + VolunteersSettings.Recurring_Job_Future_Months__c = 4; + VolunteersSettings.Grant_Guest_Users_Update_Access__c = false; + VolunteersSettings.Contact_Match_Email_Fields__c = null; + VolunteersSettings.Contact_Match_First_Name_Fields__c = null; + VolunteersSettings.Personal_Site_Org_Wide_Email_Name__c = null; + VolunteersSettings.Contact_Matching_Rule__c = 'Firstname;Lastname;Email'; + VolunteersSettings.Personal_Site_Report_Hours_Filtered__c = false; + if (UTIL_Describe.hasObjectCreateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteers_Settings__c'))) { + insert VolunteersSettings; + } + } else if (VolunteersSettings.Contact_Matching_Rule__c == null) { + VolunteersSettings.Contact_Matching_Rule__c = 'Firstname;Lastname;Email'; + if (UTIL_Describe.hasObjectUpdateAccess(UTIL_Describe.StrTokenNSPrefix('Volunteers_Settings__c'))) { + update VolunteersSettings; + } + } + } + return VolunteersSettings; + } + + set; + } + + // helper to get the AccoutId of the Bucket Account specified in Custom Settings. + global static ID SettingsBucketAccountId { + get { + if (SettingsBucketAccountId == null) { + if (VolunteersSettings.Signup_Bucket_Account_On_Create__c != null + && Account.getSObjectType().getDescribe().isAccessible()) { + Account[] acc = [select Id from Account where name = :VolunteersSettings.Signup_Bucket_Account_On_Create__c limit 1]; + if (acc.size() > 0) SettingsBucketAccountId = acc[0].Id; + } + } + return SettingsBucketAccountId; + } + + set; + } + + // test helper that allows one to override the users's Custom Settings with the settings we want to test with. + global static Volunteers_Settings__c getVolunteersSettingsForTests(Volunteers_Settings__c mySettings) { + //clear out whatever settings exist + delete [select id from Volunteers_Settings__c]; + SettingsBucketAccountId = null; + + //create our own based on what's passed in from the test + VolunteersSettings = new Volunteers_Settings__c ( + Signup_Matches_Existing_Contacts__c = mySettings.Signup_Matches_Existing_Contacts__c, + Signup_Creates_Contacts_If_No_Match__c = mySettings.Signup_Creates_Contacts_If_No_Match__c, + Signup_Bucket_Account_On_Create__c = mySettings.Signup_Bucket_Account_On_Create__c, + Recurring_Job_Future_Months__c = mySettings.Recurring_Job_Future_Months__c, + Contact_Match_Email_Fields__c = mySettings.Contact_Match_Email_Fields__c, + Contact_Match_First_Name_Fields__c = mySettings.Contact_Match_First_Name_Fields__c, + Contact_Matching_Rule__c = mySettings.Contact_Matching_Rule__c, + Personal_Site_Org_Wide_Email_Name__c = mySettings.Personal_Site_Org_Wide_Email_Name__c, + Personal_Site_Report_Hours_Filtered__c = mySettings.Personal_Site_Report_Hours_Filtered__c + ); + + insert VolunteersSettings; + return VolunteersSettings; + } + + // global helper to get the Volunteers Campaign recordtype. + private class MyException extends Exception {} + global static Id recordtypeIdVolunteersCampaign { + get { + if (recordtypeIdVolunteersCampaign == null) { + list listRT = [SELECT Id FROM RecordType WHERE DeveloperName='Volunteers_Campaign']; + if (listRT.size() == 0) { + throw (new MyException('The Volunteers Campaign Record Type is missing and must be restored.')); + } + recordtypeIdVolunteersCampaign = listRT[0].Id; + } + return recordtypeIdVolunteersCampaign; + } + set; + } + + // shared routine to get all Fields names from the specified Field Set on Contact + // also explicitly adds additional Contact fields that we will always use in our Sites pages. + global static list listStrFieldsFromContactFieldSet(Schema.FieldSet fs) { + set setStrFields = new set(); + for (Schema.FieldSetMember f : fs.getFields()) { + setStrFields.add(f.getFieldPath().toLowerCase()); + } + // also add the fields we explicitly refer to in CreateOrUpdateContactFS() + // we use a set (with lowercase) to avoid creating duplicates. + setStrFields.add('firstname'); + setStrFields.add('lastname'); + setStrFields.add('email'); + //setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_status__c').tolowerCase()); + //setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_notes__c').toLowerCase()); + list listStrFields = new list(); + listStrFields.addAll(setStrFields); + return listStrFields; + } + + // shared routine to get all Fields names form the specified Field Set + global static list listStrFieldsFromFieldSet(Schema.FieldSet fs) { + list listStrFields = new list(); + for (Schema.FieldSetMember f : fs.getFields()) { + listStrFields.add(f.getFieldPath()); + } + return listStrFields; + } + + // global code to create a new lead or contact for web volunteer signup. + // this code is used by both the VolunteersSignup page, and the VolunteersJobListing page. + // it uses the custom setting for the bucket account, but takes parameters for + // matching existing contacts, and create contacts vs. leads. this is because the two pages have different use cases. + // it also assumes that the contact that is passed in is the dummy record from the web page, and thus isn't real, and + // uses the Department field to track the user's company name. + global static ID CreateContactOrLead(Contact contact, boolean fMatchExistingContacts, boolean fCreateContacts) { + // update the date before we start + contact.Volunteer_Last_Web_Signup_Date__c = system.today(); + + // let's see if we can find any matching Contacts. + list listCon = [select Id, Lastname, Firstname, Email, Phone, HomePhone, + Volunteer_Availability__c, Volunteer_Notes__c, Volunteer_Last_Web_Signup_Date__c, + Volunteer_Status__c, Volunteer_Skills__c, Volunteer_Organization__c from Contact + where Lastname=:contact.Lastname and Firstname=:contact.Firstname and Email=:contact.Email]; + + Set setFlds = new Set{'Lastname', 'Firstname', 'Email', 'Phone', 'HomePhone', + UTIL_Describe.StrTokenNSPrefix('Volunteer_Availability__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Notes__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Last_Web_Signup_Date__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Status__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Skills__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Organization__c')}; + + // if we can match existing contacts, and we found a match, update them. + if (fMatchExistingContacts && listCon.size() > 0) { + for (Contact con : listCon) { + con.Volunteer_Last_Web_Signup_Date__c = contact.Volunteer_Last_Web_Signup_Date__c; + con.Volunteer_Availability__c = contact.Volunteer_Availability__c; + string strNotes = con.Volunteer_Notes__c; + if (strNotes != '') strNotes += ' '; + if (contact.Volunteer_Notes__c != null) { + con.Volunteer_Notes__c = strNotes + '[' + string.valueof(System.today()) + ']: ' + contact.Volunteer_Notes__c; + } + con.Volunteer_Skills__c = contact.Volunteer_Skills__c; + if (con.Volunteer_Status__c == null) con.Volunteer_Status__c = 'New Sign Up'; + if (contact.Phone != null) con.Phone = contact.Phone; + if (contact.HomePhone != null) con.HomePhone = contact.HomePhone; + // NOTE: if we find existing contact(s), we don't worry about doing anything with Company. + // but we can at least put it in the new Volunteer_Organization__c field. + if (contact.Department != null) con.Volunteer_Organization__c = contact.Department; + } + + checkUpdateAccessSites('Contact', setFlds); + access.updateRecords(listCon, dmlDuplicateOptions); + + return listCon[0].Id; + } else if (fCreateContacts) { // No Match found, create a Contact + contact.LeadSource = 'Web Volunteer Signup'; + contact.Volunteer_Status__c = 'New Sign Up'; + + Account accToUse = null; + + // see if we can find their company (which we assume the form used Department to record.) + if (contact.Department != null + && Account.getSObjectType().getDescribe().isAccessible()) { + list listAccount = [select Id, Name from Account where Name = :contact.Department limit 1]; + if (listAccount.size() > 0) accToUse = listAccount.get(0); + contact.Volunteer_Organization__c = contact.Department; + } + + // if company found, use it + if (accToUse != null) { + contact.AccountId = accToUse.Id; + } else { // otherwise use the bucket account (which may be null and imply the 1:1 model in NPSP) + contact.AccountId = VOL_SharedCode.SettingsBucketAccountId; + } + access.checkCreateAccess('Contact', setFlds); + access.insertRecords(new List{ contact }, dmlDuplicateOptions); + return contact.Id; + } else { // No Match found, create a Lead + Lead lead = new lead(); + lead.FirstName = contact.FirstName; + lead.LastName = contact.LastName; + lead.Company = (contact.Department == null ? '[not provided]' : contact.Department); + lead.Email = contact.Email; + lead.Phone = contact.Phone; + lead.MobilePhone = contact.HomePhone; // leads don't have a home phone! + lead.Volunteer_Availability__c = contact.Volunteer_Availability__c; + lead.Volunteer_Notes__c = contact.Volunteer_Notes__c; + lead.Volunteer_Skills__c = contact.Volunteer_Skills__c; + lead.Volunteer_Status__c = 'New Sign Up'; + lead.LeadSource = 'Web Volunteer Signup'; + UTIL_Describe.checkCreateAccess('Lead', + new Set{'FirstName','LastName','Company','Email','Phone','MobilePhone','LeadSource', + UTIL_Describe.StrTokenNSPrefix('Volunteer_Availability__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Notes__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Skills__c'), + UTIL_Describe.StrTokenNSPrefix('Volunteer_Status__c')}); + Database.insert(lead, dmlDuplicateOptions); + return lead.Id; + } + } + + // global code to verify the passed in ContactId is valid, as well as the email + // exists on the Contact record. + global static boolean isValidContactIdAndEmail(ID contactId, string strEmail) { + if (!Contact.getSObjectType().getDescribe().isAccessible()) { + return false; + } + + string strSoql = 'select Id from Contact where Id = :contactId '; + if (VolunteersSettings.Personal_Site_Requires_URL_Email_Match__c) { + if (strEmail == null || strEmail == '' || !Contact.Email.getDescribe().isAccessible()) + return false; + strEmail = strEmail.escapeHtml4(); + strSoql += 'and (Email= :strEmail'; + // any additional email fields to check + Map fieldByName = Contact.getSObjectType().getDescribe().fields.getMap(); + if (VolunteersSettings.Contact_Match_Email_Fields__c != null) { + list listStrEmail = new list(); + listStrEmail = VolunteersSettings.Contact_Match_Email_Fields__c.split(';'); + + for (string str : listStrEmail) { + if (fieldByName.containsKey(str)) { + strSoql += ' or ' + str + ' = :strEmail '; + } + } + + } + // handle NPSP email fields + if (IsNPSPInstalled) { + if (fieldByName.containsKey('npe01__AlternateEmail__c') && fieldByName.get('npe01__AlternateEmail__c').getDescribe().isAccessible()) { + strSoql += ' or npe01__AlternateEmail__c = :strEmail '; + } + if (fieldByName.containsKey('npe01__HomeEmail__c') && fieldByName.get('npe01__HomeEmail__c').getDescribe().isAccessible()) { + strSoql += ' or npe01__HomeEmail__c = :strEmail '; + } + if (fieldByName.containsKey('npe01__WorkEmail__c') && fieldByName.get('npe01__WorkEmail__c').getDescribe().isAccessible()) { + strSoql += ' or npe01__WorkEmail__c = :strEmail '; + } + + + } + strSoql += ') '; + } + list listCon = Database.Query(strSoql); + return listCon.size() > 0; + } + + // global code to lookup an existing contact + // listStrFields are optional fields to include in the soql call + global static list LookupContact(Contact contactRecord, list listStrFields) { + // let's see if we can find any matching Contacts. + // we need to use dynamic soql, since we allow the user to modify the FieldSet of fields to edit. + string strSoql = 'select '; + string strComma = ''; + if (listStrFields == null) { + strSoql += 'Id'; + } else { + for (string strF : listStrFields) { + strSoql += strComma + strF; + strComma = ', '; + } + } + strSoql += ' from Contact '; + string strAnd = ' where '; + + // make sure their settings haven't been completely cleared + if (VolunteersSettings.Contact_Matching_Rule__c == null || VolunteersSettings.Contact_Matching_Rule__c == '') { + VolunteersSettings.Contact_Matching_Rule__c = 'Firstname;Lastname;Email'; + } + final String rule = VolunteersSettings.Contact_Matching_Rule__c; + if (rule.containsIgnoreCase(String.valueOf(Contact.LastName))) { + strSoql += strAnd + ' (Lastname=\'' + StrEscape(contactRecord.LastName) + '\') '; + strAnd = ' and '; + } + if (rule.containsIgnoreCase(String.valueOf(Contact.FirstName))) { + strSoql += strAnd + ' (Firstname=\'' + StrEscape(contactRecord.FirstName) + '\''; + strAnd = ' and '; + + // any additional firstname fields to check + if (VolunteersSettings.Contact_Match_First_Name_Fields__c != null) { + list listStrFname = new list(); + listStrFname = VolunteersSettings.Contact_Match_First_Name_Fields__c.split(';'); + for (string str : listStrFname) { + strSoql += ' or ' + str + '=\'' + StrEscape(contactRecord.FirstName) + '\''; + } + } + strSoql += ') '; + } + if (rule.containsIgnoreCase(String.valueOf(Contact.Email))) { + strSoql += strAnd + ' (Email=\'' + contactRecord.Email + '\''; + strAnd = ' and '; + // any additional email fields to check + if (VolunteersSettings.Contact_Match_Email_Fields__c != null) { + list listStrEmail = new list(); + listStrEmail = VolunteersSettings.Contact_Match_Email_Fields__c.split(';'); + for (string str : listStrEmail) { + strSoql += ' or ' + str + '=\'' + contactRecord.Email + '\''; + } + } + // handle NPSP email fields + if (IsNPSPInstalled) { + strSoql += ' or npe01__AlternateEmail__c=\'' + contactRecord.Email + '\''; + strSoql += ' or npe01__HomeEmail__c=\'' + contactRecord.Email + '\''; + strSoql += ' or npe01__WorkEmail__c=\'' + contactRecord.Email + '\''; + } + strSoql += ') '; + } + strSoql += ' limit 999 '; + list listCon = Database.Query(strSoql); + return listCon; + } + + // global code to create a new contact, or update an existing contact, for web volunteer signup. + // this code is used by both the VolunteersSignupFS page, and the VolunteersJobListingFS page. + // if creating a new Contact, it uses the custom setting for the bucket account, but takes parameters for + // the account name to try to lookup and match. + // It also takes the list of fields on the contact object to copy over. + global static ID CreateOrUpdateContactFS(string contactIdExisting, Contact contact, string strAccountName, list listStrFields) { + return CreateOrUpdateContactFS(contactIdExisting, contact, strAccountName, listStrFields, true); + } + + global static ID CreateOrUpdateContactFS(string contactIdExisting, Contact contact, string strAccountName, list listStrFields, boolean setLastWebSignup) { + // listStrFields is the specific list of fields on the form's fieldset, which we should assume we want to save. + // we also need to special case several fields we will potentially set, but we also need to know, + // if they are in the fieldset or not. + set setStrFields = new set(); + + // store all fields in lower case + for (string strField : listStrFields) + setStrFields.add(strField.toLowerCase()); + + boolean isStatusInFS = !setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_status__c').tolowerCase()); + boolean isNotesInFS = !setStrFields.add(VOL_SharedCode.StrTokenNSPrefix('volunteer_notes__c').toLowerCase()); + + // create a new list with all the fields + list listStrFieldsAll = new list(setStrFields); + + // we will check perms on the fields in this set, so remove Id + setStrFields.remove('id'); + + list listCon = LookupContact(contact, listStrFieldsAll); + + // if we found a match + if (listCon.size() > 0) { + Contact conExisting = null; + + // match the one that has the same Id + if (contactIdExisting != null && contactIdExisting != '') { + for (integer i = 0; i < listCon.size(); i++) { + if (listCon[i].Id == contactIdExisting) + conExisting = listCon[i]; + } + } + // use first one if no match found. + if (conExisting == null) { + conExisting = listCon[0]; + } + + // special case appending Volunteer Notes, rather than overwriting. + if (isNotesInFS) { + if (contact.Volunteer_Notes__c != null && contact.Volunteer_Notes__c != conExisting.Volunteer_Notes__c) { + contact.Volunteer_Notes__c = (conExisting.Volunteer_Notes__c != null ? (conExisting.Volunteer_Notes__c + ' ') : '') + + '[' + string.valueof(System.today()) + ']: ' + contact.Volunteer_Notes__c; + } else { + contact.Volunteer_Notes__c = conExisting.Volunteer_Notes__c; + } + } + + // special case setting Volunteer Status, only if not currently set. + if (conExisting.Volunteer_Status__c != null) { + contact.Volunteer_Status__c = null; + } else { + conExisting.Volunteer_Status__c = 'New Sign Up'; + } + + // now copy over all the non-null fields from the form's contact to the existing contact. + // avoid overwriting existing first name or existing email, since we might match it from in a different field. + // special case address fields + boolean hasMailingAddress = false; + boolean hasOtherAddress = false; + for (string strF : listStrFields) { + strF = strF.toLowerCase(); + // we actually still want to copy email if it is currently empty + if (strF == 'email' && conExisting.Email == null) { + conExisting.Email = contact.Email; + continue; + } + if (strF != 'id' && strF != 'firstname' && strF != 'email' && contact.get(strF) != null) { + if (isMailingAddressField(strF)) { + hasMailingAddress = true; + continue; + } + if (isOtherAddressField(strF)) { + hasOtherAddress = true; + continue; + } + conExisting.put(strF, contact.get(strF)); + } + } + if (hasMailingAddress) + VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Mailing', conExisting, 'Mailing'); + if (hasOtherAddress) + VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Other', conExisting, 'Other'); + + if (setLastWebSignup) + conExisting.Volunteer_Last_Web_Signup_Date__c = system.today(); + + checkUpdateAccessSites('Contact', setStrFields); + access.updateRecords(new List{ conExisting }, dmlDuplicateOptions); + + // null out notes, so another update won't append them again! + contact.Volunteer_Notes__c = null; + return conExisting.Id; + } else { // No Match found, create a Contact + + // don't assume the contact object wasn't already used. + // since we can't null out Id for the insert, copy all + // the fields to a new object and use it. + Contact conNew = new Contact(); + // now copy over all the non-null fields from the form's contact to the existing contact. + // special case address fields + boolean hasMailingAddress = false; + boolean hasOtherAddress = false; + for (string strF : listStrFields) { + strF = strF.toLowerCase(); + if (strF != 'id' && contact.get(strF) != null) { + if (isMailingAddressField(strF)) { + hasMailingAddress = true; + continue; + } + if (isOtherAddressField(strF)) { + hasOtherAddress = true; + continue; + } + conNew.put(strF, contact.get(strF)); + } + } + if (hasMailingAddress) + VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Mailing', conNew, 'Mailing'); + if (hasOtherAddress) + VOL_StateCountryPicklists.copyAddressStdSObj(contact, 'Other', conNew, 'Other'); + + // see if we can find their company + Account accToUse = null; + if (strAccountName != null + && Account.getSObjectType().getDescribe().isAccessible()) { + strAccountName = strAccountName.escapeHtml4(); + list listAccount = [select Id, Name from Account where Name = :strAccountName limit 1]; + if (listAccount.size() > 0) accToUse = listAccount.get(0); + } + + // if company found, use it + if (accToUse != null) { + conNew.AccountId = accToUse.Id; + } else { // otherwise use the bucket account (which may be null and imply the 1:1 model in NPSP) + conNew.AccountId = VOL_SharedCode.SettingsBucketAccountId; + } + + if (setLastWebSignup) + conNew.Volunteer_Last_Web_Signup_Date__c = system.today(); + conNew.LeadSource = 'Web Volunteer Signup'; + conNew.Volunteer_Status__c = 'New Sign Up'; + + access.checkCreateAccess('Contact', setStrFields); + access.insertRecords(new List{ conNew }, dmlDuplicateOptions); + + // null out notes, so another update won't append them again! + contact.Volunteer_Notes__c = null; + return conNew.Id; + } + } + + /******************************************************************************************************* + * @description detects standard mailing address fields + * @param strField The field to check against + * @return Boolean True if it is a standard mailing address field, false if not. + */ + public static Boolean isMailingAddressField(String strField) { + return (strField.containsIgnoreCase('mailing') && !strField.endsWith('__c')); + } + + /******************************************************************************************************* + * @description detects standard other address fields + * @param strField The field to check against + * @return Boolean True if it is a standard other address field, false if not. + */ + public static Boolean isOtherAddressField(String strField) { + return (strField.containsIgnoreCase('other') && !strField.endsWith('__c') && !strField.equalsIgnoreCase('otherphone')); + } + + // global utility to escape a string. + global static string StrEscape(string str) { + if (str == null) return null; + return string.escapeSingleQuotes(str); + } + + // global utility to load up an existing object and copy it to the provided object + global static void LoadAndCopyObject(ID id, SObject sobj) { + LoadAndCopyObject(id, sobj, null); + } + + public static SObject LoadAndCopyObject(ID id, SObject sobj, list listStrFields) { + + + Schema.DescribeSObjectResult des = sobj.getSObjectType().getDescribe(); + + // if fields not provided, get all Contact fields + if (listStrFields == null) { + listStrFields = new list(); + // get the fields for the object + Map mapS = des.fields.getMap().clone(); + // avoid any of the API version 30 compound fields + // we only worry about Contact ones, since all callers are giving us contacts to copy. + mapS.remove('mailingaddress'); + mapS.remove('otheraddress'); + listStrFields.addAll(mapS.keySet()); + } + + string strSoql = 'select '; + string strComma = ''; + for (string strF : listStrFields) { + strSoql += strComma + strF; + strComma = ', '; + } + strSoql += ' from ' + des.getName() + ' where Id = :id '; + strSoql += ' limit 1'; + list listSObj = Security.stripInaccessible(AccessType.READABLE, Database.Query(strSoql)).getRecords(); + + if (listSObj.size() > 0) { + SObject sobjT = listSObj[0]; + // now copy over all the non-null fields from the form's contact to the existing contact. + for (string strF : listStrFields) { + if (sobjT.get(strF) != null) { + try { + sobj.put(strF, sobjT.get(strF)); + } catch(exception ex) { + + } + } + } + return sobjT; + } + return null; + } + + public static void VolunteerHoursTrigger(list listHoursOld, list listHoursNew, boolean resetTotals) { + + // consider both newMap and oldMap. + // for each hours object, there are two potential shifts it interacts with. + // within a batch of hours changes (import scenario), multiple hours can affect the same shift. + // thus need to keep track of the shifts to update, their original value, and the sum of their changed values. + + // Insert scenario: status=Confirmed or Completed. Shift <> null. Number of Volunteers <> null. + // Delete scenario: status=Confirmed or Completed. Shift <> null. Number of Volunteers <> null. + // Update scenario: just treat as a delete and an insert, since we already have to handle multiple changes to same job! + + + // WARNING: deleting, undeleting, or merging a Contact, does NOT call any trigger on the Hours! + // thus I've manually called this from the before delete & after undelete trigger on Contacts (VOL_Contact_MaintainHours). + map mpShiftIdDelta = new map(); + + // first we go through the new hours, and add up the number of volunteers per shift + if (listHoursNew != null) { + for (Volunteer_Hours__c hr : listHoursNew) { + if ((hr.Status__c == 'Confirmed' || hr.Status__c == 'Completed') && + (hr.Volunteer_Shift__c <> null && hr.Number_Of_Volunteers__c != null)) { + Double numVols = mpShiftIdDelta.get(hr.Volunteer_Shift__c); + if (numVols == null) numVols = 0; + numVols += hr.Number_of_Volunteers__c; + mpShiftIdDelta.put(hr.Volunteer_Shift__c, numVols); + } + } + } + + // second we go through the old hours, and subtract the number of volunteers per shift + if (listHoursOld != null) { + for (Volunteer_Hours__c hr : listHoursOld) { + if ((hr.Status__c == 'Confirmed' || hr.Status__c == 'Completed') && + (hr.Volunteer_Shift__c <> null && hr.Number_Of_Volunteers__c != null)) { + Double numVols = mpShiftIdDelta.get(hr.Volunteer_Shift__c); + if (numVols == null) numVols = 0; + numVols -= hr.Number_of_Volunteers__c; + mpShiftIdDelta.put(hr.Volunteer_Shift__c, numVols); + } + } + } + + // bail out if nothing found (to avoid runtime error!) + if (mpShiftIdDelta.size() == 0) + return; + + // now that we have the Id's of the shifts, let's get them from the database, update them by the number of volunteers, and then commit. + //list listShifts = new list(); + for (list listShifts : [select Id, Total_Volunteers__c from Volunteer_Shift__c where Id in :mpShiftIdDelta.keySet()]) { + + // loop through and update them + for (Volunteer_Shift__c shift : listShifts) { + Double numVols = shift.Total_Volunteers__c; + if (numVols == null || resetTotals) numVols = 0; + shift.Total_Volunteers__c = numVols + mpShiftIdDelta.get(shift.Id); + } + // Note: We will not check CRUD / FLS for this process so that rollups are always in sync regardless of user + access.updateRecords(listShifts); + } + } + + + // global utility used to detect whether the Non Profit Starter Pack is installed in this instance. + private static boolean fCheckedForNPSP = false; + global static boolean IsNPSPInstalled { + get { + if (!fCheckedForNPSP) { + Schema.SObjectType token = Schema.getGlobalDescribe().get('npe01__OppPayment__c'); + IsNPSPInstalled = (token != null); + fCheckedForNPSP = true; + } + return IsNPSPInstalled; + } + set; + } + + // global utility used to detect whether the Volunteers is running in a managed instance or unmanaged instance + private static boolean fCheckedForVolunteersNamespace = false; + global static boolean IsManagedCode { + get { + if (!fCheckedForVolunteersNamespace) { + // in order for this call to work as expected, we must be API Version 28, but we + // want to stay at version 25, so let's find another way!! + //Schema.SObjectType token = Schema.getGlobalDescribe().get('GW_Volunteers__Volunteer_Job__c'); + //IsManagedCode = (token != null); + + IsManagedCode = (getNamespace() != ''); + + fCheckedForVolunteersNamespace = true; + } + return IsManagedCode; + } + set; + } + + /****************************************************************************************************** + * @description String helper property for getNamespace() method. + *******************************************************************************************************/ + private static string plainNamespace; + + /******************************************************************************************************* + * @description Finds the namespace for the current context. + * @return string The current namespace as a string, or a blank string if we're not in a namespaced context. + ********************************************************************************************************/ + public static string getNamespace() { + if (plainNamespace == null) { + string withDotNotation = VOL_SharedCode.class.getName(); + + if (withDotNotation.contains('.')) { + plainNamespace = withDotNotation.substringBefore('.'); + } else { + plainNamespace = ''; + } + } + return plainNamespace; + } + + /******************************************************************************************************* + * @description Static method that takes a string + * If we are in a managed package, tokens in dynamic SOQL must include the package namespace prefix. + * If you ever deploy this package as unmanaged, this routine will do nothing! + * @param str token name + * @return token name, with namespace prefix, if required. + ********************************************************************************************************/ + global static string StrTokenNSPrefix(string str) { + if (getNamespace() == '') return str; + str = getNamespace() + '__' + str; + return str; + } + + // utility to verify all the specified fields are accessible to the current user. + // fields that are not accessible will have a pageMessage added to the current page + // so the warning is displayed to the user. + global static void testObjectFieldVisibility(string strObj, listlistStrField) { + + Map gd; + Schema.DescribeSObjectResult sobjDescr; + Map mapFieldDesc; + + // Obtaining the field name/token map for the object + gd = Schema.getGlobalDescribe(); + if (gd != null) + sobjDescr = gd.get(strObj).getDescribe(); + if (sobjDescr != null) + mapFieldDesc = sobjDescr.fields.getMap(); + if (mapFieldDesc != null) + for (String strField : listStrField) { + // Check if the user has access on the each field + // note that fields in our own package must not have their prefix for the Describe Field Map + Schema.SObjectField fld = mapFieldDesc.get(strField.replace(StrTokenNSPrefix(''), '')); + if (fld != null && !fld.getDescribe().isAccessible()) { + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, 'Field ' + strObj + '.' + strField + + ' You need to enable field level security for this field on the Site\'s Guest User profile.')); + } + } + } + + /******************************************************************************************************* + * @description Static method checks if running user has field update access for a set of fields + * @param objectName the name of the object the field belongs to + * @param fieldNames the set of field names to check update access + * @return void + ********************************************************************************************************/ + public static void checkUpdateAccessSites(String objectName, Set fieldNames) { + // for backward compatibility with 1000's of nonprofit customers, we can + // only enforce create permissions on the Sites user for Contacts. + if (objectName == 'Contact') { + access.checkCreateAccess(objectName, fieldNames); + } else { + access.checkUpdateAccess(objectName, fieldNames); + } + } + + /** Description: DML options to allow overriding duplicate rules to create contacts, while throwing + * exceptions for validation rules, required fields, etc. + */ + private static Database.DMLOptions dmlDuplicateOptions { + get { + if (dmlDuplicateOptions == null) { + dmlDuplicateOptions = new Database.DMLOptions(); + dmlDuplicateOptions.optAllOrNone = true; + dmlDuplicateOptions.DuplicateRuleHeader.allowSave = true; + } + return dmlDuplicateOptions; + } + } + + // This massively nested SOQL where statement looks hard coded and dumb, but as it turns out + // there's a limit on number of campaign hierarchy levels anyway, so this isn't as dumb as it + // seems. This method will get all campaigns in hierarchy, and keeps the logic in a single query + + /******************************************************************************************************* + * @description Static method that takes an Id + * Return a list of Campaign Ids that are children/grand-children &c of the given Campaign. + * @param Id for any campaign + * @return List of child campaigns + ********************************************************************************************************/ + global static List listIdsCampaignsInHierarchy(Id campaignId) { + Map campaignsInHierarchy = new Map( + [SELECT Id,Name FROM Campaign WHERE IsActive =:true + AND RecordTypeId =: recordtypeIdVolunteersCampaign AND + (Id =: campaignId + OR ParentId =: campaignId + OR Parent.ParentId =: campaignId + OR Parent.Parent.ParentId =: campaignId + OR Parent.Parent.Parent.ParentId =: campaignId + OR Parent.Parent.Parent.Parent.ParentId =: campaignId)] + ); + return new List(campaignsInHierarchy.keySet()); + } + + /******************************************************************************************************* + * @description keeps references to old labels that have been packaged, that we no longer use. + * @return void + ********************************************************************************************************/ + private static void keepUnusedLabelsInPackage() { + string str; + str = Label.labelCalendarConfirmed; + str = Label.labelContactInfoRankText; + str = Label.labelHide; + str = Label.labelMassEmailHelp1; + str = Label.labelMassEmailHelp2; + str = Label.labelMassEmailHelp3; + str = Label.labelMassEmailHelp4; + str = Label.labelMassEmailHelp5; + str = Label.labelMassEmailHelp6; + str = Label.labelMassEmailHelp7; + str = Label.labelShowHelp; + str = Label.labelFindVolunteersCriteria; + str = Label.labelFindVolunteersHelpAssign; + str = Label.labelVolunteersWizardNewCampaignTitle; + str = Label.labelContactLookupSuccess; + str = Label.labelContactLookupNotFound; + } + +} \ No newline at end of file diff --git a/src/classes/VOL_SharedCodeAPI25.cls b/src/classes/VOL_SharedCodeAPI25.cls deleted file mode 100644 index 944b8f2..0000000 --- a/src/classes/VOL_SharedCodeAPI25.cls +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright (c) 2016, Salesforce.org - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Salesforce.org nor the names of - its contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. -*/ - -public with sharing class VOL_SharedCodeAPI25 { - - // global utility to load up an existing object and copy it to the provided object - // this code moved to VOL_SharedCodeAPI25 to keep it running with api 25 - // behavior, which is that the Sites Guest User Profile can still edit - // this new contact object we created. Under api 31, the contact object is readonly. - // we needed to update the rest of the VOL_SharedCode class to api 31 to handle state & country picklists. - public static SObject LoadAndCopyObject(ID id, SObject sobj, list listStrFields) { - - - Schema.DescribeSObjectResult des = sobj.getSObjectType().getDescribe(); - - // if fields not provided, get all Contact fields - if (listStrFields == null) { - listStrFields = new list(); - // get the fields for the object - Map mapS = des.fields.getMap().clone(); - // avoid any of the API version 30 compound fields - // we only worry about Contact ones, since all callers are giving us contacts to copy. - mapS.remove('mailingaddress'); - mapS.remove('otheraddress'); - listStrFields.addAll(mapS.keySet()); - } - - string strSoql = 'select '; - string strComma = ''; - for (string strF : listStrFields) { - strSoql += strComma + strF; - strComma = ', '; - } - strSoql += ' from ' + des.getName() + ' where Id = :id '; - strSoql += ' limit 1'; - list listSObj = Database.Query(strSoql); - - if (listSObj.size() > 0) { - SObject sobjT = listSObj[0]; - // now copy over all the non-null fields from the form's contact to the existing contact. - for (string strF : listStrFields) { - if (sobjT.get(strF) != null) { - try { - sobj.put(strF, sobjT.get(strF)); - } catch(exception ex) { - - } - } - } - return sobjT; - } - return null; - } - -} \ No newline at end of file diff --git a/src/classes/VOL_SharedCode_TEST.cls b/src/classes/VOL_SharedCode_TEST.cls index 62959e7..079e2a3 100644 --- a/src/classes/VOL_SharedCode_TEST.cls +++ b/src/classes/VOL_SharedCode_TEST.cls @@ -28,12 +28,52 @@ POSSIBILITY OF SUCH DAMAGE. */ -@isTest +@IsTest public with sharing class VOL_SharedCode_TEST { - + public static VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); + private static Contact contactRecord = UTIL_UnitTest.createContact('Name ' + DateTime.now().getTime()); //==================== TEST METHOD(s) ====================================== - static testmethod void CodeCoverageTests() { + @IsTest + private static void shouldCheckCreateAccessAndUpdateMatchingContact() { + insert contactRecord; + setAccessMock(); + + VOL_SharedCode.CreateContactOrLead(contactRecord, true, true); + VOL_SharedCode_TEST.accessMock.assertMethodCalled('checkCreateAccess', Contact.SObjectType); + accessMock.assertMethodCalled('updateRecords', Contact.SObjectType); + } + + @IsTest + private static void shouldCheckCreateAccessAndInsertWhenMatchingContactNotFound() { + setAccessMock(); + + VOL_SharedCode.CreateContactOrLead(contactRecord, true, true); + VOL_SharedCode_TEST.accessMock.assertMethodCalled('checkCreateAccess', Contact.SObjectType); + accessMock.assertMethodCalled('insertRecords', Contact.SObjectType); + } + + @IsTest + private static void shouldCheckCreateAccessAndUpdateMatchingContactFS() { + insert contactRecord; + setAccessMock(); + + VOL_SharedCode.CreateOrUpdateContactFS('', contactRecord, 'ABC Company', new List{ 'FirstName', 'LastName', 'Email' }); + VOL_SharedCode_TEST.accessMock.assertMethodCalled('checkCreateAccess', Contact.SObjectType); + accessMock.assertMethodCalled('updateRecords', Contact.SObjectType); + } + + @IsTest + private static void shouldCheckCreateAccessAndInsertWhenMatchingContactNotFoundFS() { + setAccessMock(); + + VOL_SharedCode.CreateOrUpdateContactFS('', contactRecord, 'ABC Company', new List{ 'FirstName', 'LastName', 'Email' }); + VOL_SharedCode_TEST.accessMock.assertMethodCalled('checkCreateAccess', Contact.SObjectType); + accessMock.assertMethodCalled('insertRecords', Contact.SObjectType); + } + + @IsTest + private static void CodeCoverageTests() { // since this class is all shared code, it gets heavily exercised by all other test code. // we just need to add a test for hitting the scenario where there aren't any settings specified in the instance. @@ -49,7 +89,7 @@ public with sharing class VOL_SharedCode_TEST { System.assertEquals('foo', c2.LastName); Contact c3 = new Contact(); - VOL_SharedCodeAPI25.LoadAndCopyObject(c.Id, c3, null); + VOL_SharedCode.LoadAndCopyObject(c.Id, c3, null); System.assertEquals('foo', c3.LastName); } @@ -57,28 +97,36 @@ public with sharing class VOL_SharedCode_TEST { * @description test methods to test all permutations of the Contact Matching Rule. * @return void */ - static testmethod void testContactMatchRule1() { + @IsTest + private static void testContactMatchRule1() { testContactMatchRule('Firstname;Lastname;Email'); } - static testmethod void testContactMatchRule2() { + @IsTest + private static void testContactMatchRule2() { testContactMatchRule('Firstname;Lastname'); } - static testmethod void testContactMatchRule3() { + @IsTest + private static void testContactMatchRule3() { testContactMatchRule('Firstname;Email'); } - static testmethod void testContactMatchRule4() { + @IsTest + private static void testContactMatchRule4() { testContactMatchRule('Lastname;Email'); } - static testmethod void testContactMatchRule5() { + @IsTest + private static void testContactMatchRule5() { testContactMatchRule(''); } - static testmethod void testContactMatchRule6() { + @IsTest + private static void testContactMatchRule6() { testContactMatchRule('Firstname;'); } - static testmethod void testContactMatchRule7() { + @IsTest + private static void testContactMatchRule7() { testContactMatchRule('Lastname'); } - static testmethod void testContactMatchRule8() { + @IsTest + private static void testContactMatchRule8() { testContactMatchRule('Email'); } static void testContactMatchRule(string strRule) { @@ -183,7 +231,8 @@ public with sharing class VOL_SharedCode_TEST { } - static testmethod void testCampaignHierarchy() { + @IsTest + private static void testCampaignHierarchy() { // build campaign hierarchy as shown above map mapCmp = mapCampaignTestHierarchy(); @@ -210,7 +259,8 @@ public with sharing class VOL_SharedCode_TEST { } // tests for the isValidContactIdAndEmail(ID contactId, string strEmail) method - static testmethod void testIsValidContactIdAndEmail() { + @IsTest + private static void testIsValidContactIdAndEmail() { // create an account and contact Account acc = new Account(Name='my test account'); insert acc; @@ -234,7 +284,8 @@ public with sharing class VOL_SharedCode_TEST { system.assertEquals(true, VOL_SharedCode.isValidContactIdAndEmail(con.Id, 'foo@bar.com')); } - static testmethod void testAddressFieldDetection() { + @IsTest + private static void testAddressFieldDetection() { system.assertEquals(true, VOL_SharedCode.isMailingAddressField('MailingCity')); system.assertEquals(true, VOL_SharedCode.isMailingAddressField('mailingcity')); system.assertEquals(true, VOL_SharedCode.isMailingAddressField('MailingState')); @@ -271,6 +322,11 @@ public with sharing class VOL_SharedCode_TEST { } + + ///////////////// + /// Helpers + ///////////////// + /******************************************************************************************************* * @description create a campaign, job, hourly shifts for yesterday, today, and tomorrow, then create * a contact and assign Hours for each shift. @@ -317,5 +373,7 @@ public with sharing class VOL_SharedCode_TEST { return contact.Id; } - + public static void setAccessMock() { + VOL_SharedCode.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + } } \ No newline at end of file diff --git a/src/classes/VOL_TEST_VolunteerHours_Trigger.cls b/src/classes/VOL_TEST_VolunteerHours_Trigger.cls index e342f76..53ec806 100644 --- a/src/classes/VOL_TEST_VolunteerHours_Trigger.cls +++ b/src/classes/VOL_TEST_VolunteerHours_Trigger.cls @@ -30,6 +30,41 @@ @isTest private class VOL_TEST_VolunteerHours_Trigger { + private static VOL_Access_TEST.Stub accessMock = new VOL_Access_TEST.Stub(); + + @IsTest + private static void shouldUpdateShiftRecordsOnConfirmedHours() { + VOL_SharedCode.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + UTIL_UnitTest.generateDataWithShift(); + Id shiftId = UTIL_UnitTest.getId(Volunteer_Shift__c.SObjectType); + + Volunteer_Hours__c hours = UTIL_UnitTest.createHours( + UTIL_UnitTest.getId(Contact.SObjectType), + UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType), + shiftId + ); + hours.Status__c = 'Confirmed'; + insert hours; + + accessMock.assertMethodCalled('updateRecords', Volunteer_Shift__c.SObjectType); + } + + @IsTest + private static void shouldNotUpdateShiftRecordsOnPendingHours() { + VOL_SharedCode.access = (VOL_Access) Test.createStub(VOL_Access.class, accessMock); + UTIL_UnitTest.generateDataWithShift(); + Id shiftId = UTIL_UnitTest.getId(Volunteer_Shift__c.SObjectType); + + Volunteer_Hours__c hours = UTIL_UnitTest.createHours( + UTIL_UnitTest.getId(Contact.SObjectType), + UTIL_UnitTest.getId(Volunteer_Job__c.SObjectType), + shiftId + ); + hours.Status__c = 'Pending'; + insert hours; + + accessMock.assertMethodCalled('updateRecords', null); + } static testMethod void myUnitTest() { Account acc = new Account(name='Individual Test Account'); diff --git a/src/classes/VOL_VRS_TEST.cls b/src/classes/VOL_VRS_TEST.cls index 0e5beed..e56fa74 100644 --- a/src/classes/VOL_VRS_TEST.cls +++ b/src/classes/VOL_VRS_TEST.cls @@ -76,23 +76,7 @@ private with sharing class VOL_VRS_TEST { */ @TestSetup private static void generateData() { - Campaign campaignRecord = new Campaign( - RecordTypeId = VOL_SharedCode.recordtypeIdVolunteersCampaign, - Name = 'Spring Garden Party', - IsActive = true - ); - insert campaignRecord; - - Volunteer_Job__c volunteerJob = new Volunteer_Job__c( - Name = 'Classroom Helper', - Campaign__c = campaignRecord.Id - ); - insert volunteerJob; - - Contact contactRecord = new Contact( - FirstName = 'Andy', - LastName = 'Helper'); - insert contactRecord; + UTIL_UnitTest.generateData(); } //****************************************************************************************************** diff --git a/src/components/SoqlListView.component b/src/components/SoqlListView.component index 10cf033..66f3ca0 100644 --- a/src/components/SoqlListView.component +++ b/src/components/SoqlListView.component @@ -58,13 +58,14 @@ {!$Label.labelListViewFirst} - + {!$Label.labelListViewPrevious} - + {!$Label.labelListViewNext} - + {!$Label.labelListViewLast} - +     + @@ -139,13 +140,14 @@
{!$Label.labelListViewFirst} - + {!$Label.labelListViewPrevious} - + {!$Label.labelListViewNext} - + {!$Label.labelListViewLast} - +     + diff --git a/src/components/UTIL_HtmlOutput.component b/src/components/UTIL_HtmlOutput.component new file mode 100644 index 0000000..70f086b --- /dev/null +++ b/src/components/UTIL_HtmlOutput.component @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/UTIL_HtmlOutput.component-meta.xml b/src/components/UTIL_HtmlOutput.component-meta.xml new file mode 100644 index 0000000..f6481ac --- /dev/null +++ b/src/components/UTIL_HtmlOutput.component-meta.xml @@ -0,0 +1,6 @@ + + + 47.0 + + This controller has a list of allowlisted urls and tags, any additional approved items must be added to the controller. + diff --git a/src/email/Voluntarios_Espa_oles/Recordatorio_de_Jornada_Voluntaria_usando_Membrete.email b/src/email/Voluntarios_Espa_oles/Recordatorio_de_Jornada_Voluntaria_usando_Membrete.email index 5470ddc..90fb122 100644 --- a/src/email/Voluntarios_Espa_oles/Recordatorio_de_Jornada_Voluntaria_usando_Membrete.email +++ b/src/email/Voluntarios_Espa_oles/Recordatorio_de_Jornada_Voluntaria_usando_Membrete.email @@ -5,7 +5,7 @@ -Hola {!Contact.FirstName},

Estamos preparando el Evento para que usted nos ayude en nuestro evento.

{!Volunteer_Shift__c.Volunteer_Job__c}
{!Volunteer_Shift__c.Start_Date_Time__c}
{!Volunteer_Shift__c.Description__c}

Dirección:

{!Volunteer_Shift__c.Job_Location_Street__c}
{!Volunteer_Shift__c.Job_Location_City__c}, {!Volunteer_Shift__c.Job_Location_State_Province__c} {!Volunteer_Shift__c.Job_Location_Zip_Postal_Code__c}

Gracias de Parte de todos nosotros {!Organization.Name}]]> +Hola {!Contact.FirstName},

Estamos preparando el Evento para que usted nos ayude en nuestro evento.

{!Volunteer_Shift__c.Volunteer_Job__c}
{!Volunteer_Shift__c.Start_Date_Time__c}
{!Volunteer_Shift__c.Description__c}

Dirección:

{!Volunteer_Shift__c.Job_Location_Street__c}
{!Volunteer_Shift__c.Job_Location_City__c}, {!Volunteer_Shift__c.Job_Location_State_Province__c} {!Volunteer_Shift__c.Job_Location_Zip_Postal_Code__c}

Gracias de Parte de todos nosotros {!Organization.Name}]]> diff --git a/src/featureParameters/GrantGuestUsersUpdateAccess.featureParameterBoolean b/src/featureParameters/GrantGuestUsersUpdateAccess.featureParameterBoolean new file mode 100644 index 0000000..4cf497d --- /dev/null +++ b/src/featureParameters/GrantGuestUsersUpdateAccess.featureParameterBoolean @@ -0,0 +1,6 @@ + + + SubscriberToLmo + Grant Guest Users Update Access Enabled + false + \ No newline at end of file diff --git a/src/labels/CustomLabels.labels b/src/labels/CustomLabels.labels index e0e7f27..f808624 100644 --- a/src/labels/CustomLabels.labels +++ b/src/labels/CustomLabels.labels @@ -503,6 +503,14 @@ day: 'day' label displayed on PersonalSiteContactInfo if the lookup and email succeed Message sent. Please check your email for a message that contains a link to your Volunteer information. Thank you. + + labelContactLookupAmbiguous + PersonalSiteContactInfo + en_US + false + label displayed on PersonalSiteContactInfo after lookup is complete + If your contact information was found, we'll send a unique volunteer link to the email we have on file. + labelCopySchedule NewAndEditVRS @@ -709,7 +717,7 @@ day: 'day' en_US false label for page info in list view - &nbsp;&nbsp;&nbsp;Page {0} of {1} &nbsp;&nbsp;({2} records) + Page {0} of {1} ({2} records) labelListViewPrevious @@ -1149,7 +1157,7 @@ day: 'day' en_US true labelVRSBatchDescription - <p>Volunteers for Salesforce supports Volunteer Recurrence Schedules and Job Recurrence Schedules with the help of a batch process to create future Shifts and assign volunteers to them. You can run this process manually, by clicking the Run Batch button, or you can schedule it to automatically run nightly.</p><p>You will see a progress bar below for the most recent batch process. The process may take some time; it will take longer the more data you have in your database. You can close this page and the process will continue in the background.</p><p>To view currently scheduled Jobs, go to the <a href='/08e?setupid=ScheduledJobs'>Scheduled Jobs page.</a></p><p>To schedule the Recurrence Schedules process to run automatically, go to the <a href='/ui/setup/apex/batch/ScheduleBatchApexPage'>Schedule Apex Jobs page.</a></p> + <p>Volunteers for Salesforce supports Volunteer Recurrence Schedules and Job Recurrence Schedules with the help of a batch process to create future Shifts and assign volunteers to them. You can run this process manually, by clicking the Run Batch button, or you can schedule it to automatically run nightly.</p><p>You will see a progress bar below for the most recent batch process. The process may take some time; it will take longer the more data you have in your database. You can close this page and the process will continue in the background.</p><p>To view currently scheduled Jobs, go to the <a href="/08e?setupid=ScheduledJobs">Scheduled Jobs page.</a></p><p>To schedule the Recurrence Schedules process to run automatically, go to the <a href="/ui/setup/apex/batch/ScheduleBatchApexPage">Schedule Apex Jobs page.</a></p> labelVRSBatchProcess diff --git a/src/objects/Contact.object b/src/objects/Contact.object index 49d1714..f0eccda 100644 --- a/src/objects/Contact.object +++ b/src/objects/Contact.object @@ -962,16 +962,6 @@ false false - - Jigsaw - false - false - - - JigsawContactId - false - false - LastActivityDate false @@ -1282,11 +1272,6 @@ false false - - Jigsaw - false - false - LastActivityDate false diff --git a/src/objects/Volunteer_Job__c.object b/src/objects/Volunteer_Job__c.object index 8c8b249..407e364 100644 --- a/src/objects/Volunteer_Job__c.object +++ b/src/objects/Volunteer_Job__c.object @@ -762,4 +762,12 @@ false /apex/{!$Setup.PackageSettings__c.NamespacePrefix__c}SendBulkEmail?jobId={!Volunteer_Job__c.Id} + + ValidateExternalSignupUrl + true + Validates that the External Signup URL field begins with an expected URI Scheme. + NOT(OR(ISBLANK(External_Signup_Url__c), REGEX(External_Signup_Url__c,"^(http|https|mailto).*"))) + External_Signup_Url__c + The External Signup Url value must begin with http, https, or mailto. + diff --git a/src/objects/Volunteers_Settings__c.object b/src/objects/Volunteers_Settings__c.object index 58bafca..378fdb5 100644 --- a/src/objects/Volunteers_Settings__c.object +++ b/src/objects/Volunteers_Settings__c.object @@ -49,6 +49,15 @@ Text false + + Grant_Guest_Users_Update_Access__c + false + false + Allow guest users to update records based on their Create access. + + false + Checkbox + Personal_Site_Org_Wide_Email_Name__c false diff --git a/src/package.xml b/src/package.xml index df017c3..7f3ce91 100644 --- a/src/package.xml +++ b/src/package.xml @@ -1,9 +1,10 @@ Volunteers for Salesforce - Unrestricted ComponentControllerBase + DatabaseDml + DatabaseDml_TEST InstallScript InstallScript_TEST PageControllerBase @@ -11,15 +12,20 @@ QueryBuilder_TEST SoqlListView Telemetry - Telemetry_TEST TelemetryService TelemetryService_TEST + Telemetry_TEST UTIL_Describe UTIL_Describe_TEST + UTIL_HtmlOutput_CTRL + UTIL_HtmlOutput_TEST UTIL_JavaScriptSanitizer UTIL_JavaScriptSanitizer_TEST UTIL_JobProgress_CTRL UTIL_PageMessages_CTRL + UTIL_UnitTest + VOL_Access + VOL_Access_TEST VOL_BATCH_Recurrence VOL_BATCH_Recurrence_TEST VOL_CTRL_BatchProgress @@ -60,7 +66,6 @@ VOL_JRS VOL_JRS_TEST VOL_SharedCode - VOL_SharedCodeAPI25 VOL_SharedCode_TEST VOL_StateCountryPicklists VOL_StateCountryPicklists_TEST @@ -73,6 +78,7 @@ SoqlListView UTIL_FormField + UTIL_HtmlOutput UTIL_JobProgressLightning UTIL_NavigateBack UTIL_PageMessages @@ -210,6 +216,7 @@ Volunteers_Settings__c.Contact_Match_First_Name_Fields__c Volunteers_Settings__c.Contact_Matching_Rule__c Volunteers_Settings__c.Google_Maps_API_Key__c + Volunteers_Settings__c.Grant_Guest_Users_Update_Access__c Volunteers_Settings__c.Personal_Site_Org_Wide_Email_Name__c Volunteers_Settings__c.Personal_Site_Report_Hours_Filtered__c Volunteers_Settings__c.Personal_Site_Requires_URL_Email_Match__c @@ -279,6 +286,7 @@ labelContactInfoRankTitle labelContactInfoTimeTableColumn labelContactInfoUpcomingShifts + labelContactLookupAmbiguous labelContactLookupInstructions labelContactLookupNotFound labelContactLookupSuccess @@ -553,6 +561,10 @@ Volunteers_Email_Templates/Volunteers_Personal_Site_Contact_Lookup EmailTemplate + + GrantGuestUsersUpdateAccess + FeatureParameterBoolean + CampaignsWithJobs ContactsWithLastWebSignUpLastYear @@ -677,6 +689,7 @@ Job_Recurrence_Schedule__c.JRS_Weekly_Occurrence_Required Volunteer_Hours__c.HoursRequiredOnCompletion Volunteer_Hours__c.StatusMustBeSet + Volunteer_Job__c.ValidateExternalSignupUrl Volunteer_Recurrence_Schedule__c.VRS_Days_of_Week_Required Volunteer_Recurrence_Schedule__c.VRS_Weekly_Occurrence_Required ValidationRule diff --git a/src/pages/JobCalendar.page b/src/pages/JobCalendar.page index a557ca5..58ffaf4 100644 --- a/src/pages/JobCalendar.page +++ b/src/pages/JobCalendar.page @@ -37,8 +37,8 @@ cache="false" title="{!$Label.labelCalendarPageTitle}"> - - + + @@ -152,7 +152,7 @@ var strUrl; if ({!fWeb}) { - strUrl = '{!$Site.CurrentSiteUrl + IF(fPersonalSite, ns+'PersonalSiteJobListing', ns+'VolunteersJobListingFS')}' + + strUrl = '{!$Site.BaseRequestUrl + '/' + IF(fPersonalSite, ns+'PersonalSiteJobListing', ns+'VolunteersJobListingFS')}' + '?Calendar=1&volunteerShiftId=' + strShiftId + '&jobId=' + strJobId + '&dtMonthFilter=' + strApexDateUTC(dtStart) + '{!JSENCODE(IF(strParams != null, '&' + strParams, ''))}'; @@ -289,6 +289,18 @@ div.message-panel:empty { display: none; } + .fc-grid .fc-other-month .fc-day-number { + opacity: 0.8; + filter: alpha(opacity=80); + } + .fc-state-active, + .fc-state-active .fc-button-inner { + background: #333; + } + .fc-state-disabled, + .fc-state-disabled .fc-button-inner { + color: #555; + } diff --git a/src/pages/PersonalSiteContactInfo.page b/src/pages/PersonalSiteContactInfo.page index cc71f3e..e19210a 100644 --- a/src/pages/PersonalSiteContactInfo.page +++ b/src/pages/PersonalSiteContactInfo.page @@ -32,7 +32,7 @@ @@ -149,13 +149,13 @@ - + diff --git a/src/pages/PersonalSiteContactLookup.page b/src/pages/PersonalSiteContactLookup.page index 34eb728..48ca2af 100644 --- a/src/pages/PersonalSiteContactLookup.page +++ b/src/pages/PersonalSiteContactLookup.page @@ -32,15 +32,15 @@ language="{!strLanguage}" cache="false"> - - + + - + diff --git a/src/pages/PersonalSiteTemplate.page b/src/pages/PersonalSiteTemplate.page index cd916d4..458da91 100644 --- a/src/pages/PersonalSiteTemplate.page +++ b/src/pages/PersonalSiteTemplate.page @@ -31,8 +31,8 @@ - - + + @@ -49,12 +49,12 @@
+ diff --git a/src/pages/PersonalSiteTemplateEspanol.page b/src/pages/PersonalSiteTemplateEspanol.page index 54e4a80..1aa0f97 100644 --- a/src/pages/PersonalSiteTemplateEspanol.page +++ b/src/pages/PersonalSiteTemplateEspanol.page @@ -31,8 +31,8 @@ - - + + @@ -49,10 +49,10 @@ diff --git a/src/pages/SendBulkEmail.page b/src/pages/SendBulkEmail.page index f323d84..be69022 100644 --- a/src/pages/SendBulkEmail.page +++ b/src/pages/SendBulkEmail.page @@ -95,7 +95,7 @@ - +
@@ -163,7 +163,7 @@
- +
diff --git a/src/pages/VolunteersAbout.page b/src/pages/VolunteersAbout.page index 5a585bc..b527adc 100644 --- a/src/pages/VolunteersAbout.page +++ b/src/pages/VolunteersAbout.page @@ -32,7 +32,7 @@
-

+

{!$Label.labelVolunteersHelpStep1}
@@ -46,7 +46,7 @@
{!$Label.labelVolunteersHelpStep2}
- + {!$Label.labelVolunteersHelpStep2}
@@ -55,8 +55,8 @@
{!$Label.labelVolunteersHelpStep3}
- - + {!$Label.labelVolunteersHelpStep3}
diff --git a/src/pages/VolunteersBatchJobsProgress.page b/src/pages/VolunteersBatchJobsProgress.page index 4652fa6..1da73c9 100644 --- a/src/pages/VolunteersBatchJobsProgress.page +++ b/src/pages/VolunteersBatchJobsProgress.page @@ -29,49 +29,49 @@ --> - - -
+ tabStyle="Process_Recurrence_Schedules__tab" + standardStylesheets="false" + title="{!$Label.labelVRSBatchProcess}" + docType="html-5.0"> + + +
- - + + -
- -
-
-
-
- -
-
-
-
- - - -
-
+
+ +
+
+
+
+ +
+
+
+ + + + +
+
\ No newline at end of file diff --git a/src/pages/VolunteersFind.page b/src/pages/VolunteersFind.page index 8bc722c..f98b351 100644 --- a/src/pages/VolunteersFind.page +++ b/src/pages/VolunteersFind.page @@ -31,8 +31,8 @@ - - + +