Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
141e0a93
Commit
141e0a93
authored
Nov 19, 2015
by
Brian Beggs
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into dj18-release-merge
parents
0e66fad1
290de455
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
701 additions
and
475 deletions
+701
-475
cms/static/js/models/settings/course_details.js
+0
-3
cms/static/js/spec/views/settings/main_spec.js
+0
-7
common/test/acceptance/tests/lms/test_lms_user_preview.py
+8
-0
common/test/acceptance/tests/studio/test_import_export.py
+9
-0
common/test/acceptance/tests/studio/test_studio_container.py
+8
-5
common/test/acceptance/tests/studio/test_studio_outline.py
+1
-0
common/test/acceptance/tests/studio/test_studio_settings_details.py
+4
-0
lms/djangoapps/certificates/tests/factories.py
+14
-1
lms/djangoapps/certificates/tests/test_views.py
+16
-26
lms/djangoapps/certificates/tests/test_webview_views.py
+102
-11
lms/djangoapps/certificates/views/webview.py
+328
-270
lms/djangoapps/discussion_api/api.py
+3
-21
lms/djangoapps/django_comment_client/base/tests.py
+36
-2
lms/djangoapps/django_comment_client/base/views.py
+138
-112
lms/djangoapps/django_comment_client/management/commands/get_discussion_link.py
+1
-5
lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
+2
-2
lms/djangoapps/django_comment_client/tests/test_models.py
+3
-3
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
+10
-0
lms/static/js/spec/verify_student/pay_and_verify_view_spec.js
+2
-1
lms/static/js/verify_student/views/make_payment_step_view.js
+14
-4
pavelib/quality.py
+2
-2
No files found.
cms/static/js/models/settings/course_details.js
View file @
141e0a93
...
...
@@ -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."
);
}
...
...
cms/static/js/spec/views/settings/main_spec.js
View file @
141e0a93
...
...
@@ -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
),
...
...
common/test/acceptance/tests/lms/test_lms_user_preview.py
View file @
141e0a93
...
...
@@ -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.
...
...
common/test/acceptance/tests/studio/test_import_export.py
View file @
141e0a93
"""
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
...
...
common/test/acceptance/tests/studio/test_studio_container.py
View file @
141e0a93
...
...
@@ -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.
...
...
common/test/acceptance/tests/studio/test_studio_outline.py
View file @
141e0a93
...
...
@@ -1755,6 +1755,7 @@ class DeprecationWarningMessageTest(CourseOutlineTest):
)
@attr
(
'shard_4'
)
class
SelfPacedOutlineTest
(
CourseOutlineTest
):
"""Test the course outline for a self-paced course."""
...
...
common/test/acceptance/tests/studio/test_studio_settings_details.py
View file @
141e0a93
...
...
@@ -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."""
...
...
lms/djangoapps/certificates/tests/factories.py
View file @
141e0a93
...
...
@@ -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"
}
}
}"""
...
...
lms/djangoapps/certificates/tests/test_views.py
View file @
141e0a93
...
...
@@ -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
)
...
...
lms/djangoapps/certificates/tests/test_webview_views.py
View file @
141e0a93
...
...
@@ -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
...
...
@@ -222,6 +222,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
(
user_id
=
self
.
user
.
id
,
...
...
@@ -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
(
...
...
lms/djangoapps/certificates/views/webview.py
View file @
141e0a93
# 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: 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 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'
]
)
context
[
'accomplishment_more_title'
]
=
_
(
"More Information About {user_name}'s Certificate:"
)
.
format
(
user_name
=
user_fullname
# 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
)
# 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'
)
# 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"
))
c
ontext
[
'certificate_info_title'
]
=
_
(
'About {platform_name} Certificates'
)
.
format
(
platform_name
=
platform_name
)
c
ertificate_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,35 +221,103 @@ 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
def
_update_course_context
(
request
,
context
,
course
,
platform_name
):
"""
Updates context dictionary with course info.
"""
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
:
# 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
)
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
)
)
# 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"
)
share_url
=
request
.
build_absolute_uri
(
reverse
(
'certificates:html_view'
,
kwargs
=
dict
(
user_id
=
str
(
user
.
id
),
course_id
=
unicode
(
course
.
id
))
)
)
context
[
'share_url'
]
=
share_url
twitter_url
=
''
if
context
.
get
(
'twitter_share_enabled'
,
False
):
twitter_url
=
'https://twitter.com/intent/tweet?text={twitter_share_text}&url={share_url}'
.
format
(
twitter_share_text
=
smart_str
(
context
[
'twitter_share_text'
]),
share_url
=
urllib
.
quote_plus
(
smart_str
(
share_url
))
)
context
[
'twitter_url'
]
=
twitter_url
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.
linkedin_config
=
LinkedInAddToProfileConfiguration
.
current
()
certificate_type_description
=
get_certificate_description
(
user_certificate
.
mode
,
certificate_type
,
platform_name
)
if
certificate_type_description
:
context
[
'certificate_type_description'
]
=
certificate_type_description
# posting certificates to LinkedIn is not currently
# supported in microsites/White Labels
if
linkedin_config
.
enabled
and
not
microsite
.
is_request_in_microsite
():
context
[
'linked_in_url'
]
=
linkedin_config
.
add_to_profile_url
(
course
.
id
,
course
.
display_name
,
user_certificate
.
mode
,
smart_str
(
request
.
build_absolute_uri
(
get_certificate_url
(
user_id
=
user
.
id
,
course_id
=
unicode
(
course
.
id
)
)))
)
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
...
...
@@ -281,67 +334,13 @@ def _update_certificate_context(context, course, user, user_certificate):
)
@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
_get_user_certificate
(
request
,
user
,
course_key
,
course
,
preview_mode
=
None
):
"""
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
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.
"""
# 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
(
...
...
@@ -359,126 +358,54 @@ def render_html_view(request, user_id, course_id):
# 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
)
):
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
)
else
:
context
[
'certificate_data'
]
=
active_configuration
return
None
# Append/Override the existing view context values with any mode-specific ConfigurationModel values
context
.
update
(
configuration
.
get
(
user_certificate
.
mode
,
{}))
return
user_certificate
# Append/Override the existing view context values with request-time values
_update_certificate_context
(
context
,
course
,
user
,
user_certificate
)
share_url
=
request
.
build_absolute_uri
(
reverse
(
'certificates:html_view'
,
kwargs
=
dict
(
user_id
=
str
(
user_id
),
course_id
=
unicode
(
course_id
))
)
)
context
[
'share_url'
]
=
share_url
twitter_url
=
''
if
context
.
get
(
'twitter_share_enabled'
,
False
):
twitter_url
=
'https://twitter.com/intent/tweet?text={twitter_share_text}&url={share_url}'
.
format
(
twitter_share_text
=
smart_str
(
context
[
'twitter_share_text'
]),
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
))
# 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.
linkedin_config
=
LinkedInAddToProfileConfiguration
.
current
()
# posting certificates to LinkedIn is not currently
# supported in microsites/White Labels
if
linkedin_config
.
enabled
and
not
microsite
.
is_request_in_microsite
():
context
[
'linked_in_url'
]
=
linkedin_config
.
add_to_profile_url
(
course
.
id
,
course
.
display_name
,
user_certificate
.
mode
,
smart_str
(
request
.
build_absolute_uri
(
get_certificate_url
(
user_id
=
user
.
id
,
course_id
=
unicode
(
course
.
id
)
)))
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'
],
}
)
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
,
{}))
# 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
)
lms/djangoapps/discussion_api/api.py
View file @
141e0a93
...
...
@@ -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
...
...
lms/djangoapps/django_comment_client/base/tests.py
View file @
141e0a93
...
...
@@ -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
SlashSeparated
CourseKey
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
)
...
...
lms/djangoapps/django_comment_client/base/views.py
View file @
141e0a93
...
...
@@ -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
...
...
@@ -579,28 +621,12 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
@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
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
:
...
...
lms/djangoapps/django_comment_client/management/commands/get_discussion_link.py
View file @
141e0a93
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
:
...
...
lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
View file @
141e0a93
...
...
@@ -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
SlashSeparated
CourseKey
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
)
lms/djangoapps/django_comment_client/tests/test_models.py
View file @
141e0a93
...
...
@@ -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
SlashSeparated
CourseKey
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
]
...
...
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
View file @
141e0a93
...
...
@@ -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.'
);
});
});
}
);
lms/static/js/spec/verify_student/pay_and_verify_view_spec.js
View file @
141e0a93
...
...
@@ -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
();
};
...
...
lms/static/js/verify_student/views/make_payment_step_view.js
View file @
141e0a93
...
...
@@ -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
)
);
...
...
pavelib/quality.py
View file @
141e0a93
...
...
@@ -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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment