diff --git a/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnections.cmp b/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnections.cmp index 778c65ec2c..23fb06ab7a 100644 --- a/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnections.cmp +++ b/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnections.cmp @@ -1,7 +1,5 @@ - - @@ -11,7 +9,7 @@ - +
@@ -31,7 +29,7 @@
- +
    @@ -44,7 +42,6 @@
-
@@ -79,6 +76,7 @@
+
{!$Label.c.stgDefaultStudentTypeTitle} @@ -132,6 +130,7 @@
+
@@ -157,6 +156,28 @@
+
+
+ +
+
+ +
+ +
+
+ {!$Label.c.stgTitleAffiliationBackfill} +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsController.js b/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsController.js index be30bd5e78..214c5b617f 100644 --- a/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsController.js +++ b/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsController.js @@ -11,6 +11,10 @@ helper.startBackfill(component); }, + handleAffiliationBackfill : function(component, event, helper) { + helper.handleAffiliationBackfill(component); + }, + closeBackFillToast: function(component, event, helper) { helper.closeBackFillToast(component); }, diff --git a/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsHelper.js b/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsHelper.js index 603b68e3a7..2007651f4e 100644 --- a/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsHelper.js +++ b/src/aura/STG_CMP_CourseConnections/STG_CMP_CourseConnectionsHelper.js @@ -30,41 +30,74 @@ var action = component.get("c.getEnqueueCourseConnectionsBackfill"); action.setCallback(this, function(response) { if(response.getState() === "SUCCESS") { - var successMessage = $A.get("$Label.c.stgCourseConnBackFillSuccess"); - component.set('v.startBackfillMessage', successMessage); - component.set('v.backFillToastIcon', 'success'); - component.set('v.backFillToastClass', 'slds-notify slds-notify_toast slds-theme_success'); + var successMessage = $A.get("$Label.c.stgCourseConnBackFillSuccess"); + component.set('v.startBackfillMessage', successMessage); + component.set('v.backFillToastIcon', 'success'); + component.set('v.backFillToastClass', 'slds-notify slds-notify_toast slds-theme_success'); + var tst = component.find("backFillToast"); + $A.util.removeClass(tst, "slds-hide"); + $A.util.addClass(tst, "slds-show"); + window.setTimeout( + $A.getCallback(function() { + $A.util.removeClass(tst, "slds-show"); + $A.util.addClass(tst, "slds-hide"); + }), 5000 + ); + } else if(response.getState() === "ERROR") { + var errorMessage = $A.get("$Label.c.stgCourseConnBackFillError"); + component.set('v.startBackfillMessage', errorMessage); + component.set('v.backFillToastIcon', 'error'); + component.set('v.backFillToastClass', 'slds-notify slds-notify_toast slds-theme_error'); var tst = component.find("backFillToast"); $A.util.removeClass(tst, "slds-hide"); $A.util.addClass(tst, "slds-show"); - window.setTimeout( - $A.getCallback(function() { - $A.util.removeClass(tst, "slds-show"); - $A.util.addClass(tst, "slds-hide"); - }), 5000 + $A.getCallback(function() { + $A.util.removeClass(tst, "slds-show"); + $A.util.addClass(tst, "slds-hide"); + }), 5000 ); - - } else if(response.getState() === "ERROR") { - var errorMessage = $A.get("$Label.c.stgCourseConnBackFillError"); - component.set('v.startBackfillMessage', errorMessage); - component.set('v.backFillToastIcon', 'error'); - component.set('v.backFillToastClass', 'slds-notify slds-notify_toast slds-theme_error'); - var tst = component.find("backFillToast"); - $A.util.removeClass(tst, "slds-hide"); - $A.util.addClass(tst, "slds-show"); - - window.setTimeout( - $A.getCallback(function() { - $A.util.removeClass(tst, "slds-show"); - $A.util.addClass(tst, "slds-hide"); - }), 5000 - ); - } + } }); $A.enqueueAction(action); }, - + + handleAffiliationBackfill : function(component, event, helper) { + var action = component.get("c.executeAffiliationBackfillOnCourseConnection"); + action.setCallback(this, function(response) { + if(response.getState() === "SUCCESS") { + var successMessage = $A.get("$Label.c.stgBackfillQueuedEmailSent"); + component.set('v.startBackfillMessage', successMessage); + omponent.set('v.backFillToastIcon', 'success'); + component.set('v.backFillToastClass', 'slds-notify slds-notify_toast slds-theme_success'); + var tst = component.find("backFillToast"); + $A.util.removeClass(tst, "slds-hide"); + $A.util.addClass(tst, "slds-show"); + window.setTimeout( + $A.getCallback(function() { + $A.util.removeClass(tst, "slds-show"); + $A.util.addClass(tst, "slds-hide"); + }), 5000 + ); + } else if(response.getState() === "ERROR") { + var errorMessage = $A.get("$Label.c.stgCourseConnBackFillError"); + component.set('v.startBackfillMessage', errorMessage); + component.set('v.backFillToastIcon', 'error'); + component.set('v.backFillToastClass', 'slds-notify slds-notify_toast slds-theme_error'); + var tst = component.find("backFillToast"); + $A.util.removeClass(tst, "slds-hide"); + $A.util.addClass(tst, "slds-show"); + window.setTimeout( + $A.getCallback(function() { + $A.util.removeClass(tst, "slds-show"); + $A.util.addClass(tst, "slds-hide"); + }), 5000 + ); + } + }); + $A.enqueueAction(action); + }, + closeBackFillToast: function(component) { var tst = component.find("backFillToast"); $A.util.removeClass(tst, "slds-show"); diff --git a/src/classes/CCON_AffiliationBackfill_BATCH.cls b/src/classes/CCON_AffiliationBackfill_BATCH.cls new file mode 100644 index 0000000000..2498769820 --- /dev/null +++ b/src/classes/CCON_AffiliationBackfill_BATCH.cls @@ -0,0 +1,95 @@ +/* + 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 Course Enrollments +* @group-content ../../ApexDocContent/CourseEnrollments.htm +* @description This is a batch class that backfills existing Student Course Connection with missing Affiliation. +*/ +public class CCON_AffiliationBackfill_BATCH implements Database.Batchable{ + + public Database.Querylocator start(Database.BatchableContext bc) { + Id studentCCRecordTypeId = UTIL_CustomSettingsFacade.getSettings().Student_RecType__c; + String query = 'SELECT Id, Program_Enrollment__c, Affiliation__c ' + + 'FROM Course_Enrollment__c ' + + 'WHERE Affiliation__c = NULL AND Program_Enrollment__c != NULL AND RecordTypeId = :studentCCRecordTypeId'; + + return Database.getQueryLocator(String.escapeSingleQuotes(query)); + } + + public void execute(Database.BatchableContext bc, List returnCourseEnrollments){ + List pEnrollmentID = new List(); + List courseEnrollmentsToUpdate = new List(); + + if (returnCourseEnrollments.size () > 0) { + + for (Course_Enrollment__c enroll : returnCourseEnrollments) { + if (enroll.Program_Enrollment__c != NULL) { + pEnrollmentID.add(enroll.Program_Enrollment__c); + } + } + + if (pEnrollmentID.size () > 0) { + List queryPE = [SELECT Id, Affiliation__c, Account__c + FROM Program_Enrollment__c + WHERE Id = :pEnrollmentID]; + Map pEnrollmentsMap = new Map(queryPE); + + for (Course_Enrollment__c courseEnroll : returnCourseEnrollments) { + courseEnroll.Affiliation__c = pEnrollmentsMap.get(courseEnroll.Program_Enrollment__c).Affiliation__c; + courseEnroll.Account__c = pEnrollmentsMap.get(courseEnroll.Program_Enrollment__c).Account__c; + courseEnrollmentsToUpdate.add(courseEnroll); + } + + } + } + + if (courseEnrollmentsToUpdate.size() > 0) { + update courseEnrollmentsToUpdate; + } + } + + public void finish(Database.BatchableContext bc) { + AsyncApexJob a = [SELECT Id, Status, JobType, NumberOfErrors, + JobItemsProcessed, TotalJobItems, CompletedDate, + ExtendedStatus, CreatedById, CreatedBy.Email + FROM AsyncApexJob + WHERE Id =:bc.getJobId()]; + + Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); + List toAddresses = new List {a.CreatedBy.Email}; + mail.setToAddresses(toAddresses); + mail.setSubject('Backfill of Affiliation on Student Course Connection Status : ' + a.Status); + mail.setPlainTextBody('The batch Apex job processed ' + a.TotalJobItems + + ' batches with '+ a.NumberOfErrors + ' failures.'); + Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail }); + } +} \ No newline at end of file diff --git a/src/classes/CCON_AffiliationBackfill_BATCH.cls-meta.xml b/src/classes/CCON_AffiliationBackfill_BATCH.cls-meta.xml new file mode 100644 index 0000000000..252fbfd041 --- /dev/null +++ b/src/classes/CCON_AffiliationBackfill_BATCH.cls-meta.xml @@ -0,0 +1,5 @@ + + + 47.0 + Active + diff --git a/src/classes/CCON_AffiliationBackfill_TEST.cls b/src/classes/CCON_AffiliationBackfill_TEST.cls new file mode 100644 index 0000000000..e1797e5bb5 --- /dev/null +++ b/src/classes/CCON_AffiliationBackfill_TEST.cls @@ -0,0 +1,193 @@ +/* + 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 Course Enrollments +* @group-content ../../ApexDocContent/CourseEnrollments.htm +* @description Tests class for backfilling Affiliation on Student Course Connection. +*/ +@isTest +public class CCON_AffiliationBackfill_TEST { + + private static Account university; + private static Account biologyDept; + private static Course__c biologyCourse; + private static Term__c term; + private static List offeringsList; + private static List studentContacts; + private static List programEnrollments; + private static List studentConnections; + + /********************************************************************************************************* + * @description Test setup. + */ + @testSetup + public static void setup() { + // Enable Course Connections + UTIL_CustomSettingsFacade.getSettingsForTests( + new Hierarchy_Settings__c( + Enable_Course_Connections__c = true, + Populate_Affil_on_Student_Course_Cxn__c = false, + Student_RecType__c = UTIL_Describe_API.getStudentConnectionRecType() + ) + ); + + System.assertNotEquals(null, UTIL_Describe_API.getStudentConnectionRecType()); + System.debug(UTIL_Describe_API.getStudentConnectionRecType()); + + // Create university + university = new Account(Name = 'Advanced University'); + insert university; + + // Create Biology department + biologyDept = new Account(Name = 'Biology Department', Parent = university); + insert biologyDept; + + // Create Term + term = new Term__c(Account__c = university.Id, Start_Date__c = System.today() + 5, End_Date__c = System.today() + 90); + + // Create Biology Course + biologyCourse = new Course__c(Course_ID__c = 'Biology 101', Account__c = biologyDept.Id, Credit_Hours__c = 40, + Description__c = 'Biology 101'); + + insert new List{ + term, + biologyCourse + }; + + studentContacts = UTIL_UnitTestData_TEST.getMultipleTestContacts(20); + for (Integer i=0; i(); + createCourseOffering(biologyCourse, term); + insert offeringsList; + + programEnrollments = new List(); + createProgramEnrollment(biologyDept); + insert programEnrollments; + + studentConnections = new List(); + createCourseConnections(studentContacts); + insert studentConnections; + } + + /********************************************************************************************************* + * @description method responsible for creating Course Offerings. + * @param biologyCourse is a Course record. + * @param term is a Term record. + */ + private static void createCourseOffering(Course__c biologyCourse, Term__c term) { + for (Integer i=0; i students) { + for (Integer i=0; i studentContacts = [SELECT Id, LastName + FROM Contact + WHERE LastName LIKE 'Backfill%' LIMIT 20]; + List studentConnections = [SELECT Id, Program_Enrollment__c, Affiliation__c, Contact__c + FROM Course_Enrollment__c + WHERE Affiliation__c = NULL AND Program_Enrollment__c != NULL + AND Contact__c = :studentContacts]; + + for (Course_Enrollment__c studentCE : studentConnections) { + System.assertEquals(NULL, studentCE.Affiliation__c); + } + + Test.StartTest(); + CCON_AffiliationBackfill_BATCH batch = new CCON_AffiliationBackfill_BATCH(); + ID ApexJobId = Database.executeBatch(batch, 200); + Test.stopTest(); + + List studentConns = [SELECT Id, Affiliation__c, Account__c + FROM Course_Enrollment__c WHERE Id = :studentConnections]; + + for (Course_Enrollment__c ce : studentConns) { + System.assertNotEquals(NULL, ce.Affiliation__c); + System.assertNotEquals(NULL, ce.Account__c); + } + } +} \ No newline at end of file diff --git a/src/classes/CCON_AffiliationBackfill_TEST.cls-meta.xml b/src/classes/CCON_AffiliationBackfill_TEST.cls-meta.xml new file mode 100644 index 0000000000..252fbfd041 --- /dev/null +++ b/src/classes/CCON_AffiliationBackfill_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 47.0 + Active + diff --git a/src/classes/STG_CourseConnections.cls b/src/classes/STG_CourseConnections.cls index 49752eb943..d025c53c84 100644 --- a/src/classes/STG_CourseConnections.cls +++ b/src/classes/STG_CourseConnections.cls @@ -44,4 +44,21 @@ public class STG_CourseConnections { Id ApexJobId = Database.executeBatch(batch, 200); return ApexJobId; } + + /*********************************************************************************************** + * @description Enqueues and returns a Job Id for existing Student Course Connections + * with missing Affiliation backfill. + * @return Id of Student Course Connection Backfill Job + */ + @AuraEnabled + public static Id executeAffiliationBackfillOnCourseConnection() { + Id jobId; + Boolean affiliationEnabled = UTIL_CustomSettingsFacade.getSettings().Populate_Affil_on_Student_Course_Cxn__c; + if (affiliationEnabled) { + CCON_AffiliationBackfill_BATCH batch = new CCON_AffiliationBackfill_BATCH(); + Id ApexJobId = Database.executeBatch(batch, 200); + jobId = ApexJobId; + } + return jobId; + } } \ No newline at end of file diff --git a/src/classes/STG_CourseConnections_TEST.cls b/src/classes/STG_CourseConnections_TEST.cls index 0be6f9054b..bd0d674276 100644 --- a/src/classes/STG_CourseConnections_TEST.cls +++ b/src/classes/STG_CourseConnections_TEST.cls @@ -35,9 +35,26 @@ */ @isTest public with sharing class STG_CourseConnections_TEST { + /********************************************************************************************************* + * @description Tests if backfill of Course Connection batch class is successfully run. + */ @isTest public static void checkGetEnqueueCourseConnectionsBackfill() { Id apexJobId = STG_CourseConnections.getEnqueueCourseConnectionsBackfill(); System.assertNotEquals(null, apexJobId); } + + /********************************************************************************************************* + * @description Tests if backfill of Affiliation on Student Course Connection batch class is successfully run. + */ + @isTest + public static void checkExecuteAffiliationBackfillOnCourseConnection() { + UTIL_CustomSettingsFacade.getSettingsForTests(new Hierarchy_Settings__c( + Account_Processor__c = UTIL_Describe_API.getAdminAccRecTypeID(), + Enable_Course_Connections__c = true, + Populate_Affil_on_Student_Course_Cxn__c = true + )); + Id apexJobId = STG_CourseConnections.executeAffiliationBackfillOnCourseConnection(); + System.assertNotEquals(null, apexJobId); + } } \ No newline at end of file diff --git a/src/classes/UTIL_CustomSettingsFacade.cls b/src/classes/UTIL_CustomSettingsFacade.cls index 23ee3d97f1..24d5b29c21 100644 --- a/src/classes/UTIL_CustomSettingsFacade.cls +++ b/src/classes/UTIL_CustomSettingsFacade.cls @@ -163,6 +163,7 @@ public without sharing class UTIL_CustomSettingsFacade { settings.Prevent_Contact_Deletion__c = mySettings.Prevent_Contact_Deletion__c; settings.Prevent_Course_Offering_Deletion__c = mySettings.Prevent_Course_Offering_Deletion__c; settings.Prevent_Course_Connection_Deletion__c = mySettings.Prevent_Course_Connection_Deletion__c; + settings.Populate_Affil_on_Student_Course_Cxn__c = mySettings.Populate_Affil_on_Student_Course_Cxn__c; settings.Prevent_Case_Deletion__c = mySettings.Prevent_Case_Deletion__c; settings.Prevent_Term_Deletion__c = mySettings.Prevent_Term_Deletion__c; settings.Prevent_Behavior_Involvement_Deletion__c = mySettings.Prevent_Behavior_Involvement_Deletion__c; diff --git a/src/labels/CustomLabels.labels b/src/labels/CustomLabels.labels index 9813947ee9..cd445656a6 100644 --- a/src/labels/CustomLabels.labels +++ b/src/labels/CustomLabels.labels @@ -1135,6 +1135,16 @@ stgHelpAdminRecType The Account record type used for Administrative Accounts. This setting affects management of address records and the naming format of Administrative Accounts. + + stgHelpAffilBackfill + Settings + en_US + true + stgHelpAffilBackfill + Only use Affiliation Backfill when enabling "Populate Affiliation on Student Course Connection" for the first time, and only in orgs where Course Connections (Enrollments) existed before enabling the feature. + + This backfill allows you to auto-populate the Affiliation field on existing Student Course Connection records with the Affiliation that connects it to an Academic Program. If the Course Connection isn't affiliated with an Academic Program, the field on existing Student Course Connections won't auto-populate. + stgHelpAfflCopyProgramEnrollmentEndDate Settings @@ -1777,6 +1787,14 @@ stgTabSystem System + + stgTitleAffiliationBackfill + Settings + en_US + true + stgTitleAffiliationBackfill + Affiliation Backfill on Student Course Connection: + stgTitleAllowAutocreatedDuplicateRel Settings diff --git a/src/objects/Hierarchy_Settings__c.object b/src/objects/Hierarchy_Settings__c.object index c0af57cc1d..c5e9d75393 100644 --- a/src/objects/Hierarchy_Settings__c.object +++ b/src/objects/Hierarchy_Settings__c.object @@ -343,6 +343,16 @@ Text false + + Populate_Affil_on_Student_Course_Cxn__c + false + If enabled, the Affiliation field on new Student Course Connection records automatically syncs with the Affiliation record associated with the Program Enrollment on the Student Course Connection. + false + If enabled, the Affiliation field on new Student Course Connection records automatically syncs with the Affiliation record associated with the Program Enrollment on the Student Course Connection. + + false + Checkbox + Preferred_Phone_Selection__c To select a desired Preferred Phone when Phone clean up Batch job runs diff --git a/src/package.xml b/src/package.xml index dd8a5e9089..6e3320e37a 100644 --- a/src/package.xml +++ b/src/package.xml @@ -35,6 +35,8 @@ Advancement_Adapter Advancement_Adapter_TEST Advancement_Info + CCON_AffiliationBackfill_BATCH + CCON_AffiliationBackfill_TEST BEH_CannotDelete_TDTM BEH_CannotDelete_TEST CASE_CannotDelete_TDTM @@ -453,6 +455,7 @@ Hierarchy_Settings__c.Household_Account_Naming_Format__c Hierarchy_Settings__c.Household_Addresses_RecType__c Hierarchy_Settings__c.Household_Other_Name_Setting__c + Hierarchy_Settings__c.Populate_Affil_on_Student_Course_Cxn__c Hierarchy_Settings__c.Preferred_Phone_Selection__c Hierarchy_Settings__c.Prevent_Account_Deletion__c Hierarchy_Settings__c.Prevent_Behavior_Involvement_Deletion__c @@ -704,6 +707,7 @@ stgHelpAccoutsDeletedIfChildContactsDeleted stgHelpAddressAccRecType stgHelpAdminRecType + stgHelpAffilBackfill stgHelpAfflCopyProgramEnrollmentEndDate stgHelpAfflCopyProgramEnrollmentStartDate stgHelpAfflCreateFromProgramEnrollmentHeader @@ -784,6 +788,7 @@ stgTabRel stgTabSettings stgTabSystem + stgTitleAffiliationBackfill stgTitleAllowAutocreatedDuplicateRel stgTitleAppwideSettings stgTitleCourseConnectionBackfill