diff --git a/lms/djangoapps/student_enrollment/enrollment.py b/lms/djangoapps/student_enrollment/enrollment.py index ec52472ed4..f3430bfa4a 100644 --- a/lms/djangoapps/student_enrollment/enrollment.py +++ b/lms/djangoapps/student_enrollment/enrollment.py @@ -28,6 +28,11 @@ initial student onboarding/enrollment process like the Careers module. """ EXCLUDED_FROM_ONBOARDING = ['course-v1:code_institute+cc_101+2018_T1'] + +# the only reliable way to get the output ".codeinstitute[-platform].net" +# required for the enrolment exception Zaps +LMS_PLATFORM = settings.FEATURES["PREVIEW_LMS_BASE"].split("preview.")[1] + today = date.today().isoformat() @@ -85,6 +90,7 @@ def enroll(self): 'email': student['Email'], 'crm_field': 'Programme_ID', 'unexpected_value': student['Programme_ID'], + 'lms_platform': LMS_PLATFORM, 'attempted_action': 'enroll', 'message': 'Programme ID does not exist on LMS' } @@ -219,6 +225,7 @@ def enroll(self): 'email': student['Email'], 'crm_field': 'Specialisation_programme_id', 'unexpected_value': student['Specialisation_programme_id'], + 'lms_platform': LMS_PLATFORM, 'attempted_action': 'enroll specialisation', 'message': ('Student is already enrolled into this specialisation') } @@ -238,6 +245,7 @@ def enroll(self): 'email': student['Email'], 'crm_field': 'Specialisation_programme_id', 'unexpected_value': student['Specialisation_programme_id'], + 'lms_platform': LMS_PLATFORM, 'attempted_action': 'enroll specialisation', 'message': 'Specialisation programme ID does not exist on LMS' } @@ -268,6 +276,7 @@ def enroll(self): 'email': student['Email'], 'crm_field': 'Specialisation_programme_id', 'unexpected_value': student['Specialisation_programme_id'], + 'lms_platform': LMS_PLATFORM, 'attempted_action': 'enroll specialisation', 'message': ('Specialisation change field checked, but student' + ' is already enrolled into the same specialisation') diff --git a/lms/djangoapps/student_enrollment/unenrollment.py b/lms/djangoapps/student_enrollment/unenrollment.py index 8882a5411d..7e27289729 100644 --- a/lms/djangoapps/student_enrollment/unenrollment.py +++ b/lms/djangoapps/student_enrollment/unenrollment.py @@ -13,6 +13,10 @@ log = getLogger(__name__) +# the only reliable way to get the output ".codeinstitute[-platform].net" +# required for the enrolment exception Zaps +LMS_PLATFORM = settings.FEATURES["PREVIEW_LMS_BASE"].split("preview.")[1] + class Unenrollment: ''' Unenroll students from their relevant programs @@ -59,6 +63,7 @@ def unenroll(self): 'email': student['Email'], 'crm_field': 'Email', 'unexpected_value': student['Email'], + 'lms_platform': LMS_PLATFORM, 'attempted_action': 'unenroll', 'message': 'Email on Student\'s CRM profile not found on LMS' } @@ -78,6 +83,7 @@ def unenroll(self): 'email': student['Email'], 'crm_field': 'Programme_ID', 'unexpected_value': student['Programme_ID'], + 'lms_platform': LMS_PLATFORM, 'attempted_action': 'unenroll', 'message': 'Programme ID does not exist on LMS' } diff --git a/lms/djangoapps/student_enrollment/zoho.py b/lms/djangoapps/student_enrollment/zoho.py index b08f3d9256..c74153b28b 100644 --- a/lms/djangoapps/student_enrollment/zoho.py +++ b/lms/djangoapps/student_enrollment/zoho.py @@ -9,43 +9,70 @@ REFRESH_ENDPOINT = settings.ZOHO_REFRESH_ENDPOINT COQL_ENDPOINT = settings.ZOHO_COQL_ENDPOINT +LMS_CRM_ELIGIBILITY_KEY = settings.LMS_CRM_STUDENT_ELIGIBILITY_FIELD +LMS_CRM_ELIGIBLE_VALUES = settings.LMS_CRM_STUDENT_ELIGIBILITY_FIELD_VALUES + +# NOTE: ZOHO COQL's IN operator requires a comma-separated sequence of strings, either wrapped +# in () or without any wrapping. Formatting a JSON array (from settings) into a COQL-acceptable format (no [] allowed) +# with Python requires either a simple string (for a single-element array) or a tuple (for multiple elements). +# If a tuple is used for a one-element array, it results in an (X,) format, which triggers a COQL query error. +# On the other hand, if the single element is injected into the query as a simple string, it "loses" its wrapping quotes, +# which again triggers a COQL query error. +# +# Hence the solution: +# - for single-element array, index the single element, and wrap it into parentheses AND quotes before injecting it +# - for multiple-element array, convert it into a tuple (of strings) and inject it + +if len(LMS_CRM_ELIGIBLE_VALUES) == 1: + LMS_CRM_ELIGIBLE_VALUES = '(\'{}\')'.format(LMS_CRM_ELIGIBLE_VALUES[0]) +else: + LMS_CRM_ELIGIBLE_VALUES = tuple(LMS_CRM_ELIGIBLE_VALUES) + # COQL Queries -# LMS_Version can be removed from where clause when Ginkgo is decommissioned -# Target decommission date: End of Q1 2020 +# NOTE: "Excessive" parentheses added because Zoho COQL requires every subsequent +# chained condition to be wrapped in separate parentheses e.g. (A AND (B AND (C OR D))) +# otherwise a Syntax Error is received ENROLL_QUERY = """ SELECT Email, Full_Name, Programme_ID, Student_Source FROM Contacts -WHERE (( - (Lead_Status = 'Enroll') AND (Programme_ID is not null) - ) +WHERE ( + {crm_eligibility_key} in {eligible_values} AND ( - (LMS_Version = 'Upgrade to Juniper') OR (LMS_Version = 'Juniper (learn.codeinstitute.net)') - ) -) + Lead_Status = 'Enroll' + AND ( + Programme_ID is not null +))) LIMIT {page},{per_page} """ UNENROLL_QUERY = """ SELECT Email, Full_Name, Programme_ID FROM Contacts -WHERE (( - (LMS_Access_Status = 'To be removed') AND (Reason_for_Unenrollment is not null) - ) +WHERE ( + {crm_eligibility_key} in {eligible_values} AND ( - (Programme_ID is not null) AND (LMS_Version = 'Juniper (learn.codeinstitute.net)') - ) -) + LMS_Access_Status = 'To be removed' + AND ( + Reason_for_Unenrollment is not null +))) LIMIT {page},{per_page} """ ENROLL_SPECIALISATION_QUERY = """ SELECT Email, Full_Name, Programme_ID, Specialisation_programme_id, Specialization_Enrollment_Date, Specialisation_Change_Requested_Within_7_Days FROM Contacts -WHERE (Specialisation_Enrollment_Status = 'Approved') AND (Specialisation_programme_id is not null) +WHERE ( + {crm_eligibility_key} in {eligible_values} + AND ( + Specialisation_Enrollment_Status = 'Approved' + AND ( + Specialisation_programme_id is not null +))) LIMIT {page},{per_page} """ +# Currently not used - Careers enrolment handled by CRM + Zapier automation ENROLL_IN_CAREERS_MODULE_QUERY = """ SELECT Email, Full_Name, Programme_ID FROM Contacts @@ -70,8 +97,12 @@ def get_students_to_be_enrolled(): for page in count(): query = ENROLL_QUERY.format( - page=page*RECORDS_PER_PAGE, - per_page=RECORDS_PER_PAGE) + crm_eligibility_key=LMS_CRM_ELIGIBILITY_KEY, + eligible_values=LMS_CRM_ELIGIBLE_VALUES, + page=page*RECORDS_PER_PAGE, + per_page=RECORDS_PER_PAGE + ) + students_resp = requests.post( COQL_ENDPOINT, headers=auth_headers, @@ -95,6 +126,8 @@ def get_students_to_be_enrolled_into_specialisation(): for page in count(): query = ENROLL_SPECIALISATION_QUERY.format( + crm_eligibility_key=LMS_CRM_ELIGIBILITY_KEY, + eligible_values=LMS_CRM_ELIGIBLE_VALUES, page=page*RECORDS_PER_PAGE, per_page=RECORDS_PER_PAGE, ) @@ -122,8 +155,11 @@ def get_students_to_be_unenrolled(): for page in count(): query = UNENROLL_QUERY.format( - page=page*RECORDS_PER_PAGE, - per_page=RECORDS_PER_PAGE) + crm_eligibility_key=LMS_CRM_ELIGIBILITY_KEY, + eligible_values=LMS_CRM_ELIGIBLE_VALUES, + page=page*RECORDS_PER_PAGE, + per_page=RECORDS_PER_PAGE, + ) students_resp = requests.post( COQL_ENDPOINT, headers=auth_headers, @@ -136,6 +172,7 @@ def get_students_to_be_unenrolled(): return students +# Currently not used def get_students_to_be_enrolled_in_careers_module(): """Fetch from Zoho all students with the Access_to_Careers_Module status diff --git a/lms/envs/common.py b/lms/envs/common.py index 1f92b6f221..cbd76e560b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3955,3 +3955,16 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring AMOS_HOST = os.environ.get('AMOS_HOST') AMOS_PORT = int(os.environ.get('AMOS_PORT', 3306)) AMOS_DB = os.environ.get('AMOS_DB') + + +ZOHO_CLIENT_ID = os.environ.get('ZOHO_CLIENT_ID') +ZOHO_CLIENT_SECRET = os.environ.get('ZOHO_CLIENT_SECRET') +ZOHO_REFRESH_TOKEN = os.environ.get('ZOHO_REFRESH_TOKEN') +ZOHO_REFRESH_ENDPOINT = os.environ.get('ZOHO_REFRESH_ENDPOINT') +ZOHO_STUDENTS_ENDPOINT = os.environ.get('ZOHO_STUDENTS_ENDPOINT') +ZOHO_MENTORS_ENDPOINT = os.environ.get('ZOHO_MENTORS_ENDPOINT') +ZOHO_TIMEOUT_SECONDS = os.environ.get('ZOHO_TIMEOUT_SECONDS', 0.5) +ZOHO_COQL_ENDPOINT = os.environ.get('ZOHO_COQL_ENDPOINT') + +LMS_CRM_STUDENT_ELIGIBILITY_FIELD = os.environ.get('LMS_CRM_STUDENT_ELIGIBILITY_FIELD', 'Credit_Rating_Body') +LMS_CRM_STUDENT_ELIGIBILITY_FIELD_VALUES = os.environ.get('LMS_CRM_STUDENT_ELIGIBILITY_FIELD_VALUES') diff --git a/lms/envs/production.py b/lms/envs/production.py index bae358f264..5d963cf9e0 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1024,6 +1024,8 @@ def should_show_debug_toolbar(request): sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()]) # CodeInstitute +LMS_CRM_STUDENT_ELIGIBILITY_FIELD = AUTH_TOKENS.get('LMS_CRM_STUDENT_ELIGIBILITY_FIELD', 'Credit_Rating_Body') +LMS_CRM_STUDENT_ELIGIBILITY_FIELD_VALUES = AUTH_TOKENS.get('LMS_CRM_STUDENT_ELIGIBILITY_FIELD_VALUES') HUBSPOT_CONTACTS_ENDPOINT = AUTH_TOKENS.get('HUBSPOT_CONTACTS_ENDPOINT') HUBSPOT_API_KEY = AUTH_TOKENS.get('HUBSPOT_API_KEY')