Commit 57f1dc92 by Zia Fazal

ziafazal/SOL-1021: Capture additional certificate configuration events

* Changed certificate_id to configuration_id
* Fix for acceptance test faiiure
* Add waits to bok choy tests

fix for flaky test
parent d494fe22
...@@ -33,6 +33,7 @@ from django.views.decorators.http import require_http_methods ...@@ -33,6 +33,7 @@ from django.views.decorators.http import require_http_methods
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from opaque_keys.edx.keys import CourseKey, AssetKey from opaque_keys.edx.keys import CourseKey, AssetKey
from eventtracking import tracker
from student.auth import has_studio_write_access from student.auth import has_studio_write_access
from util.db import generate_int_id, MYSQL_MAX_INT from util.db import generate_int_id, MYSQL_MAX_INT
from util.json_request import JsonResponse from util.json_request import JsonResponse
...@@ -247,6 +248,20 @@ class CertificateManager(object): ...@@ -247,6 +248,20 @@ class CertificateManager(object):
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
break break
@staticmethod
def track_event(event_name, event_data):
"""Track certificate configuration event.
Arguments:
event_name (str): Name of the event to be logged.
event_data (dict): A Dictionary containing event data
Returns:
None
"""
event_name = '.'.join(['edx', 'certificate', 'configuration', event_name])
tracker.emit(event_name, event_data)
class Certificate(object): class Certificate(object):
""" """
...@@ -296,6 +311,10 @@ def certificate_activation_handler(request, course_key_string): ...@@ -296,6 +311,10 @@ def certificate_activation_handler(request, course_key_string):
break break
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
cert_event_type = 'activated' if is_active else 'deactivated'
CertificateManager.track_event(cert_event_type, {
'course_id': unicode(course.id),
})
return HttpResponse(status=200) return HttpResponse(status=200)
...@@ -375,6 +394,10 @@ def certificates_list_handler(request, course_key_string): ...@@ -375,6 +394,10 @@ def certificates_list_handler(request, course_key_string):
kwargs={'certificate_id': new_certificate.id} # pylint: disable=no-member kwargs={'certificate_id': new_certificate.id} # pylint: disable=no-member
) )
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
CertificateManager.track_event('created', {
'course_id': unicode(course.id),
'configuration_id': new_certificate.id
})
course = _get_course_and_check_access(course_key, request.user) course = _get_course_and_check_access(course_key, request.user)
return response return response
else: else:
...@@ -414,12 +437,18 @@ def certificates_detail_handler(request, course_key_string, certificate_id): ...@@ -414,12 +437,18 @@ def certificates_detail_handler(request, course_key_string, certificate_id):
return JsonResponse({"error": err.message}, status=400) return JsonResponse({"error": err.message}, status=400)
serialized_certificate = CertificateManager.serialize_certificate(new_certificate) serialized_certificate = CertificateManager.serialize_certificate(new_certificate)
cert_event_type = 'created'
if match_cert: if match_cert:
cert_event_type = 'modified'
certificates_list[match_index] = serialized_certificate certificates_list[match_index] = serialized_certificate
else: else:
certificates_list.append(serialized_certificate) certificates_list.append(serialized_certificate)
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
CertificateManager.track_event(cert_event_type, {
'course_id': unicode(course.id),
'configuration_id': serialized_certificate["id"]
})
return JsonResponse(serialized_certificate, status=201) return JsonResponse(serialized_certificate, status=201)
elif request.method == "DELETE": elif request.method == "DELETE":
...@@ -431,6 +460,10 @@ def certificates_detail_handler(request, course_key_string, certificate_id): ...@@ -431,6 +460,10 @@ def certificates_detail_handler(request, course_key_string, certificate_id):
course=course, course=course,
certificate_id=certificate_id certificate_id=certificate_id
) )
CertificateManager.track_event('deleted', {
'course_id': unicode(course.id),
'configuration_id': certificate_id
})
return JsonResponse(status=204) return JsonResponse(status=204)
......
...@@ -24,6 +24,7 @@ from student.tests.factories import UserFactory ...@@ -24,6 +24,7 @@ from student.tests.factories import UserFactory
from contentstore.views.certificates import CertificateManager from contentstore.views.certificates import CertificateManager
from django.test.utils import override_settings from django.test.utils import override_settings
from contentstore.utils import get_lms_link_for_certificate_web_view from contentstore.utils import get_lms_link_for_certificate_web_view
from util.testing import EventTestMixin
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
...@@ -194,7 +195,7 @@ class CertificatesBaseTestCase(object): ...@@ -194,7 +195,7 @@ class CertificatesBaseTestCase(object):
# pylint: disable=no-member # pylint: disable=no-member
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase, HelperMethods): class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods):
""" """
Test cases for certificates_list_handler. Test cases for certificates_list_handler.
""" """
...@@ -202,7 +203,7 @@ class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase, ...@@ -202,7 +203,7 @@ class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase,
""" """
Set up CertificatesListHandlerTestCase. Set up CertificatesListHandlerTestCase.
""" """
super(CertificatesListHandlerTestCase, self).setUp() super(CertificatesListHandlerTestCase, self).setUp('contentstore.views.certificates.tracker')
def _url(self): def _url(self):
""" """
...@@ -229,8 +230,13 @@ class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase, ...@@ -229,8 +230,13 @@ class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase,
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertIn("Location", response) self.assertIn("Location", response)
content = json.loads(response.content) content = json.loads(response.content)
self._remove_ids(content) # pylint: disable=unused-variable certificate_id = self._remove_ids(content) # pylint: disable=unused-variable
self.assertEqual(content, expected) self.assertEqual(content, expected)
self.assert_event_emitted(
'edx.certificate.configuration.created',
course_id=unicode(self.course.id),
configuration_id=certificate_id,
)
def test_cannot_create_certificate_if_user_has_no_write_permissions(self): def test_cannot_create_certificate_if_user_has_no_write_permissions(self):
""" """
...@@ -347,13 +353,19 @@ class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase, ...@@ -347,13 +353,19 @@ class CertificatesListHandlerTestCase(CourseTestCase, CertificatesBaseTestCase,
@ddt.ddt @ddt.ddt
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase, HelperMethods): class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods):
""" """
Test cases for CertificatesDetailHandlerTestCase. Test cases for CertificatesDetailHandlerTestCase.
""" """
_id = 0 _id = 0
def setUp(self): # pylint: disable=arguments-differ
"""
Set up CertificatesDetailHandlerTestCase.
"""
super(CertificatesDetailHandlerTestCase, self).setUp('contentstore.views.certificates.tracker')
def _url(self, cid=-1): def _url(self, cid=-1):
""" """
Return url for the handler. Return url for the handler.
...@@ -388,6 +400,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase ...@@ -388,6 +400,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase
) )
content = json.loads(response.content) content = json.loads(response.content)
self.assertEqual(content, expected) self.assertEqual(content, expected)
self.assert_event_emitted(
'edx.certificate.configuration.created',
course_id=unicode(self.course.id),
configuration_id=666,
)
def test_can_edit_certificate(self): def test_can_edit_certificate(self):
""" """
...@@ -415,6 +432,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase ...@@ -415,6 +432,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase
) )
content = json.loads(response.content) content = json.loads(response.content)
self.assertEqual(content, expected) self.assertEqual(content, expected)
self.assert_event_emitted(
'edx.certificate.configuration.modified',
course_id=unicode(self.course.id),
configuration_id=1,
)
self.reload_course() self.reload_course()
# Verify that certificate is properly updated in the course. # Verify that certificate is properly updated in the course.
...@@ -440,6 +462,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase ...@@ -440,6 +462,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase
HTTP_X_REQUESTED_WITH="XMLHttpRequest", HTTP_X_REQUESTED_WITH="XMLHttpRequest",
) )
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assert_event_emitted(
'edx.certificate.configuration.deleted',
course_id=unicode(self.course.id),
configuration_id='1',
)
self.reload_course() self.reload_course()
# Verify that certificates are properly updated in the course. # Verify that certificates are properly updated in the course.
certificates = self.course.certificates['certificates'] certificates = self.course.certificates['certificates']
...@@ -553,6 +580,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase ...@@ -553,6 +580,11 @@ class CertificatesDetailHandlerTestCase(CourseTestCase, CertificatesBaseTestCase
course = self.store.get_course(self.course.id) course = self.store.get_course(self.course.id)
certificates = course.certificates['certificates'] certificates = course.certificates['certificates']
self.assertEqual(certificates[0].get('is_active'), is_active) self.assertEqual(certificates[0].get('is_active'), is_active)
cert_event_type = 'activated' if is_active else 'deactivated'
self.assert_event_emitted(
'.'.join(['edx.certificate.configuration', cert_event_type]),
course_id=unicode(self.course.id),
)
@ddt.data(True, False) @ddt.data(True, False)
def test_certificate_activation_without_write_permissions(self, activate): def test_certificate_activation_without_write_permissions(self, activate):
......
...@@ -79,8 +79,18 @@ class CertificatesPage(CoursePage): ...@@ -79,8 +79,18 @@ class CertificatesPage(CoursePage):
We can't use confirm_prompt because its wait_for_notification is flaky when asynchronous operation We can't use confirm_prompt because its wait_for_notification is flaky when asynchronous operation
completed very quickly. completed very quickly.
""" """
self.wait_for_element_visibility('.prompt', 'Prompt is visible') EmptyPromise(
self.wait_for_element_visibility('.prompt .action-primary', 'Confirmation button is visible') lambda: self.q(css='.prompt').present,
'Confirmation prompt is displayed'
).fulfill()
EmptyPromise(
lambda: self.q(css='.prompt .action-primary').present,
'Primary button is displayed'
).fulfill()
EmptyPromise(
lambda: self.q(css='.prompt .action-primary').visible,
'Primary button is visible'
).fulfill()
def wait_for_first_certificate_button(self): def wait_for_first_certificate_button(self):
""" """
...@@ -108,17 +118,23 @@ class CertificatesPage(CoursePage): ...@@ -108,17 +118,23 @@ class CertificatesPage(CoursePage):
""" """
Clicks the 'Create your first certificate' button, which is only displayed at zero state Clicks the 'Create your first certificate' button, which is only displayed at zero state
""" """
self.wait_for_first_certificate_button()
self.q(css=self.certficate_css + " .new-button").first.click() self.q(css=self.certficate_css + " .new-button").first.click()
def click_add_certificate_button(self): def click_add_certificate_button(self):
""" """
Clicks the 'Add new certificate' button, which is displayed when certificates already exist Clicks the 'Add new certificate' button, which is displayed when certificates already exist
""" """
self.wait_for_add_certificate_button()
self.q(css=self.certficate_css + " .action-add").first.click() self.q(css=self.certficate_css + " .action-add").first.click()
################ def click_confirmation_prompt_primary_button(self):
# Workflows """
################ Clicks the main action presented by the prompt (such as 'Delete')
"""
self.wait_for_confirmation_prompt()
self.q(css='a.button.action-primary').first.click()
self.wait_for_ajax()
class Certificate(object): class Certificate(object):
...@@ -240,13 +256,19 @@ class Certificate(object): ...@@ -240,13 +256,19 @@ class Certificate(object):
""" """
Returns whether or not the certificate delete icon is present. Returns whether or not the certificate delete icon is present.
""" """
return self.find_css('.actions .delete').present EmptyPromise(
lambda: self.find_css('.actions .delete').present,
'Certificate delete button is displayed'
).fulfill()
def wait_for_hide_details_toggle(self): def wait_for_hide_details_toggle(self):
""" """
Certificate details are expanded. Certificate details are expanded.
""" """
return self.find_css('a.detail-toggle.hide-details').present EmptyPromise(
lambda: self.find_css('a.detail-toggle.hide-details').present,
'Certificate details are expanded'
).fulfill()
################ ################
# Click Actions # Click Actions
...@@ -290,20 +312,12 @@ class Certificate(object): ...@@ -290,20 +312,12 @@ class Certificate(object):
""" """
self.find_css('a.detail-toggle').first.click() self.find_css('a.detail-toggle').first.click()
################ def click_delete_certificate_button(self):
# Workflows
################
def delete_certificate(self):
""" """
Delete the certificate Remove the first (possibly the only) certificate from the set
""" """
self.wait_for_certificate_delete_button() self.wait_for_certificate_delete_button()
self.find_css('.actions .delete').first.click() self.find_css('.actions .delete').first.click()
self.page.wait_for_confirmation_prompt()
self.page.q(css='a.button.action-primary').first.click()
self.page.q(css='a.button.action-primary').first.click()
self.page.wait_for_ajax() self.page.wait_for_ajax()
......
...@@ -126,9 +126,12 @@ class CertificatesTest(StudioCourseTest): ...@@ -126,9 +126,12 @@ class CertificatesTest(StudioCourseTest):
self.assertEqual(len(self.certificates_page.certificates), 1) self.assertEqual(len(self.certificates_page.certificates), 1)
# Delete certificate # Delete the certificate we just created
certificate.delete_certificate() certificate.click_delete_certificate_button()
self.certificates_page.click_confirmation_prompt_primary_button()
self.certificates_page.wait_for_first_certificate_button()
# Reload the page and confirm there are no certificates
self.certificates_page.visit() self.certificates_page.visit()
self.assertEqual(len(self.certificates_page.certificates), 0) self.assertEqual(len(self.certificates_page.certificates), 0)
......
...@@ -154,7 +154,11 @@ def set_cert_generation_enabled(course_key, is_enabled): ...@@ -154,7 +154,11 @@ def set_cert_generation_enabled(course_key, is_enabled):
""" """
CertificateGenerationCourseSetting.set_enabled_for_course(course_key, is_enabled) CertificateGenerationCourseSetting.set_enabled_for_course(course_key, is_enabled)
cert_event_type = 'enabled' if is_enabled else 'disabled'
event_name = '.'.join(['edx', 'certificate', 'generation', cert_event_type])
tracker.emit(event_name, {
'course_id': unicode(course_key),
})
if is_enabled: if is_enabled:
log.info(u"Enabled self-generated certificates for course '%s'.", unicode(course_key)) log.info(u"Enabled self-generated certificates for course '%s'.", unicode(course_key))
else: else:
......
...@@ -229,13 +229,13 @@ class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase): ...@@ -229,13 +229,13 @@ class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase):
@attr('shard_1') @attr('shard_1')
@ddt.ddt @ddt.ddt
class CertificateGenerationEnabledTest(TestCase): class CertificateGenerationEnabledTest(EventTestMixin, TestCase):
"""Test enabling/disabling self-generated certificates for a course. """ """Test enabling/disabling self-generated certificates for a course. """
COURSE_KEY = CourseLocator(org='test', course='test', run='test') COURSE_KEY = CourseLocator(org='test', course='test', run='test')
def setUp(self): def setUp(self):
super(CertificateGenerationEnabledTest, self).setUp() super(CertificateGenerationEnabledTest, self).setUp('certificates.api.tracker')
# Since model-based configuration is cached, we need # Since model-based configuration is cached, we need
# to clear the cache before each test. # to clear the cache before each test.
...@@ -256,6 +256,12 @@ class CertificateGenerationEnabledTest(TestCase): ...@@ -256,6 +256,12 @@ class CertificateGenerationEnabledTest(TestCase):
if is_course_enabled is not None: if is_course_enabled is not None:
certs_api.set_cert_generation_enabled(self.COURSE_KEY, is_course_enabled) certs_api.set_cert_generation_enabled(self.COURSE_KEY, is_course_enabled)
cert_event_type = 'enabled' if is_course_enabled else 'disabled'
event_name = '.'.join(['edx', 'certificate', 'generation', cert_event_type])
self.assert_event_emitted(
event_name,
course_id=unicode(self.COURSE_KEY),
)
self._assert_enabled_for_course(self.COURSE_KEY, expect_enabled) self._assert_enabled_for_course(self.COURSE_KEY, expect_enabled)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment