feat: Certificate sharing to linkedin (optionally) consider course level organization name (#37331)

By adjusting social media sharing settings(specifically linkedin) certificate parameters are autopopulated to LinkedIn API. Additional setting parameters(such as CERTIFICATE_LINKEDIN_DEFAULTS_TO_COURSE_ORGANIZATION_NAME) are introduced to override existing(platform level parameter for organization name) parameters for an operator to configure course level organization name. This will enable learners to share certificate in to LinkedIn with an option for course associated organization to be autopopulated.
This commit is contained in:
Haftamu Kebede
2025-10-07 23:02:08 +03:00
committed by GitHub
parent 718dac1e7b
commit e46cfa6b32
6 changed files with 115 additions and 25 deletions

View File

@@ -589,7 +589,7 @@ def _cert_info(user, enrollment, cert_status):
linkedin_config = LinkedInAddToProfileConfiguration.current()
if linkedin_config.is_enabled():
status_dict['linked_in_url'] = linkedin_config.add_to_profile_url(
course_overview.display_name, cert_status.get('mode'), cert_status['download_url'],
course_overview, cert_status.get('mode'), cert_status['download_url'],
)
if status in {'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', 'unverified'}:

View File

@@ -1375,21 +1375,37 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
),
)
@property
def share_settings(self):
"""
Initialize share_settings once for reuse across methods
"""
if self._share_settings is None:
self._share_settings = configuration_helpers.get_value(
'SOCIAL_SHARING_SETTINGS',
settings.SOCIAL_SHARING_SETTINGS
)
return self._share_settings
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._share_settings = None
def is_enabled(self, *key_fields): # pylint: disable=arguments-differ
"""
Checks both the model itself and share_settings to see if LinkedIn Add to Profile is enabled
"""
enabled = super().is_enabled(*key_fields)
share_settings = configuration_helpers.get_value('SOCIAL_SHARING_SETTINGS', settings.SOCIAL_SHARING_SETTINGS)
return share_settings.get('CERTIFICATE_LINKEDIN', enabled)
return self.share_settings.get('CERTIFICATE_LINKEDIN', enabled)
def add_to_profile_url(self, course, cert_mode, cert_url, certificate=None):
def add_to_profile_url(self, course_name, cert_mode, cert_url, certificate=None):
"""
Construct the URL for the "add to profile" button. This will autofill the form based on
the params provided.
Arguments:
course_name (str): The display name of the course.
course (CourseOverview): Course/CourseOverview Object.
cert_mode (str): The course mode of the user's certificate (e.g. "verified", "honor", "professional")
cert_url (str): The URL for the certificate.
@@ -1398,11 +1414,11 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
If provided, this function will also autofill the certId and issue date for the cert.
"""
params = {
'name': self._cert_name(course_name, cert_mode),
'name': self._cert_name(course.display_name, cert_mode),
'certUrl': cert_url,
}
params.update(self._organization_information())
params.update(self._organization_information(course))
if certificate:
params.update({
@@ -1426,28 +1442,45 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
Returns:
str: The formatted string to display for the name field on the LinkedIn Add to Profile dialog.
"""
default_cert_name = self.MODE_TO_CERT_NAME.get(cert_mode, _('{platform_name} Certificate for {course_name}'))
default_cert_name = self.MODE_TO_CERT_NAME.get(
cert_mode, _('{platform_name} Certificate for {course_name}')
)
# Look for an override of the certificate name in the SOCIAL_SHARING_SETTINGS setting
share_settings = configuration_helpers.get_value('SOCIAL_SHARING_SETTINGS', settings.SOCIAL_SHARING_SETTINGS)
cert_name = share_settings.get('CERTIFICATE_LINKEDIN_MODE_TO_CERT_NAME', {}).get(cert_mode, default_cert_name)
cert_name = self.share_settings.get(
'CERTIFICATE_LINKEDIN_MODE_TO_CERT_NAME', {}
).get(cert_mode, default_cert_name)
return cert_name.format(
platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
course_name=course_name
)
def _organization_information(self):
def _organization_information(self, course=None):
"""
Returns organization information for use in the URL parameters for add to profile.
Returns organization information for use in the URL parameters for add to
profile. By default when sharing to LinkedIn, Platform Name and/or Platform
LINKEDIN_COMPANY_ID will be used. If Course specific Organization Name is
prefered when sharing Certificate to linkedIn the flag for that
CERTIFICATE_LINKEDIN_DEFAULTS_TO_COURSE_ORGANIZATION_NAME should be set
to True alongside other LinkedIn settings
Returns:
dict: Either the organization ID on LinkedIn or the organization's name
dict: Either the organization ID on LinkedIn, the organization's name or
organization name associated to a specific course
Will be used to prefill the organization on the add to profile action.
"""
org_id = configuration_helpers.get_value('LINKEDIN_COMPANY_ID', self.company_identifier)
prefer_course_organization_name = self.share_settings.get(
'CERTIFICATE_LINKEDIN_DEFAULTS_TO_COURSE_ORGANIZATION_NAME', False
)
if (prefer_course_organization_name and course):
return {"organizationName": course.display_organization}
org_id = configuration_helpers.get_value(
"LINKEDIN_COMPANY_ID", self.company_identifier
)
# Prefer organization ID per documentation at https://addtoprofile.linkedin.com/
if org_id:
return {'organizationId': org_id}
return {"organizationId": org_id}
return {'organizationName': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME)}

View File

@@ -1,9 +1,8 @@
"""Tests for LinkedIn Add to Profile configuration. """
from types import SimpleNamespace
from urllib.parse import quote
import ddt
from django.conf import settings
from django.test import TestCase
@@ -17,6 +16,7 @@ class LinkedInAddToProfileUrlTests(TestCase):
COURSE_NAME = 'Test Course ☃'
CERT_URL = 'http://s3.edx/cert'
COURSE_ORGANIZATION = 'TEST+ORGANIZATION'
SITE_CONFIGURATION = {
'SOCIAL_SHARING_SETTINGS': {
'CERTIFICATE_LINKEDIN_MODE_TO_CERT_NAME': {
@@ -27,6 +27,17 @@ class LinkedInAddToProfileUrlTests(TestCase):
}
}
}
SITE_CONFIGURATION_COURSE_LEVEL_ORG = {
'SOCIAL_SHARING_SETTINGS': {
'CERTIFICATE_LINKEDIN_DEFAULTS_TO_COURSE_ORGANIZATION_NAME': True,
'CERTIFICATE_LINKEDIN_MODE_TO_CERT_NAME': {
'honor': '{platform_name} Honor Code Credential for {course_name}',
'verified': '{platform_name} Verified Credential for {course_name}',
'professional': '{platform_name} Professional Credential for {course_name}',
'no-id-professional': '{platform_name} Professional Credential for {course_name}',
}
}
}
@ddt.data(
('honor', 'Honor+Code+Certificate+for+Test+Course+%E2%98%83'),
@@ -49,7 +60,13 @@ class LinkedInAddToProfileUrlTests(TestCase):
company_identifier=config.company_identifier,
)
actual_url = config.add_to_profile_url(self.COURSE_NAME, cert_mode, self.CERT_URL)
course_mock_object = SimpleNamespace(
display_name=self.COURSE_NAME, display_organization=self.COURSE_ORGANIZATION
)
actual_url = config.add_to_profile_url(
course_mock_object, cert_mode, self.CERT_URL
)
self.assertEqual(actual_url, expected_url)
@@ -74,8 +91,49 @@ class LinkedInAddToProfileUrlTests(TestCase):
cert_url=quote(self.CERT_URL, safe=''),
company_identifier=config.company_identifier,
)
with with_site_configuration_context(configuration=self.SITE_CONFIGURATION):
actual_url = config.add_to_profile_url(self.COURSE_NAME, cert_mode, self.CERT_URL)
course_mock_object = SimpleNamespace(
display_name=self.COURSE_NAME,
display_organization=self.COURSE_ORGANIZATION,
)
actual_url = config.add_to_profile_url(
course_mock_object, cert_mode, self.CERT_URL
)
self.assertEqual(actual_url, expected_url)
@ddt.data(
('honor', 'Honor+Code+Credential+for+Test+Course+%E2%98%83'),
('verified', 'Verified+Credential+for+Test+Course+%E2%98%83'),
('professional', 'Professional+Credential+for+Test+Course+%E2%98%83'),
('no-id-professional', 'Professional+Credential+for+Test+Course+%E2%98%83'),
('default_mode', 'Certificate+for+Test+Course+%E2%98%83')
)
@ddt.unpack
def test_linked_in_url_with_course_org_name_override(
self, cert_mode, expected_cert_name
):
config = LinkedInAddToProfileConfigurationFactory()
expected_url = (
'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&'
'name={platform}+{cert_name}&certUrl={cert_url}&'
'organizationName={course_organization_name}'
).format(
platform=quote(settings.PLATFORM_NAME.encode('utf-8')),
cert_name=expected_cert_name,
cert_url=quote(self.CERT_URL, safe=''),
course_organization_name=quote(self.COURSE_ORGANIZATION.encode('utf-8')),
)
with with_site_configuration_context(
configuration=self.SITE_CONFIGURATION_COURSE_LEVEL_ORG
):
course_mock_object = SimpleNamespace(
display_name=self.COURSE_NAME,
display_organization=self.COURSE_ORGANIZATION,
)
actual_url = config.add_to_profile_url(
course_mock_object, cert_mode, self.CERT_URL
)
self.assertEqual(actual_url, expected_url)

View File

@@ -51,8 +51,6 @@ from openedx.features.course_experience.url_helpers import make_learning_mfe_cou
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
log = logging.getLogger(__name__)
BETA_TESTER_METHOD = 'common.djangoapps.student.helpers.access.is_beta_tester'
@@ -426,6 +424,7 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
self.course.start = datetime.now(pytz.UTC) - timedelta(days=2)
self.course.end = datetime.now(pytz.UTC) - timedelta(days=1)
self.course.display_name = 'Omega'
self.course.course_organization = 'Omega Org'
self.course = self.update_course(self.course, self.user.id)
cert = GeneratedCertificateFactory.create(
@@ -449,7 +448,7 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
).format(
platform=quote(settings.PLATFORM_NAME.encode('utf-8')),
cert_url=quote(cert.download_url, safe=''),
company_identifier=linkedin_config.company_identifier
company_identifier=linkedin_config.company_identifier,
)
# Single assertion for the expected LinkedIn URL

View File

@@ -305,7 +305,7 @@ def _update_social_context(request, context, course, user_certificate, platform_
linkedin_config = LinkedInAddToProfileConfiguration.current()
if linkedin_config.is_enabled():
context['linked_in_url'] = linkedin_config.add_to_profile_url(
course.display_name, user_certificate.mode, smart_str(share_url), certificate=user_certificate
course, user_certificate.mode, smart_str(share_url), certificate=user_certificate
)

View File

@@ -288,7 +288,7 @@ class CoursewareMeta:
get_certificate_url(course_id=self.course_key, uuid=user_certificate.verify_uuid)
)
return linkedin_config.add_to_profile_url(
self.course_overview.display_name, user_certificate.mode, cert_url, certificate=user_certificate,
self.course_overview, user_certificate.mode, cert_url, certificate=user_certificate,
)
@property