From 8f843b312dfb257c634c86182509c4920689e57d Mon Sep 17 00:00:00 2001 From: Asad Ali Date: Thu, 7 Jul 2022 15:51:21 +0500 Subject: [PATCH 1/3] LP-3025 Sync user data with HubSpot --- .../mailchimp_pipeline/signals/handlers.py | 269 +----------------- common/djangoapps/mailchimp_pipeline/tasks.py | 40 +-- .../mailchimp_pipeline/tests/test_signals.py | 160 ----------- .../mailchimp_pipeline/tests/test_tasks.py | 196 +------------ common/djangoapps/nodebb/signals/handlers.py | 18 +- .../commands/update_org_metric_prompts.py | 2 - common/djangoapps/student/models.py | 14 +- common/lib/hubspot_client/client.py | 49 ++++ common/lib/hubspot_client/handlers.py | 217 ++++++++++++++ common/lib/hubspot_client/helpers.py | 60 ++++ common/lib/hubspot_client/tasks.py | 94 ++++++ lms/djangoapps/onboarding/handlers.py | 8 - lms/djangoapps/onboarding/helpers.py | 4 +- ...020_populate_organization_metric_prompt.py | 7 +- ..._re_populate_organization_metric_prompt.py | 6 +- ..._userextendedprofile_hubspot_contact_id.py | 20 ++ lms/djangoapps/onboarding/models.py | 10 + .../student_dashboard/test/test_views.py | 36 --- 18 files changed, 468 insertions(+), 742 deletions(-) delete mode 100644 common/djangoapps/mailchimp_pipeline/tests/test_signals.py create mode 100644 common/lib/hubspot_client/handlers.py create mode 100644 common/lib/hubspot_client/helpers.py create mode 100644 lms/djangoapps/onboarding/migrations/0035_userextendedprofile_hubspot_contact_id.py diff --git a/common/djangoapps/mailchimp_pipeline/signals/handlers.py b/common/djangoapps/mailchimp_pipeline/signals/handlers.py index 6e1b3ba808a..32e4e91ff69 100644 --- a/common/djangoapps/mailchimp_pipeline/signals/handlers.py +++ b/common/djangoapps/mailchimp_pipeline/signals/handlers.py @@ -5,206 +5,10 @@ from celery.task import task from django.conf import settings -from django.contrib.auth.models import User -from django.db.models.signals import post_save -from django.dispatch import receiver - -from common.lib.mandrill_client.client import MandrillClient from common.lib.hubspot_client.client import HubSpotClient -from lms.djangoapps.certificates import api as certificate_api -from lms.djangoapps.onboarding.models import EmailPreference, GranteeOptIn, Organization, UserExtendedProfile -from mailchimp_pipeline.client import ChimpClient, MailChimpException -from mailchimp_pipeline.helpers import ( - get_enrollements_course_short_ids, - get_org_data_for_mandrill, - get_user_active_enrollements -) -from mailchimp_pipeline.tasks import update_org_details_at_mailchimp -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from student.models import UserProfile - -log = getLogger(__name__) - - -def update_mailchimp(email, data): - return - try: - response = ChimpClient().add_update_member_to_list( - settings.MAILCHIMP_LEARNERS_LIST_ID, email, data) - log.info(response) - except MailChimpException as ex: - log.exception(ex) - - -@receiver(post_save, sender=EmailPreference) -def sync_email_preference_with_mailchimp(sender, instance, **kwargs): # pylint: disable=unused-argument - """ - Signal receiver that syncs the chosen email preference of a user when triggered - - Arguments: - sender: The particular object that triggered the receiver - instance: Object that contains data regarding the user and their chosen preference - - Returns: - None - """ - opt_in = '' - - if instance.opt_in == 'yes': - opt_in = 'TRUE' - elif instance.opt_in == 'no': - opt_in = 'FALSE' - - user_json = { - "merge_fields": { - "OPTIN": opt_in - } - } - - update_mailchimp(instance.user.email, user_json) - - -@receiver(post_save, sender=UserProfile) -def sync_user_profile_with_mailchimp(sender, instance, **kwargs): # pylint: disable=unused-argument - """ - Signal receiver that syncs the specific user profile that triggered the signal with mailchimp - - Arguments: - sender: The particular object that triggered the receiver - instance: Object that contains data regarding the user and their updated profile fields - - Returns: - None - """ - updated_fields = getattr(instance, '_updated_fields', {}) - - relevant_signal_fields = ['city', 'country', 'language'] - - if not any([field in updated_fields for field in relevant_signal_fields]): - return - - if instance.language or instance.country or instance.city: - user_json = { - "merge_fields": { - "LANG": instance.language or "", - "COUNTRY": instance.country.name.format() if instance.country else "", - "CITY": instance.city or "", - } - } - - update_mailchimp(instance.user.email, user_json) - - -@receiver(post_save, sender=UserExtendedProfile) -def sync_extended_profile_with_mailchimp(sender, instance, **kwargs): # pylint: disable=unused-argument - """ - Signal receiver that syncs the specific user extended profile that triggered the signal with mailchimp - - Arguments: - sender: The particular object that triggered the receiver - instance: Object that contains data regarding the user - - Returns: - None - """ - org_label, org_type, work_area = get_org_data_for_mandrill( - instance.organization) - - user_json = { - "merge_fields": { - "ORG": org_label, - "ORGTYPE": org_type, - "WORKAREA": work_area - } - } - - update_mailchimp(instance.user.email, user_json) - - -@receiver(post_save, sender=GranteeOptIn) -def sync_grantee_optin_with_mailchimp(sender, instance, **kwargs): # pylint: disable=unused-argument - if instance.organization_partner.partner == 'ECHIDNA': - user_json = { - "merge_fields": { - "ECHIDNA": 'TRUE' if instance.agreed else 'FALSE', - } - } - update_mailchimp(instance.user.email, user_json) - - -@receiver(post_save, sender=Organization) -def sync_organization_with_mailchimp(sender, instance, created, **kwargs): # pylint: disable=unused-argument - if not created: - org_label, org_type, work_area = get_org_data_for_mandrill(instance) - update_org_details_at_mailchimp.delay( - org_label, org_type, work_area, instance.id, settings.MAILCHIMP_LEARNERS_LIST_ID) - - -def sync_metric_update_prompt_with_mail_chimp(update_prompt): - """ - Syncs a specific user's preference regarding the update prompt with mailchimp - - Arguments: - update_prompt: Object containing details regarding the user and their choice of prompt update - - Returns: - None - """ - year = 'TRUE' if update_prompt.year else 'FALSE' - year_month = 'TRUE' if update_prompt.year_month else 'FALSE' - year_three_months = 'TRUE' if update_prompt.year_three_month else 'FALSE' - year_six_months = 'TRUE' if update_prompt.year_six_month else 'FALSE' - - user_json = { - "merge_fields": { - "YEAR": year, - "Y_MONTH": year_month, - "Y_3MONTHS": year_three_months, - "Y_6MONTHS": year_six_months - } - } - - update_mailchimp(update_prompt.responsible_user.email, user_json) - -def send_user_info_to_mailchimp(sender, user, created, kwargs): # pylint: disable=unused-argument - """ Create user account at nodeBB when user created at edx Platform """ - user_json = { - "merge_fields": { - "FULLNAME": user.get_full_name(), - "USERNAME": user.username - } - } - - if created: - user_json["merge_fields"].update( - {"DATEREGIS": str(user.date_joined.strftime("%m/%d/%Y"))}) - user_json.update({ - "email_address": user.email, - "status_if_new": "subscribed" - }) - - update_mailchimp(user.email, user_json) - - -def update_user_email_in_mailchimp(old_email, new_email): - """ - Update the email to new_email in mailchimp - - Arguments: - old_email (str): Current email - new_email (str): Updated email - - Returns: - None - """ - - user_json = { - "email_address": new_email, - } - - update_mailchimp(old_email, user_json) +log = getLogger(__name__) @task(routing_key=settings.HIGH_PRIORITY_QUEUE) @@ -230,74 +34,3 @@ def task_send_account_activation_email(data): } HubSpotClient().send_mail(context) - - -@task() -def task_send_user_info_to_mailchimp(data): - """ Create user account at nodeBB when user created at edx Platform """ - - user = User.objects.get(id=data['user_id']) - created = data["created"] - - send_user_info_to_mailchimp(None, user, created, {}) - - -# @task() -def send_user_enrollments_to_mailchimp(user): - """ - Send all the active enrollments of the specified user to mailchimp - - Arguments: - user: Target user - - Returns: - None - """ - # user = User.objects.get(id=data['user_id']) - - log.info("-------------------------\n fetching enrollments \n ------------------------------\n") - - enrollment_titles = get_user_active_enrollements(user.username) - enrollment_short_ids = get_enrollements_course_short_ids(user.username) - - log.info(enrollment_titles) - log.info(enrollment_short_ids) - - user_json = { - "merge_fields": { - "ENROLLS": enrollment_titles, - "ENROLL_IDS": enrollment_short_ids - } - } - - update_mailchimp(user.email, user_json) - - -@task() -def send_user_course_completions_to_mailchimp(data): - """ - Task to send all the completed courses of a user to mailchimp - - Arguments: - data: Object containing user info eg user_id - - Returns: - None - """ - user = User.objects.get(id=data['user_id']) - all_certs = [] - try: - all_certs = certificate_api.get_certificates_for_user(user.username) - except Exception as ex: # pylint: disable=broad-except - log.exception(str(ex.args)) - - completed_course_keys = [cert.get('course_key', '') for cert in all_certs - if certificate_api.is_passing_status(cert['status'])] - completed_courses = CourseOverview.objects.filter( - id__in=completed_course_keys) - user_json = { - "merge_fields": { - "COMPLETES": ", ".join([course.display_name for course in completed_courses]), - } - } - update_mailchimp(user.email, user_json) diff --git a/common/djangoapps/mailchimp_pipeline/tasks.py b/common/djangoapps/mailchimp_pipeline/tasks.py index 877c94dd98d..a746184ba1b 100644 --- a/common/djangoapps/mailchimp_pipeline/tasks.py +++ b/common/djangoapps/mailchimp_pipeline/tasks.py @@ -8,7 +8,7 @@ from django.db import connection from lms.djangoapps.certificates import api as certificate_api -from lms.djangoapps.onboarding.models import FocusArea, OrgSector, UserExtendedProfile +from lms.djangoapps.onboarding.models import FocusArea, OrgSector from mailchimp_pipeline.client import ChimpClient, MailChimpException from mailchimp_pipeline.helpers import get_enrollements_course_short_ids, get_user_active_enrollements from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -16,44 +16,6 @@ log = getLogger(__name__) -@task() -def update_org_details_at_mailchimp(org_label, org_type, work_area, org_id, list_id): - """ - Update the details of the organization associated with the org_id - - Arguments: - org_id (int): id of the target organization - org_label (str): Label of the organization to update to - org_type (str): Type of the organization to update to - work_area (str): Work area of the organization to update to - list_id (str): List id to add the update member to - - Returns: - None - """ - return - - log.info("Task to send organization details to MailChimp") - log.info(org_label) - - extended_profiles = UserExtendedProfile.objects.filter(organization_id=org_id).values("user__email") - - user_json = { - "merge_fields": { - "ORG": org_label, - "ORGTYPE": org_type, - "WORKAREA": work_area - } - } - - for extended_profile in extended_profiles: - try: - response = ChimpClient().add_update_member_to_list(list_id, extended_profile.get('user__email'), user_json) - log.info(response) - except MailChimpException as ex: - log.exception(ex) - - @task() def update_enrollments_completions_at_mailchimp(list_id): """Task to send user enrollments & course completions details to MailChimp""" diff --git a/common/djangoapps/mailchimp_pipeline/tests/test_signals.py b/common/djangoapps/mailchimp_pipeline/tests/test_signals.py deleted file mode 100644 index fa2fbe3943c..00000000000 --- a/common/djangoapps/mailchimp_pipeline/tests/test_signals.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -Tests related to the signals.py file of the mailchimp_pipeline app -""" -import json - -from django.conf import settings -from django.test import TestCase -from mock import ANY, patch - -from lms.djangoapps.onboarding.models import EmailPreference, GranteeOptIn -from lms.djangoapps.onboarding.tests.factories import UserFactory -from mailchimp_pipeline.client import Connection -from mailchimp_pipeline.helpers import get_org_data_for_mandrill -from mailchimp_pipeline.tests.helpers import ( - create_organization, - create_organization_partner_object, - generate_mailchimp_url -) - - -class MailchimpPipelineSignalTestClass(TestCase): - """ - Tests for signals handlers - """ - - @patch("nodebb.signals.handlers.get_current_request", autospec=True) - def setUp(self, mocked_nodebb_request): # pylint: disable=unused-argument, arguments-differ - super(MailchimpPipelineSignalTestClass, self).setUp() - patcher = patch('mailchimp_pipeline.client.request', autospec=True) - self.mock_request = patcher.start() - self.mock_request.return_value.status_code = 204 - self.addCleanup(patcher.stop) - self.user = UserFactory(is_staff=False, password='test') - - self.connection = Connection.get_connection() - self.mail_chimp_root_url = self.connection.root - - def test_sync_user_profile_with_mailchimp(self): - """ - Test if user profile post-save signal is generated and its handler is sending user profile - data perfectly to the MailChimp expected URL - """ - user_profile = self.user.profile - user_profile.language = 'test_language' - user_profile.city = 'test_city' - user_profile.save() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "LANG": user_profile.language, - "COUNTRY": "", - "CITY": user_profile.city - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_sync_email_preference_with_mailchimp_optin_false(self): - """ - Test if email preference post-save signal is generated and its handler is sending email - preference Opt-in option as False (user does not want to get email updates) to the - MailChimp expected URL - """ - email_preference = EmailPreference.objects.get(user=self.user) - email_preference.opt_in = "no" - email_preference.save() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "OPTIN": "FALSE" - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_sync_email_preference_with_mailchimp_optin_true(self): - """ - Test if email preference post-save signal is generated and its handler is sending email preference - Opt-in option as True (user wants to email updates) to the MailChimp expected URL - """ - email_preference = EmailPreference.objects.get(user=self.user) - email_preference.opt_in = "yes" - email_preference.save() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "OPTIN": "TRUE" - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_sync_email_preference_with_mailchimp_optin_empty(self): - """ - Test if Email preference post-save signal is generated and its handler is sending email - preference Opt-in option as an empty string to the MailChimp expected URL - """ - email_preference = EmailPreference.objects.get(user=self.user) - email_preference.opt_in = "" - email_preference.save() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "OPTIN": "" - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_sync_grantee_optin_with_mailchimp(self): - """ - Test if Grantee Opt-In post-save signal is generated and its handler is sending right data - to the MailChimp expected URL - """ - org_partner = create_organization_partner_object(self.user) - grant_opt_in = GranteeOptIn(user=self.user, agreed=True, organization_partner=org_partner) - grant_opt_in.save() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "ECHIDNA": 'TRUE', - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - @patch("nodebb.signals.handlers.get_current_request", autospec=True) - def test_sync_extended_profile_with_mailchimp(self, nodebb_request): # pylint: disable=unused-argument - """ - Test if the Extended profile post-save signal is generated and its handler is sending - user extended profile data perfectly to the MailChimp expected URL - :param nodebb_request: Mocked request used in function that sync user extended profile with NodeBB - """ - extended_profile = self.user.extended_profile - extended_profile.organization = create_organization(self.user) - extended_profile.save() # pylint: disable=no-member - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - org_label, org_type, work_area = get_org_data_for_mandrill(extended_profile.organization) - expected_data = { - "merge_fields": { - "ORG": org_label, - "ORGTYPE": org_type, - "WORKAREA": work_area - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - @patch("mailchimp_pipeline.signals.handlers.update_org_details_at_mailchimp.delay", autospec=True) - def test_sync_organization_with_mailchimp(self, mocked_delay): - """ - Test if Organization post-save signal is generated and the delay of the update_org_details_at_mailchimp - task is called with right parameters from its handler. - :param mocked_delay: Mocked task delay to check if task has been called. - """ - organization = create_organization(self.user) - org_label, org_type, work_area = get_org_data_for_mandrill(organization) - mocked_delay.assert_called_with( - org_label, org_type, work_area, organization.id, settings.MAILCHIMP_LEARNERS_LIST_ID - ) diff --git a/common/djangoapps/mailchimp_pipeline/tests/test_tasks.py b/common/djangoapps/mailchimp_pipeline/tests/test_tasks.py index f7f1be24ecf..c1a350dde0f 100644 --- a/common/djangoapps/mailchimp_pipeline/tests/test_tasks.py +++ b/common/djangoapps/mailchimp_pipeline/tests/test_tasks.py @@ -2,7 +2,6 @@ Tests related to tasks.py file of the mailchimp pipeline app """ import json -from datetime import datetime import factory import requests @@ -13,24 +12,15 @@ from common.djangoapps.mailchimp_pipeline.tests.helpers import create_organization, generate_mailchimp_url from lms.djangoapps.certificates import api as certificate_api -from lms.djangoapps.onboarding.models import FocusArea, OrganizationMetricUpdatePrompt, OrgSector +from lms.djangoapps.onboarding.models import FocusArea, OrgSector from lms.djangoapps.onboarding.tests.factories import UserFactory from mailchimp_pipeline.client import Connection, MailChimpException from mailchimp_pipeline.helpers import ( get_enrollements_course_short_ids, - get_org_data_for_mandrill, get_user_active_enrollements ) -from mailchimp_pipeline.signals.handlers import ( - send_user_course_completions_to_mailchimp, - send_user_enrollments_to_mailchimp, - send_user_info_to_mailchimp, - sync_metric_update_prompt_with_mail_chimp, - task_send_account_activation_email, - task_send_user_info_to_mailchimp, - update_mailchimp -) -from mailchimp_pipeline.tasks import update_enrollments_completions_at_mailchimp, update_org_details_at_mailchimp +from mailchimp_pipeline.signals.handlers import task_send_account_activation_email +from mailchimp_pipeline.tasks import update_enrollments_completions_at_mailchimp from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from student.models import CourseEnrollment from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -68,105 +58,6 @@ def setUp(self, mocked_nodebb_request, mocked_email_sender): # pylint: disable= self.connection = Connection.get_connection() self.mail_chimp_root_url = self.connection.root - def test_update_mailchimp(self): - """ - Test if the update_mailchimp function is sending `PUT` request on exact MailChimp URL - with expected data - """ - data = {'data': 'test_data'} - update_mailchimp(self.user.email, data) - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(data), auth=ANY, params=ANY) - - def test_update_mailchimp_for_exception(self): - """ - Test if update_mailchimp function generates MailChimp exception on getting response - (from MailChimp server request) status code of 404 - """ - self.mock_request.return_value.status_code = 404 - self.mock_request.return_value.raise_for_status.side_effect = requests.exceptions.HTTPError - update_mailchimp(self.user.email, {}) - self.assertRaises(MailChimpException) - - def test_task_send_user_course_completions_to_mailchimp(self): - """ - Test if the send_user_course_completions_to_mailchimp task is sending completed course - information perfectly to the MailChimp expected URL - """ - data = {'user_id': self.user.id, 'created': True} - result = send_user_course_completions_to_mailchimp.delay(data) - assert result.successful() - all_certs = certificate_api.get_certificates_for_user(self.user.username) - completed_course_keys = [ - cert.get('course_key', '') for cert in all_certs if certificate_api.is_passing_status(cert['status'])] - completed_courses = CourseOverview.objects.filter(id__in=completed_course_keys) - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "COMPLETES": ", ".join([course.display_name for course in completed_courses]), - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - @patch("lms.djangoapps.certificates.api.get_certificates_for_user") - def test_task_send_user_course_completions_to_mailchimp_for_exception(self, mocked_certificates_api): - """ - Test if the send_user_course_completions_to_mailchimp task is raising an exception on - certificates API failure - :mocked_certificates_api: Mocked certificate api function to produce exception - """ - mocked_certificates_api.side_effect = Exception - data = {'user_id': self.user.id, 'created': True} - send_user_course_completions_to_mailchimp.delay(data) - self.assertRaises(Exception) - - def test_send_user_info_to_mailchimp(self): - """ - Test if the send_user_info_to_mailchimp function is sending user information perfectly - to the MailChimp expected URL - """ - send_user_info_to_mailchimp('test', self.user, True, kwargs={}) - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "FULLNAME": self.user.get_full_name(), - "USERNAME": self.user.username, - "DATEREGIS": str(self.user.date_joined.strftime("%m/%d/%Y")) - }, - "email_address": self.user.email, - "status_if_new": "subscribed" - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_sync_metric_update_prompt_with_mail_chimp(self): - """ - Test if the sync_metric_update_prompt_with_mail_chimp function is sending Organization - Metric Update Prompt data perfectly to the MailChimp expected URL - """ - organization = self.user.extended_profile.organization - update_prompt = OrganizationMetricUpdatePrompt( - org=organization, - responsible_user=self.user, - year=True, - year_month=True, - latest_metric_submission=datetime.now() - ) - sync_metric_update_prompt_with_mail_chimp(update_prompt) - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "YEAR": 'TRUE', - "Y_MONTH": 'TRUE', - "Y_3MONTHS": 'FALSE', - "Y_6MONTHS": 'FALSE' - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - @patch("common.lib.mandrill_client.client.MandrillClient.send_mail") def test_task_send_account_activation_email(self, mocked_send_email): """ @@ -187,87 +78,6 @@ def test_task_send_account_activation_email(self, mocked_send_email): } mocked_send_email.assert_called_with(ANY, self.user.email, expected_context) - def test_task_send_user_info_to_mailchimp(self): - """ - Test if the task_send_user_info_to_mailchimp task is sending user information - perfectly to the MailChimp expected URL - """ - data = { - 'user_id': self.user.id, - 'created': True - } - result = task_send_user_info_to_mailchimp.delay(data) - assert result.successful() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "FULLNAME": self.user.get_full_name(), - "USERNAME": self.user.username, - "DATEREGIS": str(self.user.date_joined.strftime("%m/%d/%Y")) - }, - "email_address": self.user.email, - "status_if_new": "subscribed" - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - @factory.django.mute_signals(post_save) - def test_send_user_enrollments_to_mailchimp(self): - """ - Test if the send_user_enrollments_to_mailchimp function is sending user enrolled courses - information perfectly to the MailChimp expected URL - """ - send_user_enrollments_to_mailchimp(self.user) - enrollment_titles = get_user_active_enrollements(self.user.username) - enrollment_short_ids = get_enrollements_course_short_ids(self.user.username) - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "ENROLLS": enrollment_titles, - "ENROLL_IDS": enrollment_short_ids - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_task_update_org_details_at_mailchimp(self): - """ - Test if the update_org_details_at_mailchimp task is sending organization information - perfectly to the MailChimp expected URL - """ - organization = create_organization(self.user) - org_label, org_type, work_area = get_org_data_for_mandrill(organization) - result = update_org_details_at_mailchimp.delay( - org_label, org_type, work_area, organization.id, self.mailchimp_list_id - ) - assert result.successful() - expected_url = generate_mailchimp_url(self.mail_chimp_root_url, self.user.email) - expected_data = { - "merge_fields": { - "ORG": org_label, - "ORGTYPE": org_type, - "WORKAREA": work_area - } - } - self.mock_request.assert_called_with( - "PUT", url=expected_url, headers=ANY, data=json.dumps(expected_data), auth=ANY, params=ANY) - - def test_task_update_org_details_at_mailchimp_for_exception(self): - """ - Test if the update_org_details_at_mailchimp task is raising a MailChimp exception on - response (from MailChimp server) status code of 404 - """ - self.mock_request.return_value.status_code = 404 - self.mock_request.return_value.raise_for_status.side_effect = requests.exceptions.HTTPError - - organization = create_organization(self.user) - org_label, org_type, work_area = get_org_data_for_mandrill(organization) - update_org_details_at_mailchimp.delay( - org_label, org_type, work_area, organization.id, self.mailchimp_list_id - ) - - self.assertRaises(MailChimpException) - @factory.django.mute_signals(post_save) @patch("mailchimp_pipeline.tasks.connection") def test_update_enrollments_completions_at_mailchimp(self, mocked_connection): # pylint: disable=unused-argument diff --git a/common/djangoapps/nodebb/signals/handlers.py b/common/djangoapps/nodebb/signals/handlers.py index 3dc6c1bb75c..50f4c580aa6 100644 --- a/common/djangoapps/nodebb/signals/handlers.py +++ b/common/djangoapps/nodebb/signals/handlers.py @@ -9,15 +9,10 @@ from django.dispatch import receiver from common.lib.nodebb_client.client import NodeBBClient -from lms.djangoapps.certificates.models import GeneratedCertificate +from common.lib.hubspot_client.handlers import send_user_info_to_hubspot, send_user_enrollments_to_hubspot from lms.djangoapps.onboarding.helpers import COUNTRIES from lms.djangoapps.onboarding.models import FocusArea, Organization, UserExtendedProfile from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership -from mailchimp_pipeline.signals.handlers import ( - send_user_course_completions_to_mailchimp, - send_user_enrollments_to_mailchimp, - send_user_info_to_mailchimp -) from nodebb.helpers import get_community_id from nodebb.models import DiscussionCommunity, TeamGroupChat from nodebb.tasks import ( @@ -29,7 +24,6 @@ task_update_user_profile_on_nodebb ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED from openedx.features.badging.models import UserBadge from student.models import CourseEnrollment, UserProfile from util.model_utils import get_changed_fields_dict @@ -44,12 +38,6 @@ def log_action_response(user, status_code, response_body): log.info('Success: User(%s) has been updated on nodebb', user.username) -@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate) -def handle_course_cert_awarded(sender, user, course_key, **kwargs): # pylint: disable=unused-argument - data = {"user_id": user.id} - send_user_course_completions_to_mailchimp.delay(data) - - @receiver(pre_save, sender=UserProfile) def user_profile_pre_save_callback(sender, **kwargs): """ @@ -147,7 +135,7 @@ def update_user_profile_on_nodebb(sender, instance, created, **kwargs): """ Create user account at nodeBB when user created at edx Platform """ - send_user_info_to_mailchimp(sender, instance, created, kwargs) + send_user_info_to_hubspot(sender, instance, created, kwargs) request = get_current_request() if not request or 'login' in request.path: @@ -262,7 +250,7 @@ def manage_membership_on_nodebb_group(instance, **kwargs): category_id=community_id, username=username) # We have to sync user enrollments only in case of # un-enroll because - send_user_enrollments_to_mailchimp(instance.user) + send_user_enrollments_to_hubspot(instance.user) @receiver(pre_delete, sender=CourseTeam, dispatch_uid="nodebb.signals.handlers.delete_groupchat_on_nodebb") diff --git a/common/djangoapps/philu_commands/management/commands/update_org_metric_prompts.py b/common/djangoapps/philu_commands/management/commands/update_org_metric_prompts.py index 1663a001d22..7d5bc79f9a6 100644 --- a/common/djangoapps/philu_commands/management/commands/update_org_metric_prompts.py +++ b/common/djangoapps/philu_commands/management/commands/update_org_metric_prompts.py @@ -13,7 +13,6 @@ its_been_year_three_month ) from lms.djangoapps.onboarding.models import OrganizationMetricUpdatePrompt -from mailchimp_pipeline.signals.handlers import sync_metric_update_prompt_with_mail_chimp log = getLogger(__name__) @@ -76,6 +75,5 @@ def handle(self, *args, **options): if prompt.remind_me_later: prompt.remind_me_later = None prompt.save() - sync_metric_update_prompt_with_mail_chimp(prompt) else: log.info('No change detected') diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index e32bf3c766e..fd0868cec58 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -739,21 +739,15 @@ def activate(self): log.info(u'User %s (%s) account is successfully activated.', self.user.username, self.user.email) def _track_activation(self): - """ Update the isActive flag in mailchimp for activated users.""" + """ Update the isActive flag in segment for activated users.""" has_segment_key = getattr(settings, 'LMS_SEGMENT_KEY', None) - has_mailchimp_id = hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID') - if has_segment_key and has_mailchimp_id: + if has_segment_key: segment.identify( self.user.id, # pylint: disable=no-member { 'email': self.user.email, 'username': self.user.username, 'activated': 1, - }, - { - "MailChimp": { - "listId": settings.MAILCHIMP_NEW_USER_LIST_ID - } } ) @@ -1345,8 +1339,8 @@ def enroll(cls, user, course_key, mode=None, check_access=False): # if we listen CourseEnrollments post save, celery behaves weirdly # so we had to make this change in core - from mailchimp_pipeline.signals.handlers import send_user_enrollments_to_mailchimp - send_user_enrollments_to_mailchimp(user) + from common.lib.hubspot_client.handlers import send_user_enrollments_to_hubspot + send_user_enrollments_to_hubspot(user) enrollment.send_signal(EnrollStatusChange.enroll) diff --git a/common/lib/hubspot_client/client.py b/common/lib/hubspot_client/client.py index f971e20add7..a2030592fd8 100644 --- a/common/lib/hubspot_client/client.py +++ b/common/lib/hubspot_client/client.py @@ -69,3 +69,52 @@ def send_mail(self, email_data): logger.info(response.json()) return response + + def create_contact(self, user_json): + """ + Create new contact on HubSpot. + + Arguments: + user_json (dict): User data. + """ + logger.info('Creating HubSpot contact, Data: {user_json}'.format(user_json=user_json)) + + url = '{hubspot_api_url}/crm/v3/objects/contacts?hapikey={api_key}'.format( + hubspot_api_url=self.HUBSPOT_API_URL, + api_key=self.api_key + ) + headers = {'Content-type': 'application/json'} + response = requests.post(url, headers=headers, json=user_json) + + logger.info(response.json()) + return response + + def update_contact(self, user, user_json): + """ + Update contact on HubSpot. + + Arguments: + user_json (dict): User data. + user (User): User object. + """ + logger.info( + 'Updating HubSpot contact, User: {user} & Data: {user_json}'.format(user=user, user_json=user_json) + ) + + url = '{hubspot_api_url}/crm/v3/objects/contacts/{contact_id}?hapikey={api_key}'.format( + hubspot_api_url=self.HUBSPOT_API_URL, + api_key=self.api_key, + contact_id=user.extended_profile.hubspot_contact_id + ) + headers = {'Content-type': 'application/json'} + response = requests.patch(url, headers=headers, json=user_json) + + if response.status_code == 200: + logger.info('Contact Updated Successfully!') + else: + logger.exception( + 'Could not Update HubSpot Contact, Status Code: {status_code}'.format(status_code=response.status_code) + ) + + logger.info(response.json()) + return response diff --git a/common/lib/hubspot_client/handlers.py b/common/lib/hubspot_client/handlers.py new file mode 100644 index 00000000000..fdacc081eb6 --- /dev/null +++ b/common/lib/hubspot_client/handlers.py @@ -0,0 +1,217 @@ +""" +Handlers and signals for Mailchimp pipeline +""" +from logging import getLogger + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from common.lib.hubspot_client.helpers import prepare_user_data_for_hubspot_contact_creation +from common.lib.hubspot_client.tasks import task_create_or_update_hubspot_contact, task_update_org_details_at_hubspot +from lms.djangoapps.onboarding.models import EmailPreference, Organization, UserExtendedProfile +from mailchimp_pipeline.helpers import ( + get_enrollements_course_short_ids, + get_user_active_enrollements +) +from student.models import UserProfile + +log = getLogger(__name__) + + +@receiver(post_save, sender=EmailPreference) +def sync_email_preference_with_hubspot(sender, instance, **kwargs): # pylint: disable=unused-argument + """ + Signal receiver that syncs the chosen email preference of a user when triggered + + Arguments: + sender: The particular object that triggered the receiver + instance: Object that contains data regarding the user and their chosen preference + + Returns: + None + """ + opt_in = '' + + if instance.opt_in == 'yes': + opt_in = 'TRUE' + elif instance.opt_in == 'no': + opt_in = 'FALSE' + + user = instance.user + extended_profile = user.extended_profile if hasattr(user, 'extended_profile') else None + has_hubspot_contact = bool(extended_profile.hubspot_contact_id) if extended_profile else False + + if not has_hubspot_contact: + user_json = prepare_user_data_for_hubspot_contact_creation(instance.user) + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + return + + user_json = { + 'properties': { + 'edx_marketing_opt_in': opt_in + } + } + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + + +@receiver(post_save, sender=UserProfile) +def sync_user_profile_with_hubspot(sender, instance, **kwargs): # pylint: disable=unused-argument + """ + Signal receiver that syncs the specific user profile that triggered the signal with hubspot + + Arguments: + sender: The particular object that triggered the receiver + instance: Object that contains data regarding the user and their updated profile fields + + Returns: + None + """ + updated_fields = getattr(instance, '_updated_fields', {}) + relevant_signal_fields = ['city', 'country', 'language'] + + if not any([field in updated_fields for field in relevant_signal_fields]): + return + + user = instance.user + extended_profile = user.extended_profile if hasattr(user, 'extended_profile') else None + has_hubspot_contact = bool(extended_profile.hubspot_contact_id) if extended_profile else False + + if not has_hubspot_contact: + user_json = prepare_user_data_for_hubspot_contact_creation(user) + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + return + + if instance.language or instance.country or instance.city: + user_json = { + 'properties': { + 'edx_language': instance.language or '', + 'edx_country': instance.country.name.format() if instance.country else '', + 'edx_city': instance.city or '', + } + } + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + + +@receiver(post_save, sender=UserExtendedProfile) +def sync_extended_profile_with_hubspot(sender, instance, **kwargs): # pylint: disable=unused-argument + """ + Signal receiver that syncs the specific user extended profile that triggered the signal with hubspot + + Arguments: + sender: The particular object that triggered the receiver + instance: Object that contains data regarding the user + + Returns: + None + """ + user = instance.user + has_hubspot_contact = bool(instance.hubspot_contact_id) + + if not has_hubspot_contact: + user_json = prepare_user_data_for_hubspot_contact_creation(user) + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + return + + org_label = org_type = work_area = '' + if instance.organization: + org_label, org_type, work_area = instance.organization.hubspot_data() + + user_json = { + 'properties': { + 'edx_organization': org_label, + 'edx_organization_type': org_type, + 'edx_area_of_work': work_area + } + } + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + + +@receiver(post_save, sender=Organization) +def sync_organization_with_hubspot(sender, instance, created, **kwargs): # pylint: disable=unused-argument + """ + Sync Organization data with HubSpot. + """ + if not created: + org_label, org_type, work_area = instance.hubspot_data() + task_update_org_details_at_hubspot.delay(org_label, org_type, work_area, instance.id) + + +def send_user_info_to_hubspot(sender, user, created, kwargs): # pylint: disable=unused-argument + """ + Create user account on HubSpot when created on Platform. + """ + extended_profile = user.extended_profile if hasattr(user, 'extended_profile') else None + has_hubspot_contact = bool(extended_profile.hubspot_contact_id) if extended_profile else False + + if not has_hubspot_contact: + user_json = prepare_user_data_for_hubspot_contact_creation(user) + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + return + + user_json = { + 'properties': { + 'edx_full_name': user.get_full_name(), + 'edx_username': user.username + } + } + + if created: + user_json['properties'].update( + {'date_registered': str(user.date_joined.strftime('%m/%d/%Y'))}) + user_json.update({ + 'email': user.email, + 'edx_marketing_opt_in': 'subscribed' + }) + + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + + +def update_user_email_in_hubspot(old_email, new_email): + """ + Update the email to new_email in hubspot + + Arguments: + old_email (str): Current email + new_email (str): Updated email + """ + + user_json = { + 'properties': { + 'email': new_email + } + } + + task_create_or_update_hubspot_contact.delay(old_email, user_json, True) + + +def send_user_enrollments_to_hubspot(user): + """ + Send all the active enrollments of the specified user to hubspot + + Arguments: + user: Target user + """ + extended_profile = user.extended_profile if hasattr(user, 'extended_profile') else None + has_hubspot_contact = bool(extended_profile.hubspot_contact_id) if extended_profile else False + + if not has_hubspot_contact: + user_json = prepare_user_data_for_hubspot_contact_creation(user) + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) + return + + log.info("-------------------------\n fetching enrollments \n ------------------------------\n") + + enrollment_titles = get_user_active_enrollements(user.username) + enrollment_short_ids = get_enrollements_course_short_ids(user.username) + + log.info(enrollment_titles) + log.info(enrollment_short_ids) + + user_json = { + 'properties': { + 'edx_enrollments': enrollment_titles, + 'edx_enrollments_short_ids': enrollment_short_ids + } + } + + task_create_or_update_hubspot_contact.delay(user.email, user_json, has_hubspot_contact) diff --git a/common/lib/hubspot_client/helpers.py b/common/lib/hubspot_client/helpers.py new file mode 100644 index 00000000000..0e729c23e34 --- /dev/null +++ b/common/lib/hubspot_client/helpers.py @@ -0,0 +1,60 @@ +""" +HubSpot helpers. +""" +import logging + +from mailchimp_pipeline.helpers import ( + get_enrollements_course_short_ids, + get_user_active_enrollements +) + +log = logging.getLogger(__name__) + + +def prepare_user_data_for_hubspot_contact_creation(user): + """ + Generates user JSON to create contact on HubSpot. + """ + user_json = { + 'properties': { + 'edx_full_name': user.get_full_name(), + 'email': user.email, + 'edx_username': user.username, + 'edx_marketing_opt_in': 'subscribed', + 'date_registered': str(user.date_joined.strftime('%m/%d/%Y')) + } + } + + extended_profile = user.extended_profile if hasattr(user, 'extended_profile') else None + if extended_profile and extended_profile.organization: + org_label, org_type, work_area = extended_profile.organization.hubspot_data() + user_json['properties'].update( + { + 'edx_organization': org_label, + 'edx_organization_type': org_type, + 'edx_area_of_work': work_area + } + ) + + if extended_profile and (extended_profile.language or extended_profile.country or extended_profile.city): + user_json['properties'].update( + { + 'edx_language': extended_profile.language or '', + 'edx_country': extended_profile.country.name.format() if extended_profile.country else '', + 'edx_city': extended_profile.city or '', + } + ) + + log.info("-------------------------\n fetching enrollments \n ------------------------------\n") + + enrollment_titles = get_user_active_enrollements(user.username) + enrollment_short_ids = get_enrollements_course_short_ids(user.username) + + user_json['properties'].update( + { + 'edx_enrollments': enrollment_titles, + 'edx_enrollments_short_ids': enrollment_short_ids + } + ) + + return user_json diff --git a/common/lib/hubspot_client/tasks.py b/common/lib/hubspot_client/tasks.py index c214d10ba52..b4f11ac4adc 100644 --- a/common/lib/hubspot_client/tasks.py +++ b/common/lib/hubspot_client/tasks.py @@ -1,9 +1,17 @@ """ HubSpot client tasks """ +import logging + from celery.task import task +from django.contrib.auth.models import User from common.lib.hubspot_client.client import HubSpotClient +from common.lib.hubspot_client.helpers import prepare_user_data_for_hubspot_contact_creation +from lms.djangoapps.onboarding.models import UserExtendedProfile + + +logger = logging.getLogger(__name__) @task() @@ -12,3 +20,89 @@ def task_send_hubspot_email(data): Task to send email using HubSpot Client. """ HubSpotClient().send_mail(data) + + +@task +def task_create_or_update_hubspot_contact(user_email, user_json, is_contact_created=False): + """ + Task to Create or Update marketing contact on HubSpot. + + Following steps are being performed: + 1. Update contact on HubSpot if we have the HubSpot id of contact. + 2. If contact is not synced before then try to create contact on HubSpot. + i. If contact creation is successful, save the HubSpot Contact id. + ii. If we get an error that contact already exists with the email then save the HubSpot Contact id and make an + update request. + + Arguments: + user_email (str): Email of user. + user_json (dict): Data for HubSpot request. + is_contact_created (bool): Is contact already synced with HubSpot. + """ + logger.info('Syncing user data with HubSpot', user_email, user_json, is_contact_created) + + user = User.objects.filter(email=user_email).first() + if not user: + return + + client = HubSpotClient() + if is_contact_created: + client.update_contact(user, user_json) + return + + response = client.create_contact(user_json) + if response.status_code == 201: + hubspot_contact_id = response.json()['id'] + UserExtendedProfile.objects.update_or_create(user=user, defaults={'hubspot_contact_id': hubspot_contact_id}) + + elif response.status_code == 409: + message = response.json().get('message') + if message.startswith('Contact already exists.'): + hubspot_contact_id = message.split(' ')[-1] + UserExtendedProfile.objects.update_or_create(user=user, defaults={'hubspot_contact_id': hubspot_contact_id}) + client.update_contact(user, user_json) + else: + logger.exception( + 'Could not create or update HubSpot Contact. User: {user}, Data: {data}, Error Message: {msg}'.format( + user=user, + data=user_json, + msg=message + ) + ) + else: + logger.exception( + 'Could not create or update HubSpot Contact. User: {user}, Data: {data}'.format(user=user, data=user_json) + ) + + +@task() +def task_update_org_details_at_hubspot(org_label, org_type, work_area, org_id): + """ + Update the details of the organization associated with the org_id + + Arguments: + org_id (int): id of the target organization + org_label (str): Label of the organization to update + org_type (str): Type of the organization to update + work_area (str): Work area of the organization to update + """ + logger.info('Task to send organization details to HubSpot') + logger.info(org_label) + + extended_profiles = UserExtendedProfile.objects.filter(organization_id=org_id).select_related('user') + user_json = { + 'properties': { + 'edx_organization': org_label, + 'edx_organization_type': org_type, + 'edx_area_of_work': work_area + } + } + + for extended_profile in extended_profiles: + user = extended_profile.user + if not extended_profile.hubspot_contact_id: + user_json = prepare_user_data_for_hubspot_contact_creation(extended_profile.user) + + task_create_or_update_hubspot_contact.delay( + user.email, user_json, bool(extended_profile.hubspot_contact_id) + ) diff --git a/lms/djangoapps/onboarding/handlers.py b/lms/djangoapps/onboarding/handlers.py index eb22ebcf6c2..b2fc1dc5c1d 100644 --- a/lms/djangoapps/onboarding/handlers.py +++ b/lms/djangoapps/onboarding/handlers.py @@ -22,7 +22,6 @@ OrganizationMetricUpdatePrompt, UserExtendedProfile ) -from mailchimp_pipeline.signals.handlers import sync_metric_update_prompt_with_mail_chimp from oef.models import OrganizationOefUpdatePrompt from util.model_utils import USER_FIELD_CHANGED, get_changed_fields_dict @@ -38,13 +37,6 @@ def update_user_profile(sender, instance, update_fields, **kwargs): user_profile.save() -@receiver(post_save, sender=OrganizationMetricUpdatePrompt) -def sync_metric_update_prompts_in_mailchimp(sender, instance, update_fields, **kwargs): - # we can't put post_save on below method directly because it's being used - # in a Data Migration, Management command too. - sync_metric_update_prompt_with_mail_chimp(instance) - - @receiver(post_save, sender=Organization) def update_responsible_user_to_admin(sender, instance, update_fields, **kwargs): if not kwargs['created'] and instance.admin_id: diff --git a/lms/djangoapps/onboarding/helpers.py b/lms/djangoapps/onboarding/helpers.py index 574f19ac3e9..695e1429f37 100644 --- a/lms/djangoapps/onboarding/helpers.py +++ b/lms/djangoapps/onboarding/helpers.py @@ -11,7 +11,7 @@ from common.lib.hubspot_client.client import HubSpotClient from common.lib.hubspot_client.tasks import task_send_hubspot_email -from mailchimp_pipeline.signals.handlers import update_user_email_in_mailchimp +from common.lib.hubspot_client.handlers import update_user_email_in_hubspot from nodebb.tasks import task_update_user_profile_on_nodebb from oef.models import OrganizationOefUpdatePrompt from lms.djangoapps.onboarding.constants import ORG_SEARCH_TERM_LENGTH @@ -8104,7 +8104,7 @@ def update_user_email(user, old_email, new_email): from student.models import CourseEnrollmentAllowed, ManualEnrollmentAudit, UserProfile # update email in mailchimp - update_user_email_in_mailchimp(old_email, new_email) + update_user_email_in_hubspot(old_email, new_email) # update email in NodeBB data_to_sync = { diff --git a/lms/djangoapps/onboarding/migrations/0020_populate_organization_metric_prompt.py b/lms/djangoapps/onboarding/migrations/0020_populate_organization_metric_prompt.py index 64b82b47d63..bc9b0b5c23d 100644 --- a/lms/djangoapps/onboarding/migrations/0020_populate_organization_metric_prompt.py +++ b/lms/djangoapps/onboarding/migrations/0020_populate_organization_metric_prompt.py @@ -3,7 +3,6 @@ from django.db import migrations, models from lms.djangoapps.onboarding.helpers import its_been_year, its_been_year_month, \ its_been_year_three_month, its_been_year_six_month -from mailchimp_pipeline.signals.handlers import sync_metric_update_prompt_with_mail_chimp def get_latest_metric(org): @@ -48,8 +47,8 @@ def create_org_metric_prompts(apps, schema_editor): organizations = Organization.objects.raw( """ SELECT `onboarding_organization`.* - FROM `onboarding_organization` - INNER JOIN `onboarding_organizationmetric` + FROM `onboarding_organization` + INNER JOIN `onboarding_organizationmetric` ON (`onboarding_organization`.`id` = `onboarding_organizationmetric`.`org_id`) group by `onboarding_organization`.`id` """ @@ -69,8 +68,6 @@ def create_org_metric_prompts(apps, schema_editor): prompt.year_three_month = its_been_year_three_month(submission_date) prompt.year_six_month = its_been_year_six_month(submission_date) prompt.save() - sync_metric_update_prompt_with_mail_chimp(prompt) - class Migration(migrations.Migration): diff --git a/lms/djangoapps/onboarding/migrations/0024_re_populate_organization_metric_prompt.py b/lms/djangoapps/onboarding/migrations/0024_re_populate_organization_metric_prompt.py index 582e520e675..d36cd6575e6 100644 --- a/lms/djangoapps/onboarding/migrations/0024_re_populate_organization_metric_prompt.py +++ b/lms/djangoapps/onboarding/migrations/0024_re_populate_organization_metric_prompt.py @@ -3,7 +3,6 @@ from django.db import migrations, models from lms.djangoapps.onboarding.helpers import its_been_year, its_been_year_month, \ its_been_year_three_month, its_been_year_six_month -from mailchimp_pipeline.signals.handlers import sync_metric_update_prompt_with_mail_chimp """ NOTE: This migration is duplicate of '0020_populate_organization_metric_prompt' @@ -56,8 +55,8 @@ def create_org_metric_prompts(apps, schema_editor): organizations = Organization.objects.raw( """ SELECT `onboarding_organization`.* - FROM `onboarding_organization` - INNER JOIN `onboarding_organizationmetric` + FROM `onboarding_organization` + INNER JOIN `onboarding_organizationmetric` ON (`onboarding_organization`.`id` = `onboarding_organizationmetric`.`org_id`) group by `onboarding_organization`.`id` """ @@ -77,7 +76,6 @@ def create_org_metric_prompts(apps, schema_editor): prompt.year_three_month = its_been_year_three_month(submission_date) prompt.year_six_month = its_been_year_six_month(submission_date) prompt.save() - sync_metric_update_prompt_with_mail_chimp(prompt) class Migration(migrations.Migration): diff --git a/lms/djangoapps/onboarding/migrations/0035_userextendedprofile_hubspot_contact_id.py b/lms/djangoapps/onboarding/migrations/0035_userextendedprofile_hubspot_contact_id.py new file mode 100644 index 00000000000..fb27182c0bc --- /dev/null +++ b/lms/djangoapps/onboarding/migrations/0035_userextendedprofile_hubspot_contact_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.21 on 2022-07-07 09:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('onboarding', '0034_organization_is_organization_registered'), + ] + + operations = [ + migrations.AddField( + model_name='userextendedprofile', + name='hubspot_contact_id', + field=models.CharField(max_length=20, null=True), + ), + ] diff --git a/lms/djangoapps/onboarding/models.py b/lms/djangoapps/onboarding/models.py index a8000a8dfc3..4f4d8b6b3e9 100644 --- a/lms/djangoapps/onboarding/models.py +++ b/lms/djangoapps/onboarding/models.py @@ -268,6 +268,15 @@ def get_active_partners(self): """ Return list of active organization partners""" return self.organization_partners.filter(end_date__gt=datetime.utcnow()).values_list('partner', flat=True) + def hubspot_data(self): + """ + Create data for sync with HubSpot. + """ + org_label = self.label + org_type = OrgSector.objects.get_map().get(self.org_type, "") + focus_area = FocusArea.get_map().get(self.focus_area, "") + return org_label, org_type, focus_area + def __unicode__(self): return self.label @@ -415,6 +424,7 @@ class UserExtendedProfile(TimeStampedModel): interests = MultiSelectField(choices=choices.INTERESTS, blank=True) learners_related = MultiSelectField(choices=choices.INTERESTED_LEARNERS, blank=True) goals = MultiSelectField(choices=choices.GOALS, blank=True) + hubspot_contact_id = models.CharField(max_length=20, null=True) def __str__(self): return str(self.user) diff --git a/lms/djangoapps/student_dashboard/test/test_views.py b/lms/djangoapps/student_dashboard/test/test_views.py index 58ae8860319..6ce1ca78fdd 100644 --- a/lms/djangoapps/student_dashboard/test/test_views.py +++ b/lms/djangoapps/student_dashboard/test/test_views.py @@ -167,39 +167,3 @@ def test_get_recommended_xmodule_courses_without_course_overview(self, _from): recommended_courses = get_recommended_xmodule_courses(self.request, _from) self.assertEqual(len(recommended_courses), 0) - - def test_get_enrolled_past_courses(self): - """ - This method is covering :func:`~lms.djangoapps.student_dashboard.views.get_enrolled_past_courses` test will - assert if there no enrolled course for the user or there is any past enrollment for the current user. Because as - per the data we have initialized we are expecting one enrolled course and zero past enrolled courses. - """ - self.initialize_test() - - with mock.patch( - 'mailchimp_pipeline.signals.handlers.send_user_enrollments_to_mailchimp') \ - as send_user_enrollments_to_mailchimp: - with mock.patch.object(CourseEnrollment, 'send_signal') as send_signal: - send_signal.return_value = None - send_user_enrollments_to_mailchimp.return_value = None - - enrolled, past = get_enrolled_past_courses(self.request, self.course_enrollments) - self.assertEqual((len(enrolled), len(past)), (1, 0)) - - def test_get_enrolled_past_courses_without_course_card(self): - """ - This method is covering :func:`~lms.djangoapps.student_dashboard.views.get_enrolled_past_courses` without - setting course cards it is mandatory to have course card record in database for the past enrolled courses - so that we are expecting zero results for both past and current enrolled courses. - """ - self.initialize_test(add_course_overviews=False, initialize=False, add_settings=False) - - with mock.patch( - 'mailchimp_pipeline.signals.handlers.send_user_enrollments_to_mailchimp') \ - as send_user_enrollments_to_mailchimp: - with mock.patch.object(CourseEnrollment, 'send_signal') as send_signal: - send_signal.return_value = None - send_user_enrollments_to_mailchimp.return_value = None - - enrolled, past = get_enrolled_past_courses(self.request, self.course_enrollments) - self.assertEqual((len(enrolled), len(past)), (0, 0)) From c3576f5f6eeba69c3c571e48e691a5709697c66e Mon Sep 17 00:00:00 2001 From: Asad Ali Date: Thu, 7 Jul 2022 16:40:50 +0500 Subject: [PATCH 2/3] LP-3025 Fix extended profile issue --- common/lib/hubspot_client/helpers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/hubspot_client/helpers.py b/common/lib/hubspot_client/helpers.py index 0e729c23e34..330a1f7c1aa 100644 --- a/common/lib/hubspot_client/helpers.py +++ b/common/lib/hubspot_client/helpers.py @@ -36,12 +36,13 @@ def prepare_user_data_for_hubspot_contact_creation(user): } ) - if extended_profile and (extended_profile.language or extended_profile.country or extended_profile.city): + profile = user.profile if hasattr(user, 'profile') else None + if profile and (profile.language or profile.country or profile.city): user_json['properties'].update( { - 'edx_language': extended_profile.language or '', - 'edx_country': extended_profile.country.name.format() if extended_profile.country else '', - 'edx_city': extended_profile.city or '', + 'edx_language': profile.language or '', + 'edx_country': profile.country.name.format() if extended_profile.country else '', + 'edx_city': profile.city or '', } ) From fa2b427284e335ac6045fd018b2a2d769f4923f0 Mon Sep 17 00:00:00 2001 From: Asad Ali Date: Thu, 14 Jul 2022 18:28:48 +0500 Subject: [PATCH 3/3] LP-3025 Fix profile country error --- common/lib/hubspot_client/helpers.py | 2 +- common/lib/hubspot_client/tasks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/hubspot_client/helpers.py b/common/lib/hubspot_client/helpers.py index 330a1f7c1aa..75adf3b7068 100644 --- a/common/lib/hubspot_client/helpers.py +++ b/common/lib/hubspot_client/helpers.py @@ -41,7 +41,7 @@ def prepare_user_data_for_hubspot_contact_creation(user): user_json['properties'].update( { 'edx_language': profile.language or '', - 'edx_country': profile.country.name.format() if extended_profile.country else '', + 'edx_country': profile.country.name.format() if profile.country else '', 'edx_city': profile.city or '', } ) diff --git a/common/lib/hubspot_client/tasks.py b/common/lib/hubspot_client/tasks.py index b4f11ac4adc..5d071929c8b 100644 --- a/common/lib/hubspot_client/tasks.py +++ b/common/lib/hubspot_client/tasks.py @@ -39,7 +39,7 @@ def task_create_or_update_hubspot_contact(user_email, user_json, is_contact_crea user_json (dict): Data for HubSpot request. is_contact_created (bool): Is contact already synced with HubSpot. """ - logger.info('Syncing user data with HubSpot', user_email, user_json, is_contact_created) + logger.info('Syncing user data with HubSpot %s %s %s', user_email, user_json, is_contact_created) user = User.objects.filter(email=user_email).first() if not user: