Merge branch 'master' of github.com:edx/edx-platform into EDUCATOR-5080

This commit is contained in:
Justin Lapierre
2020-07-07 08:36:10 -04:00
85 changed files with 3472 additions and 7309 deletions

View File

@@ -64,9 +64,10 @@ class BlockCompletionTransformer(BlockStructureTransformer):
children = block_structure.get_children(block_key)
non_discussion_children = (child_key for child_key in children
if block_structure.get_xblock_field(child_key, 'category') != 'discussion')
child_complete = (block_structure.get_xblock_field(child_key, self.COMPLETE)
for child_key in non_discussion_children)
if children and all(child_complete):
all_children_complete = all(block_structure.get_xblock_field(child_key, self.COMPLETE)
for child_key in non_discussion_children)
if children and all_children_complete:
block_structure.override_xblock_field(block_key, self.COMPLETE, True)
if any(block_structure.get_xblock_field(child_key, self.RESUME_BLOCK) for child_key in children):

View File

@@ -0,0 +1,30 @@
"""
Toggles for instructor app
"""
from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, WaffleFlag
# Namespace for instructor waffle flags.
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='instructor')
# Waffle flag to use optimised is_small_course.
# .. toggle_name: verify_student.optimised_is_small_course
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Supports staged rollout to improved is_small_course method.
# .. toggle_category: instructor
# .. toggle_use_cases: incremental_release, open_edx
# .. toggle_creation_date: 2020-07-02
# .. toggle_expiration_date: n/a
# .. toggle_warnings: n/a
# .. toggle_tickets: PROD-1740
# .. toggle_status: supported
OPTIMISED_IS_SMALL_COURSE = WaffleFlag(
waffle_namespace=WAFFLE_FLAG_NAMESPACE,
flag_name='optimised_is_small_course',
flag_undefined_default=False
)
def use_optimised_is_small_course():
return OPTIMISED_IS_SMALL_COURSE.is_enabled()

View File

