Merge pull request #20265 from edx/diana/remove-diff-paid

Remove Diff Paid experimental features.
This commit is contained in:
Diana Huang
2019-05-03 16:03:48 -04:00
committed by GitHub
21 changed files with 0 additions and 1242 deletions

View File

@@ -1,71 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, Button } from '@edx/paragon/static';
import ExperimentalCarousel from './ExperimentalCarousel.jsx';
// https://openedx.atlassian.net/browse/LEARNER-3926
export class PortfolioExperimentUpsellModal extends React.Component {
constructor(props) {
super(props);
this.state = { isOpen: true };
}
render() {
const slides = [
(<div className='portfolio-slide-0'>
<p className='description'>Upgrade to access new content: a guide for building an online portfolio and creating your first project.</p>
<div className='checkmark-group-header'>By following the guide you will:</div>
<ul className='upsell-modal-checkmark-group'>
<li><span className='fa fa-check upsell-modal-checkmark' aria-hidden='true' />Use your new coding skills</li>
<li><span className='fa fa-check upsell-modal-checkmark' aria-hidden='true' />Begin to build your portfolio</li>
<li><span className='fa fa-check upsell-modal-checkmark' aria-hidden='true' />Share what you can do!</li>
</ul>
</div>),
(<div className='portfolio-slide-1'>
<h3 className='slide-header'><b>Use Your New Coding Skills</b></h3>
<p>Want to practice what you've learned? We'll give you the project idea to create your own portfolio. Get creative!</p>
</div>),
(<div className='portfolio-slide-2'>
<h3 className='slide-header'><b>Build Your Portfolio</b></h3>
<p>Apply your knowledge and show them you can code - this project is the perfect start to your portfolio.</p>
</div>),
(<div className='portfolio-slide-3'>
<h3 className='slide-header'><b>Share What You Can Do</b></h3>
<p>Get tips on where to store your project and the best way to share it with employers.</p>
</div>),
];
const body = (
<div>
<ExperimentalCarousel id='portfolio-upsell-modal' slides={slides} />
<img
className="upsell-certificate"
src="https://courses.edx.org/static/images/edx-verified-mini-cert.png"
alt="Sample verified certificate"
/>
</div>
);
return (
<Modal
open={this.state.isOpen}
className='portfolio-upsell-modal'
title={'Portfolio Builder: My First Project'}
onClose={() => {}}
body={body}
buttons={[
(<Button
label={'Upgrade ($100 USD)'}
display={'Upgrade ($100 USD)'}
buttonType='success'
// unfortunately, Button components don't have an href attribute
onClick={() => {}}
/>),
]}
/>
);
}
}

View File

@@ -1,91 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, Button } from '@edx/paragon/static';
import ExperimentalCarousel from './ExperimentalCarousel.jsx';
// https://openedx.atlassian.net/browse/LEARNER-3583
export class UpsellExperimentModal extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: true,
}
}
render() {
const slides = [
(<div>
<div className="my-stats-introduction">My Stats introduces new personalized views that help you track your progress towards completing your course!</div>
<div className="my-stats-slide-header">With My Stats you will see your:</div>
<ul className="upsell-modal-checkmark-group">
<li><span className="fa fa-check upsell-modal-checkmark" aria-hidden="true" />Course Activity Streak (log in every week to keep your streak alive)</li>
<li><span className="fa fa-check upsell-modal-checkmark" aria-hidden="true" />Grade Progress (see how you're tracking towards a passing grade)</li>
<li><span className="fa fa-check upsell-modal-checkmark" aria-hidden="true" />Discussion Forum Engagements (top learners use the forums - how do you measure up?)</li>
</ul>
</div>),
(<div>
<div className="slide-header"><b>Course Activity Streak</b></div>
<span className="course-activity-streak-information">Did you know the learners most likely to complete a course log in every week? Let us help you track your weekly streak - log in every week and learn something new! You can also see how many of the other learners in your course logged in this week.</span>
<img
className="feature-screenshot"
src="https://prod-edx-mktg-edit.edx.org/sites/default/files/week_streak.png"
alt=""
/>
</div>),
(<div>
<div className="slide-header"><b>Grade Progress</b></div>
<span className="grade-progress-information">Wonder how you're doing in the course so far? We can not only show you all your grades, and how much each assignment is worth, but also upcoming graded assignments. This is a great way to track what you might need to work on for a final exam.</span>
<img
className="feature-screenshot"
src="https://prod-edx-mktg-edit.edx.org/sites/default/files/grading.png"
alt=""
/>
</div>),
(<div>
<div className="slide-header"><b>Discussion engagements</b></div>
<span className="discussion-engagements-information">A large percentage of successful learners are engaged on the discussion forums. Compare your forum stats to previous graduates!</span>
<img
className="feature-screenshot"
src="https://prod-edx-mktg-edit.edx.org/sites/default/files/discussions.png"
alt=""
/>
</div>),
];
const body = (
<div>
<ExperimentalCarousel id="upsell-modal" slides={slides} />
<img
className="upsell-certificate"
src="https://courses.edx.org/static/images/edx-verified-mini-cert.png"
alt=""
/>
</div>
);
const { buttonDestinationURL } = this.props;
return (
<Modal
open={this.state.isOpen}
className="upsell-modal"
title={"My Stats"}
onClose={() => {}}
body={body}
buttons={[
(<Button
label={"Upgrade ($100 USD)"}
display={"Upgrade ($100 USD)"}
buttonType="success"
// unfortunately, Button components don't have an href component
onClick={() => window.location = buttonDestinationURL}
/>),
]}
/>
);
}
}
UpsellExperimentModal.propTypes = {
buttonDestinationURL: PropTypes.string.isRequired,
};

View File

@@ -2275,8 +2275,6 @@ INSTALLED_APPS = [
'openedx.features.course_search',
'openedx.features.enterprise_support.apps.EnterpriseSupportConfig',
'openedx.features.learner_profile',
'openedx.features.learner_analytics',
'openedx.features.portfolio_project',
'openedx.features.course_duration_limits',
'openedx.features.content_type_gating',

View File

@@ -32,7 +32,6 @@
@import 'features/course-search';
@import 'features/course-sock';
@import 'features/course-upgrade-message';
@import 'features/learner-analytics-dashboard';
@import 'features/journals';
@import 'features/content-type-gating';

View File

@@ -1,450 +0,0 @@
$mint-green: #92c9d3;
$slices: #386f77, #1abc9c, $mint-green, #397d1c, #a39d3d, #0a5aaa;
$table-border: rgba(155, 155, 155, 0.2);
$font-color: #636c72;
$heading-color: #292b2c;
$grouping-background: #f5f5f5;
$grouping-border: #d8d8d8;
$trophy-gold: #f39c12;
.content-wrapper {
.learner-analytics-header {
border-bottom: none;
.title {
margin-bottom: 0;
font-size: 1.75em;
font-weight: 600;
}
.lock-icon {
margin-right: 5px;
}
}
.page-content.learner-analytics-dashboard-wrapper {
flex-direction: column;
background: #fff;
}
}
.learner-analytics-dashboard {
.sidebar {
position: relative;
&.week-streak {
&::before {
content: '';
width: 100%;
height: 100px;
position: absolute;
bottom: 0;
left: 0;
background: {
image: url('#{$static-path}/images/learner_analytics_dashboard/streak-768x517.jpg');
position: center;
repeat: no-repeat;
size: cover;
}
}
&::after {
content: '';
width: 151px;
height: 192px;
position: absolute;
bottom: 0;
right: 0;
background: {
image: url('#{$static-path}/images/learner_analytics_dashboard/streak-trophy.png');
repeat: no-repeat;
position: bottom right;
}
}
}
}
.analytics-group {
background: $grouping-background;
border: 1px solid $grouping-border;
padding: 20px 20px 120px;
margin: 20px 0 40px;
&:last-of-type {
margin-bottom: 20px;
}
.group-heading {
color: $heading-color;
font-size: 1.5em;
}
.section-heading {
color: $heading-color;
font: {
size: 1em;
weight: bold;
}
}
}
.grading-weight-wrapper {
display: flex;
flex-flow: row wrap;
border-bottom: 1px solid $table-border;
margin-bottom: 25px;
.chart-wrapper {
width: 90px;
height: 90px;
margin-bottom: 20px;
.slice-1 {
fill: nth($slices, 1);
}
.slice-2 {
fill: nth($slices, 2);
}
.slice-3 {
fill: nth($slices, 3);
}
.slice-4 {
fill: nth($slices, 4);
}
.slice-5 {
fill: nth($slices, 5);
}
.slice-6 {
fill: nth($slices, 6);
}
}
}
.legend {
.legend-list {
list-style: none;
padding: 0;
margin: 0 0 20px 20px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.legend-item {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 10px;
font-size: 0.875em;
.color-swatch {
width: 20px;
height: 20px;
border-radius: 3px;
margin-right: 10px;
&.swatch-1 {
background-color: nth($slices, 1);
}
&.swatch-2 {
background-color: nth($slices, 2);
}
&.swatch-3 {
background-color: nth($slices, 3);
}
&.swatch-4 {
background-color: nth($slices, 4);
}
&.swatch-5 {
background-color: nth($slices, 5);
}
&.swatch-6 {
background-color: nth($slices, 6);
}
}
.label,
.percentage {
color: $font-color;
}
.label {
margin-right: 30px;
}
.percentage {
margin-left: auto;
}
}
}
.graded-assessments-wrapper {
.grade-table {
width: 100%;
margin-bottom: 30px;
font-size: 0.875em;
th {
font-weight: bold;
&:nth-of-type(3) {
width: 80px;
}
}
th,
td {
border: none;
color: $font-color;
&:nth-of-type(2),
&:nth-of-type(3) {
text-align: right;
}
&:nth-of-type(3) {
background: rgba(255, 193, 7, 0.15);
color: #000;
font-weight: bold;
}
}
.totals {
font-size: 18px;
border-color: $table-border;
.footer-label {
color: black;
}
}
}
.footnote {
font-size: 0.875em;
color: $font-color;
}
.table-head {
border-bottom: 2px solid $table-border;
}
.type-group {
border-bottom: 1px solid $table-border;
&:last-of-type {
border-bottom-width: 2px;
}
}
}
.due-dates {
border-bottom: 1px solid $table-border;
padding-bottom: 30px;
margin-bottom: 30px;
.date-list {
list-style: none;
padding: 0;
margin: 0;
.date-item {
display: flex;
justify-content: space-between;
color: $font-color;
}
}
}
.week-streak-wrapper {
color: $font-color;
.streak-icon-wrapper {
margin: 10px 0;
}
.fa-trophy {
color: $trophy-gold;
font-size: 21px;
margin-right: 5px;
}
.section-heading {
margin-bottom: 0;
}
.streak-encouragement {
margin-bottom: 5px;
}
.streak-criteria {
margin-bottom: 20px;
}
}
.active-users-wrapper {
.fa-user {
border: 2px solid $mint-green;
border-radius: 50%;
width: 28px;
height: 28px;
text-align: center;
position: relative;
&::after {
content: '';
height: 2px;
width: 12px;
position: absolute;
background-color: $mint-green;
left: 6px;
bottom: 0;
}
}
.label {
color: $font-color;
}
}
.discussions-wrapper {
display: flex;
flex-flow: column wrap;
.group-heading {
width: 100%;
}
.post-counts {
border-top: 1px solid $table-border;
}
}
.count-icon {
color: $mint-green;
font-size: 24px;
}
.user-count {
font-size: 1.5em;
margin-left: 10px;
}
.count-chart {
position: relative;
min-height: 50px;
margin-bottom: 20px;
padding-left: 70px;
.chart-bar {
height: 10px;
border-radius: 2px;
background: $mint-green;
}
.chart-icon {
width: 50px;
height: 50px;
background: $mint-green;
border-radius: 50%;
text-align: center;
font-size: 24px;
padding-top: 13px;
color: white;
position: absolute;
left: 0;
top: 0;
}
&:first-of-type {
.chart-bar,
.chart-icon {
background: $trophy-gold;
}
}
.chart-display {
display: flex;
flex-flow: row wrap;
align-items: baseline;
.user-count {
margin-top: -10px;
font-weight: 600;
}
}
}
@include media-breakpoint-up(md) {
.sidebar {
&.week-streak {
&::before {
background-image: url('#{$static-path}/images/learner_analytics_dashboard/streak-1140x768.jpg');
}
}
}
.analytics-group {
&:first-of-type {
background: {
image: url('#{$static-path}/images/learner_analytics_dashboard/analytics-grading.png');
repeat: no-repeat;
position: top right;
}
}
}
}
@include media-breakpoint-up(lg) {
.discussions-wrapper {
flex-direction: row;
.comparison-charts {
border-right: 1px solid $table-border;
width: 50%;
}
.post-counts {
border: none;
width: 50%;
padding-left: 20px;
}
}
}
@include media-breakpoint-up(xl) {
.sidebar {
&.week-streak {
&::before {
background-image: url('#{$static-path}/images/learner_analytics_dashboard/streak-768x517.jpg');
}
}
}
.learner-analytics-wrapper {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: flex-start;
.main-block {
width: calc(100% - 390px);
}
.sidebar {
width: 360px;
margin-left: 30px;
}
}
}
}

View File

@@ -643,22 +643,6 @@ urlpatterns += [
r'^u/',
include('openedx.features.learner_profile.urls'),
),
# Learner analytics dashboard
url(
r'^courses/{}/learner_analytics/'.format(
settings.COURSE_ID_PATTERN,
),
include('openedx.features.learner_analytics.urls'),
),
# Portfolio project experiment
url(
r'^courses/{}/xfeature/portfolio/'.format(
settings.COURSE_ID_PATTERN,
),
include('openedx.features.portfolio_project.urls'),
),
]
if settings.FEATURES.get('ENABLE_TEAMS'):

View File

@@ -15,8 +15,6 @@ from lms.djangoapps.discussion.django_comment_client.permissions import has_perm
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REVIEWS_TOOL_FLAG
from openedx.features.learner_analytics import ENABLE_DASHBOARD_TAB
from openedx.features.portfolio_project import INCLUDE_PORTFOLIO_UPSELL_MODAL
%>
<%block name="header_extras">
@@ -25,23 +23,6 @@ from openedx.features.portfolio_project import INCLUDE_PORTFOLIO_UPSELL_MODAL
<%block name="content">
<div class="course-view page-content-container" id="course-container">
% if ENABLE_DASHBOARD_TAB.is_enabled(course_key):
${static.renderReact(
component="UpsellExperimentModal",
id="upsell-modal",
props={},
)}
% endif
% if INCLUDE_PORTFOLIO_UPSELL_MODAL.is_enabled():
${static.renderReact(
component="PortfolioExperimentUpsellModal",
id="portfolio-experiment-upsell-modal",
props={}
)}
% endif
<header class="page-header has-secondary">
<div class="page-header-main">
<nav aria-label="${_('Course Outline')}" class="sr-is-focusable" tabindex="-1">

View File

@@ -1,10 +0,0 @@
Learner Analytics
-----------------
This is the current home of the Learner Analytics feature. This is not a fully
supported feature.
See LEARNER-3854 for details.
TODO: LEARNER-3854: If this feature gets implemented, this directory should
move to lms/djangoapps and out of openedx/features.

View File

@@ -1,19 +0,0 @@
"""
Learner analytics helpers and settings
"""
from openedx.core.djangoapps.waffle_utils import (
CourseWaffleFlag, WaffleFlag, WaffleFlagNamespace
)
# Namespace for learner analytics waffle flags.
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='learner_analytics')
# Simple safety valve in case the modal breaks DOM.
# Used in the Learner Analytics and Enhanced Support tests
# Please do not turn off this flag until all tests have been resolved
# https://openedx.atlassian.net/browse/LEARNER-3377
# https://openedx.atlassian.net/browse/LEARNER-3514
INCLUDE_UPSELL_MODAL = WaffleFlag(WAFFLE_FLAG_NAMESPACE, 'include_upsell_modal')
# Enables the learner analytics page for different courses via waffle course overrides.
ENABLE_DASHBOARD_TAB = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_dashboard_tab')

View File

@@ -1,67 +0,0 @@
## mako
<%page expression_filter="h"/>
<%! main_css = "style-main-v2" %>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def>
<%def name="course_name()">
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
</%def>
<%!
import json
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import course_home_page_title
%>
<%block name="bodyclass">course</%block>
<%block name="pagetitle">${course_name()}</%block>
<%include file="../courseware/course_navigation.html" args="active_page='learner_analytics'" />
<%block name="content">
<div class="course-view page-content-container" id="course-container">
<header class="page-header has-secondary learner-analytics-header">
## TODO: LEARNER-3854: Clean-up after Learner Analytics test.
## May not need/want breadcrumbs? Can maybe kill course_url and course_home_page_title
## from the context?
## Breadcrumb navigation
<div class="page-header-main">
<h2 class="title"><span class="fa ${'fa-unlock-alt' if has_access else 'fa-lock'} lock-icon" aria-hidden="true"></span> ${_('My Stats (Beta)')}</h2>
</div>
</header>
<div class="page-content learner-analytics-dashboard-wrapper">
<div class="learner-analytics-dashboard">
% if has_access:
${static.renderReact(
component="LearnerAnalyticsDashboard",
id="react-learner-analytics-dashboard",
props={
'schedule': assignment_schedule,
'schedule_raw': assignment_schedule_raw,
'grading_policy': grading_policy,
'grades': assignment_grades,
'discussion_info': discussion_info,
'weekly_active_users': weekly_active_users,
'week_streak': week_streak,
'profile_images': profile_image_urls,
'passing_grade': passing_grade,
'percent_grade': percent_grade,
}
)}
% else:
## TODO: LEARNER-3854: Clean-up after Learner Analytics test.
## If we move forward with this, the upsell information should
## be added here.
Page is not available.
% endif
</div>
</div>
</div>
</%block>
<%namespace name='static' file='../static_content.html'/>

View File

@@ -1 +0,0 @@
# TODO: LEARNER-3854: Implement tests or remove file after Learner Analytics test.

View File

@@ -1,15 +0,0 @@
"""
Url setup for learner analytics
"""
from django.conf.urls import url
from views import LearnerAnalyticsView
urlpatterns = [
url(
r'^$',
LearnerAnalyticsView.as_view(),
name='openedx.learner_analytics.dashboard',
),
]

View File

@@ -1,347 +0,0 @@
"""
Learner analytics dashboard views
"""
import logging
import math
import urllib
from datetime import datetime, timedelta
import pytz
import requests
from analyticsclient.client import Client
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.urls import reverse
from django.http import Http404
from django.shortcuts import render_to_response
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.generic import View
from opaque_keys.edx.keys import CourseKey
from student.models import CourseEnrollment
from util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore
from course_modes.models import get_cosmetic_verified_display_price
from lms.djangoapps.course_api.blocks.api import get_blocks
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.courseware.courses import get_course_with_access
from lms.djangoapps.discussion.views import create_user_profile_context
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from openedx.features.course_experience import default_course_url_name
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
from . import ENABLE_DASHBOARD_TAB
log = logging.getLogger(__name__)
class LearnerAnalyticsView(View):
"""
Displays the Learner Analytics Dashboard.
"""
def __init__(self):
View.__init__(self)
self.analytics_client = Client(base_url=settings.ANALYTICS_API_URL, auth_token=settings.ANALYTICS_API_KEY)
@method_decorator(login_required)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
@method_decorator(ensure_valid_course_key)
def get(self, request, course_id):
"""
Displays the user's Learner Analytics for the specified course.
Arguments:
request: HTTP request
course_id (unicode): course id
"""
course_key = CourseKey.from_string(course_id)
if not ENABLE_DASHBOARD_TAB.is_enabled(course_key):
raise Http404
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_url_name = default_course_url_name(course.id)
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
is_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)
has_access = is_verified or request.user.is_staff
enrollment = CourseEnrollment.get_enrollment(request.user, course_key)
upgrade_price = None
upgrade_url = None
if enrollment and enrollment.upgrade_deadline:
upgrade_url = EcommerceService().upgrade_url(request.user, course_key)
upgrade_price = get_cosmetic_verified_display_price(course)
context = {
'upgrade_price': upgrade_price,
'upgrade_link': upgrade_url,
'course': course,
'course_url': course_url,
'disable_courseware_js': True,
'uses_pattern_library': True,
'is_self_paced': course.self_paced,
'is_verified': is_verified,
'has_access': has_access,
}
if (has_access):
grading_policy = course.grading_policy
(raw_grade_data, answered_percent, percent_grade) = self.get_grade_data(request.user, course_key, grading_policy['GRADE_CUTOFFS'])
raw_schedule_data = self.get_assignments_with_due_date(request, course_key)
grade_data, schedule_data = self.sort_grade_and_schedule_data(raw_grade_data, raw_schedule_data)
# TODO: LEARNER-3854: Fix hacked defaults with real error handling if implementing Learner Analytics.
try:
weekly_active_users = self.get_weekly_course_activity_count(course_key)
week_streak = self.consecutive_weeks_of_course_activity_for_user(
request.user.username, course_key
)
except Exception as e:
logging.exception(e)
weekly_active_users = 134
week_streak = 1
context.update({
'grading_policy': grading_policy,
'assignment_grades': grade_data,
'answered_percent': answered_percent,
'assignment_schedule': schedule_data,
'assignment_schedule_raw': raw_schedule_data,
'profile_image_urls': get_profile_image_urls_for_user(request.user, request),
'discussion_info': self.get_discussion_data(request, course_key),
'passing_grade': math.ceil(100 * course.lowest_passing_grade),
'percent_grade': math.ceil(100 * percent_grade),
'weekly_active_users': weekly_active_users,
'week_streak': week_streak,
})
return render_to_response('learner_analytics/dashboard.html', context)
def get_grade_data(self, user, course_key, grade_cutoffs):
"""
Collects and formats the grades data for a particular user and course.
Args:
user (User)
course_key (CourseKey)
grade_cutoffs: # TODO: LEARNER-3854: Complete docstring if implementing Learner Analytics.
"""
course_grade = CourseGradeFactory().read(user, course_key=course_key)
grades = []
total_earned = 0
total_possible = 0
# answered_percent seems to be unused and it does not take into account assignment type weightings
answered_percent = None
chapter_grades = course_grade.chapter_grades.values()
for chapter in chapter_grades:
# Note: this code exists on the progress page. We should be able to remove it going forward.
if not chapter['display_name'] == "hidden":
for subsection_grade in chapter['sections']:
log.info(subsection_grade.display_name)
possible = subsection_grade.graded_total.possible
earned = subsection_grade.graded_total.earned
passing_grade = math.ceil(possible * grade_cutoffs['Pass'])
grades.append({
'assignment_type': subsection_grade.format,
'total_earned': earned,
'total_possible': possible,
'passing_grade': passing_grade,
'display_name': subsection_grade.display_name,
'location': unicode(subsection_grade.location),
'assigment_url': reverse('jump_to_id', kwargs={
'course_id': unicode(course_key),
'module_id': unicode(subsection_grade.location),
})
})
if earned > 0:
total_earned += earned
total_possible += possible
if total_possible > 0:
answered_percent = float(total_earned) / total_possible
return (grades, answered_percent, course_grade.percent)
def sort_grade_and_schedule_data(self, grade_data, schedule_data):
"""
Sort the assignments in grade_data and schedule_data to be in the same order.
"""
schedule_dict = {assignment['location']: assignment for assignment in schedule_data}
sorted_schedule_data = []
sorted_grade_data = []
for grade in grade_data:
assignment = schedule_dict.get(grade['location'])
if assignment:
sorted_grade_data.append(grade)
sorted_schedule_data.append(assignment)
return sorted_grade_data, sorted_schedule_data
def get_discussion_data(self, request, course_key):
"""
Collects and formats the discussion data from a particular user and course.
Args:
request (HttpRequest)
course_key (CourseKey)
"""
try:
context = create_user_profile_context(request, course_key, request.user.id)
except Exception as e:
# TODO: LEARNER-3854: Clean-up error handling if continuing support.
return {
'content_authored': 0,
'thread_votes': 0,
}
threads = context['threads']
profiled_user = context['profiled_user']
# TODO: LEARNER-3854: If implementing Learner Analytics, rename to content_authored_count.
content_authored = profiled_user['threads_count'] + profiled_user['comments_count']
thread_votes = 0
for thread in threads:
if thread['user_id'] == profiled_user['external_id']:
thread_votes += thread['votes']['count']
discussion_data = {
'content_authored': content_authored,
'thread_votes': thread_votes,
}
return discussion_data
def get_assignments_with_due_date(self, request, course_key):
"""
Returns a list of assignment (graded) blocks with due dates, including
due date and location.
Args:
request (HttpRequest)
course_key (CourseKey)
"""
course_usage_key = modulestore().make_course_usage_key(course_key)
all_blocks = get_blocks(
request,
course_usage_key,
user=request.user,
nav_depth=3,
requested_fields=['display_name', 'due', 'graded', 'format'],
block_types_filter=['sequential']
)
assignment_blocks = []
for (location, block) in all_blocks['blocks'].iteritems():
if block.get('graded', False):
assignment_blocks.append(block)
block['due'] = block['due'].isoformat() if block.get('due') is not None else None
block['location'] = unicode(location)
return assignment_blocks
def get_weekly_course_activity_count(self, course_key):
"""
Get the count of any course activity (total for all users) from previous 7 days.
Args:
course_key (CourseKey)
"""
cache_key = 'learner_analytics_{course_key}_weekly_activities'.format(course_key=course_key)
activities = cache.get(cache_key)
if not activities:
log.info(u'Weekly course activities for course {course_key} was not cached - fetching from Analytics API'
.format(course_key=course_key))
weekly_course_activities = self.analytics_client.courses(course_key).activity()
if not weekly_course_activities or 'any' not in weekly_course_activities[0]:
return 0
# weekly course activities should only have one item
activities = weekly_course_activities[0]
cache.set(cache_key, activities, LearnerAnalyticsView.seconds_to_cache_expiration())
return activities['any']
def consecutive_weeks_of_course_activity_for_user(self, username, course_key):
"""
Get the most recent count of consecutive days that a user has performed a course activity
Args:
username (str)
course_key (CourseKey)
"""
cache_key = 'learner_analytics_{username}_{course_key}_engagement_timeline'\
.format(username=username, course_key=course_key)
timeline = cache.get(cache_key)
if not timeline:
log.info(u'Engagement timeline for course {course_key} was not cached - fetching from Analytics API'
.format(course_key=course_key))
# TODO (LEARNER-3470): @jaebradley replace this once the Analytics client has an engagement timeline method
url = '{base_url}/engagement_timelines/{username}?course_id={course_key}'\
.format(base_url=settings.ANALYTICS_API_URL,
username=username,
course_key=urllib.quote_plus(unicode(course_key)))
headers = {'Authorization': u'Token {token}'.format(token=settings.ANALYTICS_API_KEY)}
response = requests.get(url=url, headers=headers)
data = response.json()
if not data or 'days' not in data or not data['days']:
return 0
# Analytics API returns data in ascending (by date) order - we want to count starting from most recent day
data_ordered_by_date_descending = list(reversed(data['days']))
cache.set(cache_key, data_ordered_by_date_descending, LearnerAnalyticsView.seconds_to_cache_expiration())
timeline = data_ordered_by_date_descending
return LearnerAnalyticsView.calculate_week_streak(timeline)
@staticmethod
def calculate_week_streak(daily_activities):
"""
Check number of weeks in a row that a user has performed some activity.
Regardless of when a week starts, a sufficient condition for checking if a specific week had any user activity
(given a list of daily activities ordered by date) is to iterate through the list of days 7 days at a time and
check to see if any of those days had any activity.
Args:
daily_activities: sorted list of dictionaries containing activities and their counts
"""
week_streak = 0
seven_day_buckets = [daily_activities[i:i + 7] for i in range(0, len(daily_activities), 7)]
for bucket in seven_day_buckets:
if any(LearnerAnalyticsView.has_activity(day) for day in bucket):
week_streak += 1
else:
return week_streak
return week_streak
@staticmethod
def has_activity(daily_activity):
"""
Validate that a course had some activity that day
Args:
daily_activity: dictionary of activities and their counts
"""
return int(daily_activity['problems_attempted']) > 0 \
or int(daily_activity['problems_completed']) > 0 \
or int(daily_activity['discussion_contributions']) > 0 \
or int(daily_activity['videos_viewed']) > 0
@staticmethod
def seconds_to_cache_expiration():
"""Calculate cache expiration seconds. Currently set to seconds until midnight UTC"""
next_midnight_utc = (datetime.today() + timedelta(days=1)).replace(hour=0, minute=0, second=0,
microsecond=0, tzinfo=pytz.utc)
now_utc = datetime.now(tz=pytz.utc)
return round((next_midnight_utc - now_utc).total_seconds())

View File

@@ -1,12 +0,0 @@
Portfolio Project
-----------------
This is the current home of the Portfolio Project feature. This is not a fully
supported feature.
See `LEARNER-3515`_ for details.
TODO: `LEARNER-3515`_: If this feature gets implemented, this directory should
move to lms/djangoapps and out of openedx/features.
.. _LEARNER-3515: https://openedx.atlassian.net/browse/LEARNER-3515

View File

@@ -1,13 +0,0 @@
"""
Portfolio project helpers and settings
"""
from openedx.core.djangoapps.waffle_utils import (
CourseWaffleFlag, WaffleFlag, WaffleFlagNamespace
)
# Namespace for portfolio project waffle flags.
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='portfolio_project')
# https://openedx.atlassian.net/browse/LEARNER-3926
INCLUDE_PORTFOLIO_UPSELL_MODAL = WaffleFlag(WAFFLE_FLAG_NAMESPACE, 'include_upsell_modal')

View File

@@ -1,10 +0,0 @@
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%block name="content">
<div class="course-view page-content-container" id="course-container">
</div>
</%block>
<%namespace name='static' file='../static_content.html'/>

View File

@@ -1,15 +0,0 @@
"""
Url setup for portfolio project.
"""
from django.conf.urls import url
from views import GenericTabView
urlpatterns = [
url(
r'^$',
GenericTabView.as_view(),
name='openedx.portfolio.generic_tab',
),
]

View File

@@ -1,45 +0,0 @@
"""
Portfolio views.
"""
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from lms.djangoapps.courseware.views.views import CourseTabView
from util.views import ensure_valid_course_key
from web_fragments.fragment import Fragment
class GenericTabView(CourseTabView):
"""
Provides a blank page that acts as its own tab in courseware for displaying content.
"""
def uses_bootstrap(self, request, course, tab):
"""
Forces the generic tab to use bootstrap styling.
"""
return True
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
@method_decorator(ensure_valid_course_key)
def get(self, request, course_id, **kwargs):
"""
Displays a generic tab for the specified course.
"""
return super(GenericTabView, self).get(request, course_id, 'courseware', **kwargs)
def render_to_fragment(self, request, course=None, tab=None, **kwargs):
"""
Render out the bootstrap page.
"""
context = {
'course': course,
'user': request.user,
'tab_name': tab,
}
html = render_to_string('portfolio_project/generic_tab.html', context)
return Fragment(html)

View File

@@ -7,8 +7,6 @@ from django.urls import reverse
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.learner_analytics import INCLUDE_UPSELL_MODAL
from openedx.features.portfolio_project import INCLUDE_PORTFOLIO_UPSELL_MODAL
%>
<%namespace name='static' file='/static_content.html'/>
@@ -86,21 +84,6 @@ from openedx.features.portfolio_project import INCLUDE_PORTFOLIO_UPSELL_MODAL
<div id="currency_data" value="${currency_data}"></div>
<div class="container">
% if INCLUDE_UPSELL_MODAL.is_enabled():
${static.renderReact(
component="UpsellExperimentModal",
id="upsell-modal",
props={},
)}
% endif
% if INCLUDE_PORTFOLIO_UPSELL_MODAL.is_enabled():
${static.renderReact(
component="PortfolioExperimentUpsellModal",
id="portfolio-experiment-upsell-modal",
props={}
)}
% endif
<section class="wrapper">
<div class="wrapper-register-choose wrapper-content-main">
<article class="register-choose content-main">

View File

@@ -19,9 +19,6 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.learner_analytics import INCLUDE_UPSELL_MODAL
from openedx.features.portfolio_project import INCLUDE_PORTFOLIO_UPSELL_MODAL
from entitlements.models import CourseEntitlement
from student.models import CourseEnrollment
%>
@@ -143,21 +140,6 @@ from student.models import CourseEnrollment
<section class="dashboard" id="dashboard-main">
<main class="main-container" id="main" aria-label="Content" tabindex="-1">
% if INCLUDE_UPSELL_MODAL.is_enabled():
${static.renderReact(
component="UpsellExperimentModal",
id="upsell-modal",
props={},
)}
% endif
% if INCLUDE_PORTFOLIO_UPSELL_MODAL.is_enabled():
${static.renderReact(
component="PortfolioExperimentUpsellModal",
id="portfolio-experiment-upsell-modal",
props={},
)}
% endif
<section class="my-courses" id="my-courses">
<header class="wrapper-header-courses">
<h2 class="header-courses">${_("My Courses")}</h2>

View File

@@ -84,9 +84,6 @@ module.exports = Merge.smart({
// LMS
SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx',
AlertStatusBar: './lms/static/js/accessible_components/StatusBarAlert.jsx',
LearnerAnalyticsDashboard: './lms/static/js/learner_analytics_dashboard/LearnerAnalyticsDashboard.jsx',
UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx',
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx',
PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx',
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',