Skip to content
Snippets Groups Projects
Commit 141e0a93 authored by Brian Beggs's avatar Brian Beggs
Browse files

Merge branch 'master' into dj18-release-merge

parents 0e66fad1 290de455
No related branches found
No related tags found
No related merge requests found
Showing
with 699 additions and 473 deletions
......@@ -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.
......
"""
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(
......
This diff is collapsed.
......@@ -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:
......
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 ) );
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment