AA-385: Add in LinkedIn Add to Profile to courseware meta API
A major update to this function allows it to actually autofill the certificate information again! I believe LinkedIn changed their API and we never updated our end. This fixes that!
This commit is contained in:
@@ -33,7 +33,6 @@ from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, verification_for_datetime
|
||||
from openedx.core.djangoapps.certificates.api import certificates_viewable_for_course
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
from openedx.core.djangoapps.theming.helpers import get_themes
|
||||
from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect
|
||||
from student.models import (
|
||||
@@ -447,19 +446,12 @@ def cert_info(user, course_overview):
|
||||
course_overview (CourseOverview): A course.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with keys:
|
||||
'status': one of 'generating', 'downloadable', 'notpassing', 'processing', 'restricted', 'unavailable', or
|
||||
'certificate_earned_but_not_available'
|
||||
'download_url': url, only present if show_download_url is True
|
||||
'show_survey_button': bool
|
||||
'survey_url': url, only if show_survey_button is True
|
||||
'grade': if status is not 'processing'
|
||||
'can_unenroll': if status allows for unenrollment
|
||||
See _cert_info
|
||||
"""
|
||||
return _cert_info(
|
||||
user,
|
||||
course_overview,
|
||||
certificate_status_for_student(user, course_overview.id)
|
||||
certificate_status_for_student(user, course_overview.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -470,6 +462,23 @@ def _cert_info(user, course_overview, cert_status):
|
||||
Arguments:
|
||||
user (User): A user.
|
||||
course_overview (CourseOverview): A course.
|
||||
cert_status (dict): dictionary containing information about certificate status for the user
|
||||
|
||||
Returns:
|
||||
dictionary containing:
|
||||
'status': one of 'generating', 'downloadable', 'notpassing', 'restricted', 'auditing',
|
||||
'processing', 'unverified', 'unavailable', or 'certificate_earned_but_not_available'
|
||||
'show_survey_button': bool
|
||||
'can_unenroll': if status allows for unenrollment
|
||||
|
||||
The dictionary may also contain:
|
||||
'linked_in_url': url to add cert to LinkedIn profile
|
||||
'survey_url': url, only if course_overview.end_of_course_survey_url is not None
|
||||
'show_cert_web_view': bool if html web certs are enabled and there is an active web cert
|
||||
'cert_web_view_url': url if html web certs are enabled and there is an active web cert
|
||||
'download_url': url to download a cert
|
||||
'grade': if status is in 'generating', 'downloadable', 'notpassing', 'restricted',
|
||||
'auditing', or 'unverified'
|
||||
"""
|
||||
# simplify the status for the template using this lookup table
|
||||
template_state = {
|
||||
@@ -496,7 +505,7 @@ def _cert_info(user, course_overview, cert_status):
|
||||
return default_info
|
||||
|
||||
status = template_state.get(cert_status['status'], default_status)
|
||||
is_hidden_status = status in ('unavailable', 'processing', 'generating', 'notpassing', 'auditing')
|
||||
is_hidden_status = status in ('processing', 'generating', 'notpassing', 'auditing')
|
||||
|
||||
if (
|
||||
not certificates_viewable_for_course(course_overview) and
|
||||
@@ -552,15 +561,9 @@ def _cert_info(user, course_overview, cert_status):
|
||||
# Clicking this button sends the user to LinkedIn where they
|
||||
# can add the certificate information to their profile.
|
||||
linkedin_config = LinkedInAddToProfileConfiguration.current()
|
||||
|
||||
# posting certificates to LinkedIn is not currently
|
||||
# supported in White Labels
|
||||
if linkedin_config.enabled and not theming_helpers.is_request_in_themed_site():
|
||||
if linkedin_config.is_enabled():
|
||||
status_dict['linked_in_url'] = linkedin_config.add_to_profile_url(
|
||||
course_overview.id,
|
||||
course_overview.display_name,
|
||||
cert_status.get('mode'),
|
||||
cert_status['download_url']
|
||||
course_overview.display_name, cert_status.get('mode'), cert_status['download_url'],
|
||||
)
|
||||
|
||||
if status in {'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', 'unverified'}:
|
||||
|
||||
@@ -16,10 +16,11 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from collections import OrderedDict, defaultdict, namedtuple
|
||||
from collections import defaultdict, namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from functools import total_ordering
|
||||
from importlib import import_module
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import six
|
||||
from config_models.models import ConfigurationModel
|
||||
@@ -51,7 +52,6 @@ from pytz import UTC
|
||||
from simple_history.models import HistoricalRecords
|
||||
from six import text_type
|
||||
from six.moves import range
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from slumber.exceptions import HttpClientError, HttpServerError
|
||||
from user_util import user_util
|
||||
|
||||
@@ -2527,23 +2527,21 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
|
||||
"""
|
||||
LinkedIn Add to Profile Configuration
|
||||
|
||||
This configuration enables the "Add to Profile" LinkedIn
|
||||
button on the student dashboard. The button appears when
|
||||
users have a certificate available; when clicked,
|
||||
users are sent to the LinkedIn site with a pre-filled
|
||||
form allowing them to add the certificate to their
|
||||
LinkedIn profile.
|
||||
This configuration enables the 'Add to Profile' LinkedIn button. The button
|
||||
appears when users have a certificate available; when clicked, users are sent
|
||||
to the LinkedIn site with a pre-filled form allowing them to add the
|
||||
certificate to their LinkedIn profile.
|
||||
|
||||
See https://addtoprofile.linkedin.com/ for documentation on parameters
|
||||
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
MODE_TO_CERT_NAME = {
|
||||
"honor": _(u"{platform_name} Honor Code Certificate for {course_name}"),
|
||||
"verified": _(u"{platform_name} Verified Certificate for {course_name}"),
|
||||
"professional": _(u"{platform_name} Professional Certificate for {course_name}"),
|
||||
"no-id-professional": _(
|
||||
u"{platform_name} Professional Certificate for {course_name}"
|
||||
),
|
||||
'honor': _('{platform_name} Honor Code Certificate for {course_name}'),
|
||||
'verified': _('{platform_name} Verified Certificate for {course_name}'),
|
||||
'professional': _('{platform_name} Professional Certificate for {course_name}'),
|
||||
'no-id-professional': _('{platform_name} Professional Certificate for {course_name}'),
|
||||
}
|
||||
|
||||
company_identifier = models.TextField(
|
||||
@@ -2567,33 +2565,43 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
|
||||
)
|
||||
)
|
||||
|
||||
def add_to_profile_url(self, course_key, course_name, cert_mode, cert_url, source="o", target="dashboard"):
|
||||
"""Construct the URL for the "add to profile" button.
|
||||
def is_enabled(self, *key_fields):
|
||||
"""
|
||||
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)
|
||||
|
||||
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_key (CourseKey): The identifier for the course.
|
||||
course_name (unicode): The display name of the course.
|
||||
course_name (str): The display name of the course.
|
||||
cert_mode (str): The course mode of the user's certificate (e.g. "verified", "honor", "professional")
|
||||
cert_url (str): The download URL for the certificate.
|
||||
cert_url (str): The URL for the certificate.
|
||||
|
||||
Keyword Arguments:
|
||||
source (str): Either "o" (for onsite/UI), "e" (for emails), or "m" (for mobile)
|
||||
target (str): An identifier for the occurrance of the button.
|
||||
|
||||
certificate (GeneratedCertificate): a GeneratedCertificate object for the user and course.
|
||||
If provided, this function will also autofill the certId and issue date for the cert.
|
||||
"""
|
||||
company_identifier = configuration_helpers.get_value('LINKEDIN_COMPANY_ID', self.company_identifier)
|
||||
params = OrderedDict([
|
||||
('_ed', company_identifier),
|
||||
('pfCertificationName', self._cert_name(course_name, cert_mode).encode('utf-8')),
|
||||
('pfCertificationUrl', cert_url),
|
||||
('source', source)
|
||||
])
|
||||
params = {
|
||||
'name': self._cert_name(course_name, cert_mode),
|
||||
'certUrl': cert_url,
|
||||
}
|
||||
|
||||
tracking_code = self._tracking_code(course_key, cert_mode, target)
|
||||
if tracking_code is not None:
|
||||
params['trk'] = tracking_code
|
||||
params.update(self._organization_information())
|
||||
|
||||
return u'http://www.linkedin.com/profile/add?{params}'.format(
|
||||
if certificate:
|
||||
params.update({
|
||||
'certId': certificate.verify_uuid,
|
||||
'issueYear': certificate.created_date.year,
|
||||
'issueMonth': certificate.created_date.month,
|
||||
})
|
||||
|
||||
return 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&{params}'.format(
|
||||
params=urlencode(params)
|
||||
)
|
||||
|
||||
@@ -2608,10 +2616,7 @@ 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,
|
||||
_(u"{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)
|
||||
@@ -2621,41 +2626,19 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
|
||||
course_name=course_name
|
||||
)
|
||||
|
||||
def _tracking_code(self, course_key, cert_mode, target):
|
||||
"""Create a tracking code for the button.
|
||||
|
||||
Tracking codes are used by LinkedIn to collect
|
||||
analytics about certifications users are adding
|
||||
to their profiles.
|
||||
|
||||
The tracking code format is:
|
||||
&trk=[partner name]-[certificate type]-[date]-[target field]
|
||||
|
||||
In our case, we're sending:
|
||||
&trk=edx-{COURSE ID}_{COURSE MODE}-{TARGET}
|
||||
|
||||
If no partner code is configured, then this will
|
||||
return None, indicating that tracking codes are disabled.
|
||||
|
||||
Arguments:
|
||||
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
cert_mode (str): The enrollment mode for the course.
|
||||
target (str): Identifier for where the button is located.
|
||||
def _organization_information(self):
|
||||
"""
|
||||
Returns organization information for use in the URL parameters for add to profile.
|
||||
|
||||
Returns:
|
||||
unicode or None
|
||||
|
||||
dict: Either the organization ID on LinkedIn or the organization's name
|
||||
Will be used to prefill the organization on the add to profile action.
|
||||
"""
|
||||
return (
|
||||
u"{partner}-{course_key}_{cert_mode}-{target}".format(
|
||||
partner=self.trk_partner_name,
|
||||
course_key=text_type(course_key),
|
||||
cert_mode=cert_mode,
|
||||
target=target
|
||||
)
|
||||
if self.trk_partner_name else None
|
||||
)
|
||||
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 {'organizationName': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME)}
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
|
||||
@@ -5,7 +5,6 @@ import datetime
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
@@ -15,8 +14,9 @@ from pytz import UTC
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.certificates.api import get_certificate_url
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from student.models import LinkedInAddToProfileConfiguration
|
||||
from lms.djangoapps.certificates.tests.factories import (
|
||||
GeneratedCertificateFactory, LinkedInAddToProfileConfigurationFactory
|
||||
)
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -191,10 +191,12 @@ class CertificateDisplayTest(CertificateDisplayTestBase):
|
||||
u'do not have a current verified identity with {platform_name}'
|
||||
.format(platform_name=settings.PLATFORM_NAME))
|
||||
|
||||
def test_post_to_linkedin_invisibility(self):
|
||||
def test_post_to_linkedin_visibility(self):
|
||||
"""
|
||||
Verifies that the post certificate to linked button
|
||||
does not appear by default (when config is not set)
|
||||
Then Verifies that the post certificate to linked button appears
|
||||
as expected once a config is set
|
||||
"""
|
||||
self._create_certificate('honor')
|
||||
|
||||
@@ -202,39 +204,10 @@ class CertificateDisplayTest(CertificateDisplayTestBase):
|
||||
# button should not be visible
|
||||
self._check_linkedin_visibility(False)
|
||||
|
||||
def test_post_to_linkedin_visibility(self):
|
||||
"""
|
||||
Verifies that the post certificate to linked button appears
|
||||
as expected
|
||||
"""
|
||||
self._create_certificate('honor')
|
||||
|
||||
config = LinkedInAddToProfileConfiguration(
|
||||
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
||||
enabled=True
|
||||
)
|
||||
config.save()
|
||||
|
||||
LinkedInAddToProfileConfigurationFactory()
|
||||
# now we should see it
|
||||
self._check_linkedin_visibility(True)
|
||||
|
||||
@mock.patch("openedx.core.djangoapps.theming.helpers.is_request_in_themed_site", mock.Mock(return_value=True))
|
||||
def test_post_to_linkedin_site_specific(self):
|
||||
"""
|
||||
Verifies behavior for themed sites which disables the post to LinkedIn
|
||||
feature (for now)
|
||||
"""
|
||||
self._create_certificate('honor')
|
||||
|
||||
config = LinkedInAddToProfileConfiguration(
|
||||
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
||||
enabled=True
|
||||
)
|
||||
config.save()
|
||||
|
||||
# now we should not see it because we are in a themed site
|
||||
self._check_linkedin_visibility(False)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
|
||||
@@ -2,120 +2,101 @@
|
||||
"""Tests for LinkedIn Add to Profile configuration. """
|
||||
|
||||
|
||||
from urllib.parse import quote
|
||||
import ddt
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from six.moves.urllib.parse import quote, urlencode
|
||||
|
||||
from lms.djangoapps.certificates.tests.factories import LinkedInAddToProfileConfigurationFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from student.models import LinkedInAddToProfileConfiguration
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class LinkedInAddToProfileUrlTests(TestCase):
|
||||
"""Tests for URL generation of LinkedInAddToProfileConfig. """
|
||||
|
||||
COURSE_KEY = CourseLocator(org="edx", course="DemoX", run="Demo_Course")
|
||||
COURSE_NAME = u"Test Course ☃"
|
||||
CERT_URL = u"http://s3.edx/cert"
|
||||
COURSE_NAME = 'Test Course ☃'
|
||||
CERT_URL = 'http://s3.edx/cert'
|
||||
SITE_CONFIGURATION = {
|
||||
'SOCIAL_SHARING_SETTINGS': {
|
||||
'CERTIFICATE_LINKEDIN_MODE_TO_CERT_NAME': {
|
||||
'honor': u'{platform_name} Honor Code Credential for {course_name}',
|
||||
'verified': u'{platform_name} Verified Credential for {course_name}',
|
||||
'professional': u'{platform_name} Professional Credential for {course_name}',
|
||||
'no-id-professional': u'{platform_name} Professional Credential for {course_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', u'Honor+Code+Certificate+for+Test+Course+%E2%98%83'),
|
||||
('verified', u'Verified+Certificate+for+Test+Course+%E2%98%83'),
|
||||
('professional', u'Professional+Certificate+for+Test+Course+%E2%98%83'),
|
||||
('default_mode', u'Certificate+for+Test+Course+%E2%98%83')
|
||||
('honor', 'Honor+Code+Certificate+for+Test+Course+%E2%98%83'),
|
||||
('verified', 'Verified+Certificate+for+Test+Course+%E2%98%83'),
|
||||
('professional', 'Professional+Certificate+for+Test+Course+%E2%98%83'),
|
||||
('default_mode', 'Certificate+for+Test+Course+%E2%98%83')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_linked_in_url(self, cert_mode, expected_cert_name):
|
||||
config = LinkedInAddToProfileConfiguration(
|
||||
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
||||
enabled=True
|
||||
)
|
||||
config = LinkedInAddToProfileConfigurationFactory()
|
||||
|
||||
expected_url = (
|
||||
'http://www.linkedin.com/profile/add'
|
||||
'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
|
||||
'pfCertificationName={platform_name}+{expected_cert_name}&'
|
||||
'pfCertificationUrl=http%3A%2F%2Fs3.edx%2Fcert&'
|
||||
'source=o'
|
||||
).format(
|
||||
expected_cert_name=expected_cert_name,
|
||||
platform_name=quote(settings.PLATFORM_NAME.encode('utf-8'))
|
||||
)
|
||||
# We can switch to this once edx-platform reaches Python 3.8
|
||||
# expected_url = (
|
||||
# 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&'
|
||||
# 'name={platform}+{cert_name}&certUrl={cert_url}&'
|
||||
# 'organizationId={company_identifier}'
|
||||
# ).format(
|
||||
# platform=quote(settings.PLATFORM_NAME.encode('utf-8')),
|
||||
# cert_name=expected_cert_name,
|
||||
# cert_url=quote(self.CERT_URL, safe=''),
|
||||
# company_identifier=config.company_identifier,
|
||||
# )
|
||||
|
||||
actual_url = config.add_to_profile_url(
|
||||
self.COURSE_KEY,
|
||||
self.COURSE_NAME,
|
||||
cert_mode,
|
||||
self.CERT_URL
|
||||
)
|
||||
actual_url = config.add_to_profile_url(self.COURSE_NAME, cert_mode, self.CERT_URL)
|
||||
|
||||
self.assertEqual(actual_url, expected_url)
|
||||
# We can switch to this instead of the assertIn once edx-platform reaches Python 3.8
|
||||
# There was a problem with dict ordering in the add_to_profile_url function that will go away then.
|
||||
# self.assertEqual(actual_url, expected_url)
|
||||
|
||||
self.assertIn('https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME', actual_url)
|
||||
self.assertIn('&name={platform}+{cert_name}'.format(
|
||||
platform=quote(settings.PLATFORM_NAME.encode('utf-8')), cert_name=expected_cert_name
|
||||
), actual_url)
|
||||
self.assertIn('&certUrl={cert_url}'.format(cert_url=quote(self.CERT_URL, safe='')), actual_url)
|
||||
self.assertIn('&organizationId={org_id}'.format(org_id=config.company_identifier), actual_url)
|
||||
|
||||
@ddt.data(
|
||||
('honor', u'Honor+Code+Credential+for+Test+Course+%E2%98%83'),
|
||||
('verified', u'Verified+Credential+for+Test+Course+%E2%98%83'),
|
||||
('professional', u'Professional+Credential+for+Test+Course+%E2%98%83'),
|
||||
('no-id-professional', u'Professional+Credential+for+Test+Course+%E2%98%83'),
|
||||
('default_mode', u'Certificate+for+Test+Course+%E2%98%83')
|
||||
('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_cert_name_override(self, cert_mode, expected_cert_name):
|
||||
config = LinkedInAddToProfileConfiguration(
|
||||
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
||||
enabled=True
|
||||
)
|
||||
config = LinkedInAddToProfileConfigurationFactory()
|
||||
|
||||
expected_url = (
|
||||
'http://www.linkedin.com/profile/add'
|
||||
'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
|
||||
'pfCertificationName={platform_name}+{expected_cert_name}&'
|
||||
'pfCertificationUrl=http%3A%2F%2Fs3.edx%2Fcert&'
|
||||
'source=o'
|
||||
).format(
|
||||
expected_cert_name=expected_cert_name,
|
||||
platform_name=quote(settings.PLATFORM_NAME.encode('utf-8'))
|
||||
)
|
||||
# We can switch to this once edx-platform reaches Python 3.8
|
||||
# expected_url = (
|
||||
# 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&'
|
||||
# 'name={platform}+{cert_name}&certUrl={cert_url}&'
|
||||
# 'organizationId={company_identifier}'
|
||||
# ).format(
|
||||
# platform=quote(settings.PLATFORM_NAME.encode('utf-8')),
|
||||
# cert_name=expected_cert_name,
|
||||
# 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_KEY,
|
||||
self.COURSE_NAME,
|
||||
cert_mode,
|
||||
self.CERT_URL
|
||||
)
|
||||
actual_url = config.add_to_profile_url(self.COURSE_NAME, cert_mode, self.CERT_URL)
|
||||
|
||||
self.assertEqual(actual_url, expected_url)
|
||||
# We can switch to this instead of the assertIn once edx-platform reaches Python 3.8
|
||||
# There was a problem with dict ordering in the add_to_profile_url function that will go away then.
|
||||
# self.assertEqual(actual_url, expected_url)
|
||||
|
||||
def test_linked_in_url_tracking_code(self):
|
||||
config = LinkedInAddToProfileConfiguration(
|
||||
company_identifier="abcd123",
|
||||
trk_partner_name="edx",
|
||||
enabled=True
|
||||
)
|
||||
|
||||
expected_param = urlencode({
|
||||
'trk': u'edx-{course_key}_honor-dashboard'.format(
|
||||
course_key=self.COURSE_KEY
|
||||
)
|
||||
})
|
||||
|
||||
actual_url = config.add_to_profile_url(
|
||||
self.COURSE_KEY,
|
||||
self.COURSE_NAME,
|
||||
'honor',
|
||||
self.CERT_URL
|
||||
)
|
||||
|
||||
self.assertIn(expected_param, actual_url)
|
||||
self.assertIn('https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME', actual_url)
|
||||
self.assertIn('&name={platform}+{cert_name}'.format(
|
||||
platform=quote(settings.PLATFORM_NAME.encode('utf-8')), cert_name=expected_cert_name
|
||||
), actual_url)
|
||||
self.assertIn('&certUrl={cert_url}'.format(cert_url=quote(self.CERT_URL, safe='')), actual_url)
|
||||
self.assertIn('&organizationId={org_id}'.format(org_id=config.company_identifier), actual_url)
|
||||
|
||||
@@ -7,10 +7,10 @@ Miscellaneous tests for the student app.
|
||||
import logging
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote
|
||||
|
||||
import ddt
|
||||
import pytz
|
||||
import six
|
||||
from config_models.models import cache
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
@@ -22,11 +22,7 @@ from mock import Mock, patch
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import CourseLocator
|
||||
from pyquery import PyQuery as pq
|
||||
from six import text_type
|
||||
from six.moves import range
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
from bulk_email.models import Optout
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses
|
||||
@@ -34,6 +30,7 @@ from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFact
|
||||
from lms.djangoapps.verify_student.tests import TestVerificationBase
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory, generate_course_run_key
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
||||
@@ -58,7 +55,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
class CourseEndingTest(TestCase):
|
||||
class CourseEndingTest(ModuleStoreTestCase):
|
||||
"""Test things related to course endings: certificates, surveys, etc"""
|
||||
|
||||
def test_process_survey_link(self):
|
||||
@@ -74,12 +71,19 @@ class CourseEndingTest(TestCase):
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
|
||||
def test_cert_info(self):
|
||||
user = Mock(username="fred", id="1")
|
||||
user = UserFactory.create()
|
||||
survey_url = "http://a_survey.com"
|
||||
course = Mock(
|
||||
course = CourseOverviewFactory.create(
|
||||
end_of_course_survey_url=survey_url,
|
||||
certificates_display_behavior='end',
|
||||
id=CourseLocator(org="x", course="y", run="z"),
|
||||
)
|
||||
cert = GeneratedCertificateFactory.create(
|
||||
user=user,
|
||||
course_id=course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='honor',
|
||||
grade='67',
|
||||
download_url='http://s3.edx/cert'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
@@ -91,19 +95,19 @@ class CourseEndingTest(TestCase):
|
||||
}
|
||||
)
|
||||
|
||||
cert_status = {'status': 'unavailable'}
|
||||
cert_status = {'status': 'unavailable', 'mode': 'honor', 'uuid': None}
|
||||
self.assertEqual(
|
||||
_cert_info(user, course, cert_status),
|
||||
{
|
||||
'status': 'processing',
|
||||
'show_survey_button': False,
|
||||
'mode': None,
|
||||
'mode': 'honor',
|
||||
'linked_in_url': None,
|
||||
'can_unenroll': True,
|
||||
}
|
||||
)
|
||||
|
||||
cert_status = {'status': 'generating', 'grade': '0.67', 'mode': 'honor'}
|
||||
cert_status = {'status': 'generating', 'grade': '0.67', 'mode': 'honor', 'uuid': None}
|
||||
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
|
||||
patch_persisted_grade.return_value = Mock(percent=1.0)
|
||||
self.assertEqual(
|
||||
@@ -119,7 +123,7 @@ class CourseEndingTest(TestCase):
|
||||
}
|
||||
)
|
||||
|
||||
cert_status = {'status': 'generating', 'grade': '0.67', 'mode': 'honor'}
|
||||
cert_status = {'status': 'generating', 'grade': '0.67', 'mode': 'honor', 'uuid': None}
|
||||
self.assertEqual(
|
||||
_cert_info(user, course, cert_status),
|
||||
{
|
||||
@@ -133,19 +137,18 @@ class CourseEndingTest(TestCase):
|
||||
}
|
||||
)
|
||||
|
||||
download_url = 'http://s3.edx/cert'
|
||||
cert_status = {
|
||||
'status': 'downloadable',
|
||||
'grade': '0.67',
|
||||
'download_url': download_url,
|
||||
'mode': 'honor'
|
||||
'download_url': cert.download_url,
|
||||
'mode': 'honor',
|
||||
'uuid': 'fakeuuidbutitsfine',
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
_cert_info(user, course, cert_status),
|
||||
{
|
||||
'status': 'downloadable',
|
||||
'download_url': download_url,
|
||||
'download_url': cert.download_url,
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': '0.67',
|
||||
@@ -157,8 +160,9 @@ class CourseEndingTest(TestCase):
|
||||
|
||||
cert_status = {
|
||||
'status': 'notpassing', 'grade': '0.67',
|
||||
'download_url': download_url,
|
||||
'mode': 'honor'
|
||||
'download_url': cert.download_url,
|
||||
'mode': 'honor',
|
||||
'uuid': 'fakeuuidbutitsfine',
|
||||
}
|
||||
self.assertEqual(
|
||||
_cert_info(user, course, cert_status),
|
||||
@@ -177,7 +181,7 @@ class CourseEndingTest(TestCase):
|
||||
course2 = Mock(end_of_course_survey_url=None, id=CourseLocator(org="a", course="b", run="c"))
|
||||
cert_status = {
|
||||
'status': 'notpassing', 'grade': '0.67',
|
||||
'download_url': download_url, 'mode': 'honor'
|
||||
'download_url': cert.download_url, 'mode': 'honor', 'uuid': 'fakeuuidbutitsfine'
|
||||
}
|
||||
self.assertEqual(
|
||||
_cert_info(user, course2, cert_status),
|
||||
@@ -193,7 +197,7 @@ class CourseEndingTest(TestCase):
|
||||
|
||||
# test when the display is unavailable or notpassing, we get the correct results out
|
||||
course2.certificates_display_behavior = 'early_no_info'
|
||||
cert_status = {'status': 'unavailable'}
|
||||
cert_status = {'status': 'unavailable', 'mode': 'honor', 'uuid': None}
|
||||
self.assertEqual(
|
||||
_cert_info(user, course2, cert_status),
|
||||
{
|
||||
@@ -205,8 +209,9 @@ class CourseEndingTest(TestCase):
|
||||
|
||||
cert_status = {
|
||||
'status': 'notpassing', 'grade': '0.67',
|
||||
'download_url': download_url,
|
||||
'mode': 'honor'
|
||||
'download_url': cert.download_url,
|
||||
'mode': 'honor',
|
||||
'uuid': 'fakeuuidbutitsfine'
|
||||
}
|
||||
self.assertEqual(
|
||||
_cert_info(user, course2, cert_status),
|
||||
@@ -234,18 +239,17 @@ class CourseEndingTest(TestCase):
|
||||
from the certs table is used on the learner dashboard.
|
||||
"""
|
||||
expected_grade = max(filter(lambda x: x is not None, [persisted_grade, cert_grade]))
|
||||
user = Mock(username="fred", id="1")
|
||||
user = UserFactory.create()
|
||||
survey_url = "http://a_survey.com"
|
||||
course = Mock(
|
||||
course = CourseOverviewFactory.create(
|
||||
end_of_course_survey_url=survey_url,
|
||||
certificates_display_behavior='end',
|
||||
id=CourseLocator(org="x", course="y", run="z"),
|
||||
)
|
||||
|
||||
if cert_grade is not None:
|
||||
cert_status = {'status': 'generating', 'grade': six.text_type(cert_grade), 'mode': 'honor'}
|
||||
cert_status = {'status': 'generating', 'grade': str(cert_grade), 'mode': 'honor', 'uuid': None}
|
||||
else:
|
||||
cert_status = {'status': 'generating', 'mode': 'honor'}
|
||||
cert_status = {'status': 'generating', 'mode': 'honor', 'uuid': None}
|
||||
|
||||
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
|
||||
patch_persisted_grade.return_value = Mock(percent=persisted_grade)
|
||||
@@ -255,7 +259,7 @@ class CourseEndingTest(TestCase):
|
||||
'status': 'generating',
|
||||
'show_survey_button': True,
|
||||
'survey_url': survey_url,
|
||||
'grade': six.text_type(expected_grade),
|
||||
'grade': str(expected_grade),
|
||||
'mode': 'honor',
|
||||
'linked_in_url': None,
|
||||
'can_unenroll': False,
|
||||
@@ -268,14 +272,13 @@ class CourseEndingTest(TestCase):
|
||||
when the learner has no persisted grade or grade
|
||||
in the certs table.
|
||||
"""
|
||||
user = Mock(username="fred", id="1")
|
||||
user = UserFactory.create()
|
||||
survey_url = "http://a_survey.com"
|
||||
course = Mock(
|
||||
course = CourseOverviewFactory.create(
|
||||
end_of_course_survey_url=survey_url,
|
||||
certificates_display_behavior='end',
|
||||
id=CourseLocator(org="x", course="y", run="z"),
|
||||
)
|
||||
cert_status = {'status': 'generating', 'mode': 'honor'}
|
||||
cert_status = {'status': 'generating', 'mode': 'honor', 'uuid': None}
|
||||
|
||||
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
|
||||
patch_persisted_grade.return_value = None
|
||||
@@ -422,7 +425,7 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'Add Certificate to LinkedIn')
|
||||
|
||||
response_url = 'http://www.linkedin.com/profile/add?_ed='
|
||||
response_url = 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME'
|
||||
self.assertNotContains(response, escape(response_url))
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@@ -432,49 +435,55 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
|
||||
# should be visible. and it has URL value with valid parameters.
|
||||
self.client.login(username="jack", password="test")
|
||||
|
||||
LinkedInAddToProfileConfiguration.objects.create(
|
||||
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
||||
enabled=True
|
||||
)
|
||||
|
||||
linkedin_config = LinkedInAddToProfileConfiguration.objects.create(company_identifier='1337', enabled=True)
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug='verified',
|
||||
mode_display_name='verified',
|
||||
expiration_datetime=datetime.now(pytz.UTC) - timedelta(days=1)
|
||||
)
|
||||
|
||||
self.course.certificate_available_date = datetime.now(pytz.UTC) - timedelta(days=1)
|
||||
CourseEnrollment.enroll(self.user, self.course.id, mode='honor')
|
||||
|
||||
self.course.certificate_available_date = datetime.now(pytz.UTC) - timedelta(days=1)
|
||||
self.course.start = datetime.now(pytz.UTC) - timedelta(days=2)
|
||||
self.course.end = datetime.now(pytz.UTC) - timedelta(days=1)
|
||||
self.course.display_name = u"Omega"
|
||||
self.course.display_name = 'Omega'
|
||||
self.course = self.update_course(self.course, self.user.id)
|
||||
|
||||
download_url = 'www.edx.org'
|
||||
GeneratedCertificateFactory.create(
|
||||
cert = GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='honor',
|
||||
grade='67',
|
||||
download_url=download_url
|
||||
download_url='https://www.edx.org'
|
||||
)
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Add Certificate to LinkedIn')
|
||||
|
||||
expected_url = (
|
||||
u'http://www.linkedin.com/profile/add'
|
||||
u'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
|
||||
u'pfCertificationName={platform}+Honor+Code+Certificate+for+Omega&'
|
||||
u'pfCertificationUrl=www.edx.org&'
|
||||
u'source=o'
|
||||
).format(platform=quote(settings.PLATFORM_NAME.encode('utf-8')))
|
||||
# We can switch to this and the commented out assertContains once edx-platform reaches Python 3.8
|
||||
# expected_url = (
|
||||
# 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&'
|
||||
# 'name={platform}+Honor+Code+Certificate+for+Omega&certUrl={cert_url}&'
|
||||
# 'organizationId={company_identifier}'
|
||||
# ).format(
|
||||
# platform=quote(settings.PLATFORM_NAME.encode('utf-8')),
|
||||
# cert_url=quote(cert.download_url, safe=''),
|
||||
# company_identifier=linkedin_config.company_identifier,
|
||||
# )
|
||||
|
||||
self.assertContains(response, escape(expected_url))
|
||||
# self.assertContains(response, escape(expected_url))
|
||||
|
||||
# These can be removed (in favor of the above) once we are on Python 3.8. Fails in 3.5 because of dict ordering
|
||||
self.assertContains(response, escape('https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME'))
|
||||
self.assertContains(response, escape('&name={platform}+Honor+Code+Certificate+for+Omega'.format(
|
||||
platform=quote(settings.PLATFORM_NAME.encode('utf-8'))
|
||||
)))
|
||||
self.assertContains(response, escape('&certUrl={cert_url}'.format(cert_url=quote(cert.download_url, safe=''))))
|
||||
self.assertContains(response, escape('&organizationId={company_identifier}'.format(
|
||||
company_identifier=linkedin_config.company_identifier
|
||||
)))
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@@ -649,7 +658,7 @@ class EnrollmentEventTestMixin(EventTestMixin):
|
||||
self.mock_tracker.emit.assert_called_once_with(
|
||||
'edx.course.enrollment.mode_changed',
|
||||
{
|
||||
'course_id': text_type(course_key),
|
||||
'course_id': str(course_key),
|
||||
'user_id': user.pk,
|
||||
'mode': mode
|
||||
}
|
||||
@@ -661,7 +670,7 @@ class EnrollmentEventTestMixin(EventTestMixin):
|
||||
self.mock_tracker.emit.assert_called_once_with(
|
||||
'edx.course.enrollment.activated',
|
||||
{
|
||||
'course_id': text_type(course_key),
|
||||
'course_id': str(course_key),
|
||||
'user_id': user.pk,
|
||||
'mode': CourseMode.DEFAULT_MODE_SLUG
|
||||
}
|
||||
@@ -673,7 +682,7 @@ class EnrollmentEventTestMixin(EventTestMixin):
|
||||
self.mock_tracker.emit.assert_called_once_with(
|
||||
'edx.course.enrollment.deactivated',
|
||||
{
|
||||
'course_id': text_type(course_key),
|
||||
'course_id': str(course_key),
|
||||
'user_id': user.pk,
|
||||
'mode': CourseMode.DEFAULT_MODE_SLUG
|
||||
}
|
||||
@@ -880,7 +889,7 @@ class ChangeEnrollmentViewTest(ModuleStoreTestCase):
|
||||
""" Enroll a student in a course. """
|
||||
response = self.client.post(
|
||||
reverse('change_enrollment'), {
|
||||
'course_id': text_type(course.id),
|
||||
'course_id': course.id,
|
||||
'enrollment_action': 'enroll'
|
||||
}
|
||||
)
|
||||
@@ -1020,7 +1029,7 @@ class RelatedProgramsTests(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
|
||||
self.create_programs_config()
|
||||
self.client.login(username=self.user.username, password=self.password)
|
||||
|
||||
course_run = CourseRunFactory(key=six.text_type(self.course.id)) # pylint: disable=no-member
|
||||
course_run = CourseRunFactory(key=str(self.course.id)) # pylint: disable=no-member
|
||||
course = CatalogCourseFactory(course_runs=[course_run])
|
||||
self.programs = [ProgramFactory(courses=[course]) for __ in range(2)]
|
||||
|
||||
@@ -1094,4 +1103,4 @@ class UserAttributeTests(TestCase):
|
||||
def test_unicode(self):
|
||||
UserAttribute.set_user_attribute(self.user, self.name, self.value)
|
||||
for field in (self.name, self.value, self.user.username):
|
||||
self.assertIn(field, six.text_type(UserAttribute.objects.get(user=self.user)))
|
||||
self.assertIn(field, str(UserAttribute.objects.get(user=self.user)))
|
||||
|
||||
@@ -87,5 +87,4 @@ class LinkedInAddToProfileConfigurationFactory(DjangoModelFactory):
|
||||
model = LinkedInAddToProfileConfiguration
|
||||
|
||||
enabled = True
|
||||
company_identifier = "0_0dPSPyS070e0HsE9HNz_13_d11_"
|
||||
trk_partner_name = 'unittest'
|
||||
company_identifier = "1337"
|
||||
|
||||
@@ -14,8 +14,7 @@ from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from mock import patch
|
||||
from six.moves import range
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.badges.events.course_complete import get_completion_badge
|
||||
@@ -97,7 +96,7 @@ class CommonCertificatesTestCase(ModuleStoreTestCase):
|
||||
self.user.profile.save()
|
||||
self.client.login(username=self.user.username, password='foo')
|
||||
self.request = RequestFactory().request()
|
||||
self.linkedin_url = u'http://www.linkedin.com/profile/add?{params}'
|
||||
self.linkedin_url = 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&{params}'
|
||||
|
||||
self.cert = GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
@@ -264,14 +263,17 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase)
|
||||
test_url = get_certificate_url(course_id=self.course.id, uuid=self.cert.verify_uuid)
|
||||
response = self.client.get(test_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
params = OrderedDict([
|
||||
('_ed', '0_0dPSPyS070e0HsE9HNz_13_d11_',),
|
||||
('pfCertificationName', u'{platform_name} Honor Code Certificate for {course_name}'.format(
|
||||
platform_name=settings.PLATFORM_NAME,
|
||||
course_name=self.course.display_name,
|
||||
).encode('utf-8'),),
|
||||
('pfCertificationUrl', self.request.build_absolute_uri(test_url),),
|
||||
])
|
||||
params = {
|
||||
'name': '{platform_name} Honor Code Certificate for {course_name}'.format(
|
||||
platform_name=settings.PLATFORM_NAME, course_name=self.course.display_name,
|
||||
).encode('utf-8'),
|
||||
'certUrl': self.request.build_absolute_uri(test_url),
|
||||
# default value from the LinkedInAddToProfileConfigurationFactory company_identifier
|
||||
'organizationId': 1337,
|
||||
'certId': self.cert.verify_uuid,
|
||||
'issueYear': self.cert.created_date.year,
|
||||
'issueMonth': self.cert.created_date.month,
|
||||
}
|
||||
self.assertContains(
|
||||
response,
|
||||
js_escaped_string(self.linkedin_url.format(params=urlencode(params))),
|
||||
@@ -280,7 +282,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase)
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
@with_site_configuration(
|
||||
configuration={
|
||||
'platform_name': 'My Platform Site', 'LINKEDIN_COMPANY_ID': 'test_linkedin_my_site',
|
||||
'platform_name': 'My Platform Site', 'LINKEDIN_COMPANY_ID': 2448,
|
||||
},
|
||||
)
|
||||
def test_linkedin_share_url_site(self):
|
||||
@@ -292,13 +294,16 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase)
|
||||
response = self.client.get(test_url, HTTP_HOST='test.localhost')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# the linkedIn share URL with appropriate parameters should be present
|
||||
params = OrderedDict([
|
||||
('_ed', 'test_linkedin_my_site',),
|
||||
('pfCertificationName', u'My Platform Site Honor Code Certificate for {course_name}'.format(
|
||||
params = {
|
||||
'name': 'My Platform Site Honor Code Certificate for {course_name}'.format(
|
||||
course_name=self.course.display_name,
|
||||
).encode('utf-8'),),
|
||||
('pfCertificationUrl', 'http://test.localhost' + test_url,),
|
||||
])
|
||||
).encode('utf-8'),
|
||||
'certUrl': 'http://test.localhost' + test_url,
|
||||
'organizationId': 2448,
|
||||
'certId': self.cert.verify_uuid,
|
||||
'issueYear': self.cert.created_date.year,
|
||||
'issueMonth': self.cert.created_date.month,
|
||||
}
|
||||
self.assertContains(
|
||||
response,
|
||||
js_escaped_string(self.linkedin_url.format(params=urlencode(params))),
|
||||
|
||||
@@ -291,13 +291,9 @@ def _update_social_context(request, context, course, user, user_certificate, pla
|
||||
# Clicking this button sends the user to LinkedIn where they
|
||||
# can add the certificate information to their profile.
|
||||
linkedin_config = LinkedInAddToProfileConfiguration.current()
|
||||
linkedin_share_enabled = share_settings.get('CERTIFICATE_LINKEDIN', linkedin_config.enabled)
|
||||
if linkedin_share_enabled:
|
||||
if linkedin_config.is_enabled():
|
||||
context['linked_in_url'] = linkedin_config.add_to_profile_url(
|
||||
course.id,
|
||||
course.display_name,
|
||||
user_certificate.mode,
|
||||
smart_str(share_url)
|
||||
course.display_name, user_certificate.mode, smart_str(share_url), certificate=user_certificate
|
||||
)
|
||||
|
||||
|
||||
@@ -348,7 +344,8 @@ def _get_user_certificate(request, user, course_key, course, preview_mode=None):
|
||||
user_certificate = GeneratedCertificate(
|
||||
mode=preview_mode,
|
||||
verify_uuid=six.text_type(uuid4().hex),
|
||||
modified_date=modified_date
|
||||
modified_date=modified_date,
|
||||
created_date=datetime.now().date(),
|
||||
)
|
||||
elif certificates_viewable_for_course(course):
|
||||
# certificate is being viewed by learner or public
|
||||
|
||||
@@ -95,6 +95,7 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract-
|
||||
course_exit_page_is_active = serializers.BooleanField()
|
||||
certificate_data = CertificateDataSerializer()
|
||||
verify_identity_url = AbsoluteURLField()
|
||||
linkedin_add_to_profile_url = serializers.URLField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -3,13 +3,19 @@ Tests for courseware API
|
||||
"""
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
|
||||
from lms.djangoapps.certificates.api import get_certificate_url
|
||||
from lms.djangoapps.certificates.tests.factories import (
|
||||
GeneratedCertificateFactory, LinkedInAddToProfileConfigurationFactory
|
||||
)
|
||||
from lms.djangoapps.courseware.access_utils import ACCESS_DENIED, ACCESS_GRANTED
|
||||
from lms.djangoapps.courseware.tabs import ExternalLinkCourseTab
|
||||
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
|
||||
@@ -73,6 +79,7 @@ class CourseApiTestViews(BaseCoursewareTests):
|
||||
ExternalLinkCourseTab.load('external_link', name='Hidden', link='http://hidden.com', is_hidden=True)
|
||||
)
|
||||
cls.store.update_item(cls.course, cls.user.id)
|
||||
LinkedInAddToProfileConfigurationFactory.create()
|
||||
|
||||
@ddt.data(
|
||||
(True, None, ACCESS_DENIED),
|
||||
@@ -82,6 +89,7 @@ class CourseApiTestViews(BaseCoursewareTests):
|
||||
(False, None, ACCESS_GRANTED),
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
|
||||
@mock.patch('openedx.core.djangoapps.courseware_api.views.CoursewareMeta.is_microfrontend_enabled_for_user')
|
||||
def test_course_metadata(self, logged_in, enrollment_mode, enable_anonymous, is_microfrontend_enabled_for_user):
|
||||
is_microfrontend_enabled_for_user.return_value = True
|
||||
@@ -92,6 +100,14 @@ class CourseApiTestViews(BaseCoursewareTests):
|
||||
self.client.logout()
|
||||
if enrollment_mode:
|
||||
CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode)
|
||||
if enrollment_mode == 'verified':
|
||||
cert = GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
status='downloadable',
|
||||
mode='verified',
|
||||
)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
if enrollment_mode:
|
||||
@@ -114,12 +130,32 @@ class CourseApiTestViews(BaseCoursewareTests):
|
||||
'The audit track does not include a certificate.')
|
||||
assert response.data['certificate_data']['msg'] == expected_audit_message
|
||||
assert response.data['verify_identity_url'] is None
|
||||
assert response.data['linkedin_add_to_profile_url'] is None
|
||||
else:
|
||||
# Not testing certificate data for verified learner here. That is tested elsewhere
|
||||
assert response.data['certificate_data'] is None
|
||||
assert response.data['certificate_data']['cert_status'] == 'earned_but_not_available'
|
||||
expected_verify_identity_url = reverse('verify_student_verify_now', args=[self.course.id])
|
||||
# The response contains an absolute URL so this is only checking the path of the final
|
||||
assert expected_verify_identity_url in response.data['verify_identity_url']
|
||||
|
||||
request = RequestFactory().request()
|
||||
cert_url = get_certificate_url(course_id=self.course.id, uuid=cert.verify_uuid)
|
||||
linkedin_url_params = {
|
||||
'name': '{platform_name} Verified Certificate for {course_name}'.format(
|
||||
platform_name=settings.PLATFORM_NAME, course_name=self.course.display_name,
|
||||
),
|
||||
'certUrl': request.build_absolute_uri(cert_url),
|
||||
# default value from the LinkedInAddToProfileConfigurationFactory company_identifier
|
||||
'organizationId': 1337,
|
||||
'certId': cert.verify_uuid,
|
||||
'issueYear': cert.created_date.year,
|
||||
'issueMonth': cert.created_date.month,
|
||||
}
|
||||
expected_linkedin_url = (
|
||||
'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&{params}'.format(
|
||||
params=urlencode(linkedin_url_params)
|
||||
)
|
||||
)
|
||||
assert response.data['linkedin_add_to_profile_url'] == expected_linkedin_url
|
||||
elif enable_anonymous and not logged_in:
|
||||
# multiple checks use this handler
|
||||
check_public_access.assert_called()
|
||||
|
||||
@@ -21,6 +21,8 @@ from rest_framework.views import APIView
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.edxnotes.helpers import is_feature_enabled
|
||||
from lms.djangoapps.certificates.api import get_certificate_url
|
||||
from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from lms.djangoapps.course_api.api import course_detail
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.access_response import (
|
||||
@@ -41,7 +43,7 @@ from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
from openedx.features.course_duration_limits.access import generate_course_expired_message
|
||||
from openedx.features.discounts.utils import generate_offer_html
|
||||
from student.models import CourseEnrollment, CourseEnrollmentCelebration
|
||||
from student.models import CourseEnrollment, CourseEnrollmentCelebration, LinkedInAddToProfileConfiguration
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
|
||||
@@ -53,15 +55,16 @@ class CoursewareMeta:
|
||||
Encapsulates courseware and enrollment metadata.
|
||||
"""
|
||||
def __init__(self, course_key, request, username=''):
|
||||
self.request = request
|
||||
self.overview = course_detail(
|
||||
request,
|
||||
username or request.user.username,
|
||||
self.request,
|
||||
username or self.request.user.username,
|
||||
course_key,
|
||||
)
|
||||
self.original_user_is_staff = has_access(request.user, 'staff', self.overview).has_access
|
||||
self.original_user_is_staff = has_access(self.request.user, 'staff', self.overview).has_access
|
||||
self.course_key = course_key
|
||||
self.course_masquerade, self.effective_user = setup_masquerade(
|
||||
request,
|
||||
self.request,
|
||||
course_key,
|
||||
staff_access=self.original_user_is_staff,
|
||||
)
|
||||
@@ -244,6 +247,32 @@ class CoursewareMeta:
|
||||
else:
|
||||
return IDVerificationService.get_verify_location('verify_student_verify_now', self.course_key)
|
||||
|
||||
@property
|
||||
def linkedin_add_to_profile_url(self):
|
||||
"""
|
||||
Returns a URL to add a certificate to a LinkedIn profile (will autofill fields).
|
||||
|
||||
Requires LinkedIn sharing to be enabled, either via a site configuration or a
|
||||
LinkedInAddToProfileConfiguration object being enabled.
|
||||
"""
|
||||
if self.effective_user.is_anonymous:
|
||||
return
|
||||
|
||||
linkedin_config = LinkedInAddToProfileConfiguration.current()
|
||||
if linkedin_config.is_enabled():
|
||||
try:
|
||||
user_certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
user=self.effective_user, course_id=self.course_key
|
||||
)
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
return
|
||||
cert_url = self.request.build_absolute_uri(
|
||||
get_certificate_url(course_id=self.course_key, uuid=user_certificate.verify_uuid)
|
||||
)
|
||||
return linkedin_config.add_to_profile_url(
|
||||
self.overview.display_name, user_certificate.mode, cert_url, certificate=user_certificate,
|
||||
)
|
||||
|
||||
|
||||
class CoursewareInformation(RetrieveAPIView):
|
||||
"""
|
||||
@@ -294,6 +323,7 @@ class CoursewareInformation(RetrieveAPIView):
|
||||
* certificate_data: data regarding the effective user's certificate for the given course
|
||||
* verify_identity_url: URL for a learner to verify their identity. Only returned for learners enrolled in a
|
||||
verified mode. Will update to reverify URL if necessary.
|
||||
* linkedin_add_to_profile_url: URL to add the effective user's certificate to a LinkedIn Profile.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ from django.http import HttpResponse
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from freezegun import freeze_time
|
||||
from mock import patch
|
||||
from six.moves import range
|
||||
|
||||
from openedx.core.djangoapps.password_policy.compliance import (
|
||||
NonCompliantPasswordException,
|
||||
|
||||
Reference in New Issue
Block a user