@@ -7,6 +7,7 @@ import datetime
import logging
import uuid
from functools import reduce
import time
import pytz
import six
@@ -32,6 +33,7 @@ from xblock.fields import ScopeIds
from bulk_email.api import is_bulk_email_feature_enabled
from course_modes.models import CourseMode, CourseModesArchive
from edxmako.shortcuts import render_to_response
from instructor.toggles import use_optimised_is_small_course
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.models import (
CertificateGenerationConfiguration,
@@ -132,6 +134,7 @@ def instructor_dashboard_2(request, course_id):
reports_enabled = configuration_helpers.get_value('SHOW_ECOMMERCE_REPORTS', False)
sections = []
start_time = time.time() # starts time before _section_student_admin (further calls is_small_course)
if access['staff']:
sections.extend([
_section_course_info(course, access),
@@ -140,6 +143,8 @@ def instructor_dashboard_2(request, course_id):
_section_discussions_management(course, access),
_section_student_admin(course, access),
])
if course_id == 'course-v1:HarvardX+CS50+X':
log.info('Investigating log at %s : after _section_student_admin', time.time() - start_time)
if access['data_researcher']:
sections.append(_section_data_download(course, access))
@@ -195,7 +200,8 @@ def instructor_dashboard_2(request, course_id):
if can_see_special_exams:
sections.append(_section_special_exams(course, access))
if course_id == 'course-v1:HarvardX+CS50+X':
log.info('Investigating log at %s : section certificate', time.time() - start_time)
# Certificates panel
# This is used to generate example certificates
# and enable self-generated certificates for a course.
@@ -214,7 +220,14 @@ def instructor_dashboard_2(request, course_id):
if len(openassessment_blocks) > 0 and access['staff']:
sections.append(_section_open_response_assessment(request, course, openassessment_blocks, access))
disable_buttons = not _is_small_course(course_key)
if course_id == 'course-v1:HarvardX+CS50+X':
log.info('Investigating log at %s : before Disable Button (calling is_small_course)', time.time() - start_time)
if use_optimised_is_small_course():
disable_buttons = not CourseEnrollment.objects.is_small_course(course_key)
else:
disable_buttons = not _is_small_course(course_key)
if course_id == 'course-v1:HarvardX+CS50+X':
log.info('Investigating log at %s : after Disable Button (calling is_small_course)', time.time() - start_time)
certificate_white_list = CertificateWhitelist.get_certificate_white_list(course_key)
generate_certificate_exceptions_url = reverse(
@@ -235,6 +248,8 @@ def instructor_dashboard_2(request, course_id):
kwargs={'course_id': six.text_type(course_key)}
)
if course_id == 'course-v1:HarvardX+CS50+X':
log.info('Investigating log at %s : Before Context', time.time() - start_time)
certificate_invalidations = CertificateInvalidation.get_certificate_invalidations(course_key)
context = {
@@ -617,7 +632,10 @@ def _is_small_course(course_key):
def _section_student_admin(course, access):
""" Provide data for the corresponding dashboard section """
course_key = course.id
is_small_course = _is_small_course(course_key)
if use_optimised_is_small_course():
is_small_course = CourseEnrollment.objects.is_small_course(course_key)
else:
is_small_course = _is_small_course(course_key)
section_data = {
'section_key': 'student_admin',

View File

@@ -13,7 +13,10 @@ define([
});
mockGetTopic = function(topicId) {
return $.Deferred().resolve(TeamSpecHelpers.createMockTopic({id: topicId}));
return $.Deferred().resolve(TeamSpecHelpers.createMockTopic({
id: topicId,
name: 'teamset-name-' + topicId,
}));
};
createMyTeamsView = function(myTeams) {
@@ -78,5 +81,11 @@ define([
);
AjaxHelpers.respondWithJson(requests, {});
});
it('sets showTeamsetOnTeamCards on child Teams view', function() {
var teams = TeamSpecHelpers.createMockTeams({results: []}),
myTeamsView = createMyTeamsView(teams);
TeamSpecHelpers.verifyCards(myTeamsView, [], true);
});
});
});

View File

@@ -1,41 +1,54 @@
define(['jquery',
'underscore',
'moment',
'teams/js/spec_helpers/team_spec_helpers',
'teams/js/views/team_card',
'teams/js/models/team',
'teams/js/models/topic'],
function($, _, moment, TeamCardView, Team, Topic) {
function($, _, moment, TeamSpecHelpers, TeamCardView, Team, Topic) {
'use strict';
describe('TeamCardView', function() {
var createTeamCardView, view;
var teamName = 'Test Team',
teamID = 'test-team',
courseID = TeamSpecHelpers.testCourseID,
teamsetID = TeamSpecHelpers.testTopicID,
teamsetName = 'Team Set',
description = 'A team for testing';
createTeamCardView = function(topicOptions) {
var model = new Team({
id: 'test-team',
name: 'Test Team',
id: teamID,
name: teamName,
course_id: courseID,
topic_id: teamsetID,
description: description,
is_active: true,
course_id: 'test/course/id',
topic_id: 'test-topic',
description: 'A team for testing',
last_activity_at: '2015-08-21T18:53:01.145Z',
country: 'us',
language: 'en',
membership: []
}),
topic = new Topic(_.extend({id: 'test-topic'}, topicOptions)),
topic = new Topic(_.extend({
id: teamsetID,
name: teamsetName,
}, topicOptions)),
TeamCardClass = TeamCardView.extend({
courseMaxTeamSize: '100',
srInfo: {
id: 'test-sr-id',
text: 'Screenreader text'
},
showTeamset: true,
countries: {us: 'United States of America'},
languages: {en: 'English'},
// eslint-disable-next-line no-unused-vars
getTopic: function(topicId) { return $.Deferred().resolve(topic); }
});
return new TeamCardClass({
model: model
model: model,
showTopic: true,
});
};
@@ -47,8 +60,9 @@ define(['jquery',
it('can render itself', function() {
expect(view.$el).toHaveClass('list-card');
expect(view.$el.find('.card-title').text()).toContain('Test Team');
expect(view.$el.find('.card-description').text()).toContain('A team for testing');
expect(view.$el.find('.card-title').text()).toContain(teamName);
expect(view.$el.find('.card-description').text()).toContain(description);
expect(view.$el.find('.card-type').text()).toContain(teamsetName);
expect(view.$el.find('.team-activity abbr').attr('title')).toContain('August 21st 2015');
expect(view.$el.find('.team-activity').text()).toContain('Last activity');
expect(view.$el.find('.card-meta').text()).toContain('0 / 100 Members');
@@ -56,8 +70,14 @@ define(['jquery',
expect(view.$el.find('.team-language').text()).toContain('English');
});
it('does not show teamset name is showTeamset is false', function() {
view.showTeamset = false;
view.render();
expect(view.$el.find('.card-type').length).toEqual(0);
});
it('navigates to the associated team page when its action button is clicked', function() {
expect(view.$('.action').attr('href')).toEqual('#teams/test-topic/test-team');
expect(view.$('.action').attr('href')).toEqual('#teams/' + teamsetID + '/' + teamID);
});
describe('Profile Image Thumbnails', function() {

View File

@@ -16,13 +16,17 @@ define([
var MockTeamsView = TeamsView.extend({
// eslint-disable-next-line no-unused-vars
getTopic: function(topicId) {
return $.Deferred().resolve(TeamSpecHelpers.createMockTopic({}));
}
return $.Deferred().resolve(TeamSpecHelpers.createMockTopic({
id: topicId,
name: 'teamset-name-' + topicId,
}));
},
});
return new MockTeamsView({
el: '.teams-container',
collection: options.teams || TeamSpecHelpers.createMockTeams(),
showActions: true,
showTeamset: options.showTeamset,
context: TeamSpecHelpers.testContext
}).render();
};
@@ -40,7 +44,18 @@ define([
expect(footerEl.text()).toMatch('1\\s+out of\\s+\/\\s+2'); // eslint-disable-line no-useless-escape
expect(footerEl).not.toHaveClass('hidden');
TeamSpecHelpers.verifyCards(teamsView, testTeamData);
TeamSpecHelpers.verifyCards(teamsView, testTeamData, false);
});
it('forwards the showTeamset option to loaded team cards)', function() {
var testTeamData = TeamSpecHelpers.createMockTeamData(1, 5),
teamsView = createTeamsView({
teams: TeamSpecHelpers.createMockTeams({
results: testTeamData
}),
showTeamset: true,
});
TeamSpecHelpers.verifyCards(teamsView, testTeamData, true);
});
});
});

View File

@@ -113,13 +113,31 @@ define([
);
};
var verifyCards = function(view, teams) {
/**
* Verify that the given view shows cards for each of the teams included.
* If showTeamset is included (true or false), the test will also verify that the teamset
* label/penannt is or is not included in the card accordingly.
*
* @param {JQ Element} view - jquery DOM element for the card container
* @param {TeamModel[]} teams - list of teams to verify.
* @param {[bool]} showTeamset - should show teamset string? (if not included, do not check
* the teamset string at all)
*/
var verifyCards = function(view, teams, showTeamset) {
var teamCards = view.$('.team-card');
_.each(teams, function(team, index) {
var currentCard = teamCards.eq(index);
var teamsetString = 'teamset-name-' + team.topic_id;
expect(currentCard.text()).toMatch(team.name);
expect(currentCard.text()).toMatch(_.object(testLanguages)[team.language]);
expect(currentCard.text()).toMatch(_.object(testCountries)[team.country]);
if (showTeamset === false) {
expect(currentCard.text()).not.toMatch(teamsetString);
}
if (showTeamset === true) {
expect(currentCard.text()).toMatch(teamsetString);
}
});
};

View File

@@ -7,7 +7,13 @@
initialize: function(options) {
this.getTopic = options.getTopic;
TeamsView.prototype.initialize.call(this, options);
TeamsView.prototype.initialize.call(
this,
_.extend(
{ showTeamset: true },
options
)
);
},
render: function() {

View File

@@ -138,11 +138,17 @@
this.model.on('change:membership', function() {
this.detailViews[0].memberships = this.model.get('membership');
}, this);
this.teamsetName = null;
this.getTopic(this.model.get('topic_id')).done(_.bind(function(teamset) {
this.teamsetName = teamset.get('name');
}, this));
},
configuration: 'list_card',
cardClass: 'team-card',
title: function() { return this.model.get('name'); },
pennant: function() { return this.showTeamset ? this.teamsetName : undefined; },
description: function() { return this.model.get('description'); },
details: function() { return this.detailViews; },
actionClass: 'action-view',

View File

@@ -24,6 +24,7 @@
router: options.router,
courseMaxTeamSize: this.context.courseMaxTeamSize,
srInfo: this.srInfo,
showTeamset: options.showTeamset,
countries: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries),
languages: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.languages),
getTopic: function(topicId) { return view.getTopic(topicId); }

View File

@@ -1155,7 +1155,7 @@ def results_callback(request):
)
if use_new_templates_for_id_verification_emails():
context = {'user_id': user, 'expiry_date': expiry_date.strftime("%m/%d/%Y")}
context = {'user': user, 'expiry_date': expiry_date.strftime("%m/%d/%Y")}
send_verification_approved_email(context=context)
else:
verification_status_email_vars['expiry_date'] = expiry_date.strftime("%m/%d/%Y")

View File

@@ -362,15 +362,14 @@ CREDENTIALS_SERVICE_USERNAME = 'credentials_worker'
COURSE_CATALOG_URL_ROOT = 'http://edx.devstack.discovery:18381'
COURSE_CATALOG_API_URL = '{}/api/v1'.format(COURSE_CATALOG_URL_ROOT)
# Uncomment the lines below if you'd like to see SQL statements in your devstack LMS log.
# LOGGING['handlers']['console']['level'] = 'DEBUG'
# LOGGING['loggers']['django.db.backends'] = {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}
# Enable enterprise integration so that we can include enterprise System-wide
# role logic without having to manipulate private settings.
FEATURES['ENABLE_ENTERPRISE_INTEGRATION'] = True
SYSTEM_WIDE_ROLE_CLASSES = os.environ.get("SYSTEM_WIDE_ROLE_CLASSES", SYSTEM_WIDE_ROLE_CLASSES)
SYSTEM_WIDE_ROLE_CLASSES.extend(['system_wide_roles.SystemWideRoleAssignment'])
if FEATURES['ENABLE_ENTERPRISE_INTEGRATION']:
SYSTEM_WIDE_ROLE_CLASSES.extend(['enterprise.SystemWideEnterpriseUserRoleAssignment'])
SYSTEM_WIDE_ROLE_CLASSES.extend([
'system_wide_roles.SystemWideRoleAssignment',
'enterprise.SystemWideEnterpriseUserRoleAssignment',
])
# List of enterprise customer uuids to exclude from transition to use of enterprise-catalog
ENTERPRISE_CUSTOMERS_EXCLUDED_FROM_CATALOG = ()
@@ -398,3 +397,7 @@ if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')):
# ]
# TEMPLATES[1]["DIRS"] = _make_mako_template_dirs
# derive_settings(__name__)
# Uncomment the lines below if you'd like to see SQL statements in your devstack LMS log.
# LOGGING['handlers']['console']['level'] = 'DEBUG'
# LOGGING['loggers']['django.db.backends'] = {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}