Merge branch 'master' into dj18-release-merge
This commit is contained in:
@@ -35,9 +35,6 @@ var CourseDetails = Backbone.Model.extend({
|
||||
if (newattrs.start_date === null) {
|
||||
errors.start_date = gettext("The course must have an assigned start date.");
|
||||
}
|
||||
if (this.hasChanged("start_date") && this.get("has_cert_config") === false){
|
||||
errors.start_date = gettext("The course must have at least one active certificate configuration before it can be started.");
|
||||
}
|
||||
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
|
||||
errors.end_date = gettext("The course end date must be later than the course start date.");
|
||||
}
|
||||
|
||||
@@ -72,13 +72,6 @@ define([
|
||||
);
|
||||
});
|
||||
|
||||
it('Changing course start date without active certificate configuration should result in error', function () {
|
||||
this.view.$el.find('#course-start-date')
|
||||
.val('10/06/2014')
|
||||
.trigger('change');
|
||||
expect(this.view.$el.find('span.message-error').text()).toContain("course must have at least one active certificate configuration");
|
||||
});
|
||||
|
||||
it('Selecting a course in pre-requisite drop down should save it as part of course details', function () {
|
||||
var pre_requisite_courses = ['test/CSS101/2012_T1'];
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
Tests the "preview" selector in the LMS that allows changing between Staff, Student, and Content Groups.
|
||||
"""
|
||||
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from ..helpers import UniqueCourseTest, create_user_partition_json
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
@@ -14,6 +17,7 @@ from xmodule.partitions.partitions import Group
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class StaffViewTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that verify the staff view.
|
||||
@@ -51,6 +55,7 @@ class StaffViewTest(UniqueCourseTest):
|
||||
return staff_page
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class CourseWithoutContentGroupsTest(StaffViewTest):
|
||||
"""
|
||||
Setup for tests that have no content restricted to specific content groups.
|
||||
@@ -81,6 +86,7 @@ class CourseWithoutContentGroupsTest(StaffViewTest):
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class StaffViewToggleTest(CourseWithoutContentGroupsTest):
|
||||
"""
|
||||
Tests for the staff view toggle button.
|
||||
@@ -97,6 +103,7 @@ class StaffViewToggleTest(CourseWithoutContentGroupsTest):
|
||||
self.assertFalse(course_page.has_tab('Instructor'))
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
"""
|
||||
Tests that verify the staff debug info.
|
||||
@@ -228,6 +235,7 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class CourseWithContentGroupsTest(StaffViewTest):
|
||||
"""
|
||||
Verifies that changing the "View this course as" selector works properly for content groups.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Acceptance tests for the Import and Export pages
|
||||
"""
|
||||
from nose.plugins.attrib import attr
|
||||
from datetime import datetime
|
||||
|
||||
from abc import abstractmethod
|
||||
@@ -33,6 +34,7 @@ class ExportTestMixin(object):
|
||||
self.assertTrue(is_tarball_mimetype)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestCourseExport(ExportTestMixin, StudioCourseTest):
|
||||
"""
|
||||
Export tests for courses.
|
||||
@@ -55,6 +57,7 @@ class TestCourseExport(ExportTestMixin, StudioCourseTest):
|
||||
self.assertEqual(self.export_page.header_text, 'Course Export')
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestLibraryExport(ExportTestMixin, StudioLibraryTest):
|
||||
"""
|
||||
Export tests for libraries.
|
||||
@@ -103,6 +106,7 @@ class BadExportMixin(object):
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestLibraryBadExport(BadExportMixin, StudioLibraryTest):
|
||||
"""
|
||||
Verify exporting a bad library causes an error.
|
||||
@@ -126,6 +130,7 @@ class TestLibraryBadExport(BadExportMixin, StudioLibraryTest):
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestCourseBadExport(BadExportMixin, StudioCourseTest):
|
||||
"""
|
||||
Verify exporting a bad course causes an error.
|
||||
@@ -157,6 +162,7 @@ class TestCourseBadExport(BadExportMixin, StudioCourseTest):
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class ImportTestMixin(object):
|
||||
"""
|
||||
Tests to run for both course and library import pages.
|
||||
@@ -271,6 +277,7 @@ class ImportTestMixin(object):
|
||||
self.import_page.wait_for_tasks(fail_on='Updating')
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest):
|
||||
"""
|
||||
Tests the Course import page
|
||||
@@ -316,6 +323,7 @@ class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest):
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestCourseImport(ImportTestMixin, StudioCourseTest):
|
||||
"""
|
||||
Tests the Course import page
|
||||
@@ -357,6 +365,7 @@ class TestCourseImport(ImportTestMixin, StudioCourseTest):
|
||||
self.assertEqual(self.import_page.header_text, 'Course Import')
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class TestLibraryImport(ImportTestMixin, StudioLibraryTest):
|
||||
"""
|
||||
Tests the Library import page
|
||||
|
||||
@@ -312,6 +312,7 @@ class EditContainerTest(NestedVerticalTest):
|
||||
self.assertEqual(component.student_content, "modified content")
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class EditVisibilityModalTest(ContainerBase):
|
||||
"""
|
||||
Tests of the visibility settings modal for components on the unit
|
||||
@@ -397,6 +398,7 @@ class EditVisibilityModalTest(ContainerBase):
|
||||
# Re-open the modal and inspect its selected inputs
|
||||
visibility_editor = self.edit_component_visibility(component)
|
||||
self.verify_selected_labels(visibility_editor, expected_labels)
|
||||
visibility_editor.save()
|
||||
|
||||
def verify_component_validation_error(self, component):
|
||||
"""
|
||||
@@ -427,14 +429,13 @@ class EditVisibilityModalTest(ContainerBase):
|
||||
self.browser.refresh()
|
||||
self.container_page.wait_for_page()
|
||||
|
||||
def remove_missing_groups(self, component):
|
||||
def remove_missing_groups(self, visibility_editor, component):
|
||||
"""
|
||||
Deselect the missing groups for a component. After save,
|
||||
verify that there are no missing group messages in the modal
|
||||
and that there is no validation error on the component.
|
||||
"""
|
||||
visibility_editor = self.edit_component_visibility(component)
|
||||
for option in self.edit_component_visibility(component).selected_options:
|
||||
for option in visibility_editor.selected_options:
|
||||
if option.text == self.MISSING_GROUP_LABEL:
|
||||
option.click()
|
||||
visibility_editor.save()
|
||||
@@ -541,7 +542,7 @@ class EditVisibilityModalTest(ContainerBase):
|
||||
self.verify_component_validation_error(self.html_component)
|
||||
visibility_editor = self.edit_component_visibility(self.html_component)
|
||||
self.verify_selected_labels(visibility_editor, [self.MISSING_GROUP_LABEL] * 2)
|
||||
self.remove_missing_groups(self.html_component)
|
||||
self.remove_missing_groups(visibility_editor, self.html_component)
|
||||
self.verify_visibility_set(self.html_component, False)
|
||||
|
||||
def test_found_and_missing_groups(self):
|
||||
@@ -565,7 +566,7 @@ class EditVisibilityModalTest(ContainerBase):
|
||||
self.verify_component_validation_error(self.html_component)
|
||||
visibility_editor = self.edit_component_visibility(self.html_component)
|
||||
self.verify_selected_labels(visibility_editor, ['Dogs', 'Cats'] + [self.MISSING_GROUP_LABEL] * 2)
|
||||
self.remove_missing_groups(self.html_component)
|
||||
self.remove_missing_groups(visibility_editor, self.html_component)
|
||||
visibility_editor = self.edit_component_visibility(self.html_component)
|
||||
self.verify_selected_labels(visibility_editor, ['Dogs', 'Cats'])
|
||||
self.verify_visibility_set(self.html_component, True)
|
||||
@@ -1041,6 +1042,7 @@ class UnitPublishingTest(ContainerBase):
|
||||
# self.assertEqual('discussion', self.courseware.xblock_component_type(1))
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class DisplayNameTest(ContainerBase):
|
||||
"""
|
||||
Test consistent use of display_name_with_default
|
||||
@@ -1077,6 +1079,7 @@ class DisplayNameTest(ContainerBase):
|
||||
self.assertEqual(container.name, title_on_unit_page)
|
||||
|
||||
|
||||
@attr('shard_3')
|
||||
class ProblemCategoryTabsTest(ContainerBase):
|
||||
"""
|
||||
Test to verify tabs in problem category.
|
||||
|
||||
@@ -1755,6 +1755,7 @@ class DeprecationWarningMessageTest(CourseOutlineTest):
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class SelfPacedOutlineTest(CourseOutlineTest):
|
||||
"""Test the course outline for a self-paced course."""
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Acceptance tests for Studio's Settings Details pages
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from nose.plugins.attrib import attr
|
||||
from unittest import skip
|
||||
|
||||
from .base_studio_test import StudioCourseTest
|
||||
@@ -18,6 +19,7 @@ from ..helpers import (
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class StudioSettingsDetailsTest(StudioCourseTest):
|
||||
"""Base class for settings and details page tests."""
|
||||
|
||||
@@ -35,6 +37,7 @@ class StudioSettingsDetailsTest(StudioCourseTest):
|
||||
self.assertTrue(self.settings_detail.is_browser_on_page())
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class SettingsMilestonesTest(StudioSettingsDetailsTest):
|
||||
"""
|
||||
Tests for milestones feature in Studio's settings tab
|
||||
@@ -201,6 +204,7 @@ class SettingsMilestonesTest(StudioSettingsDetailsTest):
|
||||
))
|
||||
|
||||
|
||||
@attr('shard_4')
|
||||
class CoursePacingTest(StudioSettingsDetailsTest):
|
||||
"""Tests for setting a course to self-paced."""
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ class BadgeAssertionFactory(DjangoModelFactory):
|
||||
model = BadgeAssertion
|
||||
|
||||
mode = 'honor'
|
||||
data = {
|
||||
'image': 'http://www.example.com/image.png',
|
||||
'json': {'id': 'http://www.example.com/assertion.json'},
|
||||
'issuer': 'http://www.example.com/issuer.json',
|
||||
}
|
||||
|
||||
|
||||
class BadgeImageConfigurationFactory(DjangoModelFactory):
|
||||
@@ -75,7 +80,8 @@ class CertificateHtmlViewConfigurationFactory(DjangoModelFactory):
|
||||
},
|
||||
"honor": {
|
||||
"certificate_type": "Honor Code",
|
||||
"certificate_title": "Certificate of Achievement"
|
||||
"certificate_title": "Certificate of Achievement",
|
||||
"logo_url": "http://www.edx.org/honor_logo.png"
|
||||
},
|
||||
"verified": {
|
||||
"certificate_type": "Verified",
|
||||
@@ -84,6 +90,13 @@ class CertificateHtmlViewConfigurationFactory(DjangoModelFactory):
|
||||
"xseries": {
|
||||
"certificate_title": "XSeries Certificate of Achievement",
|
||||
"certificate_type": "XSeries"
|
||||
},
|
||||
"microsites": {
|
||||
"testmicrosite": {
|
||||
"company_about_url": "http://www.testmicrosite.org/about-us",
|
||||
"company_privacy_url": "http://www.testmicrosite.org/edx-privacy-policy",
|
||||
"company_tos_url": "http://www.testmicrosite.org/edx-terms-service"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
@@ -187,16 +187,6 @@ class UpdateExampleCertificateViewTest(TestCase):
|
||||
self.assertEqual(content['return_code'], 0)
|
||||
|
||||
|
||||
def fakemicrosite(name, default=None):
|
||||
"""
|
||||
This is a test mocking function to return a microsite configuration
|
||||
"""
|
||||
if name == 'microsite_config_key':
|
||||
return 'test_microsite'
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -270,7 +260,6 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
self.course.save()
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
@patch("microsite_configuration.microsite.get_value", fakemicrosite)
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
def test_html_view_for_microsite(self):
|
||||
test_configuration_string = """{
|
||||
@@ -285,18 +274,20 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
"logo_src": "/static/certificates/images/logo-edx.svg",
|
||||
"logo_url": "http://www.edx.org"
|
||||
},
|
||||
"test_microsite": {
|
||||
"accomplishment_class_append": "accomplishment-certificate",
|
||||
"platform_name": "platform_microsite",
|
||||
"company_about_url": "http://www.microsite.org/about-us",
|
||||
"company_privacy_url": "http://www.microsite.org/edx-privacy-policy",
|
||||
"company_tos_url": "http://www.microsite.org/microsite-terms-service",
|
||||
"company_verified_certificate_url": "http://www.microsite.org/verified-certificate",
|
||||
"document_stylesheet_url_application": "/static/certificates/sass/main-ltr.css",
|
||||
"logo_src": "/static/certificates/images/logo-microsite.svg",
|
||||
"logo_url": "http://www.microsite.org",
|
||||
"company_about_description": "This is special microsite aware company_about_description content",
|
||||
"company_about_title": "Microsite title"
|
||||
"microsites": {
|
||||
"testmicrosite": {
|
||||
"accomplishment_class_append": "accomplishment-certificate",
|
||||
"platform_name": "platform_microsite",
|
||||
"company_about_url": "http://www.microsite.org/about-us",
|
||||
"company_privacy_url": "http://www.microsite.org/edx-privacy-policy",
|
||||
"company_tos_url": "http://www.microsite.org/microsite-terms-service",
|
||||
"company_verified_certificate_url": "http://www.microsite.org/verified-certificate",
|
||||
"document_stylesheet_url_application": "/static/certificates/sass/main-ltr.css",
|
||||
"logo_src": "/static/certificates/images/logo-microsite.svg",
|
||||
"logo_url": "http://www.microsite.org",
|
||||
"company_about_description": "This is special microsite aware company_about_description content",
|
||||
"company_about_title": "Microsite title"
|
||||
}
|
||||
},
|
||||
"honor": {
|
||||
"certificate_type": "Honor Code"
|
||||
@@ -310,13 +301,12 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
response = self.client.get(test_url)
|
||||
response = self.client.get(test_url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
|
||||
self.assertIn('platform_microsite', response.content)
|
||||
self.assertIn('http://www.microsite.org', response.content)
|
||||
self.assertIn('This is special microsite aware company_about_description content', response.content)
|
||||
self.assertIn('Microsite title', response.content)
|
||||
|
||||
@patch("microsite_configuration.microsite.get_value", fakemicrosite)
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
def test_html_view_microsite_configuration_missing(self):
|
||||
test_configuration_string = """{
|
||||
@@ -343,7 +333,7 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
response = self.client.get(test_url)
|
||||
response = self.client.get(test_url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
|
||||
self.assertIn('edX', response.content)
|
||||
self.assertNotIn('platform_microsite', response.content)
|
||||
self.assertNotIn('http://www.microsite.org', response.content)
|
||||
|
||||
@@ -23,7 +23,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from certificates.api import get_certificate_url
|
||||
from certificates.models import (
|
||||
GeneratedCertificate,
|
||||
BadgeAssertion,
|
||||
CertificateStatuses,
|
||||
CertificateSocialNetworks,
|
||||
CertificateTemplate,
|
||||
@@ -33,6 +32,7 @@ from certificates.models import (
|
||||
from certificates.tests.factories import (
|
||||
CertificateHtmlViewConfigurationFactory,
|
||||
LinkedInAddToProfileConfigurationFactory,
|
||||
BadgeAssertionFactory,
|
||||
)
|
||||
from util import organizations_helpers as organizations_api
|
||||
from django.test.client import RequestFactory
|
||||
@@ -221,6 +221,104 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
)
|
||||
self.assertIn('logo_test1.png', response.content)
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
@patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", {
|
||||
"CERTIFICATE_TWITTER": True,
|
||||
"CERTIFICATE_FACEBOOK": True,
|
||||
})
|
||||
def test_rendering_maximum_data(self):
|
||||
"""
|
||||
Tests at least one data item from different context update methods to
|
||||
make sure every context update method is invoked while rendering certificate template.
|
||||
"""
|
||||
long_org_name = 'Long org name'
|
||||
short_org_name = 'short_org_name'
|
||||
test_organization_data = {
|
||||
'name': long_org_name,
|
||||
'short_name': short_org_name,
|
||||
'description': 'Test Organization Description',
|
||||
'active': True,
|
||||
'logo': '/logo_test1.png'
|
||||
}
|
||||
test_org = organizations_api.add_organization(organization_data=test_organization_data)
|
||||
organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id))
|
||||
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
|
||||
BadgeAssertionFactory.create(
|
||||
user=self.user, course_id=self.course_id,
|
||||
)
|
||||
self.course.cert_html_view_overrides = {
|
||||
"logo_src": "/static/certificates/images/course_override_logo.png"
|
||||
}
|
||||
|
||||
self.course.save()
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
test_url = get_certificate_url(
|
||||
user_id=self.user.id,
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
response = self.client.get(test_url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
|
||||
|
||||
# Test an item from basic info
|
||||
self.assertIn(
|
||||
'Terms of Service & Honor Code',
|
||||
response.content
|
||||
)
|
||||
self.assertIn(
|
||||
'Certificate ID Number',
|
||||
response.content
|
||||
)
|
||||
# Test an item from html cert configuration
|
||||
self.assertIn(
|
||||
'<a class="logo" href="http://www.edx.org/honor_logo.png">',
|
||||
response.content
|
||||
)
|
||||
# Test an item from course info
|
||||
self.assertIn(
|
||||
'course_title_0',
|
||||
response.content
|
||||
)
|
||||
# Test an item from user info
|
||||
self.assertIn(
|
||||
"{fullname}, you've earned a certificate!".format(fullname=self.user.profile.name),
|
||||
response.content
|
||||
)
|
||||
# Test an item from social info
|
||||
self.assertIn(
|
||||
"Post on Facebook",
|
||||
response.content
|
||||
)
|
||||
self.assertIn(
|
||||
"Share on Twitter",
|
||||
response.content
|
||||
)
|
||||
# Test an item from certificate/org info
|
||||
self.assertIn(
|
||||
"a course of study offered by {partner_short_name}, "
|
||||
"an online learning initiative of {partner_long_name} "
|
||||
"through {platform_name}.".format(
|
||||
partner_short_name=short_org_name,
|
||||
partner_long_name=long_org_name,
|
||||
platform_name='Test Microsite'
|
||||
),
|
||||
response.content
|
||||
)
|
||||
# Test item from badge info
|
||||
self.assertIn(
|
||||
"Add to Mozilla Backpack",
|
||||
response.content
|
||||
)
|
||||
# Test item from microsite info
|
||||
self.assertIn(
|
||||
"http://www.testmicrosite.org/about-us",
|
||||
response.content
|
||||
)
|
||||
# Test course overrides
|
||||
self.assertIn(
|
||||
"/static/certificates/images/course_override_logo.png",
|
||||
response.content
|
||||
)
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
def test_render_html_view_valid_certificate(self):
|
||||
test_url = get_certificate_url(
|
||||
@@ -398,7 +496,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
response = self.client.get(test_url + '?preview=honor')
|
||||
#accessing certificate web view in preview mode without
|
||||
# accessing certificate web view in preview mode without
|
||||
# staff or instructor access should show invalid certificate
|
||||
self.assertIn('Cannot Find Certificate', response.content)
|
||||
|
||||
@@ -495,16 +593,9 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
test_url = '{}?evidence_visit=1'.format(cert_url)
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
self.recreate_tracker()
|
||||
assertion = BadgeAssertion(
|
||||
user=self.user, course_id=self.course_id, mode='honor',
|
||||
data={
|
||||
'image': 'http://www.example.com/image.png',
|
||||
'json': {'id': 'http://www.example.com/assertion.json'},
|
||||
'issuer': 'http://www.example.com/issuer.json',
|
||||
|
||||
}
|
||||
assertion = BadgeAssertionFactory.create(
|
||||
user=self.user, course_id=self.course_id,
|
||||
)
|
||||
assertion.save()
|
||||
response = self.client.get(test_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
assert_event_matches(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=bad-continuation
|
||||
"""
|
||||
Certificate HTML webview.
|
||||
"""
|
||||
@@ -26,6 +27,7 @@ from student.models import LinkedInAddToProfileConfiguration
|
||||
from util import organizations_helpers as organization_api
|
||||
from util.views import handle_500
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from certificates.api import (
|
||||
get_active_web_certificate,
|
||||
@@ -44,13 +46,6 @@ from certificates.models import (
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseDoesNotExist(Exception):
|
||||
"""
|
||||
This exception is raised in the case where None is returned from the modulestore
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def get_certificate_description(mode, certificate_type, platform_name):
|
||||
"""
|
||||
:return certificate_type_description on the basis of current mode
|
||||
@@ -81,63 +76,13 @@ def get_certificate_description(mode, certificate_type, platform_name):
|
||||
return certificate_type_description
|
||||
|
||||
|
||||
# pylint: disable=bad-continuation
|
||||
# pylint: disable=too-many-statements
|
||||
def _update_certificate_context(context, course, user, user_certificate):
|
||||
def _update_certificate_context(context, user_certificate, platform_name):
|
||||
"""
|
||||
Build up the certificate web view context using the provided values
|
||||
(Helper method to keep the view clean)
|
||||
"""
|
||||
# Populate dynamic output values using the course/certificate data loaded above
|
||||
user_fullname = user.profile.name
|
||||
platform_name = microsite.get_value("platform_name", settings.PLATFORM_NAME)
|
||||
certificate_type = context.get('certificate_type')
|
||||
partner_short_name = course.display_organization if course.display_organization else course.org
|
||||
partner_long_name = None
|
||||
organizations = organization_api.get_course_organizations(course_id=course.id)
|
||||
if organizations:
|
||||
#TODO Need to add support for multiple organizations, Currently we are interested in the first one.
|
||||
organization = organizations[0]
|
||||
partner_long_name = organization.get('name', partner_long_name)
|
||||
partner_short_name = organization.get('short_name', partner_short_name)
|
||||
context['organization_long_name'] = partner_long_name
|
||||
context['organization_short_name'] = partner_short_name
|
||||
context['organization_logo'] = organization.get('logo', None)
|
||||
|
||||
context['username'] = user.username
|
||||
context['course_mode'] = user_certificate.mode
|
||||
context['accomplishment_user_id'] = user.id
|
||||
context['accomplishment_copy_name'] = user_fullname
|
||||
context['accomplishment_copy_username'] = user.username
|
||||
context['accomplishment_copy_course_org'] = partner_short_name
|
||||
course_title_from_cert = context['certificate_data'].get('course_title', '')
|
||||
accomplishment_copy_course_name = course_title_from_cert if course_title_from_cert else course.display_name
|
||||
context['accomplishment_copy_course_name'] = accomplishment_copy_course_name
|
||||
share_settings = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {})
|
||||
context['facebook_share_enabled'] = share_settings.get('CERTIFICATE_FACEBOOK', False)
|
||||
context['facebook_app_id'] = getattr(settings, "FACEBOOK_APP_ID", None)
|
||||
context['facebook_share_text'] = share_settings.get(
|
||||
'CERTIFICATE_FACEBOOK_TEXT',
|
||||
_("I completed the {course_title} course on {platform_name}.").format(
|
||||
course_title=accomplishment_copy_course_name,
|
||||
platform_name=platform_name
|
||||
)
|
||||
)
|
||||
context['twitter_share_enabled'] = share_settings.get('CERTIFICATE_TWITTER', False)
|
||||
context['twitter_share_text'] = share_settings.get(
|
||||
'CERTIFICATE_TWITTER_TEXT',
|
||||
_("I completed a course on {platform_name}. Take a look at my certificate.").format(
|
||||
platform_name=platform_name
|
||||
)
|
||||
)
|
||||
|
||||
course_number = course.display_coursenumber if course.display_coursenumber else course.number
|
||||
context['course_number'] = course_number
|
||||
try:
|
||||
badge = BadgeAssertion.objects.get(user=user, course_id=course.location.course_key)
|
||||
except BadgeAssertion.DoesNotExist:
|
||||
badge = None
|
||||
context['badge'] = badge
|
||||
|
||||
# Override the defaults with any mode-specific static values
|
||||
context['certificate_id_number'] = user_certificate.verify_uuid
|
||||
@@ -154,39 +99,33 @@ def _update_certificate_context(context, course, user, user_certificate):
|
||||
year=user_certificate.modified_date.year
|
||||
)
|
||||
|
||||
if partner_long_name:
|
||||
context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, an '
|
||||
'online learning initiative of {partner_long_name} '
|
||||
'through {platform_name}.').format(
|
||||
partner_short_name=partner_short_name,
|
||||
partner_long_name=partner_long_name,
|
||||
platform_name=platform_name
|
||||
)
|
||||
else:
|
||||
context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, '
|
||||
'through {platform_name}.').format(
|
||||
partner_short_name=partner_short_name,
|
||||
platform_name=platform_name
|
||||
)
|
||||
# Translators: This text represents the verification of the certificate
|
||||
context['document_meta_description'] = _('This is a valid {platform_name} certificate for {user_name}, '
|
||||
'who participated in {partner_short_name} {course_number}').format(
|
||||
platform_name=platform_name,
|
||||
user_name=context['accomplishment_copy_name'],
|
||||
partner_short_name=context['organization_short_name'],
|
||||
course_number=context['course_number']
|
||||
)
|
||||
|
||||
# Translators: Accomplishments describe the awards/certifications obtained by students on this platform
|
||||
context['accomplishment_copy_about'] = _('About {platform_name} Accomplishments').format(
|
||||
# Translators: This text is bound to the HTML 'title' element of the page and appears in the browser title bar
|
||||
context['document_title'] = _("{partner_short_name} {course_number} Certificate | {platform_name}").format(
|
||||
partner_short_name=context['organization_short_name'],
|
||||
course_number=context['course_number'],
|
||||
platform_name=platform_name
|
||||
)
|
||||
|
||||
context['accomplishment_more_title'] = _("More Information About {user_name}'s Certificate:").format(
|
||||
user_name=user_fullname
|
||||
)
|
||||
# Translators: This text fragment appears after the student's name (displayed in a large font) on the certificate
|
||||
# screen. The text describes the accomplishment represented by the certificate information displayed to the user
|
||||
context['accomplishment_copy_description_full'] = _("successfully completed, received a passing grade, and was "
|
||||
"awarded a {platform_name} {certificate_type} "
|
||||
"Certificate of Completion in ").format(
|
||||
platform_name=platform_name,
|
||||
certificate_type=context.get("certificate_type"))
|
||||
|
||||
# Translators: This line appears on the page just before the generation date for the certificate
|
||||
context['certificate_date_issued_title'] = _("Issued On:")
|
||||
|
||||
# Translators: The Certificate ID Number is an alphanumeric value unique to each individual certificate
|
||||
context['certificate_id_number_title'] = _('Certificate ID Number')
|
||||
|
||||
context['certificate_info_title'] = _('About {platform_name} Certificates').format(
|
||||
platform_name=platform_name
|
||||
)
|
||||
certificate_type_description = get_certificate_description(user_certificate.mode, certificate_type, platform_name)
|
||||
if certificate_type_description:
|
||||
context['certificate_type_description'] = certificate_type_description
|
||||
|
||||
# Translators: This text describes the purpose (and therefore, value) of a course certificate
|
||||
# 'verifying your identity' refers to the process for establishing the authenticity of the student
|
||||
@@ -197,7 +136,54 @@ def _update_certificate_context(context, course, user, user_certificate):
|
||||
"<a href='{verified_cert_url}'> verifying your identity</a>.").format(
|
||||
platform_name=platform_name,
|
||||
tos_url=context.get('company_tos_url'),
|
||||
verified_cert_url=context.get('company_verified_certificate_url')
|
||||
verified_cert_url=context.get('company_verified_certificate_url'))
|
||||
|
||||
|
||||
def _update_context_with_basic_info(context, course_id, platform_name, configuration):
|
||||
"""
|
||||
Updates context dictionary with basic info required before rendering simplest
|
||||
certificate templates.
|
||||
"""
|
||||
context['platform_name'] = platform_name
|
||||
context['course_id'] = course_id
|
||||
|
||||
# Update the view context with the default ConfigurationModel settings
|
||||
context.update(configuration.get('default', {}))
|
||||
|
||||
# Translators: 'All rights reserved' is a legal term used in copyrighting to protect published content
|
||||
reserved = _("All rights reserved")
|
||||
context['copyright_text'] = '© {year} {platform_name}. {reserved}.'.format(
|
||||
year=settings.COPYRIGHT_YEAR,
|
||||
platform_name=platform_name,
|
||||
reserved=reserved
|
||||
)
|
||||
|
||||
# Translators: This text is bound to the HTML 'title' element of the page and appears
|
||||
# in the browser title bar when a requested certificate is not found or recognized
|
||||
context['document_title'] = _("Invalid Certificate")
|
||||
|
||||
# Translators: The & characters represent an ampersand character and can be ignored
|
||||
context['company_tos_urltext'] = _("Terms of Service & Honor Code")
|
||||
|
||||
# Translators: A 'Privacy Policy' is a legal document/statement describing a website's use of personal information
|
||||
context['company_privacy_urltext'] = _("Privacy Policy")
|
||||
|
||||
# Translators: This line appears as a byline to a header image and describes the purpose of the page
|
||||
context['logo_subtitle'] = _("Certificate Validation")
|
||||
|
||||
# Translators: Accomplishments describe the awards/certifications obtained by students on this platform
|
||||
context['accomplishment_copy_about'] = _('About {platform_name} Accomplishments').format(
|
||||
platform_name=platform_name
|
||||
)
|
||||
|
||||
# Translators: This line appears on the page just before the generation date for the certificate
|
||||
context['certificate_date_issued_title'] = _("Issued On:")
|
||||
|
||||
# Translators: The Certificate ID Number is an alphanumeric value unique to each individual certificate
|
||||
context['certificate_id_number_title'] = _('Certificate ID Number')
|
||||
|
||||
context['certificate_info_title'] = _('About {platform_name} Certificates').format(
|
||||
platform_name=platform_name
|
||||
)
|
||||
|
||||
context['certificate_verify_title'] = _("How {platform_name} Validates Student Certificates").format(
|
||||
@@ -218,8 +204,7 @@ def _update_certificate_context(context, course, user, user_certificate):
|
||||
"world's best universities, including MIT, Harvard, Berkeley, University "
|
||||
"of Texas, and many others. {platform_name} is a non-profit online "
|
||||
"initiative created by founding partners Harvard and MIT.").format(
|
||||
platform_name=platform_name
|
||||
)
|
||||
platform_name=platform_name)
|
||||
|
||||
context['company_about_title'] = _("About {platform_name}").format(platform_name=platform_name)
|
||||
|
||||
@@ -236,187 +221,59 @@ def _update_certificate_context(context, course, user, user_certificate):
|
||||
platform_name=platform_name
|
||||
)
|
||||
|
||||
# Translators: This text represents the verification of the certificate
|
||||
context['document_meta_description'] = _('This is a valid {platform_name} certificate for {user_name}, '
|
||||
'who participated in {partner_short_name} {course_number}').format(
|
||||
platform_name=platform_name,
|
||||
user_name=user_fullname,
|
||||
partner_short_name=partner_short_name,
|
||||
course_number=course_number
|
||||
)
|
||||
|
||||
# Translators: This text is bound to the HTML 'title' element of the page and appears in the browser title bar
|
||||
context['document_title'] = _("{partner_short_name} {course_number} Certificate | {platform_name}").format(
|
||||
partner_short_name=partner_short_name,
|
||||
course_number=course_number,
|
||||
platform_name=platform_name
|
||||
)
|
||||
|
||||
# Translators: This text fragment appears after the student's name (displayed in a large font) on the certificate
|
||||
# screen. The text describes the accomplishment represented by the certificate information displayed to the user
|
||||
context['accomplishment_copy_description_full'] = _("successfully completed, received a passing grade, and was "
|
||||
"awarded a {platform_name} {certificate_type} "
|
||||
"Certificate of Completion in ").format(
|
||||
platform_name=platform_name,
|
||||
certificate_type=context.get("certificate_type")
|
||||
)
|
||||
|
||||
certificate_type_description = get_certificate_description(user_certificate.mode, certificate_type, platform_name)
|
||||
if certificate_type_description:
|
||||
context['certificate_type_description'] = certificate_type_description
|
||||
|
||||
# Translators: This line is displayed to a user who has completed a course and achieved a certification
|
||||
context['accomplishment_banner_opening'] = _("{fullname}, you've earned a certificate!").format(
|
||||
fullname=user_fullname
|
||||
)
|
||||
|
||||
# Translators: This line congratulates the user and instructs them to share their accomplishment on social networks
|
||||
context['accomplishment_banner_congrats'] = _("Congratulations! This page summarizes all of the details of what "
|
||||
"you've accomplished. Show it off to family, friends, and colleagues "
|
||||
"in your social and professional networks.")
|
||||
|
||||
# Translators: This line leads the reader to understand more about the certificate that a student has been awarded
|
||||
context['accomplishment_copy_more_about'] = _("More about {fullname}'s accomplishment").format(
|
||||
fullname=user_fullname
|
||||
)
|
||||
|
||||
|
||||
@handle_500(
|
||||
template_path="certificates/server-error.html",
|
||||
test_func=lambda request: request.GET.get('preview', None)
|
||||
)
|
||||
def render_html_view(request, user_id, course_id):
|
||||
def _update_course_context(request, context, course, platform_name):
|
||||
"""
|
||||
This public view generates an HTML representation of the specified student's certificate
|
||||
If a certificate is not available, we display a "Sorry!" screen instead
|
||||
Updates context dictionary with course info.
|
||||
"""
|
||||
|
||||
# Create the initial view context, bootstrapping with Django settings and passed-in values
|
||||
context = {}
|
||||
context['platform_name'] = microsite.get_value("platform_name", settings.PLATFORM_NAME)
|
||||
context['course_id'] = course_id
|
||||
preview_mode = request.GET.get('preview', None)
|
||||
|
||||
# Update the view context with the default ConfigurationModel settings
|
||||
configuration = CertificateHtmlViewConfiguration.get_config()
|
||||
# if we are in a microsite, then let's first see if there is an override
|
||||
# section in our config
|
||||
config_key = microsite.get_value('microsite_config_key', 'default')
|
||||
# if there is no special microsite override, then let's use default
|
||||
if config_key not in configuration:
|
||||
config_key = 'default'
|
||||
context.update(configuration.get(config_key, {}))
|
||||
|
||||
# Translators: 'All rights reserved' is a legal term used in copyrighting to protect published content
|
||||
reserved = _("All rights reserved")
|
||||
context['copyright_text'] = '© {year} {platform_name}. {reserved}.'.format(
|
||||
year=settings.COPYRIGHT_YEAR,
|
||||
platform_name=context.get('platform_name'),
|
||||
reserved=reserved
|
||||
)
|
||||
|
||||
# Translators: This text is bound to the HTML 'title' element of the page and appears
|
||||
# in the browser title bar when a requested certificate is not found or recognized
|
||||
context['document_title'] = _("Invalid Certificate")
|
||||
|
||||
# Translators: The & characters represent an ampersand character and can be ignored
|
||||
context['company_tos_urltext'] = _("Terms of Service & Honor Code")
|
||||
|
||||
# Translators: A 'Privacy Policy' is a legal document/statement describing a website's use of personal information
|
||||
context['company_privacy_urltext'] = _("Privacy Policy")
|
||||
|
||||
# Translators: This line appears as a byline to a header image and describes the purpose of the page
|
||||
context['logo_subtitle'] = _("Certificate Validation")
|
||||
invalid_template_path = 'certificates/invalid.html'
|
||||
|
||||
# Kick the user back to the "Invalid" screen if the feature is disabled
|
||||
if not has_html_certificates_enabled(course_id):
|
||||
return render_to_response(invalid_template_path, context)
|
||||
|
||||
# Load the core building blocks for the view context
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = User.objects.get(id=user_id)
|
||||
course = modulestore().get_course(course_key)
|
||||
|
||||
if not course:
|
||||
raise CourseDoesNotExist
|
||||
|
||||
# Attempt to load the user's generated certificate data
|
||||
if preview_mode:
|
||||
user_certificate = GeneratedCertificate.objects.get(
|
||||
user=user,
|
||||
course_id=course_key,
|
||||
mode=preview_mode
|
||||
)
|
||||
else:
|
||||
user_certificate = GeneratedCertificate.objects.get(
|
||||
user=user,
|
||||
course_id=course_key
|
||||
)
|
||||
|
||||
# If there's no generated certificate data for this user, we need to see if we're in 'preview' mode...
|
||||
# If we are, we'll need to create a mock version of the user_certificate container for previewing
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
if preview_mode and (
|
||||
has_access(request.user, 'instructor', course)
|
||||
or has_access(request.user, 'staff', course)
|
||||
):
|
||||
user_certificate = GeneratedCertificate(
|
||||
mode=preview_mode,
|
||||
verify_uuid=unicode(uuid4().hex),
|
||||
modified_date=datetime.now().date()
|
||||
)
|
||||
else:
|
||||
return render_to_response(invalid_template_path, context)
|
||||
|
||||
# For any other expected exceptions, kick the user back to the "Invalid" screen
|
||||
except (InvalidKeyError, CourseDoesNotExist, User.DoesNotExist):
|
||||
return render_to_response(invalid_template_path, context)
|
||||
|
||||
# Badge Request Event Tracking Logic
|
||||
if 'evidence_visit' in request.GET:
|
||||
try:
|
||||
badge = BadgeAssertion.objects.get(user=user, course_id=course_key)
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.evidence_visited',
|
||||
{
|
||||
'user_id': user.id,
|
||||
'course_id': unicode(course_key),
|
||||
'enrollment_mode': badge.mode,
|
||||
'assertion_id': badge.id,
|
||||
'assertion_image_url': badge.data['image'],
|
||||
'assertion_json_url': badge.data['json']['id'],
|
||||
'issuer': badge.data['issuer'],
|
||||
}
|
||||
)
|
||||
except BadgeAssertion.DoesNotExist:
|
||||
log.warn(
|
||||
"Could not find badge for %s on course %s.",
|
||||
user.id,
|
||||
course_key,
|
||||
)
|
||||
|
||||
# Okay, now we have all of the pieces, time to put everything together
|
||||
|
||||
# Get the active certificate configuration for this course
|
||||
# If we do not have an active certificate, we'll need to send the user to the "Invalid" screen
|
||||
# Passing in the 'preview' parameter, if specified, will return a configuration, if defined
|
||||
active_configuration = get_active_web_certificate(course, preview_mode)
|
||||
if active_configuration is None:
|
||||
return render_to_response(invalid_template_path, context)
|
||||
context['full_course_image_url'] = request.build_absolute_uri(course_image_url(course))
|
||||
course_title_from_cert = context['certificate_data'].get('course_title', '')
|
||||
accomplishment_copy_course_name = course_title_from_cert if course_title_from_cert else course.display_name
|
||||
context['accomplishment_copy_course_name'] = accomplishment_copy_course_name
|
||||
course_number = course.display_coursenumber if course.display_coursenumber else course.number
|
||||
context['course_number'] = course_number
|
||||
if context['organization_long_name']:
|
||||
# Translators: This text represents the description of course
|
||||
context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, '
|
||||
'an online learning initiative of {partner_long_name} '
|
||||
'through {platform_name}.').format(
|
||||
partner_short_name=context['organization_short_name'],
|
||||
partner_long_name=context['organization_long_name'],
|
||||
platform_name=platform_name)
|
||||
else:
|
||||
context['certificate_data'] = active_configuration
|
||||
# Translators: This text represents the description of course
|
||||
context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, '
|
||||
'through {platform_name}.').format(
|
||||
partner_short_name=context['organization_short_name'],
|
||||
platform_name=platform_name)
|
||||
|
||||
# Append/Override the existing view context values with any mode-specific ConfigurationModel values
|
||||
context.update(configuration.get(user_certificate.mode, {}))
|
||||
|
||||
# Append/Override the existing view context values with request-time values
|
||||
_update_certificate_context(context, course, user, user_certificate)
|
||||
def _update_social_context(request, context, course, user, user_certificate, platform_name):
|
||||
"""
|
||||
Updates context dictionary with info required for social sharing.
|
||||
"""
|
||||
share_settings = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {})
|
||||
context['facebook_share_enabled'] = share_settings.get('CERTIFICATE_FACEBOOK', False)
|
||||
context['facebook_app_id'] = getattr(settings, "FACEBOOK_APP_ID", None)
|
||||
context['facebook_share_text'] = share_settings.get(
|
||||
'CERTIFICATE_FACEBOOK_TEXT',
|
||||
_("I completed the {course_title} course on {platform_name}.").format(
|
||||
course_title=context['accomplishment_copy_course_name'],
|
||||
platform_name=platform_name
|
||||
)
|
||||
)
|
||||
context['twitter_share_enabled'] = share_settings.get('CERTIFICATE_TWITTER', False)
|
||||
context['twitter_share_text'] = share_settings.get(
|
||||
'CERTIFICATE_TWITTER_TEXT',
|
||||
_("I completed a course on {platform_name}. Take a look at my certificate.").format(
|
||||
platform_name=platform_name
|
||||
)
|
||||
)
|
||||
|
||||
share_url = request.build_absolute_uri(
|
||||
reverse(
|
||||
'certificates:html_view',
|
||||
kwargs=dict(user_id=str(user_id), course_id=unicode(course_id))
|
||||
kwargs=dict(user_id=str(user.id), course_id=unicode(course.id))
|
||||
)
|
||||
)
|
||||
context['share_url'] = share_url
|
||||
@@ -427,8 +284,7 @@ def render_html_view(request, user_id, course_id):
|
||||
share_url=urllib.quote_plus(smart_str(share_url))
|
||||
)
|
||||
context['twitter_url'] = twitter_url
|
||||
context['full_course_image_url'] = request.build_absolute_uri(course_image_url(course))
|
||||
|
||||
context['linked_in_url'] = None
|
||||
# If enabled, show the LinkedIn "add to profile" button
|
||||
# Clicking this button sends the user to LinkedIn where they
|
||||
# can add the certificate information to their profile.
|
||||
@@ -446,39 +302,110 @@ def render_html_view(request, user_id, course_id):
|
||||
course_id=unicode(course.id)
|
||||
)))
|
||||
)
|
||||
else:
|
||||
context['linked_in_url'] = None
|
||||
|
||||
# Microsites will need to be able to override any hard coded
|
||||
# content that was put into the context in the
|
||||
# _update_certificate_context() call above. For example the
|
||||
# 'company_about_description' talks about edX, which we most likely
|
||||
# do not want to keep in a microsite
|
||||
#
|
||||
# So we need to re-apply any configuration/content that
|
||||
# we are sourceing from the database. This is somewhat duplicative of
|
||||
# the code at the beginning of this method, but we
|
||||
# need the configuration at the top as some error code paths
|
||||
# require that to be set up early on in the pipeline
|
||||
#
|
||||
microsite_config_key = microsite.get_value('microsite_config_key')
|
||||
if microsite_config_key:
|
||||
context.update(configuration.get(microsite_config_key, {}))
|
||||
|
||||
def _update_context_with_user_info(context, user, user_certificate):
|
||||
"""
|
||||
Updates context dictionary with user related info.
|
||||
"""
|
||||
user_fullname = user.profile.name
|
||||
context['username'] = user.username
|
||||
context['course_mode'] = user_certificate.mode
|
||||
context['accomplishment_user_id'] = user.id
|
||||
context['accomplishment_copy_name'] = user_fullname
|
||||
context['accomplishment_copy_username'] = user.username
|
||||
|
||||
context['accomplishment_more_title'] = _("More Information About {user_name}'s Certificate:").format(
|
||||
user_name=user_fullname
|
||||
)
|
||||
# Translators: This line is displayed to a user who has completed a course and achieved a certification
|
||||
context['accomplishment_banner_opening'] = _("{fullname}, you've earned a certificate!").format(
|
||||
fullname=user_fullname
|
||||
)
|
||||
|
||||
# Translators: This line congratulates the user and instructs them to share their accomplishment on social networks
|
||||
context['accomplishment_banner_congrats'] = _("Congratulations! This page summarizes all of the details of what "
|
||||
"you've accomplished. Show it off to family, friends, and colleagues "
|
||||
"in your social and professional networks.")
|
||||
|
||||
# Translators: This line leads the reader to understand more about the certificate that a student has been awarded
|
||||
context['accomplishment_copy_more_about'] = _("More about {fullname}'s accomplishment").format(
|
||||
fullname=user_fullname
|
||||
)
|
||||
|
||||
|
||||
def _get_user_certificate(request, user, course_key, course, preview_mode=None):
|
||||
"""
|
||||
Retrieves user's certificate from db. Creates one in case of preview mode.
|
||||
Returns None if there is no certificate generated for given user
|
||||
otherwise returns `GeneratedCertificate` instance.
|
||||
"""
|
||||
try:
|
||||
# Attempt to load the user's generated certificate data
|
||||
if preview_mode:
|
||||
user_certificate = GeneratedCertificate.objects.get(
|
||||
user=user,
|
||||
course_id=course_key,
|
||||
mode=preview_mode
|
||||
)
|
||||
else:
|
||||
user_certificate = GeneratedCertificate.objects.get(
|
||||
user=user,
|
||||
course_id=course_key
|
||||
)
|
||||
|
||||
# If there's no generated certificate data for this user, we need to see if we're in 'preview' mode...
|
||||
# If we are, we'll need to create a mock version of the user_certificate container for previewing
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
if preview_mode and (
|
||||
has_access(request.user, 'instructor', course) or
|
||||
has_access(request.user, 'staff', course)):
|
||||
user_certificate = GeneratedCertificate(
|
||||
mode=preview_mode,
|
||||
verify_uuid=unicode(uuid4().hex),
|
||||
modified_date=datetime.now().date()
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
return user_certificate
|
||||
|
||||
|
||||
def _track_certificate_events(request, context, course, user, user_certificate):
|
||||
"""
|
||||
Tracks web certificate view related events.
|
||||
"""
|
||||
badge = context['badge']
|
||||
# Badge Request Event Tracking Logic
|
||||
if 'evidence_visit' in request.GET and badge:
|
||||
tracker.emit(
|
||||
'edx.badge.assertion.evidence_visited',
|
||||
{
|
||||
'user_id': user.id,
|
||||
'course_id': unicode(course.id),
|
||||
'enrollment_mode': badge.mode,
|
||||
'assertion_id': badge.id,
|
||||
'assertion_image_url': badge.data['image'],
|
||||
'assertion_json_url': badge.data['json']['id'],
|
||||
'issuer': badge.data['issuer'],
|
||||
}
|
||||
)
|
||||
|
||||
# track certificate evidence_visited event for analytics when certificate_user and accessing_user are different
|
||||
if request.user and request.user.id != user.id:
|
||||
emit_certificate_event('evidence_visited', user, course_id, course, {
|
||||
emit_certificate_event('evidence_visited', user, unicode(course.id), course, {
|
||||
'certificate_id': user_certificate.verify_uuid,
|
||||
'enrollment_mode': user_certificate.mode,
|
||||
'social_network': CertificateSocialNetworks.linkedin
|
||||
})
|
||||
|
||||
# Append/Override the existing view context values with any course-specific static values from Advanced Settings
|
||||
context.update(course.cert_html_view_overrides)
|
||||
|
||||
# FINALLY, generate and send the output the client
|
||||
def _render_certificate_template(request, context, course, user_certificate):
|
||||
"""
|
||||
Picks appropriate certificate templates and renders it.
|
||||
"""
|
||||
if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False):
|
||||
custom_template = get_certificate_template(course_key, user_certificate.mode)
|
||||
custom_template = get_certificate_template(course.id, user_certificate.mode)
|
||||
if custom_template:
|
||||
template = Template(
|
||||
custom_template,
|
||||
@@ -491,3 +418,134 @@ def render_html_view(request, user_id, course_id):
|
||||
return HttpResponse(template.render(context))
|
||||
|
||||
return render_to_response("certificates/valid.html", context)
|
||||
|
||||
|
||||
def _update_microsite_context(context, configuration):
|
||||
"""
|
||||
Updates context with microsites data.
|
||||
Microsites will need to be able to override any hard coded
|
||||
content that was put into the context in the
|
||||
_update_certificate_context() call above. For example the
|
||||
'company_about_description' talks about edX, which we most likely
|
||||
do not want to keep in a microsite
|
||||
|
||||
So we need to re-apply any configuration/content that
|
||||
we are sourcing from the database. This is somewhat duplicative of
|
||||
the code at the beginning of this method, but we
|
||||
need the configuration at the top as some error code paths
|
||||
require that to be set up early on in the pipeline
|
||||
"""
|
||||
|
||||
microsite_config_key = microsite.get_value('domain_prefix')
|
||||
microsites_config = configuration.get("microsites", {})
|
||||
if microsite_config_key and microsites_config:
|
||||
context.update(microsites_config.get(microsite_config_key, {}))
|
||||
|
||||
|
||||
def _update_badge_context(context, course, user):
|
||||
"""
|
||||
Updates context with badge info.
|
||||
"""
|
||||
try:
|
||||
badge = BadgeAssertion.objects.get(user=user, course_id=course.location.course_key)
|
||||
except BadgeAssertion.DoesNotExist:
|
||||
badge = None
|
||||
context['badge'] = badge
|
||||
|
||||
|
||||
def _update_organization_context(context, course):
|
||||
"""
|
||||
Updates context with organization related info.
|
||||
"""
|
||||
partner_long_name, organization_logo = None, None
|
||||
partner_short_name = course.display_organization if course.display_organization else course.org
|
||||
organizations = organization_api.get_course_organizations(course_id=course.id)
|
||||
if organizations:
|
||||
#TODO Need to add support for multiple organizations, Currently we are interested in the first one.
|
||||
organization = organizations[0]
|
||||
partner_long_name = organization.get('name', partner_long_name)
|
||||
partner_short_name = organization.get('short_name', partner_short_name)
|
||||
organization_logo = organization.get('logo', None)
|
||||
|
||||
context['organization_long_name'] = partner_long_name
|
||||
context['organization_short_name'] = partner_short_name
|
||||
context['accomplishment_copy_course_org'] = partner_short_name
|
||||
context['organization_logo'] = organization_logo
|
||||
|
||||
|
||||
@handle_500(
|
||||
template_path="certificates/server-error.html",
|
||||
test_func=lambda request: request.GET.get('preview', None)
|
||||
)
|
||||
def render_html_view(request, user_id, course_id):
|
||||
"""
|
||||
This public view generates an HTML representation of the specified student's certificate
|
||||
If a certificate is not available, we display a "Sorry!" screen instead
|
||||
"""
|
||||
preview_mode = request.GET.get('preview', None)
|
||||
platform_name = microsite.get_value("platform_name", settings.PLATFORM_NAME)
|
||||
configuration = CertificateHtmlViewConfiguration.get_config()
|
||||
# Create the initial view context, bootstrapping with Django settings and passed-in values
|
||||
context = {}
|
||||
_update_context_with_basic_info(context, course_id, platform_name, configuration)
|
||||
invalid_template_path = 'certificates/invalid.html'
|
||||
|
||||
# Kick the user back to the "Invalid" screen if the feature is disabled
|
||||
if not has_html_certificates_enabled(course_id):
|
||||
return render_to_response(invalid_template_path, context)
|
||||
|
||||
# Load the course and user objects
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = User.objects.get(id=user_id)
|
||||
course = modulestore().get_course(course_key)
|
||||
|
||||
# For any other expected exceptions, kick the user back to the "Invalid" screen
|
||||
except (InvalidKeyError, ItemNotFoundError, User.DoesNotExist):
|
||||
return render_to_response(invalid_template_path, context)
|
||||
|
||||
# Load user's certificate
|
||||
user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode)
|
||||
if not user_certificate:
|
||||
return render_to_response(invalid_template_path, context)
|
||||
|
||||
# Get the active certificate configuration for this course
|
||||
# If we do not have an active certificate, we'll need to send the user to the "Invalid" screen
|
||||
# Passing in the 'preview' parameter, if specified, will return a configuration, if defined
|
||||
active_configuration = get_active_web_certificate(course, preview_mode)
|
||||
if active_configuration is None:
|
||||
return render_to_response(invalid_template_path, context)
|
||||
context['certificate_data'] = active_configuration
|
||||
|
||||
# Append/Override the existing view context values with any mode-specific ConfigurationModel values
|
||||
context.update(configuration.get(user_certificate.mode, {}))
|
||||
|
||||
# Append organization info
|
||||
_update_organization_context(context, course)
|
||||
|
||||
# Append course info
|
||||
_update_course_context(request, context, course, platform_name)
|
||||
|
||||
# Append user info
|
||||
_update_context_with_user_info(context, user, user_certificate)
|
||||
|
||||
# Append social sharing info
|
||||
_update_social_context(request, context, course, user, user_certificate, platform_name)
|
||||
|
||||
# Append/Override the existing view context values with certificate specific values
|
||||
_update_certificate_context(context, user_certificate, platform_name)
|
||||
|
||||
# Append badge info
|
||||
_update_badge_context(context, course, user)
|
||||
|
||||
# Append microsite overrides
|
||||
_update_microsite_context(context, configuration)
|
||||
|
||||
# Append/Override the existing view context values with any course-specific static values from Advanced Settings
|
||||
context.update(course.cert_html_view_overrides)
|
||||
|
||||
# Track certificate view events
|
||||
_track_certificate_events(request, context, course, user, user_certificate)
|
||||
|
||||
# FINALLY, render appropriate certificate
|
||||
return _render_certificate_template(request, context, course, user_certificate)
|
||||
|
||||
@@ -25,13 +25,7 @@ from discussion_api.permissions import (
|
||||
get_initializable_thread_fields,
|
||||
)
|
||||
from discussion_api.serializers import CommentSerializer, ThreadSerializer, get_context
|
||||
from django_comment_client.base.views import (
|
||||
THREAD_CREATED_EVENT_NAME,
|
||||
get_comment_created_event_data,
|
||||
get_comment_created_event_name,
|
||||
get_thread_created_event_data,
|
||||
track_forum_event,
|
||||
)
|
||||
from django_comment_client.base.views import track_comment_created_event, track_thread_created_event
|
||||
from django_comment_common.signals import (
|
||||
thread_created,
|
||||
thread_edited,
|
||||
@@ -566,13 +560,7 @@ def create_thread(request, thread_data):
|
||||
api_thread = serializer.data
|
||||
_do_extra_actions(api_thread, cc_thread, thread_data.keys(), actions_form, context)
|
||||
|
||||
track_forum_event(
|
||||
request,
|
||||
THREAD_CREATED_EVENT_NAME,
|
||||
course,
|
||||
cc_thread,
|
||||
get_thread_created_event_data(cc_thread, followed=actions_form.cleaned_data["following"])
|
||||
)
|
||||
track_thread_created_event(request, course, cc_thread, actions_form.cleaned_data["following"])
|
||||
|
||||
return api_thread
|
||||
|
||||
@@ -616,13 +604,7 @@ def create_comment(request, comment_data):
|
||||
api_comment = serializer.data
|
||||
_do_extra_actions(api_comment, cc_comment, comment_data.keys(), actions_form, context)
|
||||
|
||||
track_forum_event(
|
||||
request,
|
||||
get_comment_created_event_name(cc_comment),
|
||||
context["course"],
|
||||
cc_comment,
|
||||
get_comment_created_event_data(cc_comment, cc_thread["commentable_id"], followed=False)
|
||||
)
|
||||
track_comment_created_event(request, context["course"], cc_comment, cc_thread["commentable_id"], followed=False)
|
||||
|
||||
return api_comment
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse
|
||||
from request_cache.middleware import RequestCache
|
||||
from mock import patch, ANY, Mock
|
||||
from nose.tools import assert_true, assert_equal # pylint: disable=no-name-in-module
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from lms.lib.comment_client import Thread
|
||||
|
||||
from common.test.utils import MockSignalHandlerMixin, disable_signal
|
||||
@@ -1641,6 +1641,40 @@ class ForumEventTestCase(ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
self.assertEqual(name, event_name)
|
||||
self.assertEqual(event['team_id'], team.team_id)
|
||||
|
||||
@ddt.data(
|
||||
('vote_for_thread', 'thread_id', 'thread'),
|
||||
('undo_vote_for_thread', 'thread_id', 'thread'),
|
||||
('vote_for_comment', 'comment_id', 'response'),
|
||||
('undo_vote_for_comment', 'comment_id', 'response'),
|
||||
)
|
||||
@ddt.unpack
|
||||
@patch('eventtracking.tracker.emit')
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
def test_thread_voted_event(self, view_name, obj_id_name, obj_type, mock_request, mock_emit):
|
||||
undo = view_name.startswith('undo')
|
||||
|
||||
self._set_mock_request_data(mock_request, {
|
||||
'closed': False,
|
||||
'commentable_id': 'test_commentable_id',
|
||||
'username': 'gumprecht',
|
||||
})
|
||||
request = RequestFactory().post('dummy_url', {})
|
||||
request.user = self.student
|
||||
request.view_name = view_name
|
||||
view_function = getattr(views, view_name)
|
||||
kwargs = dict(course_id=unicode(self.course.id))
|
||||
kwargs[obj_id_name] = obj_id_name
|
||||
if not undo:
|
||||
kwargs.update(value='up')
|
||||
view_function(request, **kwargs)
|
||||
|
||||
self.assertTrue(mock_emit.called)
|
||||
event_name, event = mock_emit.call_args[0]
|
||||
self.assertEqual(event_name, 'edx.forum.{}.voted'.format(obj_type))
|
||||
self.assertEqual(event['target_username'], 'gumprecht')
|
||||
self.assertEqual(event['undo_vote'], undo)
|
||||
self.assertEqual(event['vote_value'], 'up')
|
||||
|
||||
|
||||
class UsersEndpointTestCase(ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
|
||||
@@ -1699,7 +1733,7 @@ class UsersEndpointTestCase(ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
self.assertNotIn("users", content)
|
||||
|
||||
def test_course_does_not_exist(self):
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string("does/not/exist")
|
||||
course_id = CourseKey.from_string("does/not/exist")
|
||||
response = self.make_request(course_id=course_id, username="other")
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@@ -12,7 +12,6 @@ from django.utils.translation import ugettext as _
|
||||
from django.views.decorators import csrf
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.access import has_access
|
||||
from util.file import store_uploaded_file
|
||||
@@ -49,40 +48,7 @@ import lms.lib.comment_client as cc
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
TRACKING_MAX_FORUM_BODY = 2000
|
||||
|
||||
THREAD_CREATED_EVENT_NAME = "edx.forum.thread.created"
|
||||
RESPONSE_CREATED_EVENT_NAME = 'edx.forum.response.created'
|
||||
COMMENT_CREATED_EVENT_NAME = 'edx.forum.comment.created'
|
||||
|
||||
|
||||
def permitted(fn):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
def fetch_content():
|
||||
if "thread_id" in kwargs:
|
||||
content = cc.Thread.find(kwargs["thread_id"]).to_dict()
|
||||
elif "comment_id" in kwargs:
|
||||
content = cc.Comment.find(kwargs["comment_id"]).to_dict()
|
||||
elif "commentable_id" in kwargs:
|
||||
content = cc.Commentable.find(kwargs["commentable_id"]).to_dict()
|
||||
else:
|
||||
content = None
|
||||
return content
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id'])
|
||||
if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name):
|
||||
return fn(request, *args, **kwargs)
|
||||
else:
|
||||
return JsonError("unauthorized", status=401)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ajax_content_response(request, course_key, content):
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
annotated_content_info = get_annotated_content_info(course_key, content, request.user, user_info)
|
||||
return JsonResponse({
|
||||
'content': prepare_content(content, course_key),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
_EVENT_NAME_TEMPLATE = 'edx.forum.{obj_type}.{action_name}'
|
||||
|
||||
|
||||
def track_forum_event(request, event_name, course, obj, data, id_map=None):
|
||||
@@ -100,16 +66,9 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None):
|
||||
|
||||
if id_map is None:
|
||||
id_map = get_cached_discussion_id_map(course, [commentable_id], user)
|
||||
|
||||
if commentable_id in id_map:
|
||||
data['category_name'] = id_map[commentable_id]["title"]
|
||||
data['category_id'] = commentable_id
|
||||
if len(obj.body) > TRACKING_MAX_FORUM_BODY:
|
||||
data['truncated'] = True
|
||||
else:
|
||||
data['truncated'] = False
|
||||
|
||||
data['body'] = obj.body[:TRACKING_MAX_FORUM_BODY]
|
||||
data['url'] = request.META.get('HTTP_REFERER', '')
|
||||
data['user_forums_roles'] = [
|
||||
role.name for role in user.roles.filter(course_id=course.id)
|
||||
@@ -121,12 +80,24 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None):
|
||||
tracker.emit(event_name, data)
|
||||
|
||||
|
||||
def get_thread_created_event_data(thread, followed):
|
||||
def track_created_event(request, event_name, course, obj, data):
|
||||
"""
|
||||
Get the event data payload for thread creation (excluding fields populated
|
||||
by track_forum_event)
|
||||
Send analytics event for a newly created thread, response or comment.
|
||||
"""
|
||||
return {
|
||||
if len(obj.body) > TRACKING_MAX_FORUM_BODY:
|
||||
data['truncated'] = True
|
||||
else:
|
||||
data['truncated'] = False
|
||||
data['body'] = obj.body[:TRACKING_MAX_FORUM_BODY]
|
||||
track_forum_event(request, event_name, course, obj, data)
|
||||
|
||||
|
||||
def track_thread_created_event(request, course, thread, followed):
|
||||
"""
|
||||
Send analytics event for a newly created thread.
|
||||
"""
|
||||
event_name = _EVENT_NAME_TEMPLATE.format(obj_type='thread', action_name='created')
|
||||
event_data = {
|
||||
'commentable_id': thread.commentable_id,
|
||||
'group_id': thread.get("group_id"),
|
||||
'thread_type': thread.thread_type,
|
||||
@@ -139,29 +110,84 @@ def get_thread_created_event_data(thread, followed):
|
||||
# However, the view does not contain that data, and including it will
|
||||
# likely require changes elsewhere.
|
||||
}
|
||||
track_created_event(request, event_name, course, thread, event_data)
|
||||
|
||||
|
||||
def get_comment_created_event_name(comment):
|
||||
"""Get the appropriate event name for creating a response/comment"""
|
||||
return COMMENT_CREATED_EVENT_NAME if comment.get("parent_id") else RESPONSE_CREATED_EVENT_NAME
|
||||
|
||||
|
||||
def get_comment_created_event_data(comment, commentable_id, followed):
|
||||
def track_comment_created_event(request, course, comment, commentable_id, followed):
|
||||
"""
|
||||
Get the event data payload for comment creation (excluding fields populated
|
||||
by track_forum_event)
|
||||
Send analytics event for a newly created response or comment.
|
||||
"""
|
||||
obj_type = 'comment' if comment.get("parent_id") else 'response'
|
||||
event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='created')
|
||||
event_data = {
|
||||
'discussion': {'id': comment.thread_id},
|
||||
'commentable_id': commentable_id,
|
||||
'options': {'followed': followed},
|
||||
}
|
||||
|
||||
parent_id = comment.get("parent_id")
|
||||
parent_id = comment.get('parent_id')
|
||||
if parent_id:
|
||||
event_data['response'] = {'id': parent_id}
|
||||
track_created_event(request, event_name, course, comment, event_data)
|
||||
|
||||
return event_data
|
||||
|
||||
def track_voted_event(request, course, obj, vote_value, undo_vote=False):
|
||||
"""
|
||||
Send analytics event for a vote on a thread or response.
|
||||
"""
|
||||
if isinstance(obj, cc.Thread):
|
||||
obj_type = 'thread'
|
||||
else:
|
||||
obj_type = 'response'
|
||||
event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='voted')
|
||||
event_data = {
|
||||
'commentable_id': obj.commentable_id,
|
||||
'target_username': obj.get('username'),
|
||||
'undo_vote': undo_vote,
|
||||
'vote_value': vote_value,
|
||||
}
|
||||
track_forum_event(request, event_name, course, obj, event_data)
|
||||
|
||||
|
||||
def permitted(func):
|
||||
"""
|
||||
View decorator to verify the user is authorized to access this endpoint.
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
"""
|
||||
Wrapper for the view that only calls the view if the user is authorized.
|
||||
"""
|
||||
def fetch_content():
|
||||
"""
|
||||
Extract the forum object from the keyword arguments to the view.
|
||||
"""
|
||||
if "thread_id" in kwargs:
|
||||
content = cc.Thread.find(kwargs["thread_id"]).to_dict()
|
||||
elif "comment_id" in kwargs:
|
||||
content = cc.Comment.find(kwargs["comment_id"]).to_dict()
|
||||
elif "commentable_id" in kwargs:
|
||||
content = cc.Commentable.find(kwargs["commentable_id"]).to_dict()
|
||||
else:
|
||||
content = None
|
||||
return content
|
||||
course_key = CourseKey.from_string(kwargs['course_id'])
|
||||
if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name):
|
||||
return func(request, *args, **kwargs)
|
||||
else:
|
||||
return JsonError("unauthorized", status=401)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ajax_content_response(request, course_key, content):
|
||||
"""
|
||||
Standard AJAX response returning the content hierarchy of the current thread.
|
||||
"""
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
annotated_content_info = get_annotated_content_info(course_key, content, request.user, user_info)
|
||||
return JsonResponse({
|
||||
'content': prepare_content(content, course_key),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -173,7 +199,7 @@ def create_thread(request, course_id, commentable_id):
|
||||
"""
|
||||
|
||||
log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
post = request.POST
|
||||
user = request.user
|
||||
@@ -234,12 +260,11 @@ def create_thread(request, course_id, commentable_id):
|
||||
cc_user = cc.User.from_django_user(user)
|
||||
cc_user.follow(thread)
|
||||
|
||||
event_data = get_thread_created_event_data(thread, follow)
|
||||
data = thread.to_dict()
|
||||
|
||||
add_courseware_context([data], course, user)
|
||||
|
||||
track_forum_event(request, THREAD_CREATED_EVENT_NAME, course, thread, event_data)
|
||||
track_thread_created_event(request, course, thread, follow)
|
||||
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_key, data)
|
||||
@@ -259,7 +284,7 @@ def update_thread(request, course_id, thread_id):
|
||||
if 'body' not in request.POST or not request.POST['body'].strip():
|
||||
return JsonError(_("Body can't be empty"))
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
# Get thread context first in order to be safe from reseting the values of thread object later
|
||||
thread_context = getattr(thread, "context", "course")
|
||||
@@ -330,9 +355,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None):
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
cc_user.follow(comment.thread)
|
||||
|
||||
event_name = get_comment_created_event_name(comment)
|
||||
event_data = get_comment_created_event_data(comment, comment.thread.commentable_id, followed)
|
||||
track_forum_event(request, event_name, course, comment, event_data)
|
||||
track_comment_created_event(request, course, comment, comment.thread.commentable_id, followed)
|
||||
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_key, comment.to_dict())
|
||||
@@ -350,7 +373,7 @@ def create_comment(request, course_id, thread_id):
|
||||
"""
|
||||
if is_comment_too_deep(parent=None):
|
||||
return JsonError(_("Comment level too deep"))
|
||||
return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread_id=thread_id)
|
||||
return _create_comment(request, CourseKey.from_string(course_id), thread_id=thread_id)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -361,7 +384,7 @@ def delete_thread(request, course_id, thread_id): # pylint: disable=unused-argu
|
||||
given a course_id and thread_id, delete this thread
|
||||
this is ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.delete()
|
||||
thread_deleted.send(sender=None, user=request.user, post=thread)
|
||||
@@ -376,7 +399,7 @@ def update_comment(request, course_id, comment_id):
|
||||
given a course_id and comment_id, update the comment with payload attributes
|
||||
handles static and ajax submissions
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
if 'body' not in request.POST or not request.POST['body'].strip():
|
||||
return JsonError(_("Body can't be empty"))
|
||||
@@ -399,7 +422,7 @@ def endorse_comment(request, course_id, comment_id):
|
||||
given a course_id and comment_id, toggle the endorsement of this comment,
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user = request.user
|
||||
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
|
||||
@@ -417,7 +440,7 @@ def openclose_thread(request, course_id, thread_id):
|
||||
given a course_id and thread_id, toggle the status of this thread
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.closed = request.POST.get('closed', 'false').lower() == 'true'
|
||||
thread.save()
|
||||
@@ -438,7 +461,7 @@ def create_sub_comment(request, course_id, comment_id):
|
||||
"""
|
||||
if is_comment_too_deep(parent=cc.Comment(comment_id)):
|
||||
return JsonError(_("Comment level too deep"))
|
||||
return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), parent_id=comment_id)
|
||||
return _create_comment(request, CourseKey.from_string(course_id), parent_id=comment_id)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -449,27 +472,42 @@ def delete_comment(request, course_id, comment_id):
|
||||
given a course_id and comment_id delete this comment
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.delete()
|
||||
comment_deleted.send(sender=None, user=request.user, post=comment)
|
||||
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
def _vote_or_unvote(request, course_id, obj, value='up', undo_vote=False):
|
||||
"""
|
||||
Vote or unvote for a thread or a response.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
if undo_vote:
|
||||
user.unvote(obj)
|
||||
# TODO(smarnach): Determine the value of the vote that is undone. Currently, you can
|
||||
# only cast upvotes in the user interface, so it is assumed that the vote value is 'up'.
|
||||
# (People could theoretically downvote by handcrafting AJAX requests.)
|
||||
else:
|
||||
user.vote(obj, value)
|
||||
track_voted_event(request, course, obj, value, undo_vote)
|
||||
return JsonResponse(prepare_content(obj.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def vote_for_comment(request, course_id, comment_id, value):
|
||||
"""
|
||||
given a course_id and comment_id,
|
||||
Given a course_id and comment_id, vote for this response. AJAX only.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = request.user
|
||||
cc_user = cc.User.from_django_user(user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
cc_user.vote(comment, value)
|
||||
comment_voted.send(sender=None, user=user, post=comment)
|
||||
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
||||
result = _vote_or_unvote(request, course_id, comment, value)
|
||||
comment_voted.send(sender=None, user=request.user, post=comment)
|
||||
return result
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -480,11 +518,7 @@ def undo_vote_for_comment(request, course_id, comment_id):
|
||||
given a course id and comment id, remove vote
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.unvote(comment)
|
||||
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
||||
return _vote_or_unvote(request, course_id, cc.Comment.find(comment_id), undo_vote=True)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -495,13 +529,21 @@ def vote_for_thread(request, course_id, thread_id, value):
|
||||
given a course id and thread id vote for this thread
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = request.user
|
||||
cc_user = cc.User.from_django_user(user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
cc_user.vote(thread, value)
|
||||
thread_voted.send(sender=None, user=user, post=thread)
|
||||
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
||||
result = _vote_or_unvote(request, course_id, thread, value)
|
||||
thread_voted.send(sender=None, user=request.user, post=thread)
|
||||
return result
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def undo_vote_for_thread(request, course_id, thread_id):
|
||||
"""
|
||||
given a course id and thread id, remove users vote for thread
|
||||
ajax only
|
||||
"""
|
||||
return _vote_or_unvote(request, course_id, cc.Thread.find(thread_id), undo_vote=True)
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -512,7 +554,7 @@ def flag_abuse_for_thread(request, course_id, thread_id):
|
||||
given a course_id and thread_id flag this thread for abuse
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.flagAbuse(user, thread)
|
||||
@@ -529,7 +571,7 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
|
||||
ajax only
|
||||
"""
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
remove_all = bool(
|
||||
@@ -549,7 +591,7 @@ def flag_abuse_for_comment(request, course_id, comment_id):
|
||||
given a course and comment id, flag comment for abuse
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.flagAbuse(user, comment)
|
||||
@@ -565,7 +607,7 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
|
||||
ajax only
|
||||
"""
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
remove_all = bool(
|
||||
has_permission(request.user, 'openclose_thread', course_key) or
|
||||
@@ -576,22 +618,6 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
|
||||
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
def undo_vote_for_thread(request, course_id, thread_id):
|
||||
"""
|
||||
given a course id and thread id, remove users vote for thread
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.unvote(thread)
|
||||
|
||||
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permitted
|
||||
@@ -600,7 +626,7 @@ def pin_thread(request, course_id, thread_id):
|
||||
given a course id and thread id, pin this thread
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.pin(user, thread_id)
|
||||
@@ -616,7 +642,7 @@ def un_pin_thread(request, course_id, thread_id):
|
||||
given a course id and thread id, remove pin from this thread
|
||||
ajax only
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.un_pin(user, thread_id)
|
||||
@@ -742,7 +768,7 @@ def users(request, course_id):
|
||||
Only exact matches are supported here, so the length of the result set will either be 0 or 1.
|
||||
"""
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
try:
|
||||
get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
except Http404:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.courses import get_course
|
||||
|
||||
@@ -16,10 +15,7 @@ class Command(BaseCommand):
|
||||
raise CommandError("Only one course id may be specifiied")
|
||||
course_id = args[0]
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
except InvalidKeyError:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
|
||||
course = get_course(course_key)
|
||||
if not course:
|
||||
|
||||
@@ -3,7 +3,7 @@ Management command to seed default permissions and roles.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,6 +15,6 @@ class Command(BaseCommand):
|
||||
raise CommandError("Please provide a course id")
|
||||
if len(args) > 1:
|
||||
raise CommandError("Too many arguments")
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(args[0])
|
||||
course_id = CourseKey.from_string(args[0])
|
||||
|
||||
seed_permissions_roles(course_id)
|
||||
|
||||
@@ -3,7 +3,7 @@ Tests for the django comment client integration models
|
||||
"""
|
||||
from django.test.testcases import TestCase
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE
|
||||
import django_comment_common.models as models
|
||||
@@ -23,7 +23,7 @@ class RoleClassTestCase(ModuleStoreTestCase):
|
||||
# For course ID, syntax edx/classname/classdate is important
|
||||
# because xmodel.course_module.id_to_location looks for a string to split
|
||||
|
||||
self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.course_id = CourseKey.from_string("edX/toy/2012_Fall")
|
||||
self.student_role = models.Role.objects.get_or_create(name="Student",
|
||||
course_id=self.course_id)[0]
|
||||
self.student_role.add_permission("delete_thread")
|
||||
@@ -31,7 +31,7 @@ class RoleClassTestCase(ModuleStoreTestCase):
|
||||
course_id=self.course_id)[0]
|
||||
self.TA_role = models.Role.objects.get_or_create(name="Community TA",
|
||||
course_id=self.course_id)[0]
|
||||
self.course_id_2 = SlashSeparatedCourseKey("edx", "6.002x", "2012_Fall")
|
||||
self.course_id_2 = CourseKey.from_string("edX/6.002x/2012_Fall")
|
||||
self.TA_role_2 = models.Role.objects.get_or_create(name="Community TA",
|
||||
course_id=self.course_id_2)[0]
|
||||
|
||||
|
||||
@@ -206,6 +206,16 @@ define([
|
||||
expectPaymentButtonEnabled( true );
|
||||
});
|
||||
|
||||
it('displays an error if no payment processors are available', function () {
|
||||
var view = createView({processors: []});
|
||||
expect(view.errorModel.get('shown')).toBe(true);
|
||||
expect(view.errorModel.get('errorTitle')).toEqual(
|
||||
'All payment options are currently unavailable.'
|
||||
);
|
||||
expect(view.errorModel.get('errorMsg')).toEqual(
|
||||
'Try the transaction again in a few minutes.'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -56,7 +56,8 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
|
||||
var createView = function( displaySteps, currentStep ) {
|
||||
return new PayAndVerifyView({
|
||||
displaySteps: displaySteps,
|
||||
currentStep: currentStep
|
||||
currentStep: currentStep,
|
||||
errorModel: new ( Backbone.Model.extend({}) )()
|
||||
}).render();
|
||||
};
|
||||
|
||||
|
||||
@@ -105,10 +105,20 @@ var edx = edx || {};
|
||||
self._getProductText( templateContext.courseModeSlug, templateContext.upgrade )
|
||||
);
|
||||
|
||||
// create a button for each payment processor
|
||||
_.each(processors.reverse(), function(processorName) {
|
||||
$( 'div.payment-buttons' ).append( self._getPaymentButtonHtml(processorName) );
|
||||
});
|
||||
if (processors.length === 0) {
|
||||
// No payment processors are enabled at the moment, so show an error message
|
||||
this.errorModel.set({
|
||||
errorTitle: gettext('All payment options are currently unavailable.'),
|
||||
errorMsg: gettext('Try the transaction again in a few minutes.'),
|
||||
shown: true
|
||||
})
|
||||
}
|
||||
else {
|
||||
// create a button for each payment processor
|
||||
_.each(processors.reverse(), function(processorName) {
|
||||
$( 'div.payment-buttons' ).append( self._getPaymentButtonHtml(processorName) );
|
||||
});
|
||||
}
|
||||
|
||||
// Handle payment submission
|
||||
$( '.payment-button' ).on( 'click', _.bind( this.createOrder, this ) );
|
||||
|
||||
@@ -273,8 +273,8 @@ def run_jshint(options):
|
||||
_prepare_report_dir(jshint_report_dir)
|
||||
|
||||
sh(
|
||||
"jshint {root} --config .jshintrc >> {jshint_report}".format(
|
||||
root=Env.REPO_ROOT, jshint_report=jshint_report
|
||||
"jshint . --config .jshintrc >> {jshint_report}".format(
|
||||
jshint_report=jshint_report
|
||||
),
|
||||
ignore_error=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user