Merge pull request #20945 from edx/tuchfarber/remove_journals
Remove all references to Journals
This commit is contained in:
@@ -45,7 +45,6 @@ from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_bann
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.enterprise_support.api import get_dashboard_consent_notification
|
||||
from openedx.features.journals.api import journals_enabled
|
||||
from shoppingcart.api import order_history
|
||||
from shoppingcart.models import CourseRegistrationCode, DonationConfiguration
|
||||
from student.helpers import cert_info, check_verify_status_by_course, get_resume_urls_for_enrollments
|
||||
@@ -873,7 +872,6 @@ def student_dashboard(request):
|
||||
'nav_hidden': True,
|
||||
'inverted_programs': inverted_programs,
|
||||
'show_program_listing': ProgramsApiConfig.is_enabled(),
|
||||
'show_journal_listing': journals_enabled(), # TODO: Dashboard Plugin required
|
||||
'show_dashboard_tabs': True,
|
||||
'disable_courseware_js': True,
|
||||
'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard,
|
||||
|
||||
@@ -64,7 +64,6 @@ from openedx.core.djangoapps.user_api.errors import UserAPIInternalError, UserNo
|
||||
from openedx.core.djangoapps.user_api.models import UserRetirementRequest
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.journals.api import get_journals_context
|
||||
from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES, cert_info, generate_activation_email_context
|
||||
from student.message_types import EmailChange, EmailChangeConfirmation, PasswordReset, RecoveryEmailCreate
|
||||
@@ -176,9 +175,6 @@ def index(request, extra_context=None, user=AnonymousUser()):
|
||||
# Add marketable programs to the context.
|
||||
context['programs_list'] = get_programs_with_type(request.site, include_hidden=False)
|
||||
|
||||
# TODO: Course Listing Plugin required
|
||||
context['journal_info'] = get_journals_context(request)
|
||||
|
||||
return render_to_response('index.html', context)
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@ from openedx.features.course_experience.views.course_dates import CourseDatesFra
|
||||
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
|
||||
from openedx.features.course_experience.waffle import waffle as course_experience_waffle
|
||||
from openedx.features.enterprise_support.api import data_sharing_consent_required
|
||||
from openedx.features.journals.api import get_journals_context
|
||||
from shoppingcart.utils import is_shopping_cart_enabled
|
||||
from student.models import CourseEnrollment, UserTestGroup
|
||||
from track import segment
|
||||
@@ -247,7 +246,6 @@ def courses(request):
|
||||
'courses': courses_list,
|
||||
'course_discovery_meanings': course_discovery_meanings,
|
||||
'programs_list': programs_list,
|
||||
'journal_info': get_journals_context(request), # TODO: Course Listing Plugin required
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.views.decorators.http import require_GET
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.learner_dashboard.programs import ProgramDetailsFragmentView, ProgramsFragmentView
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.features.journals.api import journals_enabled
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -23,7 +22,6 @@ def program_listing(request):
|
||||
'nav_hidden': True,
|
||||
'show_dashboard_tabs': True,
|
||||
'show_program_listing': programs_config.enabled,
|
||||
'show_journal_listing': journals_enabled(), # TODO: Dashboard Plugin required
|
||||
'uses_pattern_library': True,
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
@import 'features/bookmarks-v1';
|
||||
@import "features/announcements";
|
||||
@import 'features/learner-profile';
|
||||
@import 'features/journals';
|
||||
@import 'features/_unsupported-browser-alert';
|
||||
@import 'features/content-type-gating';
|
||||
@import 'features/course-duration-limits';
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
@import 'features/course-search';
|
||||
@import 'features/course-sock';
|
||||
@import 'features/course-upgrade-message';
|
||||
@import 'features/journals';
|
||||
@import 'features/content-type-gating';
|
||||
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// journal catalog listing
|
||||
.journals-listing-item {
|
||||
box-sizing: border-box;
|
||||
box-shadow: 1px 2px 5px #ccc;
|
||||
position: relative;
|
||||
height: 360px;
|
||||
overflow: visible;
|
||||
min-height: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
margin: 0 auto 40px;
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
|
||||
.journal-image {
|
||||
height: 142px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover-image {
|
||||
height: 142px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
background: black;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
left: calc(50% - 100px);
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 62px;
|
||||
padding: 0 20px;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
border-color: #0074b4;
|
||||
border-radius: 3px;
|
||||
background: #0074b4;
|
||||
color: #fff;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
text-transform: none;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
background: #065784;
|
||||
color: white;
|
||||
|
||||
@include padding-right(15px);
|
||||
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 0.7em;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.journal-info {
|
||||
padding: 12px 15px 5px;
|
||||
|
||||
.journal-org {
|
||||
font-weight: normal;
|
||||
font-size: 0.9em;
|
||||
color: #3d3e3f;
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.journal-title {
|
||||
max-height: 55px;
|
||||
overflow: hidden;
|
||||
color: #222;
|
||||
font-size: 1.25em;
|
||||
line-height: 1.333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.journal-subtitle {
|
||||
font-size: 1em;
|
||||
margin-bottom: 33px;
|
||||
line-height: 1.25em;
|
||||
height: 40px;
|
||||
color: #646464;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.journal-footer {
|
||||
display: table;
|
||||
width: 100%;
|
||||
padding: 0 15px 15px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
.availability,
|
||||
.journal-logo {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.availability {
|
||||
text-align: left;
|
||||
font-size: 0.9em;
|
||||
line-height: 20px;
|
||||
color: #3d3e3f;
|
||||
}
|
||||
|
||||
.journal-logo {
|
||||
text-align: right;
|
||||
width: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #d5d5d5;
|
||||
border: 1px solid #b5b5b5;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@include left(-5px);
|
||||
|
||||
top: -5px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include left(-10px);
|
||||
|
||||
top: -10px;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
.journal-image {
|
||||
.cover-image {
|
||||
.learn-more {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,6 @@
|
||||
|
||||
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
|
||||
<section class="courses">
|
||||
<ul class="journal-list">
|
||||
% for bundle in journal_info.get('journal_bundles'):
|
||||
<li class="courses-listing-item">
|
||||
<%include file="journals/bundle_card.html" args="bundle=bundle"/>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
<ul class="journal-list">
|
||||
%for journal in journal_info.get('journals'):
|
||||
<li class="courses-listing-item">
|
||||
<%include file="journals/journal_card.html" args="journal=journal" />
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
<ul class="courses-listing">
|
||||
## limiting the course number by using HOMEPAGE_COURSE_MAX as the maximum number of courses
|
||||
%for course in courses[:homepage_course_max]:
|
||||
|
||||
@@ -57,20 +57,6 @@
|
||||
% endif
|
||||
|
||||
<div class="courses${'' if course_discovery_enabled else ' no-course-discovery'}" role="region" aria-label="${_('List of Courses')}">
|
||||
<ul class="journal-list">
|
||||
% for bundle in journal_info.get('journal_bundles'):
|
||||
<li class="courses-listing-item">
|
||||
<%include file="../journals/bundle_card.html" args="bundle=bundle" />
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
<ul class="journal-list">
|
||||
%for journal in journal_info.get('journals'):
|
||||
<li class="courses-listing-item">
|
||||
<%include file="../journals/journal_card.html" args="journal=journal" />
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
<ul class="courses-listing courses-list">
|
||||
%for course in courses:
|
||||
<li class="courses-listing-item">
|
||||
|
||||
@@ -43,14 +43,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
% if show_journal_listing:
|
||||
<div class="mobile-nav-item hidden-mobile nav-item nav-tab">
|
||||
<a class="${'active ' if reverse('openedx.journals.dashboard') in request.path else ''}tab-nav-link" href="${reverse('openedx.journals.dashboard')}"
|
||||
aria-current="${'page' if reverse('openedx.journals.dashboard') == request.path else 'false'}">
|
||||
${_("Journals")}
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
% if show_explore_courses:
|
||||
<div class="mobile-nav-item hidden-mobile nav-item nav-tab">
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<header class="wrapper-header-courses">
|
||||
<h2 class="header-courses">${_("My Journals")}</h2>
|
||||
</header>
|
||||
@@ -46,13 +46,6 @@ from django.utils.translation import ugettext as _
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% if show_journal_listing:
|
||||
<li class="nav-item mt-2 nav-item-open-collapsed">
|
||||
<a class="nav-link active" href="${reverse('openedx.journals.dashboard')}">
|
||||
${_("Journals")}
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD','') and user.is_staff:
|
||||
|
||||
@@ -29,13 +29,6 @@ from django.utils.translation import ugettext as _
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% if show_journal_listing:
|
||||
<li class="tab-nav-item">
|
||||
<a class="${'active ' if reverse('openedx.journals.dashboard') in request.path else ''}tab-nav-link" href="${reverse('openedx.journals.dashboard')}">
|
||||
${_("Journals")}
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
% if settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD','') and user.is_staff:
|
||||
<li class="item">
|
||||
|
||||
@@ -39,10 +39,6 @@ class Command(BaseCommand):
|
||||
discovery_user = None
|
||||
discovery_base_url_fmt = None
|
||||
discovery_oidc_url = None
|
||||
journals = False
|
||||
journals_user = None
|
||||
journals_base_url_fmt = None
|
||||
journals_oidc_url = None
|
||||
|
||||
configuration_filename = None
|
||||
|
||||
@@ -70,13 +66,6 @@ class Command(BaseCommand):
|
||||
help="Use devstack config, otherwise sandbox config is assumed",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--enable-journals",
|
||||
dest="journals",
|
||||
action="store_true",
|
||||
help="Enable journal configuration",
|
||||
)
|
||||
|
||||
def _create_oauth2_client(self, url, site_name, service_name, service_user):
|
||||
"""
|
||||
Creates the oauth2 client and add it in trusted clients.
|
||||
@@ -192,7 +181,7 @@ class Command(BaseCommand):
|
||||
|
||||
def get_or_create_service_user(self, username):
|
||||
"""
|
||||
Creates the service user for ecommerce, discovery and journals.
|
||||
Creates the service user for ecommerce and discovery.
|
||||
"""
|
||||
service_user, _ = User.objects.get_or_create(username=username)
|
||||
service_user.is_active = True
|
||||
@@ -212,7 +201,6 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
self.dns_name = options['dns_name']
|
||||
self.theme_path = options['theme_path']
|
||||
self.journals = options['journals']
|
||||
|
||||
if options['devstack']:
|
||||
configuration_prefix = "devstack"
|
||||
@@ -220,22 +208,16 @@ class Command(BaseCommand):
|
||||
self.discovery_base_url_fmt = "http://discovery-{site_domain}:18381/"
|
||||
self.ecommerce_oidc_url = "http://ecommerce-{}.e2e.devstack:18130/complete/edx-oidc/".format(self.dns_name)
|
||||
self.ecommerce_base_url_fmt = "http://ecommerce-{site_domain}:18130/"
|
||||
self.journals_oidc_url = "http://journals-{}.e2e.devstack:18606/complete/edx-oidc/".format(self.dns_name)
|
||||
self.journals_base_url_fmt = "http://journals-{site_domain}:18606/"
|
||||
else:
|
||||
configuration_prefix = "sandbox"
|
||||
self.discovery_oidc_url = "https://discovery-{}.sandbox.edx.org/complete/edx-oidc/".format(self.dns_name)
|
||||
self.discovery_base_url_fmt = "https://discovery-{site_domain}/"
|
||||
self.ecommerce_oidc_url = "https://ecommerce-{}.sandbox.edx.org/complete/edx-oidc/".format(self.dns_name)
|
||||
self.ecommerce_base_url_fmt = "https://ecommerce-{site_domain}/"
|
||||
self.journals_oidc_url = "https://journals-{}.sandbox.edx.org/complete/edx-oidc/".format(self.dns_name)
|
||||
self.journals_base_url_fmt = "https://journals-{site_domain}/"
|
||||
|
||||
self.configuration_filename = '{}_configuration.json'.format(configuration_prefix)
|
||||
self.discovery_user = self.get_or_create_service_user("lms_catalog_service_user")
|
||||
self.ecommerce_user = self.get_or_create_service_user("ecommerce_worker")
|
||||
if self.journals:
|
||||
self.journals_user = self.get_or_create_service_user("journals_worker")
|
||||
|
||||
all_sites = self._get_sites_data()
|
||||
self._update_default_clients()
|
||||
@@ -246,7 +228,6 @@ class Command(BaseCommand):
|
||||
|
||||
discovery_url = self.discovery_base_url_fmt.format(site_domain=site_domain)
|
||||
ecommerce_url = self.ecommerce_base_url_fmt.format(site_domain=site_domain)
|
||||
journals_url = self.journals_base_url_fmt.format(site_domain=site_domain)
|
||||
|
||||
LOG.info(u"Creating '{site_name}' Site".format(site_name=site_name))
|
||||
self._create_sites(site_domain, site_data['theme_dir_name'], site_data['configuration'])
|
||||
@@ -257,8 +238,4 @@ class Command(BaseCommand):
|
||||
LOG.info(u"Creating ecommerce oauth2 client for '{site_name}' site".format(site_name=site_name))
|
||||
self._create_oauth2_client(ecommerce_url, site_name, 'ecommerce', self.ecommerce_user)
|
||||
|
||||
if self.journals:
|
||||
LOG.info(u"Creating journals oauth2 client for '{site_name}' site".format(site_name=site_name))
|
||||
self._create_oauth2_client(journals_url, site_name, 'journals', self.journals_user)
|
||||
|
||||
self._enable_commerce_configuration()
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
Journals
|
||||
---------
|
||||
|
||||
This directory contains a Django application that allows a learners to interact
|
||||
with a content Journal. The majority of the capabilities
|
||||
are provided through the Journals IDA here: `https://github.com/edx/journals`
|
||||
and through Journal modules added to Discovery and Ecommerce services.
|
||||
|
||||
**Journal**:
|
||||
|
||||
The Journal is a new product type that will be offered for purchase in the LMS. It is indepedent from a course, and contains a collection of resources/content-types (documents, videos, rich text, etc) that can be updated easily through the Journal service. A Journal is linked to an organization and you can purchase/receive access to it. One notable difference is that a Journal will have an access_length, which determines the amount of time the learner will have access to it post-purchase. This is our first stage towards a subscription model. The Discovery Service is the source-of-truth for all Journal related marketing information that is displayed in the LMS.
|
||||
|
||||
**JournalBundle**:
|
||||
|
||||
The Journal Bundle is a collection of Journals and Courses. It works similar to a program in the bundling aspect, the difference lies in the fact that it doesn't necessarily constitute a progression of courses. The first (and possibly most common) use case that will use this is bundling a single course with a single journal. Journal Bundles are defined in Discovery Service.
|
||||
|
||||
**Things to note**:
|
||||
|
||||
- The Journals app was intentionally decoupled as much as possible from the rest of the LMS, both for future developer sanity, and to minimally affect the rest of the platform should the scope of the Journals product change.
|
||||
|
||||
- The Journals product has a seperate IDA (Journals) which works as the publishing platform, the consumption platform, and the marketing platform (for standalone Journals). This is different from the structure of studio/lms and so some information may be handled differently in this application.
|
||||
|
||||
**Functionality Added to LMS to support Journals**:
|
||||
|
||||
- "Cards" for Journals and Journal Bundles on main LMS index page such that learners can discover and purchase Journals. These are the equivalent to course cards and Program cards in the LMS.
|
||||
|
||||
- Journals dashboard, that lists Journals that have been purchased by the learner. Similar to Programs dashboard in LMS.
|
||||
|
||||
**API**:
|
||||
|
||||
api.py - This class provides an abstraction to Journal specific information that is needed by the LMS. Specifically, it provides an api to fetch Journals and Journal Bundles from the Discovery Service, both of which are needed to display Journals information on main LMS index page as well as dashboard. Additionally it provides an api to fetch JournalAccess records from the Journal Service. This is used to determine which Journals a user has access to and displays this information on the "Journals" Dashboard.
|
||||
|
||||
**Views**:
|
||||
|
||||
learner_dashboard.py: This view adds a "Journals" tab to the dashboard and displays a list of Journals that the learner has access to. Clicking on a Journal from the dashboard will direct learner to the Journal Service to view the Journal.
|
||||
|
||||
marketing.py: This view provides a marketing page for Journal Bundles. It can be thought of as the equivalent to a Program About page. Specifically, it will show information about Journal(s) and Course(s) which have been bundled together for marketing and discouting purposes. The bundle definition and meta-data lives in the Discovery service.
|
||||
@@ -1,360 +0,0 @@
|
||||
"""
|
||||
APIs providing support for Journals functionality.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
import hashlib
|
||||
import logging
|
||||
import six
|
||||
|
||||
from six.moves.urllib.parse import urljoin, urlsplit, urlunsplit # pylint: disable=import-error
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
from slumber.exceptions import HttpClientError, HttpServerError
|
||||
|
||||
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
|
||||
|
||||
LOGGER = logging.getLogger("edx.journals")
|
||||
JOURNALS_CACHE_TIMEOUT = 3600 # Value is in seconds
|
||||
JOURNALS_API_PATH = '/journal/api/v1/'
|
||||
JOURNAL_WORKER_USERNAME = 'journals_worker'
|
||||
User = get_user_model()
|
||||
|
||||
# Waffle switches namespace for journals
|
||||
WAFFLE_NAMESPACE = 'journals'
|
||||
WAFFLE_SWITCHES = WaffleSwitchNamespace(name=WAFFLE_NAMESPACE)
|
||||
|
||||
# Waffle switch for enabling/disabling journals
|
||||
JOURNAL_INTEGRATION = 'enable_journal_integration'
|
||||
|
||||
|
||||
class DiscoveryApiClient(object):
|
||||
"""
|
||||
Class for interacting with the discovery service journals endpoint
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize an authenticated Discovery service API client by using the
|
||||
provided user.
|
||||
"""
|
||||
catalog_integration = CatalogIntegration.current()
|
||||
|
||||
# Client can't be used if there is no catalog integration
|
||||
if not (catalog_integration and catalog_integration.enabled):
|
||||
LOGGER.error("Unable to create DiscoveryApiClient because catalog integration not set up or enabled")
|
||||
return None
|
||||
|
||||
try:
|
||||
user = catalog_integration.get_service_user()
|
||||
except ObjectDoesNotExist:
|
||||
LOGGER.error("Unable to retrieve catalog integration service user")
|
||||
return None
|
||||
|
||||
jwt = create_jwt_for_user(user)
|
||||
base_url = configuration_helpers.get_value('COURSE_CATALOG_URL_BASE', settings.COURSE_CATALOG_URL_BASE)
|
||||
self.client = EdxRestApiClient(
|
||||
'{base_url}{journals_path}'.format(base_url=base_url, journals_path=JOURNALS_API_PATH),
|
||||
jwt=jwt
|
||||
)
|
||||
|
||||
def get_journals(self, orgs):
|
||||
"""
|
||||
get_journals from discovery, filter on orgs is supplied
|
||||
"""
|
||||
try:
|
||||
if orgs:
|
||||
response = self.client.journals.get(orgs=','.join(orgs), status='active')
|
||||
else:
|
||||
response = self.client.journals.get(status='active')
|
||||
LOGGER.debug(u'response is type=%s', type(response))
|
||||
return response.get('results')
|
||||
except (HttpClientError, HttpServerError) as err:
|
||||
LOGGER.exception(
|
||||
u'Failed to get journals from discovery-service [%s]',
|
||||
err.content
|
||||
)
|
||||
return []
|
||||
|
||||
def get_journal_bundles(self, uuid=''):
|
||||
"""
|
||||
get_journal_bundles from discovery on the base of uuid (optional)
|
||||
"""
|
||||
try:
|
||||
response = self.client.journal_bundles(uuid).get()
|
||||
except (HttpClientError, HttpServerError) as err:
|
||||
LOGGER.exception(
|
||||
u'Failed to get journal bundles from discovery-service [%s]',
|
||||
err.content
|
||||
)
|
||||
return []
|
||||
return [response] if uuid else response.get('results')
|
||||
|
||||
|
||||
class JournalsApiClient(object):
|
||||
"""
|
||||
Class for interacting with the Journals Service
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize an authenticated Journals service API client by using the
|
||||
provided user.
|
||||
"""
|
||||
try:
|
||||
self.user = self.get_journals_worker()
|
||||
except ObjectDoesNotExist:
|
||||
error = u'Unable to retrieve {} service user'.format(JOURNAL_WORKER_USERNAME)
|
||||
LOGGER.error(error)
|
||||
raise ValueError(error)
|
||||
|
||||
jwt = create_jwt_for_user(self.user)
|
||||
self.client = EdxRestApiClient(
|
||||
configuration_helpers.get_value('JOURNALS_API_URL', settings.JOURNALS_API_URL),
|
||||
jwt=jwt
|
||||
)
|
||||
|
||||
def get_journals_worker(self):
|
||||
""" Return journals worker """
|
||||
return User.objects.get(username=JOURNAL_WORKER_USERNAME)
|
||||
|
||||
|
||||
def fetch_journal_access(site, user, block_id=None): # pylint: disable=unused-argument
|
||||
"""
|
||||
Retrieve journal access record for given user.
|
||||
Retrieve if from the cache if present, otherwise send GET request to the journal access api
|
||||
and store it in the cache
|
||||
|
||||
Args:
|
||||
site (Site)
|
||||
user (username | str): user to retrieve access records for
|
||||
|
||||
Returns:
|
||||
list of dicts: list of journal access dicts
|
||||
|
||||
Raises:
|
||||
ConnectionError: raised if lms is unable to connect to the journals service.
|
||||
SlumberBaseException: raised if API response contains http error status like 4xx, 5xx etc...
|
||||
Timeout: raised if API is talking to long to respond
|
||||
"""
|
||||
try:
|
||||
# TODO: WL-1560:
|
||||
# LMS should cache responses from Journal Access API
|
||||
# Need strategy for updating cache when new purchase happens
|
||||
endpoint_params = {
|
||||
"user": user,
|
||||
"get_latest": True,
|
||||
}
|
||||
if block_id:
|
||||
endpoint_params['block_id'] = block_id
|
||||
journal_access_records = JournalsApiClient().client.journalaccess.get(**endpoint_params)
|
||||
return journal_access_records.get('results', [])
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
|
||||
def get_cache_key(**kwargs):
|
||||
"""
|
||||
Get MD5 encoded cache key for given arguments.
|
||||
|
||||
Here is the format of key before MD5 encryption.
|
||||
key1:value1__key2:value2 ...
|
||||
|
||||
Example:
|
||||
>>> get_cache_key(site_domain="example.com", resource="enterprise-learner")
|
||||
# Here is key format for above call
|
||||
# "site_domain:example.com__resource:enterprise-learner"
|
||||
a54349175618ff1659dee0978e3149ca
|
||||
|
||||
Arguments:
|
||||
**kwargs: Key word arguments that need to be present in cache key.
|
||||
|
||||
Returns:
|
||||
An MD5 encoded key uniquely identified by the key word arguments.
|
||||
"""
|
||||
key = '__'.join(['{}:{}'.format(item, value) for item, value in six.iteritems(kwargs)])
|
||||
|
||||
return hashlib.md5(key).hexdigest()
|
||||
|
||||
|
||||
def journals_enabled():
|
||||
"""
|
||||
Determines whether the Journals app is installed and enabled for the current Site
|
||||
Returns:
|
||||
True if global setting via waffle switch
|
||||
'journals.enable_journal_integration' is enabled and
|
||||
Site specific setting JOURNALS_ENABLED is True.
|
||||
False if either of these settings is not enabled
|
||||
"""
|
||||
return 'openedx.features.journals.apps.JournalsConfig' in settings.INSTALLED_APPS and \
|
||||
WAFFLE_SWITCHES.is_enabled(JOURNAL_INTEGRATION) and \
|
||||
configuration_helpers.get_value('JOURNALS_ENABLED', settings.FEATURES.get('JOURNALS_ENABLED', False))
|
||||
|
||||
|
||||
def get_journals(site):
|
||||
"""Retrieve journals from the discovery service.
|
||||
|
||||
Keyword Arguments:
|
||||
site (Site): Site object for the request
|
||||
will be returned
|
||||
|
||||
Returns:
|
||||
list of dict, representing journals
|
||||
"""
|
||||
if not journals_enabled():
|
||||
return []
|
||||
|
||||
api_resource = 'journals'
|
||||
orgs = configuration_helpers.get_current_site_orgs()
|
||||
|
||||
cache_key = get_cache_key(
|
||||
site_domain=site.domain,
|
||||
resource=api_resource,
|
||||
orgs=orgs
|
||||
)
|
||||
|
||||
# look up in cache
|
||||
journals = cache.get(cache_key)
|
||||
|
||||
if not journals:
|
||||
api_client = DiscoveryApiClient()
|
||||
if not api_client:
|
||||
return []
|
||||
journals = api_client.get_journals(orgs)
|
||||
cache.set(cache_key, journals, JOURNALS_CACHE_TIMEOUT)
|
||||
|
||||
return journals
|
||||
|
||||
|
||||
def fix_course_images(bundle):
|
||||
"""
|
||||
Set the image for a course. If the course has an image, use that. Otherwise use the first
|
||||
course run that has an image.
|
||||
"""
|
||||
for course in bundle['courses']:
|
||||
course_image = course['image'].get('src') if course.get('image') else None
|
||||
if course_image:
|
||||
# Course already had image and we don't need to check course runs
|
||||
continue
|
||||
for course_run in course['course_runs']:
|
||||
if course_run['image']:
|
||||
course['image'] = course_run['image']
|
||||
break
|
||||
|
||||
|
||||
def get_journal_bundles(site, bundle_uuid=''):
|
||||
"""Retrieve journal bundles from the discovery service.
|
||||
|
||||
Returns:
|
||||
list of dict, representing journal bundles
|
||||
"""
|
||||
if not journals_enabled():
|
||||
return []
|
||||
|
||||
api_resource = 'journal_bundles'
|
||||
|
||||
cache_key = get_cache_key(
|
||||
site_domain=site.domain,
|
||||
resource=api_resource,
|
||||
bundle_uuid=bundle_uuid
|
||||
)
|
||||
|
||||
_CACHE_MISS = object()
|
||||
journal_bundles = cache.get(cache_key, _CACHE_MISS)
|
||||
|
||||
if journal_bundles is _CACHE_MISS:
|
||||
api_client = DiscoveryApiClient()
|
||||
if not api_client:
|
||||
return []
|
||||
journal_bundles = api_client.get_journal_bundles(uuid=bundle_uuid)
|
||||
cache.set(cache_key, journal_bundles, JOURNALS_CACHE_TIMEOUT)
|
||||
|
||||
for bundle in journal_bundles:
|
||||
fix_course_images(bundle)
|
||||
|
||||
return journal_bundles
|
||||
|
||||
|
||||
def get_journals_root_url():
|
||||
"""
|
||||
Return the base url for the journals service
|
||||
"""
|
||||
if journals_enabled():
|
||||
return configuration_helpers.get_value(
|
||||
'JOURNALS_URL_ROOT',
|
||||
settings.JOURNALS_URL_ROOT
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_journals_frontend_url():
|
||||
"""
|
||||
Return the frontend url used to display Journals
|
||||
"""
|
||||
if journals_enabled():
|
||||
return configuration_helpers.get_value(
|
||||
'JOURNALS_FRONTEND_URL',
|
||||
settings.JOURNALS_FRONTEND_URL
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_journal_about_page_url(about_page_id=0, auth=True):
|
||||
"""
|
||||
Return url to journal about page.
|
||||
If auth=True, the url will redirect through the journals service log in page
|
||||
which will prevent the "purchase now" button being shown.
|
||||
If auth=False, the url will point to Journal About Page with purchase button shown
|
||||
|
||||
Arguments:
|
||||
about_page_id (int): id of Journal About Page as found in Discovery
|
||||
auth (boolen): authorization flag, if true will force login to journal service
|
||||
and redirect to last visited page in Journal after login. If false, this method
|
||||
will return direct url to journal about page.
|
||||
|
||||
Returns:
|
||||
url (str): url pointing to Journals Service login, w/ a redirect to last visited journal page
|
||||
or url pointing directly to journal about page.
|
||||
"""
|
||||
if not auth:
|
||||
return urljoin(get_journals_frontend_url(), '{id}/about'.format(id=about_page_id))
|
||||
|
||||
# by providing just the about_page_id in the url, the user will be redirected
|
||||
# to the last page viewed after logging in
|
||||
about_page_url = urljoin(get_journals_frontend_url(), '{id}'.format(id=about_page_id))
|
||||
login_url = urljoin(get_journals_root_url(), 'require_auth')
|
||||
query = 'forward={next_url}'.format(next_url=about_page_url)
|
||||
|
||||
split_url = urlsplit(login_url)
|
||||
url = urlunsplit((
|
||||
split_url.scheme,
|
||||
split_url.netloc,
|
||||
split_url.path,
|
||||
query,
|
||||
split_url.fragment,
|
||||
))
|
||||
return url
|
||||
|
||||
|
||||
def get_journals_context(request):
|
||||
"""
|
||||
Return dict of Journal context information for a given request
|
||||
|
||||
Args:
|
||||
request: The request to process
|
||||
|
||||
Returns:
|
||||
dict containing the following information:
|
||||
dict['journals'] - list of Journals available for purchase
|
||||
dict['journal_bundles'] - list of JournalBundles available for purchase
|
||||
"""
|
||||
journal_info = {}
|
||||
journal_info['journals'] = get_journals(request.site)
|
||||
journal_info['journal_bundles'] = get_journal_bundles(request.site)
|
||||
|
||||
return journal_info
|
||||
@@ -1,33 +0,0 @@
|
||||
"""
|
||||
Journals Application Configuration
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import PluginSettings, PluginURLs, ProjectType, SettingsType
|
||||
|
||||
|
||||
class JournalsConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for Journals.
|
||||
"""
|
||||
name = u'openedx.features.journals'
|
||||
|
||||
plugin_app = {
|
||||
PluginURLs.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
PluginURLs.NAMESPACE: u'',
|
||||
PluginURLs.REGEX: r'^journals/',
|
||||
PluginURLs.RELATIVE_PATH: u'urls',
|
||||
}
|
||||
},
|
||||
PluginSettings.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
SettingsType.PRODUCTION: {PluginSettings.RELATIVE_PATH: u'settings.production'},
|
||||
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: u'settings.common'},
|
||||
SettingsType.DEVSTACK: {PluginSettings.RELATIVE_PATH: u'settings.devstack'},
|
||||
SettingsType.TEST: {PluginSettings.RELATIVE_PATH: u'settings.test'},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
'''Common Settings for Journals'''
|
||||
|
||||
|
||||
def plugin_settings(settings):
|
||||
"""
|
||||
Common settings for Journals
|
||||
"""
|
||||
settings.JOURNALS_URL_ROOT = None
|
||||
settings.JOURNALS_FRONTEND_URL = None
|
||||
settings.JOURNALS_API_URL = None
|
||||
settings.FEATURES['JOURNALS_ENABLED'] = False
|
||||
settings.COURSE_CATALOG_URL_BASE = None
|
||||
settings.MAKO_TEMPLATE_DIRS_BASE.append(settings.OPENEDX_ROOT / 'features' / 'journals' / 'templates')
|
||||
@@ -1,12 +0,0 @@
|
||||
'''devstack settings for Journals'''
|
||||
|
||||
|
||||
def plugin_settings(settings):
|
||||
"""
|
||||
Devstack settings for Journals
|
||||
"""
|
||||
settings.JOURNALS_URL_ROOT = 'http://localhost:18606'
|
||||
settings.JOURNALS_FRONTEND_URL = 'http://localhost:1991'
|
||||
settings.JOURNALS_API_URL = 'http://journals.app:18606/api/v1/'
|
||||
settings.FEATURES['JOURNALS_ENABLED'] = True
|
||||
settings.COURSE_CATALOG_URL_BASE = 'http://edx.devstack.discovery:18381'
|
||||
@@ -1,12 +0,0 @@
|
||||
'''AWS Settings for Journals'''
|
||||
|
||||
|
||||
def plugin_settings(settings):
|
||||
"""
|
||||
Settings for AWS
|
||||
"""
|
||||
settings.JOURNALS_URL_ROOT = settings.ENV_TOKENS.get('JOURNALS_URL_ROOT', settings.JOURNALS_URL_ROOT)
|
||||
settings.JOURNALS_FRONTEND_URL = settings.ENV_TOKENS.get('JOURNALS_FRONTEND_URL', settings.JOURNALS_FRONTEND_URL)
|
||||
settings.JOURNALS_API_URL = settings.ENV_TOKENS.get('JOURNALS_API_URL', settings.JOURNALS_API_URL)
|
||||
settings.COURSE_CATALOG_URL_BASE = settings.ENV_TOKENS.get(
|
||||
'COURSE_CATALOG_URL_BASE', settings.COURSE_CATALOG_URL_BASE)
|
||||
@@ -1,9 +0,0 @@
|
||||
'''Test Settings for Journals'''
|
||||
|
||||
|
||||
def plugin_settings(settings): # pylint ignore:Unused argument
|
||||
"""
|
||||
Test settings for Journals
|
||||
"""
|
||||
settings.COURSE_CATALOG_URL_BASE = 'https://catalog.example.com'
|
||||
settings.FEATURES['JOURNALS_ENABLED'] = False
|
||||
@@ -1,169 +0,0 @@
|
||||
## mako
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%inherit file="/main.html" />
|
||||
<%!
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from mako import exceptions
|
||||
|
||||
from urlparse import urljoin
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.journals.api import get_journal_about_page_url
|
||||
%>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
## Override the default styles_version to use Bootstrap
|
||||
<%! main_css = "css/bootstrap/lms-main.css" %>
|
||||
|
||||
|
||||
<%
|
||||
%>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script src="${static.url('js/leanModal.js')}"></script>
|
||||
<script src="${static.url('js/program_marketing.js')}"></script>
|
||||
</%block>
|
||||
|
||||
<%block name="pagetitle">${bundle['title']}</%block>
|
||||
<%block name="marketing_hero">
|
||||
<%
|
||||
banner_image = bundle.get('banner_image', {}).get('large', {}).get('url', '')
|
||||
price_format = '{0:.0f}' if bundle['pricing_data']['total_incl_tax'].is_integer() else '{0:.2f}'
|
||||
%>
|
||||
<div id="program-details-hero">
|
||||
<div class="main-banner"
|
||||
style="background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url(${banner_image});">
|
||||
<div class="container" >
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-8">
|
||||
<div>
|
||||
<h1 class="program_title">${bundle.get('title', '')}</h1>
|
||||
</div>
|
||||
<div>
|
||||
## Note: Weird formatting to fix the inline spacing issue.
|
||||
<a href="${bundle['pricing_data']['purchase_url']}" class="btn btn-success">
|
||||
<span>${_('Purchase the Bundle (')}</span
|
||||
% if bundle['pricing_data']['is_discounted']:
|
||||
><span aria-label="${_('Original Price')}" class="original-price"
|
||||
>${Text('${oldPrice}').format(
|
||||
oldPrice=price_format.format(bundle['pricing_data']['total_incl_tax_excl_discounts'])
|
||||
)}</span
|
||||
><span aria-label="${_('Discounted Price')}" class="discount">
|
||||
${Text('${newPrice}').format(
|
||||
newPrice=price_format.format(bundle['pricing_data']['total_incl_tax']),
|
||||
)}
|
||||
</span
|
||||
><span class="savings">
|
||||
${Text('{discount_value} {currency})').format(
|
||||
discount_value=price_format.format(bundle['pricing_data']['discount_value']),
|
||||
currency=bundle['pricing_data']['currency']
|
||||
)}
|
||||
</span>
|
||||
% else:
|
||||
><span>${Text('${price})').format(
|
||||
price=price_format.format(bundle['pricing_data']['total_incl_tax']),
|
||||
)}
|
||||
</span>
|
||||
% endif
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-nav">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<ul class="nav nav-fill col-lg-12">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<div id="program-details-page" class="container">
|
||||
% if bundle.get('courses') or bundle.get('journals'):
|
||||
<hr class="thick_rule">
|
||||
<div id="courses">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
${_('Courses included')}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
% for course in bundle.get('courses'):
|
||||
<%
|
||||
course_run = course['course_runs'][0]
|
||||
course_img = course.get('image', {}).get('src', '') if course.get('image') else ''
|
||||
course_about_url = reverse('about_course', args=[course_run['key']])
|
||||
%>
|
||||
<div class="row course">
|
||||
<div class="col-3 col-lg-2 course-image">
|
||||
% if course_img:
|
||||
<img alt="" src="${course_img}" alt=""/>
|
||||
% endif
|
||||
</div>
|
||||
<div class="col-9 col-lg-6">
|
||||
<div>
|
||||
<a href="${course_about_url}">${course_run['title']}</a>
|
||||
</div>
|
||||
<div>${course['short_description'] or ''}</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 course-enroll">
|
||||
<div>
|
||||
${Text(_('Starts on {}')).format(
|
||||
datetime.strptime(course_run['start'], '%Y-%m-%dT%H:%M:%SZ').strftime('%B %-d, %Y')
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-primary btn-block btn-success" href="${course_about_url}">${_("View Course")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
% for journal in bundle.get('journals'):
|
||||
<%
|
||||
about_page_id = journal['about_page_id']
|
||||
about_page_url = get_journal_about_page_url(about_page_id, False)
|
||||
%>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>
|
||||
${_('Journals included')}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row course">
|
||||
<div class="col-3 col-lg-2 course-image">
|
||||
% if journal.get('card_image_url'):
|
||||
<img alt="" src="${journal['card_image_url']}" alt=""/>
|
||||
% endif
|
||||
</div>
|
||||
<div class="col-9 col-lg-6">
|
||||
<div>
|
||||
<a href="${about_page_url}">${journal['title']}</a>
|
||||
</div>
|
||||
<div>${journal['short_description'] or ''}</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 course-enroll">
|
||||
<div>
|
||||
${_('{access_length} Day Access').format(
|
||||
access_length=journal['access_length']
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-primary btn-block btn-success" href="${about_page_url}">${_("View Journal")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
@@ -1,48 +0,0 @@
|
||||
<%def name="online_help_token()"><% return "course" %></%def>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from six import text_type
|
||||
from urlparse import urljoin
|
||||
%>
|
||||
<%page args="bundle" expression_filter="h"/>
|
||||
<article class="bundle" id="${bundle.get('uuid')}" role="region" aria-label="${bundle.get('title')}">
|
||||
<%
|
||||
images = []
|
||||
orgs = []
|
||||
|
||||
for journal in bundle['journals']:
|
||||
images.append(journal.get('card_image_url', ''))
|
||||
orgs.append(journal.get('organization', ''))
|
||||
|
||||
for course in bundle['courses']:
|
||||
if course.get('image'):
|
||||
images.append(course['image'].get('src', ''))
|
||||
orgs.append(course.get('partner', ''))
|
||||
|
||||
card_img = next((img for img in images if img), '')
|
||||
organization = next((org for org in orgs if org), '')
|
||||
%>
|
||||
<a class="journals-listing-item" href="${reverse('openedx.journals.bundle_about', kwargs={'bundle_uuid':bundle['uuid']})}">
|
||||
<header class="journal-image">
|
||||
<div class="cover-image">
|
||||
<img src="${card_img}" alt="${bundle.get('title')}" />
|
||||
<div class="learn-more" aria-hidden="true">${_("LEARN MORE")}</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="banner">
|
||||
${_("Bundle")}
|
||||
</div>
|
||||
<div class="journal-info" aria-hidden="true">
|
||||
<span class="journal-org">${organization}</span>
|
||||
<h3 class="journal-title">${bundle.get('title')}</h3>
|
||||
</div>
|
||||
<div class="sr">
|
||||
<ul>
|
||||
<li>${organization}</li>
|
||||
<li>${bundle.get('title')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
@@ -1,53 +0,0 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ungettext, ugettext as _
|
||||
from django.core.urlresolvers import reverse
|
||||
from six import text_type
|
||||
from urlparse import urljoin
|
||||
from openedx.features.journals.api import get_journal_about_page_url
|
||||
%>
|
||||
<%page args="journal" expression_filter="h"/>
|
||||
<%
|
||||
about_page_id = journal.get('about_page_id')
|
||||
about_page_url = get_journal_about_page_url(about_page_id, False)
|
||||
%>
|
||||
<article class="journal" id="${about_page_id}" role="region" aria-label="${journal.get('title')}">
|
||||
<a class="journals-listing-item" href="${about_page_url}">
|
||||
<header class="journal-image">
|
||||
<div class="cover-image">
|
||||
<img src="${journal.get('card_image_url')}" alt="${journal.get('title')}" />
|
||||
<div class="learn-more" aria-hidden="true">${_("LEARN MORE")}</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="banner">
|
||||
${_("Journal")}
|
||||
</div>
|
||||
|
||||
<div class="journal-info" aria-hidden="true">
|
||||
<span class="journal-org">${journal.get('organization')}</span>
|
||||
<h3 class="journal-title">${journal.get('title')}</h3>
|
||||
</div>
|
||||
|
||||
<span class="journal-footer">
|
||||
<%
|
||||
if journal.get('access_length') is not None:
|
||||
access_length_string = ungettext(
|
||||
'{num_months} month',
|
||||
'{num_months} months',
|
||||
int(journal.get('access_length')/30)
|
||||
).format(num_months=int(journal.get('access_length')/30))
|
||||
else:
|
||||
access_length_string = _("unlimited")
|
||||
%>
|
||||
<div class="course-date" aria-hidden="true">${_("Access Length")}: ${access_length_string}</div>
|
||||
</span>
|
||||
|
||||
<div class="sr">
|
||||
<ul>
|
||||
<li>${journal.get('organization')}</li>
|
||||
<li>${journal.get('title')}</li>
|
||||
<li>${_("Access Length")}: ${access_length_string}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
@@ -1,136 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="/main.html" />
|
||||
<%def name="online_help_token()"><% return "learnerdashboard" %></%def>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
from openedx.features.journals.views.learner_dashboard import format_expiration_date, has_access_expired
|
||||
from openedx.features.journals.api import get_journal_about_page_url
|
||||
%>
|
||||
|
||||
<%block name="pagetitle">${_("Journal Dashboard")}</%block>
|
||||
<%block name="bodyclass">view-dashboard is-authenticated</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["donation"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="dashboard/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script src="${static.url('js/commerce/credit.js')}"></script>
|
||||
<%static:js group='dashboard'/>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
edx.dashboard.legacy.init({
|
||||
dashboard: "${reverse('dashboard') | n, js_escaped_string}",
|
||||
signInUser: "${reverse('signin_user') | n, js_escaped_string}",
|
||||
changeEmailSettings: "${reverse('change_email_settings') | n, js_escaped_string}"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
|
||||
DashboardSearchFactory();
|
||||
</%static:require_module>
|
||||
% endif
|
||||
% if redirect_message:
|
||||
<%static:require_module module_name="js/views/message_banner" class_name="MessageBannerView">
|
||||
var banner = new MessageBannerView({urgency: 'low', type: 'warning'});
|
||||
$('#content').prepend(banner.$el);
|
||||
banner.showMessage(${redirect_message | n, dump_js_escaped_json})
|
||||
</%static:require_module>
|
||||
% endif
|
||||
</%block>
|
||||
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="dashboard" id="dashboard-main">
|
||||
<div class="main-container">
|
||||
<div class="my-courses" id="my-courses">
|
||||
<%include file="/learner_dashboard/_dashboard_navigation_journals.html"/>
|
||||
|
||||
% if len(journals) > 0:
|
||||
<ul class="listing-courses">
|
||||
% for journal in journals:
|
||||
<%
|
||||
about_page_url = get_journal_about_page_url(about_page_id=journal['journal']['journalaboutpage']['id'])
|
||||
formatted_expiration_date = format_expiration_date(journal['expiration_date'])
|
||||
access_expired = has_access_expired(journal['expiration_date'])
|
||||
%>
|
||||
<li class="course-item">
|
||||
<div class="course-container">
|
||||
<article class="course" aria-labelledby="journal-title-${journal['journal']['name']}" id="course-card-${journal['journal']['name']}">
|
||||
<section class="details" aria-labelledby="details-heading-${journal['journal']['name']}">
|
||||
<h2 class="hd hd-2 sr" id="details-heading-${journal['journal']['name']}">$_('Journal details')}</h2>
|
||||
<div class="wrapper-course-image" aria-label="true">
|
||||
<a href="${about_page_url}" class="cover" tabindex="-1">
|
||||
<img src="${journal['journal']['journalaboutpage']['card_image_absolute_url']}" class="course-image" alt="${_('{journal_title} Cover Image').format(journal_title=journal['journal']['name'])}"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="wrapper-course-details">
|
||||
<h3 class="course-title" id="course-title-${journal['journal']['name']}" href="${about_page_url}">
|
||||
<a data-course-key="${journal['uuid']}">${journal['journal']['name']}</a>
|
||||
</h3>
|
||||
<div class="course-info">
|
||||
<span class="info-university">${journal['journal']['organization']}</span>
|
||||
<span class="info-date-block-container">
|
||||
% if access_expired:
|
||||
<span class="info-date-block" aria-live="polite">
|
||||
<span class="icon fa fa-warning" aria-hidden="true"></span>
|
||||
${_('Access Expired: {date}').format(date=formatted_expiration_date)}
|
||||
</span>
|
||||
% else:
|
||||
<span class="info-date-block">
|
||||
${_('Access Expires: {date}').format(date=formatted_expiration_date)}
|
||||
</span>
|
||||
% endif
|
||||
</span>
|
||||
</div>
|
||||
<div class="wrapper-course-actions">
|
||||
<div class="course-actions">
|
||||
% if access_expired:
|
||||
<a href="${about_page_url}"
|
||||
class="enter-course"
|
||||
data-course-key="${journal['uuid']}">
|
||||
${_('Renew Access')}
|
||||
<span class="sr">
|
||||
${journal['journal']['name']}
|
||||
</span>
|
||||
</a>
|
||||
% else:
|
||||
<a href="${about_page_url}"
|
||||
class="enter-course"
|
||||
data-course-key="${journal['uuid']}">
|
||||
${_('View Journal')}
|
||||
<span class="sr">
|
||||
${journal['journal']['name']}
|
||||
</span>
|
||||
</a>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<div class="empty-dashboard-message">
|
||||
<p>${_("You have not purchased access to any journals yet.")}</p>
|
||||
<a class="btn btn-primary" href="${marketing_link('COURSES')}">
|
||||
${_("Explore journals and courses")}
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-container"></div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,84 +0,0 @@
|
||||
"""
|
||||
Test cases for journal page views.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse
|
||||
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from openedx.features.journals.api import JOURNAL_INTEGRATION
|
||||
from openedx.features.journals.tests.utils import get_mocked_journal_access, override_switch
|
||||
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {"JOURNALS_ENABLED": True})
|
||||
class RenderXblockByJournalAccessViewTest(LoginEnrollmentTestCase, CacheIsolationTestCase, SiteMixin):
|
||||
""" Tests for views responsible for rendering xblock in journals """
|
||||
|
||||
def setUp(self):
|
||||
super(RenderXblockByJournalAccessViewTest, self).setUp()
|
||||
self.setup_user()
|
||||
self.path = reverse(
|
||||
"openedx.journals.render_xblock_by_journal_access",
|
||||
kwargs={
|
||||
"usage_key_string": "block-v1:edX+DemoX+Demo_Course+type@video+block@5c90cffecd9b48b188cbfea176bf7fe9"
|
||||
}
|
||||
)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('openedx.features.journals.views.journal_xblock.fetch_journal_access')
|
||||
@mock.patch('openedx.features.journals.views.journal_xblock.render_xblock')
|
||||
def test_without_journal_access(self, mocked_render_xblock, mocked_journal_access):
|
||||
"""
|
||||
Test the journal page without journal access.
|
||||
"""
|
||||
mocked_journal_access.return_value = []
|
||||
mocked_render_xblock.return_value = []
|
||||
path = "{path}?journal_uuid={journal_uuid}".format(
|
||||
path=self.path,
|
||||
journal_uuid=str(uuid.uuid4())
|
||||
)
|
||||
response = self.client.get(path=path)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('openedx.features.journals.views.journal_xblock.fetch_journal_access')
|
||||
@mock.patch('openedx.features.journals.views.journal_xblock.render_xblock')
|
||||
def test_unauthenticated_journal_access(self, mocked_render_xblock, mocked_journal_access):
|
||||
"""
|
||||
Test when not logged in
|
||||
"""
|
||||
self.logout()
|
||||
mocked_journal_access.return_value = []
|
||||
mocked_render_xblock.return_value = []
|
||||
path = "{path}?journal_uuid={journal_uuid}".format(
|
||||
path=self.path,
|
||||
journal_uuid=str(uuid.uuid4())
|
||||
)
|
||||
response = self.client.get(path=path)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('openedx.features.journals.views.journal_xblock.fetch_journal_access')
|
||||
@mock.patch('openedx.features.journals.views.journal_xblock.render_xblock')
|
||||
def test_with_journal_access(self, mocked_render_xblock, mocked_journal_access):
|
||||
"""
|
||||
Test the journal page with journal access.
|
||||
"""
|
||||
journal_uuid = str(uuid.uuid4())
|
||||
mocked_journal_access.return_value = get_mocked_journal_access(journal_uuid=journal_uuid)
|
||||
mocked_render_xblock.return_value = HttpResponse("")
|
||||
path = "{path}?journal_uuid={journal_uuid}".format(
|
||||
path=self.path,
|
||||
journal_uuid=journal_uuid
|
||||
)
|
||||
response = self.client.get(path=path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mocked_render_xblock.assert_called_once()
|
||||
@@ -1,75 +0,0 @@
|
||||
""" Tests for journals learner dashboard views. """
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from openedx.features.journals.api import JOURNAL_INTEGRATION
|
||||
from openedx.features.journals.tests.utils import get_mocked_journal_access, override_switch
|
||||
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {"JOURNALS_ENABLED": True})
|
||||
class JournalLearnerDashboardTest(LoginEnrollmentTestCase):
|
||||
""" Tests for the Leaner Dashboard views for journals data """
|
||||
|
||||
def setUp(self):
|
||||
super(JournalLearnerDashboardTest, self).setUp()
|
||||
self.setup_user()
|
||||
self.path = reverse('openedx.journals.dashboard')
|
||||
|
||||
def test_without_authenticated_user(self):
|
||||
"""
|
||||
Test the learner dashboard without authenticated user.
|
||||
"""
|
||||
self.logout()
|
||||
response = self.client.get(path=self.path)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch("openedx.features.journals.views.learner_dashboard.fetch_journal_access")
|
||||
def test_with_empty_journals(self, mocked_journal_access):
|
||||
"""
|
||||
Test the learner dashboard without journal access data.
|
||||
"""
|
||||
mocked_journal_access.return_value = []
|
||||
response = self.client.get(path=self.path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "My Journals")
|
||||
self.assertContains(response, "You have not purchased access to any journals yet.")
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch("openedx.features.journals.views.learner_dashboard.fetch_journal_access")
|
||||
def test_with_with_valid_data(self, mocked_journal_access):
|
||||
"""
|
||||
Test the learner dashboard with journal access data.
|
||||
"""
|
||||
journals = get_mocked_journal_access()
|
||||
mocked_journal_access.return_value = journals
|
||||
response = self.client.get(path=self.path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "View Journal")
|
||||
for journal in journals:
|
||||
self.assertContains(response, journal["journal"]["name"])
|
||||
self.assertContains(response, journal["journal"]["organization"])
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, False)
|
||||
def test_journals_waffle_disabled(self):
|
||||
"""
|
||||
Test the journal dashboard is not displayed if
|
||||
waffle switch is off
|
||||
"""
|
||||
response = self.client.get(path=self.path)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch.dict(settings.FEATURES, {"JOURNALS_ENABLED": False})
|
||||
def test_journals_setting_disabled(self):
|
||||
"""
|
||||
Test the journal dashboard is not displayed if
|
||||
waffle switch is on but setting is off
|
||||
"""
|
||||
response = self.client.get(path=self.path)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
@@ -1,130 +0,0 @@
|
||||
""" Tests for journals marketing views. """
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from openedx.features.journals.api import JOURNAL_INTEGRATION
|
||||
from openedx.features.journals.tests.utils import (
|
||||
get_mocked_journal_bundles,
|
||||
get_mocked_journals,
|
||||
get_mocked_pricing_data,
|
||||
override_switch
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {"JOURNALS_ENABLED": True})
|
||||
class JournalBundleViewTest(CacheIsolationTestCase, SiteMixin):
|
||||
""" Tests for journals marketing views. """
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('openedx.features.journals.api.DiscoveryApiClient.get_journal_bundles')
|
||||
def test_journal_bundle_with_empty_data(self, mock_bundles):
|
||||
"""
|
||||
Test the marketing page without journal bundle data.
|
||||
"""
|
||||
mock_bundles.return_value = []
|
||||
response = self.client.get(
|
||||
path=reverse(
|
||||
"openedx.journals.bundle_about",
|
||||
kwargs={'bundle_uuid': str(uuid.uuid4())}
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('openedx.features.journals.views.marketing.get_pricing_data')
|
||||
@mock.patch('openedx.features.journals.api.DiscoveryApiClient.get_journal_bundles')
|
||||
def test_journal_bundle_with_valid_data(self, mock_bundles, mock_pricing_data):
|
||||
"""
|
||||
Test the marketing page with journal bundle data.
|
||||
"""
|
||||
journal_bundles = get_mocked_journal_bundles()
|
||||
journal_bundle = journal_bundles[0]
|
||||
mock_pricing_data.return_value = get_mocked_pricing_data()
|
||||
mock_bundles.return_value = journal_bundles
|
||||
response = self.client.get(
|
||||
path=reverse(
|
||||
"openedx.journals.bundle_about",
|
||||
kwargs={'bundle_uuid': str(uuid.uuid4())}
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Purchase the Bundle")
|
||||
self.assertContains(response, journal_bundle["title"])
|
||||
self.assertContains(response, journal_bundle["courses"][0]["short_description"])
|
||||
self.assertContains(response, journal_bundle["courses"][0]["course_runs"][0]["title"])
|
||||
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {"JOURNALS_ENABLED": True})
|
||||
class JournalIndexViewTest(SiteMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for Journals Listing in Marketing Pages.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(JournalIndexViewTest, self).setUp()
|
||||
self.journal_bundles = get_mocked_journal_bundles()
|
||||
self.journal_bundle = self.journal_bundles[0]
|
||||
self.journals = get_mocked_journals()
|
||||
|
||||
def assert_journal_data(self, response):
|
||||
"""
|
||||
Checks the journal data in given response
|
||||
"""
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Bundle")
|
||||
self.assertContains(response, self.journal_bundle["uuid"])
|
||||
self.assertContains(response, self.journal_bundle["title"])
|
||||
self.assertContains(response, self.journal_bundle["organization"])
|
||||
for journal in self.journals:
|
||||
self.assertContains(response, "Journal")
|
||||
self.assertContains(response, journal["title"])
|
||||
self.assertContains(response, journal["organization"])
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('student.views.management.get_journals_context')
|
||||
def test_journals_index_page(self, mock_journals_context):
|
||||
"""
|
||||
Test the journal data on index page.
|
||||
"""
|
||||
mock_journals_context.return_value = {'journal_bundles': self.journal_bundles, 'journals': self.journals}
|
||||
|
||||
response = self.client.get(reverse('root'))
|
||||
self.assert_journal_data(response)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, False)
|
||||
def test_journals_index_page_disabled(self):
|
||||
"""
|
||||
Test the index page can load with journals disabled
|
||||
"""
|
||||
response = self.client.get(reverse('root'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, True)
|
||||
@mock.patch('openedx.features.journals.api.DiscoveryApiClient.get_journals')
|
||||
@mock.patch('openedx.features.journals.api.DiscoveryApiClient.get_journal_bundles')
|
||||
def test_journals_courses_page(self, mock_journal_bundles, mock_journals):
|
||||
"""
|
||||
Test the journal data on courses page.
|
||||
"""
|
||||
mock_journal_bundles.return_value = self.journal_bundles
|
||||
mock_journals.return_value = self.journals
|
||||
|
||||
response = self.client.get(reverse('courses'))
|
||||
self.assert_journal_data(response)
|
||||
|
||||
@override_switch(JOURNAL_INTEGRATION, False)
|
||||
def test_journals_courses_page_disabled(self):
|
||||
"""
|
||||
Test the courses pages can load with journals disabled
|
||||
"""
|
||||
response = self.client.get(reverse('courses'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1,144 +0,0 @@
|
||||
""" Returns the dummy data for journals endpoint of discovery."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import uuid
|
||||
from functools import wraps
|
||||
|
||||
from openedx.features.journals.api import WAFFLE_SWITCHES
|
||||
|
||||
|
||||
def override_switch(switch, active):
|
||||
"""
|
||||
Overrides the given waffle switch to `active` boolean.
|
||||
|
||||
Arguments:
|
||||
switch(str): switch name
|
||||
active(bool): A boolean representing (to be overridden) value
|
||||
"""
|
||||
def decorate(function):
|
||||
"""
|
||||
decorator function
|
||||
"""
|
||||
@wraps(function)
|
||||
def inner(*args, **kwargs):
|
||||
with WAFFLE_SWITCHES.override(switch, active=active):
|
||||
function(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def get_mocked_journal_access(journal_uuid=None):
|
||||
"""
|
||||
Returns the dummy data of journal access
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"expiration_date": "2050-11-08",
|
||||
"uuid": uuid.uuid4(),
|
||||
"journal": {
|
||||
"name": "dummy-name1",
|
||||
"uuid": journal_uuid if journal_uuid else str(uuid.uuid4()),
|
||||
"organization": "edx",
|
||||
"journalaboutpage": {
|
||||
"id": "5",
|
||||
"card_image_absolute_url": "dummy-url"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"expiration_date": "2050-10-08",
|
||||
"uuid": uuid.uuid4(),
|
||||
"journal": {
|
||||
"name": "dummy-name2",
|
||||
"uuid": str(uuid.uuid4()),
|
||||
"organization": "edx",
|
||||
"journalaboutpage": {
|
||||
"id": "5",
|
||||
"card_image_absolute_url": "dummy-url"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_mocked_journal_bundles():
|
||||
"""
|
||||
Returns the dummy data of journal bundle.
|
||||
"""
|
||||
return [{
|
||||
"uuid": "1918b738-979f-42cb-bde0-13335366fa86",
|
||||
"title": "dummy-title",
|
||||
"partner": "edx",
|
||||
"organization": "edx",
|
||||
"journals": [
|
||||
{
|
||||
"title": "dummy-title",
|
||||
"sku": "ASZ1GZ",
|
||||
"card_image_url": "dummy-url",
|
||||
"about_page_id": "5",
|
||||
"access_length": "8 weeks",
|
||||
"short_description": "dummy short description"
|
||||
}
|
||||
],
|
||||
"courses": [
|
||||
{
|
||||
"short_description": "dummy short description",
|
||||
"course_runs": [
|
||||
{
|
||||
"key": "course-v1:ABC+ABC101+2015_T1",
|
||||
"title": "Matt edX test course",
|
||||
"start": "2015-01-08T00:00:00Z",
|
||||
"end": "2016-12-30T00:00:00Z",
|
||||
"image": {
|
||||
"src": "dummy/url"
|
||||
},
|
||||
"seats": [
|
||||
{
|
||||
"type": "verified",
|
||||
"sku": "unit03",
|
||||
"bulk_sku": "2DF467D"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"applicable_seat_types": ["credit", "honor", "verified"]
|
||||
}]
|
||||
|
||||
|
||||
def get_mocked_journals():
|
||||
"""
|
||||
Returns the dummy data of journals
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"title": "dummy-title1",
|
||||
"card_image_url": "dummy-url1",
|
||||
"about_page_id": "5",
|
||||
"access_length": 60,
|
||||
"organization": "edx"
|
||||
},
|
||||
{
|
||||
"title": "dummy-title2",
|
||||
"card_image_url": "dummy-url2",
|
||||
"about_page_id": "5",
|
||||
"access_length": 60,
|
||||
"organization": "edx"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_mocked_pricing_data():
|
||||
"""
|
||||
Returns the dummy data for e-commerce pricing
|
||||
"""
|
||||
return {
|
||||
"currency": "USD",
|
||||
"discount_value": 0.3,
|
||||
"is_discounted": False,
|
||||
"total_incl_tax": 23.01,
|
||||
"purchase_url": "dummy-url",
|
||||
"total_incl_tax_excl_discounts": 40
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
"""
|
||||
Defines URLs for course bookmarks.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
from openedx.features.journals.views import learner_dashboard
|
||||
from openedx.features.journals.views.journal_xblock import render_xblock_by_journal_access
|
||||
from openedx.features.journals.views.marketing import bundle_about
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^bundles/{}/about'.format(r'(?P<bundle_uuid>[0-9a-f-]+)',),
|
||||
bundle_about,
|
||||
name='openedx.journals.bundle_about'
|
||||
),
|
||||
url(r'^$',
|
||||
learner_dashboard.journal_listing,
|
||||
name='openedx.journals.dashboard'
|
||||
),
|
||||
url(r'^render_journal_block/{usage_key_string}'.format(usage_key_string=settings.USAGE_KEY_PATTERN),
|
||||
render_xblock_by_journal_access,
|
||||
name='openedx.journals.render_xblock_by_journal_access'
|
||||
),
|
||||
]
|
||||
@@ -1,68 +0,0 @@
|
||||
"""
|
||||
View for journal page
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
|
||||
from lms.djangoapps.courseware.views.views import render_xblock
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.features.journals.api import fetch_journal_access
|
||||
|
||||
XBLOCK_JOURNAL_ACCESS_KEY = "journal_access_for_{username}_{journal_uuid}_{block_id}"
|
||||
|
||||
|
||||
def render_xblock_by_journal_access(request, usage_key_string):
|
||||
"""
|
||||
Its a wrapper function for lms.djangoapps.courseware.views.views.render_xblock.
|
||||
It disables 'check_if_enrolled' flag by checking that user has access on journal.
|
||||
"""
|
||||
block_id = UsageKey.from_string(usage_key_string).block_id
|
||||
user_access = _get_cache_data(request, block_id)
|
||||
if not user_access:
|
||||
raise PermissionDenied()
|
||||
return render_xblock(request, usage_key_string, check_if_enrolled=False)
|
||||
|
||||
|
||||
def _get_cache_data(request, block_id):
|
||||
"""
|
||||
Get the cache data from cache if not then hit the end point
|
||||
in journals to fetch the access of user on given block_id.
|
||||
"""
|
||||
if request.user.is_staff:
|
||||
return True
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
date_format = '%Y-%m-%d'
|
||||
journal_uuid = request.GET.get('journal_uuid')
|
||||
cache_key = XBLOCK_JOURNAL_ACCESS_KEY.format(
|
||||
username=request.user.username,
|
||||
journal_uuid=journal_uuid,
|
||||
block_id=block_id
|
||||
)
|
||||
user_access = cache.get(cache_key)
|
||||
if user_access is None:
|
||||
journal_access_data = fetch_journal_access(
|
||||
request.site,
|
||||
request.user,
|
||||
block_id=block_id
|
||||
)
|
||||
for journal_access in journal_access_data:
|
||||
if journal_access['journal']['uuid'] == journal_uuid:
|
||||
expiration_date = datetime.datetime.strptime(journal_access['expiration_date'], date_format)
|
||||
now = datetime.datetime.strptime(datetime.datetime.now().strftime(date_format), date_format)
|
||||
if expiration_date >= now:
|
||||
user_access = True
|
||||
|
||||
cache.set(
|
||||
cache_key,
|
||||
user_access,
|
||||
configuration_helpers.get_value("JOURNAL_ACCESS_CACHE_TTL", 3600)
|
||||
)
|
||||
return user_access
|
||||
@@ -1,74 +0,0 @@
|
||||
""" Journal Tab of Learner Dashboard views """
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from datetime import datetime, time
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.features.journals.api import fetch_journal_access, journals_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def journal_listing(request):
|
||||
""" View a list of journals which the user has or had access to"""
|
||||
|
||||
user = request.user
|
||||
|
||||
if not journals_enabled() or not user.is_authenticated():
|
||||
raise Http404
|
||||
|
||||
journals = fetch_journal_access(
|
||||
site=request.site,
|
||||
user=request.user
|
||||
)
|
||||
|
||||
context = {
|
||||
'journals': journals,
|
||||
'show_dashboard_tabs': True,
|
||||
'show_program_listing': ProgramsApiConfig.is_enabled(),
|
||||
'show_journal_listing': journals_enabled()
|
||||
}
|
||||
|
||||
return render_to_response('journals/learner_dashboard/journal_dashboard.html', context)
|
||||
|
||||
|
||||
def format_expiration_date(expiration_date):
|
||||
"""
|
||||
Formats Expiration Date
|
||||
|
||||
Arguments:
|
||||
expiration_date (str): in format 'YYYY-mm-dd' (ex. April 26, 2018 is: '2018-26-04')
|
||||
|
||||
Returns:
|
||||
formatted expiration date (str): in format 'Mmm dd YYYY' (ex. April 26, 2018 is: 'Apr 26 2018')
|
||||
"""
|
||||
|
||||
# set expiration date to be the last second of the day it expires
|
||||
expiration_datetime = datetime.combine(
|
||||
date=datetime.strptime(expiration_date, '%Y-%m-%d').date(),
|
||||
time=time.max
|
||||
)
|
||||
return expiration_datetime.strftime(u"%b %d %Y")
|
||||
|
||||
|
||||
def has_access_expired(expiration_date):
|
||||
"""
|
||||
Returns true if it is now past the expiration date.
|
||||
|
||||
Arguments:
|
||||
expiration_date (str): in format 'YYYY-mm-dd' (ex. April 26, 2018 is: '2018-26-04')
|
||||
|
||||
Returns:
|
||||
has access expired (boolean): True if access has expired
|
||||
"""
|
||||
# set expiration date to be the last second of the day it expires
|
||||
expiration_datetime = datetime.combine(
|
||||
date=datetime.strptime(expiration_date, '%Y-%m-%d').date(),
|
||||
time=time.max
|
||||
)
|
||||
now = datetime.today()
|
||||
return now > expiration_datetime
|
||||
@@ -1,77 +0,0 @@
|
||||
""" Journal bundle about page's view """
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import Http404
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from openedx.core.djangoapps.catalog.models import CatalogIntegration
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.features.journals.api import get_journal_bundles, get_journals_root_url
|
||||
|
||||
|
||||
def bundle_about(request, bundle_uuid):
|
||||
"""
|
||||
Journal bundle about page's view.
|
||||
"""
|
||||
bundle = get_journal_bundles(request.site, bundle_uuid=bundle_uuid)
|
||||
if not bundle:
|
||||
raise Http404
|
||||
bundle = bundle[0] # get_journal_bundles always returns list of bundles
|
||||
bundle = extend_bundle(bundle)
|
||||
context = {
|
||||
'journals_root_url': get_journals_root_url(),
|
||||
'discovery_root_url': CatalogIntegration.current().get_internal_api_url(),
|
||||
'bundle': bundle,
|
||||
'uses_bootstrap': True,
|
||||
}
|
||||
return render_to_response('journals/bundle_about.html', context)
|
||||
|
||||
|
||||
def extend_bundle(bundle):
|
||||
"""
|
||||
Extend the pricing data in journal bundle.
|
||||
"""
|
||||
applicable_seat_types = bundle['applicable_seat_types']
|
||||
matching_seats = [
|
||||
get_matching_seat(course, applicable_seat_types)
|
||||
for course in bundle['courses']
|
||||
]
|
||||
# Remove `None`s from above.
|
||||
matching_seats = [seat for seat in matching_seats if seat]
|
||||
course_skus = [seat['sku'] for seat in matching_seats]
|
||||
journal_skus = [journal['sku'] for journal in bundle['journals']]
|
||||
all_skus = course_skus + journal_skus
|
||||
pricing_data = get_pricing_data(all_skus)
|
||||
bundle.update({
|
||||
'pricing_data': pricing_data
|
||||
})
|
||||
return bundle
|
||||
|
||||
|
||||
def get_matching_seat(course, seat_types):
|
||||
""" Filtered out the course runs on the bases of applicable_seat_types """
|
||||
for course_run in course['course_runs']:
|
||||
for seat in course_run['seats']:
|
||||
if seat['type'] in seat_types:
|
||||
return seat
|
||||
|
||||
|
||||
def get_pricing_data(skus):
|
||||
"""
|
||||
Get the pricing data from ecommerce for given skus.
|
||||
"""
|
||||
user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
|
||||
api = ecommerce_api_client(user)
|
||||
pricing_data = api.baskets.calculate.get(sku=skus, is_anonymous=True)
|
||||
discount_value = float(pricing_data['total_incl_tax_excl_discounts']) - float(pricing_data['total_incl_tax'])
|
||||
ecommerce_service = EcommerceService()
|
||||
purchase_url = ecommerce_service.get_checkout_page_url(*skus)
|
||||
pricing_data.update({
|
||||
'is_discounted': pricing_data['total_incl_tax'] != pricing_data['total_incl_tax_excl_discounts'],
|
||||
'discount_value': discount_value,
|
||||
'purchase_url': purchase_url,
|
||||
})
|
||||
return pricing_data
|
||||
@@ -22,7 +22,6 @@ from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.features.learner_profile.toggles import should_redirect_to_profile_microfrontend
|
||||
from openedx.features.learner_profile.views.learner_achievements import LearnerAchievementsFragmentView
|
||||
from openedx.features.journals.api import journals_enabled
|
||||
from student.models import User
|
||||
|
||||
|
||||
@@ -111,7 +110,6 @@ def learner_profile_context(request, profile_username, user_is_staff):
|
||||
'social_platforms': settings.SOCIAL_PLATFORMS,
|
||||
},
|
||||
'show_program_listing': ProgramsApiConfig.is_enabled(),
|
||||
'show_journal_listing': journals_enabled(),
|
||||
'show_dashboard_tabs': True,
|
||||
'disable_courseware_js': True,
|
||||
'nav_hidden': True,
|
||||
|
||||
1
setup.py
1
setup.py
@@ -74,7 +74,6 @@ setup(
|
||||
"credentials = openedx.core.djangoapps.credentials.apps:CredentialsConfig",
|
||||
"discussion = lms.djangoapps.discussion.apps:DiscussionConfig",
|
||||
"grades = lms.djangoapps.grades.apps:GradesConfig",
|
||||
"journals = openedx.features.journals.apps:JournalsConfig",
|
||||
"plugins = openedx.core.djangoapps.plugins.apps:PluginsConfig",
|
||||
"schedules = openedx.core.djangoapps.schedules.apps:SchedulesConfig",
|
||||
"theming = openedx.core.djangoapps.theming.apps:ThemingConfig",
|
||||
|
||||
@@ -45,14 +45,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
% if show_journal_listing:
|
||||
<div class="mobile-nav-item hidden-mobile nav-item nav-tab">
|
||||
<a class="${'active ' if reverse('openedx.journals.dashboard') in request.path else ''}tab-nav-link" href="${reverse('openedx.journals.dashboard')}"
|
||||
aria-current="${'page' if reverse('openedx.journals.dashboard') == request.path else 'false'}">
|
||||
${_("Journals")}
|
||||
</a>
|
||||
</div>
|
||||
% endif
|
||||
% if show_explore_courses:
|
||||
<div class="mobile-nav-item hidden-mobile nav-item nav-tab">
|
||||
<a class="tab-nav-link discover-new-link" href="${marketing_link('COURSES')}"
|
||||
|
||||
Reference in New Issue
Block a user