Merge pull request #22990 from edx/jlajoie/AA-6
AA-6: Adds in Dates tab for Course Overview
This commit is contained in:
@@ -203,7 +203,7 @@ class PrimitiveTabEdit(ModuleStoreTestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
tabs.primitive_delete(course, 1)
|
||||
with self.assertRaises(IndexError):
|
||||
tabs.primitive_delete(course, 6)
|
||||
tabs.primitive_delete(course, 7)
|
||||
tabs.primitive_delete(course, 2)
|
||||
self.assertNotIn({u'type': u'textbooks'}, course.tabs)
|
||||
# Check that discussion has shifted up
|
||||
|
||||
@@ -413,6 +413,7 @@ class CourseTabList(List):
|
||||
discussion_tab,
|
||||
CourseTab.load('wiki'),
|
||||
CourseTab.load('progress'),
|
||||
CourseTab.load('dates'),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -57,6 +57,8 @@ from util.date_utils import strftime_localized
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
import lms.djangoapps.course_blocks.api as course_blocks_api
|
||||
from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -394,7 +396,8 @@ def get_course_info_section(request, user, course, section_key):
|
||||
return html
|
||||
|
||||
|
||||
def get_course_date_blocks(course, user, request=None, include_past_dates=False, num_assignments=None):
|
||||
def get_course_date_blocks(course, user, request=None, include_access=False,
|
||||
include_past_dates=False, num_assignments=None):
|
||||
"""
|
||||
Return the list of blocks to display on the course info page,
|
||||
sorted by date.
|
||||
@@ -413,7 +416,9 @@ def get_course_date_blocks(course, user, request=None, include_past_dates=False,
|
||||
if DATE_WIDGET_V2_FLAG.is_enabled(course.id):
|
||||
blocks.append(CourseExpiredDate(course, user))
|
||||
blocks.extend(get_course_assignment_due_dates(
|
||||
course, user, request, num_return=num_assignments, include_past_dates=include_past_dates))
|
||||
course, user, request, num_return=num_assignments,
|
||||
include_access=include_access, include_past_dates=include_past_dates,
|
||||
))
|
||||
|
||||
return sorted((b for b in blocks if b.date and (b.is_enabled or include_past_dates)), key=date_block_key_fn)
|
||||
|
||||
@@ -426,7 +431,8 @@ def date_block_key_fn(block):
|
||||
return block.date or datetime.max.replace(tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
def get_course_assignment_due_dates(course, user, request, num_return=None, include_past_dates=False):
|
||||
def get_course_assignment_due_dates(course, user, request, num_return=None,
|
||||
include_past_dates=False, include_access=False):
|
||||
"""
|
||||
Returns a list of assignment (at the subsection/sequential level) due date
|
||||
blocks for the given course. Will return num_return results or all results
|
||||
@@ -445,6 +451,9 @@ def get_course_assignment_due_dates(course, user, request, num_return=None, incl
|
||||
date_block = CourseAssignmentDate(course, user)
|
||||
date_block.date = date
|
||||
|
||||
if include_access:
|
||||
date_block.requires_full_access = _requires_full_access(store, user, block_key)
|
||||
|
||||
block_url = None
|
||||
now = datetime.now().replace(tzinfo=pytz.UTC)
|
||||
assignment_released = item.start < now if item.start else None
|
||||
@@ -461,6 +470,22 @@ def get_course_assignment_due_dates(course, user, request, num_return=None, incl
|
||||
return date_blocks
|
||||
|
||||
|
||||
def _requires_full_access(store, user, block_key):
|
||||
"""
|
||||
Returns a boolean if any child of the block_key specified has a group_access array consisting of just full_access
|
||||
"""
|
||||
child_block_keys = course_blocks_api.get_course_blocks(user, block_key)
|
||||
for child_block_key in child_block_keys:
|
||||
child_block = store.get_item(child_block_key)
|
||||
# If group_access is set on the block, and the content gating is
|
||||
# only full access, set the value on the CourseAssignmentDate object
|
||||
if(child_block.group_access and child_block.group_access.get(CONTENT_GATING_PARTITION_ID) == [
|
||||
settings.CONTENT_TYPE_GATE_GROUP_IDS['full_access']
|
||||
]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
|
||||
# arjun will address this by the end of October if no one does so prior to
|
||||
# then.
|
||||
|
||||
@@ -346,6 +346,7 @@ class CourseAssignmentDate(DateSummary):
|
||||
self.assignment_date = None
|
||||
self.assignment_title = None
|
||||
self.assignment_title_html = None
|
||||
self.requires_full_access = None
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
|
||||
@@ -11,11 +11,21 @@ from django.utils.translation import ugettext_noop
|
||||
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.entrance_exams import user_can_skip_entrance_exam
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, default_course_url_name
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.tabs import CourseTab, CourseTabList, course_reverse_func_from_name_func, key_checker
|
||||
|
||||
COURSEWARE_TABS_NAMESPACE = WaffleFlagNamespace(name=u'courseware_tabs')
|
||||
|
||||
ENABLE_DATES_TAB = CourseWaffleFlag(
|
||||
waffle_namespace=COURSEWARE_TABS_NAMESPACE,
|
||||
flag_name="enable_dates_tab",
|
||||
flag_undefined_default=False
|
||||
)
|
||||
|
||||
|
||||
class EnrolledTab(CourseTab):
|
||||
"""
|
||||
@@ -307,6 +317,25 @@ class SingleTextbookTab(CourseTab):
|
||||
raise NotImplementedError('SingleTextbookTab should not be serialized.')
|
||||
|
||||
|
||||
class DatesTab(CourseTab):
|
||||
"""
|
||||
A tab representing the relevant dates for a course.
|
||||
"""
|
||||
type = "dates"
|
||||
title = ugettext_noop(
|
||||
"Dates") # We don't have the user in this context, so we don't want to translate it at this level.
|
||||
view_name = "dates"
|
||||
is_dynamic = True
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
"""Returns true if this tab is enabled."""
|
||||
# We want to only limit this feature to instructor led courses for now
|
||||
if ENABLE_DATES_TAB.is_enabled(course.id):
|
||||
return CourseOverview.get_from_id(course.id) == 'instructor'
|
||||
return False
|
||||
|
||||
|
||||
def get_course_tab_list(user, course):
|
||||
"""
|
||||
Retrieves the course tab list from xmodule.tabs and manipulates the set as necessary
|
||||
|
||||
@@ -8,6 +8,7 @@ import itertools
|
||||
import json
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import utc
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
@@ -60,6 +61,7 @@ from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
|
||||
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
|
||||
@@ -77,6 +79,7 @@ from openedx.features.course_duration_limits.models import CourseDurationLimitCo
|
||||
from openedx.features.course_experience import (
|
||||
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
|
||||
COURSE_OUTLINE_PAGE_FLAG,
|
||||
DATE_WIDGET_V2_FLAG,
|
||||
UNIFIED_COURSE_TAB_FLAG
|
||||
)
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
@@ -3122,3 +3125,88 @@ class AccessUtilsTestCase(ModuleStoreTestCase):
|
||||
course = CourseFactory.create(start=start_date)
|
||||
|
||||
self.assertEqual(bool(check_course_open_for_learner(staff_user, course)), expected_value)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class DatesTabTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Ensure that the dates page renders with the correct data for both a verified and audit learner
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(DatesTabTestCase, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
|
||||
now = datetime.now(utc)
|
||||
self.course = CourseFactory.create(start=now + timedelta(days=-1))
|
||||
self.course.end = now + timedelta(days=3)
|
||||
|
||||
CourseModeFactory(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory(
|
||||
course_id=self.course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
expiration_datetime=now + timedelta(days=1)
|
||||
)
|
||||
VerificationDeadline.objects.create(
|
||||
course_key=self.course.id,
|
||||
deadline=now + timedelta(days=2)
|
||||
)
|
||||
|
||||
def _get_response(self, course):
|
||||
""" Returns the HTML for the progress page """
|
||||
return self.client.get(reverse('dates', args=[six.text_type(course.id)]))
|
||||
|
||||
@override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True)
|
||||
def test_defaults(self):
|
||||
request = RequestFactory().request()
|
||||
request.user = self.user
|
||||
enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.VERIFIED)
|
||||
now = datetime.now(utc)
|
||||
with self.store.bulk_operations(self.course.id):
|
||||
section = ItemFactory.create(category='chapter', parent_location=self.course.location)
|
||||
subsection = ItemFactory.create(
|
||||
category='sequential',
|
||||
display_name='Released',
|
||||
parent_location=section.location,
|
||||
start=now - timedelta(days=1),
|
||||
due=now, # Setting this today so it'll show the 'Due Today' pill
|
||||
graded=True,
|
||||
)
|
||||
|
||||
with patch('lms.djangoapps.courseware.courses.get_dates_for_course') as mock_get_dates:
|
||||
with patch('lms.djangoapps.courseware.views.views.get_enrollment') as mock_get_enrollment:
|
||||
mock_get_dates.return_value = {
|
||||
(subsection.location, 'due'): subsection.due,
|
||||
(subsection.location, 'start'): subsection.start,
|
||||
}
|
||||
mock_get_enrollment.return_value = {
|
||||
'mode': enrollment.mode
|
||||
}
|
||||
response = self._get_response(self.course)
|
||||
self.assertContains(response, subsection.display_name)
|
||||
# Show the Verification Deadline for everyone
|
||||
self.assertContains(response, 'Verification Deadline')
|
||||
# Make sure pill exists for assignment due today
|
||||
self.assertContains(response, '<div class="pill due">')
|
||||
# No pills for verified enrollments
|
||||
self.assertNotContains(response, '<div class="pill verified">')
|
||||
|
||||
enrollment.delete()
|
||||
subsection.due = now + timedelta(days=1)
|
||||
enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.AUDIT)
|
||||
mock_get_dates.return_value = {
|
||||
(subsection.location, 'due'): subsection.due,
|
||||
(subsection.location, 'start'): subsection.start,
|
||||
}
|
||||
mock_get_enrollment.return_value = {
|
||||
'mode': enrollment.mode
|
||||
}
|
||||
|
||||
response = self._get_response(self.course)
|
||||
self.assertContains(response, subsection.display_name)
|
||||
# Show the Verification Deadline for everyone
|
||||
self.assertContains(response, 'Verification Deadline')
|
||||
# Pill doesn't exist for assignment due tomorrow
|
||||
self.assertNotContains(response, '<div class="pill due">')
|
||||
# Should have verified pills for audit enrollments
|
||||
self.assertContains(response, '<div class="pill verified">')
|
||||
|
||||
@@ -57,6 +57,7 @@ from lms.djangoapps.courseware.courses import (
|
||||
can_self_enroll_in_course,
|
||||
course_open_for_self_enrollment,
|
||||
get_course,
|
||||
get_course_date_blocks,
|
||||
get_course_overview_with_access,
|
||||
get_course_with_access,
|
||||
get_courses,
|
||||
@@ -66,6 +67,7 @@ from lms.djangoapps.courseware.courses import (
|
||||
sort_by_announcement,
|
||||
sort_by_start_date
|
||||
)
|
||||
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
|
||||
from lms.djangoapps.courseware.masquerade import setup_masquerade
|
||||
from lms.djangoapps.courseware.model_data import FieldDataCache
|
||||
from lms.djangoapps.courseware.models import BaseStudentModuleHistory, StudentModule
|
||||
@@ -94,7 +96,7 @@ from openedx.core.djangoapps.credit.api import (
|
||||
is_credit_course,
|
||||
is_user_eligible_for_credit
|
||||
)
|
||||
from openedx.core.djangoapps.enrollments.api import add_enrollment
|
||||
from openedx.core.djangoapps.enrollments.api import add_enrollment, get_enrollment
|
||||
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
|
||||
from openedx.core.djangoapps.models.course_details import CourseDetails
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
@@ -1027,6 +1029,33 @@ def program_marketing(request, program_uuid):
|
||||
return render_to_response('courseware/program_marketing.html', context)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@ensure_valid_course_key
|
||||
def dates(request, course_id):
|
||||
"""
|
||||
Display the course's dates.html, or 404 if there is no such course.
|
||||
Assumes the course_id is in a valid format.
|
||||
"""
|
||||
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False)
|
||||
course_date_blocks = get_course_date_blocks(course, request.user, request,
|
||||
include_access=True, include_past_dates=True)
|
||||
enrollment = get_enrollment(request.user.username, course_id)
|
||||
learner_is_verified = False
|
||||
if enrollment:
|
||||
learner_is_verified = enrollment.get('mode') == 'verified'
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'course_date_blocks': [block for block in course_date_blocks if block.title != 'current_datetime'],
|
||||
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
|
||||
'learner_is_verified': learner_is_verified,
|
||||
}
|
||||
|
||||
return render_to_response('courseware/dates.html', context)
|
||||
|
||||
|
||||
@transaction.non_atomic_requests
|
||||
@login_required
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
@import "course/profile";
|
||||
@import "course/tabs";
|
||||
@import "course/student-notes";
|
||||
@import "course/dates";
|
||||
@import "views/teams";
|
||||
|
||||
// course - instructor-only views
|
||||
|
||||
120
lms/static/sass/course/_dates.scss
Normal file
120
lms/static/sass/course/_dates.scss
Normal file
@@ -0,0 +1,120 @@
|
||||
.date-wrapper {
|
||||
@extend .content;
|
||||
|
||||
font-family: $helvetica;
|
||||
|
||||
.date-title {
|
||||
color: #414141;
|
||||
font-weight: 500;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
border-left: solid 1px #2d323e;
|
||||
color: #2d323e;
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
margin-left: 6px;
|
||||
padding-bottom: 32px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-left: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.date-circle {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
background-color: #2d323e;
|
||||
border-radius: 50%;
|
||||
|
||||
&.active {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
left: -7px;
|
||||
background-color: #2d323e;
|
||||
}
|
||||
}
|
||||
|
||||
.date-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
top: -7px;
|
||||
flex: 100%;
|
||||
line-height: 1.25;
|
||||
|
||||
&.active {
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-date-content {
|
||||
@include font-size(16);
|
||||
|
||||
display: flex;
|
||||
flex: 100%;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
@include font-size(14);
|
||||
|
||||
display: flex;
|
||||
flex: 100%;
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
line-height: 1.25;
|
||||
|
||||
a {
|
||||
color: #2d323e;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-description {
|
||||
@include font-size(14);
|
||||
|
||||
display: flex;
|
||||
flex: 100%;
|
||||
line-height: 1.25;
|
||||
|
||||
a {
|
||||
color: #2d323e;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
@include font-size(12);
|
||||
|
||||
padding: 2px 8px 2px 8px;
|
||||
border-radius: 5px;
|
||||
margin-left: 8px;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
vertical-align: top;
|
||||
|
||||
&.due {
|
||||
background-color: #ffdb87;
|
||||
color: #2d323e;
|
||||
}
|
||||
|
||||
&.verified {
|
||||
background-color: #2d323e;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.verified-icon {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
73
lms/templates/courseware/dates.html
Normal file
73
lms/templates/courseware/dates.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="/main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from lms.djangoapps.courseware.date_summary import CourseAssignmentDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<%block name="bodyclass">view-in-course view-progress</%block>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course-vendor'/>
|
||||
<%static:css group='style-course'/>
|
||||
</%block>
|
||||
|
||||
<%block name="pagetitle">${_("{course.display_number_with_default} Course Info").format(course=course)}</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='dates'" />
|
||||
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<div class="container">
|
||||
<div class="date-wrapper">
|
||||
<section class="course-info" id="course-info-dates">
|
||||
<h2 class="hd hd-2 date-title">
|
||||
${_("Important Dates")}
|
||||
</h2>
|
||||
% for block in course_date_blocks:
|
||||
<% active = 'active' if block.date and (block.date.strftime(block.date_format) == block.current_time.strftime(block.date_format)) else '' %>
|
||||
<% block_is_verified = (hasattr(block, 'requires_full_access') and block.requires_full_access) or isinstance(block, VerificationDeadlineDate) %>
|
||||
<% is_assignment = isinstance(block, CourseAssignmentDate) %>
|
||||
% if not (learner_is_verified and isinstance(block, VerifiedUpgradeDeadlineDate)):
|
||||
<div class="timeline-item ${active}">
|
||||
<div class="date-circle ${active}"></div>
|
||||
<div class="date-content ${active}">
|
||||
<div class="timeline-date-content">
|
||||
% if block.date:
|
||||
<div class="timeline-date">
|
||||
${block.date.strftime(block.date_format)}
|
||||
</div>
|
||||
% if active:
|
||||
<div class="pill due">${_('Due Today')}</div>
|
||||
% endif
|
||||
% if block_is_verified and not learner_is_verified:
|
||||
<div class="pill verified"><span class="fa fa-lock verified-icon" aria-hidden="true"></span>${_('Verified Only')}</div>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
<div class="timeline-title ">
|
||||
% if block.title_html and is_assignment:
|
||||
${block.title_html}
|
||||
% else:
|
||||
${block.title}
|
||||
% endif
|
||||
</div>
|
||||
<div class="timeline-description">
|
||||
${block.description}
|
||||
% if block_is_verified and verified_upgrade_link and not learner_is_verified:
|
||||
${Text(_('{a_start}Upgrade{a_end}{space}to a Verified Certificate for full access.')).format(
|
||||
a_start=HTML('<a href={link}>').format(link=verified_upgrade_link),
|
||||
a_end=HTML('</a>'),
|
||||
space=HTML(' '),
|
||||
)}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
% endfor
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -451,6 +451,15 @@ urlpatterns += [
|
||||
name='progress',
|
||||
),
|
||||
|
||||
# dates page
|
||||
url(
|
||||
r'^courses/{}/dates'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
courseware_views.dates,
|
||||
name='dates',
|
||||
),
|
||||
|
||||
# Takes optional student_id for instructor use--shows profile as that student sees it.
|
||||
url(
|
||||
r'^courses/{}/progress/(?P<student_id>[^/]*)/$'.format(
|
||||
|
||||
@@ -65,7 +65,7 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
None: None,
|
||||
}
|
||||
|
||||
COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress'}
|
||||
COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress', 'dates'}
|
||||
|
||||
ENABLED_SIGNALS = ['course_deleted', 'course_published']
|
||||
|
||||
|
||||
1
setup.py
1
setup.py
@@ -21,6 +21,7 @@ setup(
|
||||
"ccx = lms.djangoapps.ccx.plugins:CcxCourseTab",
|
||||
"courseware = lms.djangoapps.courseware.tabs:CoursewareTab",
|
||||
"course_info = lms.djangoapps.courseware.tabs:CourseInfoTab",
|
||||
"dates = lms.djangoapps.courseware.tabs:DatesTab",
|
||||
"discussion = lms.djangoapps.discussion.plugins:DiscussionTab",
|
||||
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesTab",
|
||||
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourseTab",
|
||||
|
||||
Reference in New Issue
Block a user