Commit 82daaa5a by Harry Rein

Adding a reviews page to the course and updating the reviews module on the…

Adding a reviews page to the course and updating the reviews module on the course about page. Removed the existing coursetalk implementation and put it in a fragment, removed the tests as well. Added configuration settings to specify the reviews template as well as the reviews provider. This is all protected by a waffle flag.
parent 4622b24a
...@@ -897,7 +897,6 @@ INSTALLED_APPS = ( ...@@ -897,7 +897,6 @@ INSTALLED_APPS = (
'openedx.core.djangoapps.external_auth', 'openedx.core.djangoapps.external_auth',
'student', # misleading name due to sharing with lms 'student', # misleading name due to sharing with lms
'openedx.core.djangoapps.course_groups', # not used in cms (yet), but tests run 'openedx.core.djangoapps.course_groups', # not used in cms (yet), but tests run
'openedx.core.djangoapps.coursetalk', # not used in cms (yet), but tests run
'xblock_config', 'xblock_config',
# Maintenance tools # Maintenance tools
......
...@@ -7,7 +7,7 @@ from nose.plugins.attrib import attr ...@@ -7,7 +7,7 @@ from nose.plugins.attrib import attr
from ...fixtures.course import CourseFixture, XBlockFixtureDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...pages.lms.bookmarks import BookmarksPage from ...pages.lms.bookmarks import BookmarksPage
from ...pages.lms.course_home import CourseHomePage, CourseSearchResultsPage from ...pages.lms.course_home import CourseHomePage
from ...pages.lms.courseware import CoursewarePage from ...pages.lms.courseware import CoursewarePage
from ..helpers import UniqueCourseTest, auto_auth, load_data_str from ..helpers import UniqueCourseTest, auto_auth, load_data_str
...@@ -124,7 +124,7 @@ class CourseHomeTest(CourseHomeBaseTest): ...@@ -124,7 +124,7 @@ class CourseHomeTest(CourseHomeBaseTest):
@attr('a11y') @attr('a11y')
class CourseHomeA11yTest(CourseHomeBaseTest): class CourseHomeA11yTest(CourseHomeBaseTest):
""" """
Tests the accessibility of the course home page with course outline. Tests the accessibility of the course home page
""" """
def test_course_home_a11y(self): def test_course_home_a11y(self):
......
...@@ -385,7 +385,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest ...@@ -385,7 +385,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_paced(self): def test_num_queries_instructor_paced(self):
self.fetch_course_info_with_queries(self.instructor_paced_course, 24, 4) self.fetch_course_info_with_queries(self.instructor_paced_course, 26, 4)
def test_num_queries_self_paced(self): def test_num_queries_self_paced(self):
self.fetch_course_info_with_queries(self.self_paced_course, 24, 4) self.fetch_course_info_with_queries(self.self_paced_course, 26, 4)
...@@ -32,8 +32,8 @@ from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_cour ...@@ -32,8 +32,8 @@ from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_cour
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG, default_course_url_name from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG, default_course_url_name
from openedx.features.enterprise_support.api import data_sharing_consent_required
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
from openedx.features.enterprise_support.api import data_sharing_consent_required
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from student.views import is_course_blocked from student.views import is_course_blocked
......
...@@ -6,7 +6,6 @@ import logging ...@@ -6,7 +6,6 @@ import logging
import urllib import urllib
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytz import utc
import analytics import analytics
from django.conf import settings from django.conf import settings
...@@ -30,6 +29,7 @@ from ipware.ip import get_ip ...@@ -30,6 +29,7 @@ from ipware.ip import get_ip
from markupsafe import escape from markupsafe import escape
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from pytz import utc
from rest_framework import status from rest_framework import status
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
...@@ -73,7 +73,6 @@ from lms.djangoapps.instructor.views.api import require_global_staff ...@@ -73,7 +73,6 @@ from lms.djangoapps.instructor.views.api import require_global_staff
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.catalog.utils import get_programs, get_programs_with_type from openedx.core.djangoapps.catalog.utils import get_programs, get_programs_with_type
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context
from openedx.core.djangoapps.credit.api import ( from openedx.core.djangoapps.credit.api import (
get_credit_requirement_status, get_credit_requirement_status,
is_credit_course, is_credit_course,
...@@ -85,7 +84,7 @@ from openedx.core.djangoapps.plugin_api.views import EdxFragmentView ...@@ -85,7 +84,7 @@ from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, UNIFIED_COURSE_VIEW_FLAG, course_home_url_name from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
from openedx.features.enterprise_support.api import data_sharing_consent_required from openedx.features.enterprise_support.api import data_sharing_consent_required
from shoppingcart.utils import is_shopping_cart_enabled from shoppingcart.utils import is_shopping_cart_enabled
...@@ -324,6 +323,14 @@ def course_info(request, course_id): ...@@ -324,6 +323,14 @@ def course_info(request, course_id):
if SelfPacedConfiguration.current().enable_course_home_improvements: if SelfPacedConfiguration.current().enable_course_home_improvements:
dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id) dates_fragment = CourseDatesFragmentView().render_to_fragment(request, course_id=course_id)
# This local import is due to the circularity of lms and openedx references.
# This may be resolved by using stevedore to allow web fragments to be used
# as plugins, and to avoid the direct import.
from openedx.features.course_experience.views.course_reviews import CourseReviewsModuleFragmentView
# Decide whether or not to show the reviews link in the course tools bar
show_reviews_link = CourseReviewsModuleFragmentView.is_configured()
context = { context = {
'request': request, 'request': request,
'masquerade_user': user, 'masquerade_user': user,
...@@ -338,6 +345,7 @@ def course_info(request, course_id): ...@@ -338,6 +345,7 @@ def course_info(request, course_id):
'user_is_enrolled': user_is_enrolled, 'user_is_enrolled': user_is_enrolled,
'dates_fragment': dates_fragment, 'dates_fragment': dates_fragment,
'url_to_enroll': url_to_enroll, 'url_to_enroll': url_to_enroll,
'show_reviews_link': show_reviews_link,
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, user, course.id), 'upgrade_link': check_and_get_upgrade_link(request, user, course.id),
'upgrade_price': get_cosmetic_verified_display_price(course), 'upgrade_price': get_cosmetic_verified_display_price(course),
...@@ -355,9 +363,6 @@ def course_info(request, course_id): ...@@ -355,9 +363,6 @@ def course_info(request, course_id):
context['disable_student_access'] = True context['disable_student_access'] = True
context['supports_preview_menu'] = False context['supports_preview_menu'] = False
if CourseEnrollment.is_enrolled(request.user, course.id):
inject_coursetalk_keys_into_context(context, course_key)
return render_to_response('courseware/info.html', context) return render_to_response('courseware/info.html', context)
...@@ -694,7 +699,6 @@ def course_about(request, course_id): ...@@ -694,7 +699,6 @@ def course_about(request, course_id):
""" """
Display the course's about page. Display the course's about page.
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
if hasattr(course_key, 'ccx'): if hasattr(course_key, 'ccx'):
...@@ -776,7 +780,7 @@ def course_about(request, course_id): ...@@ -776,7 +780,7 @@ def course_about(request, course_id):
# - Student is already registered for course # - Student is already registered for course
# - Course is already full # - Course is already full
# - Student cannot enroll in course # - Student cannot enroll in course
active_reg_button = not(registered or is_course_full or not can_enroll) active_reg_button = not (registered or is_course_full or not can_enroll)
is_shib_course = uses_shib(course) is_shib_course = uses_shib(course)
...@@ -786,6 +790,14 @@ def course_about(request, course_id): ...@@ -786,6 +790,14 @@ def course_about(request, course_id):
# Overview # Overview
overview = CourseOverview.get_from_id(course.id) overview = CourseOverview.get_from_id(course.id)
# This local import is due to the circularity of lms and openedx references.
# This may be resolved by using stevedore to allow web fragments to be used
# as plugins, and to avoid the direct import.
from openedx.features.course_experience.views.course_reviews import CourseReviewsModuleFragmentView
# Embed the course reviews tool
reviews_fragment_view = CourseReviewsModuleFragmentView().render_to_fragment(request, course=course)
context = { context = {
'course': course, 'course': course,
'course_details': course_details, 'course_details': course_details,
...@@ -814,8 +826,8 @@ def course_about(request, course_id): ...@@ -814,8 +826,8 @@ def course_about(request, course_id):
'cart_link': reverse('shoppingcart.views.show_cart'), 'cart_link': reverse('shoppingcart.views.show_cart'),
'pre_requisite_courses': pre_requisite_courses, 'pre_requisite_courses': pre_requisite_courses,
'course_image_urls': overview.image_urls, 'course_image_urls': overview.image_urls,
'reviews_fragment_view': reviews_fragment_view,
} }
inject_coursetalk_keys_into_context(context, course_key)
return render_to_response('courseware/course_about.html', context) return render_to_response('courseware/course_about.html', context)
......
...@@ -389,6 +389,10 @@ FEATURES = { ...@@ -389,6 +389,10 @@ FEATURES = {
'DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR': False, 'DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR': False,
} }
# Settings for the course reviews tool template and identification key, set either to None to disable course reviews
COURSE_REVIEWS_TOOL_PROVIDER_FRAGMENT_NAME = 'coursetalk-reviews-fragment.html'
COURSE_REVIEWS_TOOL_PROVIDER_PLATFORM_KEY = 'edx'
# Ignore static asset files on import which match this pattern # Ignore static asset files on import which match this pattern
ASSET_IGNORE_REGEX = r"(^\._.*$)|(^\.DS_Store$)|(^.*~$)" ASSET_IGNORE_REGEX = r"(^\._.*$)|(^\.DS_Store$)|(^.*~$)"
...@@ -2172,9 +2176,6 @@ INSTALLED_APPS = ( ...@@ -2172,9 +2176,6 @@ INSTALLED_APPS = (
# Static i18n support # Static i18n support
'statici18n', 'statici18n',
# Review widgets
'openedx.core.djangoapps.coursetalk',
# API access administration # API access administration
'openedx.core.djangoapps.api_admin', 'openedx.core.djangoapps.api_admin',
......
...@@ -295,6 +295,17 @@ div.info-wrapper { ...@@ -295,6 +295,17 @@ div.info-wrapper {
a { a {
color: $link-color; color: $link-color;
display: block;
font-size: 16px;
span {
width: 20px;
text-align: center;
}
&:not(:first-child) {
margin-top: 10px;
}
} }
&:after { &:after {
...@@ -307,7 +318,7 @@ div.info-wrapper { ...@@ -307,7 +318,7 @@ div.info-wrapper {
@extend %t-strong; @extend %t-strong;
@extend %t-title6; @extend %t-title6;
margin-bottom: 0; margin-bottom: 0;
padding: 12px 26px 20px 0; padding: 12px 26px 10px 0;
} }
ul { ul {
......
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
.course-sidebar { .course-sidebar {
@include margin-left(0); @include margin-left(0);
@include padding-left($baseline); @include padding-left($baseline);
.section-tools li:not(:first-child) {
margin-top: ($baseline / 5);
}
} }
// Course outline // Course outline
...@@ -194,3 +198,8 @@ ...@@ -194,3 +198,8 @@
} }
} }
} }
// Course Reviews Page
.course-reviews-tool {
margin: ($baseline * 2) ($baseline * 3);
}
...@@ -392,11 +392,6 @@ ...@@ -392,11 +392,6 @@
} }
} }
>.coursetalk-read-reviews {
margin-top: -200px;
margin-bottom: 220px;
}
header { header {
margin-bottom: 30px; margin-bottom: 30px;
padding-bottom: 16px; padding-bottom: 16px;
......
...@@ -5,6 +5,7 @@ from django.core.urlresolvers import reverse ...@@ -5,6 +5,7 @@ from django.core.urlresolvers import reverse
from courseware.courses import get_course_about_section from courseware.courses import get_course_about_section
from django.conf import settings from django.conf import settings
from edxmako.shortcuts import marketing_link from edxmako.shortcuts import marketing_link
from openedx.core.djangolib.markup import HTML
from openedx.core.lib.courses import course_image_url from openedx.core.lib.courses import course_image_url
%> %>
...@@ -17,10 +18,6 @@ from openedx.core.lib.courses import course_image_url ...@@ -17,10 +18,6 @@ from openedx.core.lib.courses import course_image_url
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
## CourseTalk widget js script
% if show_coursetalk_widget:
<script src="//d3q6qq2zt8nhwv.cloudfront.net/s/js/widgets/coursetalk-read-reviews.js"></script>
% endif
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
$(".register").click(function(event) { $(".register").click(function(event) {
...@@ -296,11 +293,9 @@ from openedx.core.lib.courses import course_image_url ...@@ -296,11 +293,9 @@ from openedx.core.lib.courses import course_image_url
</ol> </ol>
</div> </div>
## CourseTalk widget ## Course reviews tool
% if show_coursetalk_widget: % if reviews_fragment_view:
<div class="coursetalk-read-reviews"> ${HTML(reviews_fragment_view.body_html())}
<div id="ct-custom-read-review-widget" data-provider="${platform_key}" data-course="${course_review_key}"></div>
</div>
% endif % endif
## For now, ocw links are the only thing that goes in additional resources ## For now, ocw links are the only thing that goes in additional resources
......
...@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _ ...@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _
from courseware.courses import get_course_info_section, get_course_date_blocks from courseware.courses import get_course_info_section, get_course_date_blocks
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangolib.markup import HTML, Text from openedx.core.djangolib.markup import HTML, Text
from openedx.features.course_experience import SHOW_REVIEWS_TOOL_FLAG
%> %>
<%block name="pagetitle">${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block> <%block name="pagetitle">${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block>
...@@ -48,13 +49,6 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -48,13 +49,6 @@ from openedx.core.djangolib.markup import HTML, Text
CourseHomeEvents(); CourseHomeEvents();
</%static:require_module_async> </%static:require_module_async>
<%block name="js_extra">
## CourseTalk widget js script
% if show_coursetalk_widget:
<script src="//d3q6qq2zt8nhwv.cloudfront.net/s/js/widgets/coursetalk-write-reviews.js"></script>
% endif
</%block>
<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block> <%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block>
<main id="main" aria-label="Content" tabindex="-1"> <main id="main" aria-label="Content" tabindex="-1">
...@@ -89,14 +83,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -89,14 +83,7 @@ from openedx.core.djangolib.markup import HTML, Text
<h3 class="hd hd-3">${_("Course Updates and News")}</h3> <h3 class="hd hd-3">${_("Course Updates and News")}</h3>
${HTML(get_course_info_section(request, masquerade_user, course, 'updates'))} ${HTML(get_course_info_section(request, masquerade_user, course, 'updates'))}
## CourseTalk widget
% if show_coursetalk_widget:
<div class="coursetalk-write-reviews">
<div id="ct-custom-read-review-widget" data-provider="${platform_key}" data-course="${course_review_key}"></div>
</div>
% endif
</section> </section>
<section aria-label="${_('Handout Navigation')}" class="handouts"> <section aria-label="${_('Handout Navigation')}" class="handouts">
<h3 class="hd hd-3 handouts-header">${_("Course Tools")}</h3> <h3 class="hd hd-3 handouts-header">${_("Course Tools")}</h3>
<div> <div>
...@@ -104,6 +91,12 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -104,6 +91,12 @@ from openedx.core.djangolib.markup import HTML, Text
<span class="icon fa fa-bookmark" aria-hidden="true"></span> <span class="icon fa fa-bookmark" aria-hidden="true"></span>
${_("Bookmarks")} ${_("Bookmarks")}
</a> </a>
% if SHOW_REVIEWS_TOOL_FLAG.is_enabled(course.id) and show_reviews_link:
<a href="${reverse('openedx.course_experience.course_reviews', args=[course.id])}">
<span class="icon fa fa-star" aria-hidden="true"></span>
${_("Reviews")}
</a>
% endif
</div> </div>
% if SelfPacedConfiguration.current().enable_course_home_improvements: % if SelfPacedConfiguration.current().enable_course_home_improvements:
......
"""Manage coursetalk configuration. """
from config_models.admin import ConfigurationModelAdmin
from django.contrib import admin
from openedx.core.djangoapps.coursetalk.models import CourseTalkWidgetConfiguration
admin.site.register(CourseTalkWidgetConfiguration, ConfigurationModelAdmin)
"""
CourseTalk widget helpers
"""
from __future__ import unicode_literals
from openedx.core.djangoapps.coursetalk import models
def get_coursetalk_course_key(course_key):
"""
Return course key for coursetalk widget
CourseTalk unique key for a course contains only organization and course code.
:param course_key: SlashSeparatedCourseKey instance
:type course_key: SlashSeparatedCourseKey
:return: CourseTalk course key
:rtype: str
"""
return '{0.org}_{0.course}'.format(course_key)
def inject_coursetalk_keys_into_context(context, course_key):
"""
Set params to view context based on course_key and CourseTalkWidgetConfiguration
:param context: view context
:type context: dict
:param course_key: SlashSeparatedCourseKey instance
:type course_key: SlashSeparatedCourseKey
"""
show_coursetalk_widget = models.CourseTalkWidgetConfiguration.is_enabled()
if show_coursetalk_widget:
context['show_coursetalk_widget'] = True
context['platform_key'] = models.CourseTalkWidgetConfiguration.get_platform_key()
context['course_review_key'] = get_coursetalk_course_key(course_key)
"""
Models for CourseTalk configurations
"""
from __future__ import unicode_literals
from config_models.models import ConfigurationModel
from django.db import models
from django.utils.translation import ugettext_lazy as _
class CourseTalkWidgetConfiguration(ConfigurationModel):
"""
This model represents Enable Configuration for CourseTalk widget.
If the setting enabled, widget will will be available on course
info page and on course about page.
"""
platform_key = models.fields.CharField(
max_length=50,
help_text=_(
"The platform key associates CourseTalk widgets with your platform. "
"Generally, it is the domain name for your platform. For example, "
"if your platform is http://edx.org, the platform key is \"edx\"."
)
)
@classmethod
def get_platform_key(cls):
"""
Return platform_key for current active configuration.
If current configuration is not enabled - return empty string
:return: Platform key
:rtype: unicode
"""
return cls.current().platform_key if cls.is_enabled() else ''
def __unicode__(self):
return 'CourseTalkWidgetConfiguration - {0}'.format(self.enabled)
""" CourseTalk widget helpers tests """
from __future__ import unicode_literals
from django import test
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from openedx.core.djangoapps.coursetalk import helpers
from openedx.core.djangoapps.coursetalk import models
from openedx.core.djangolib.testing.utils import skip_unless_lms
@skip_unless_lms
class CourseTalkKeyTests(test.TestCase):
"""
CourseTalkKeyTests:
tests for function get_coursetalk_course_key
tests for function inject_coursetalk_keys_into_context
"""
PLATFORM_KEY = 'some_platform'
def setUp(self):
super(CourseTalkKeyTests, self).setUp()
self.course_key = SlashSeparatedCourseKey('org', 'course', 'run')
self.context = {}
def db_set_up(self, enabled):
"""
Setup database for this test:
Create CourseTalkWidgetConfiguration
"""
config = models.CourseTalkWidgetConfiguration.current()
config.enabled = enabled
config.platform_key = self.PLATFORM_KEY
config.save()
def test_simple_key(self):
coursetalk_course_key = helpers.get_coursetalk_course_key(self.course_key)
self.assertEqual(coursetalk_course_key, 'org_course')
def test_inject_coursetalk_keys_when_widget_not_enabled(self):
self.db_set_up(False)
helpers.inject_coursetalk_keys_into_context(self.context, self.course_key)
self.assertNotIn('show_coursetalk_widget', self.context)
self.assertNotIn('platform_key', self.context)
self.assertNotIn('course_review_key', self.context)
def test_inject_coursetalk_keys_when_widget_enabled(self):
self.db_set_up(True)
helpers.inject_coursetalk_keys_into_context(self.context, self.course_key)
self.assertIn('show_coursetalk_widget', self.context)
self.assertIn('platform_key', self.context)
self.assertIn('course_review_key', self.context)
self.assertEqual(self.context.get('show_coursetalk_widget'), True)
self.assertEqual(self.context.get('platform_key'), self.PLATFORM_KEY)
self.assertEqual(self.context.get('course_review_key'), 'org_course')
...@@ -20,7 +20,10 @@ WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience') ...@@ -20,7 +20,10 @@ WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab') UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
# Waffle flag to enable the sock on the footer of the home and courseware pages # Waffle flag to enable the sock on the footer of the home and courseware pages
DISPLAY_COURSE_SOCK = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'display_course_sock') DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'display_course_sock')
# Waffle flag to enable a review page link from the unified home page
SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_tool')
def course_home_page_title(course): # pylint: disable=unused-argument def course_home_page_title(course): # pylint: disable=unused-argument
......
...@@ -14,7 +14,7 @@ from django.core.urlresolvers import reverse ...@@ -14,7 +14,7 @@ from django.core.urlresolvers import reverse
from django_comment_client.permissions import has_permission from django_comment_client.permissions import has_permission
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REVIEWS_TOOL_FLAG
%> %>
<%block name="content"> <%block name="content">
...@@ -75,6 +75,14 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG ...@@ -75,6 +75,14 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
${_("Bookmarks")} ${_("Bookmarks")}
</a> </a>
</li> </li>
% if SHOW_REVIEWS_TOOL_FLAG.is_enabled(course.id) and show_reviews_link:
<li>
<a href="${reverse('openedx.course_experience.course_reviews', args=[course.id])}">
<span class="icon fa fa-star" aria-hidden="true"></span>
${_("Reviews")}
</a>
</li>
% endif
% if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id): % if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
<li> <li>
<a href="${reverse('openedx.course_experience.course_updates', args=[course.id])}"> <a href="${reverse('openedx.course_experience.course_updates', args=[course.id])}">
......
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import course_home_page_title
%>
<div class="course-reviews container" tabindex="-1">
<header class="page-header has-secondary">
## Breadcrumb navigation
<div class="page-header-main">
<nav aria-label="${_('Course Reviews')}" class="sr-is-focusable" tabindex="-1">
<div class="has-breadcrumbs">
<div class="breadcrumbs">
<span class="nav-item">
<a href="${course_url}">${course_home_page_title(course)}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('Course Reviews')}</span>
</div>
</div>
</nav>
</div>
</header>
<div class="course-reviews-tool">
% if course_reviews_provider_fragment:
${HTML(course_reviews_provider_fragment.body_html())}
% endif
</div>
</div>
...@@ -5,11 +5,11 @@ ...@@ -5,11 +5,11 @@
<%! <%!
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import DISPLAY_COURSE_SOCK from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
%> %>
<%block name="content"> <%block name="content">
% if show_course_sock and DISPLAY_COURSE_SOCK.is_enabled(course_id): % if show_course_sock and DISPLAY_COURSE_SOCK_FLAG.is_enabled(course_id):
<div class="verification-sock"> <div class="verification-sock">
<button type="button" class="btn btn-brand focusable action-toggle-verification-sock"> <button type="button" class="btn btn-brand focusable action-toggle-verification-sock">
Learn About Verified Certificate Learn About Verified Certificate
......
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from openedx.features.course_experience import SHOW_REVIEWS_TOOL_FLAG
%>
% if SHOW_REVIEWS_TOOL_FLAG.is_enabled(course.id):
<div class="coursetalk-read-reviews">
## Coursetalk Widget
<div id="ct-custom-read-review-widget" data-provider="${platform_key}" data-course="${course.id}"></div>
</div>
<script src="//d3q6qq2zt8nhwv.cloudfront.net/s/js/widgets/coursetalk-read-reviews.js"></script>
% endif
...@@ -89,7 +89,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase): ...@@ -89,7 +89,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
course_home_url(self.course) course_home_url(self.course)
# Fetch the view and verify the query counts # Fetch the view and verify the query counts
with self.assertNumQueries(49): with self.assertNumQueries(51):
with check_mongo_calls(5): with check_mongo_calls(5):
url = course_home_url(self.course) url = course_home_url(self.course)
self.client.get(url) self.client.get(url)
...@@ -7,7 +7,7 @@ import ddt ...@@ -7,7 +7,7 @@ import ddt
from course_modes.models import CourseMode from course_modes.models import CourseMode
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import DISPLAY_COURSE_SOCK from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -51,7 +51,7 @@ class TestCourseSockView(SharedModuleStoreTestCase): ...@@ -51,7 +51,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
# Log the user in # Log the user in
self.client.login(username=self.user.username, password=TEST_PASSWORD) self.client.login(username=self.user.username, password=TEST_PASSWORD)
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_standard_course(self): def test_standard_course(self):
""" """
Assure that a course that cannot be verified does Assure that a course that cannot be verified does
...@@ -61,7 +61,7 @@ class TestCourseSockView(SharedModuleStoreTestCase): ...@@ -61,7 +61,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
self.assertEqual(self.is_verified_sock_visible(response), False, self.assertEqual(self.is_verified_sock_visible(response), False,
'Student should not be able to see sock in a unverifiable course.') 'Student should not be able to see sock in a unverifiable course.')
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_verified_course(self): def test_verified_course(self):
""" """
Assure that a course that can be verified has a Assure that a course that can be verified has a
...@@ -71,7 +71,7 @@ class TestCourseSockView(SharedModuleStoreTestCase): ...@@ -71,7 +71,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
self.assertEqual(self.is_verified_sock_visible(response), True, self.assertEqual(self.is_verified_sock_visible(response), True,
'Student should be able to see sock in a verifiable course.') 'Student should be able to see sock in a verifiable course.')
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_verified_course_updated_expired(self): def test_verified_course_updated_expired(self):
""" """
Assure that a course that has an expired upgrade Assure that a course that has an expired upgrade
...@@ -81,7 +81,7 @@ class TestCourseSockView(SharedModuleStoreTestCase): ...@@ -81,7 +81,7 @@ class TestCourseSockView(SharedModuleStoreTestCase):
self.assertEqual(self.is_verified_sock_visible(response), False, self.assertEqual(self.is_verified_sock_visible(response), False,
'Student should be able to see sock in a verifiable course if the update expiration date has passed.') 'Student should be able to see sock in a verifiable course if the update expiration date has passed.')
@override_waffle_flag(DISPLAY_COURSE_SOCK, active=True) @override_waffle_flag(DISPLAY_COURSE_SOCK_FLAG, active=True)
def test_verified_course_user_already_upgraded(self): def test_verified_course_user_already_upgraded(self):
""" """
Assure that a user that has already upgraded to a Assure that a user that has already upgraded to a
......
...@@ -6,6 +6,7 @@ from django.conf.urls import url ...@@ -6,6 +6,7 @@ from django.conf.urls import url
from views.course_home import CourseHomeFragmentView, CourseHomeView from views.course_home import CourseHomeFragmentView, CourseHomeView
from views.course_outline import CourseOutlineFragmentView from views.course_outline import CourseOutlineFragmentView
from views.course_reviews import CourseReviewsView
from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView from views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
from views.course_sock import CourseSockFragmentView from views.course_sock import CourseSockFragmentView
from views.welcome_message import WelcomeMessageFragmentView, dismiss_welcome_message from views.welcome_message import WelcomeMessageFragmentView, dismiss_welcome_message
...@@ -22,6 +23,11 @@ urlpatterns = [ ...@@ -22,6 +23,11 @@ urlpatterns = [
name='openedx.course_experience.course_updates', name='openedx.course_experience.course_updates',
), ),
url( url(
r'^reviews$',
CourseReviewsView.as_view(),
name='openedx.course_experience.course_reviews',
),
url(
r'^home_fragment$', r'^home_fragment$',
CourseHomeFragmentView.as_view(), CourseHomeFragmentView.as_view(),
name='openedx.course_experience.course_home_fragment_view', name='openedx.course_experience.course_home_fragment_view',
......
...@@ -19,6 +19,7 @@ from util.views import ensure_valid_course_key ...@@ -19,6 +19,7 @@ from util.views import ensure_valid_course_key
from ..utils import get_course_outline_block_tree from ..utils import get_course_outline_block_tree
from .course_dates import CourseDatesFragmentView from .course_dates import CourseDatesFragmentView
from .course_outline import CourseOutlineFragmentView from .course_outline import CourseOutlineFragmentView
from .course_reviews import CourseReviewsModuleFragmentView
from .welcome_message import WelcomeMessageFragmentView from .welcome_message import WelcomeMessageFragmentView
from .course_sock import CourseSockFragmentView from .course_sock import CourseSockFragmentView
...@@ -112,6 +113,9 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -112,6 +113,9 @@ class CourseHomeFragmentView(EdxFragmentView):
# Get the handouts # Get the handouts
handouts_html = get_course_info_section(request, request.user, course, 'handouts') handouts_html = get_course_info_section(request, request.user, course, 'handouts')
# Only show the reviews button if configured
show_reviews_link = CourseReviewsModuleFragmentView.is_configured()
# Render the course home fragment # Render the course home fragment
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
...@@ -126,6 +130,7 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -126,6 +130,7 @@ class CourseHomeFragmentView(EdxFragmentView):
'course_sock_fragment': course_sock_fragment, 'course_sock_fragment': course_sock_fragment,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True, 'uses_pattern_library': True,
'show_reviews_link': show_reviews_link,
} }
html = render_to_string('course_experience/course-home-fragment.html', context) html = render_to_string('course_experience/course-home-fragment.html', context)
return Fragment(html) return Fragment(html)
"""
Fragment for rendering the course reviews panel
"""
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
from courseware.courses import get_course_with_access
from lms.djangoapps.courseware.views.views import CourseTabView
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.features.course_experience import default_course_url_name
class CourseReviewsView(CourseTabView):
"""
The course reviews page.
"""
@method_decorator(login_required)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
def get(self, request, course_id, **kwargs):
"""
Displays the reviews page for the specified course.
"""
return super(CourseReviewsView, self).get(request, course_id, 'courseware', **kwargs)
def render_to_fragment(self, request, course=None, tab=None, **kwargs):
course_id = unicode(course.id)
reviews_fragment_view = CourseReviewsFragmentView()
return reviews_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs)
class CourseReviewsFragmentView(EdxFragmentView):
"""
A fragment to display course reviews.
"""
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Fragment to render the course reviews fragment.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_url_name = default_course_url_name(request)
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
# Create the fragment
course_reviews_provider_fragment = CourseReviewsModuleFragmentView().render_to_fragment(
request,
course=course,
**kwargs
)
context = {
'course': course,
'course_url': course_url,
'course_reviews_provider_fragment': course_reviews_provider_fragment
}
html = render_to_string('course_experience/course-reviews-fragment.html', context)
return Fragment(html)
class CourseReviewsModuleFragmentView(EdxFragmentView):
"""
A fragment to display the course reviews module as specified by
the configured template.
"""
def render_to_fragment(self, request, course=None, **kwargs):
"""
Renders the configured template as a module.
There are two relevant configuration settings:
COURSE_REVIEWS_TOOL_PROVIDER_FRAGMENT_NAME points to the template that
will be rendered and returned.
COURSE_REVIEWS_TOOL_PROVIDER_PLATFORM_KEY references the platform that
hosts the course. Generally, this is the domain name of the platform,
for example, 'edx.org' would have a platform key of 'edx'.
"""
# Grab the fragment type and provider from the configuration file
course_reviews_fragment_provider_template = \
settings.COURSE_REVIEWS_TOOL_PROVIDER_FRAGMENT_NAME
course_platform_key = \
settings.COURSE_REVIEWS_TOOL_PROVIDER_PLATFORM_KEY
if not self.is_configured():
return None
context = {
'course': course,
'platform_key': course_platform_key
}
# Create the fragment from the given template
provider_reviews_template = 'course_experience/course_reviews_modules/%s' \
% course_reviews_fragment_provider_template
html = render_to_string(provider_reviews_template, context)
return Fragment(html)
@classmethod
def is_configured(self):
return settings.COURSE_REVIEWS_TOOL_PROVIDER_FRAGMENT_NAME \
and settings.COURSE_REVIEWS_TOOL_PROVIDER_PLATFORM_KEY
""" """
Fragment for rendering the course's sock and associated toggle button. Fragment for rendering the course's sock and associated toggle button.
""" """
from datetime import datetime
from django.conf import settings
from django.template.loader import render_to_string from django.template.loader import render_to_string
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment from web_fragments.fragment import Fragment
...@@ -11,7 +8,6 @@ from web_fragments.fragment import Fragment ...@@ -11,7 +8,6 @@ from web_fragments.fragment import Fragment
from student.models import CourseEnrollment from student.models import CourseEnrollment
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.date_summary import VerifiedUpgradeDeadlineDate from courseware.date_summary import VerifiedUpgradeDeadlineDate
from courseware.courses import get_course_with_access
from courseware.views.views import get_course_prices from courseware.views.views import get_course_prices
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
......
...@@ -39,7 +39,7 @@ class CourseUpdatesView(CourseTabView): ...@@ -39,7 +39,7 @@ class CourseUpdatesView(CourseTabView):
class CourseUpdatesFragmentView(EdxFragmentView): class CourseUpdatesFragmentView(EdxFragmentView):
""" """
A fragment to render the home page for a course. A fragment to render the updates page for a course.
""" """
STATUS_VISIBLE = 'visible' STATUS_VISIBLE = 'visible'
STATUS_DELETED = 'deleted' STATUS_DELETED = 'deleted'
......
...@@ -130,6 +130,7 @@ var wpconfig = { ...@@ -130,6 +130,7 @@ var wpconfig = {
externals: { externals: {
backbone: 'Backbone', backbone: 'Backbone',
coursetalk: 'CourseTalk',
gettext: 'gettext', gettext: 'gettext',
jquery: 'jQuery', jquery: 'jQuery',
logger: 'Logger', logger: 'Logger',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment