Refactor server-size code to enable enrollment tracks.
EDUCATOR-11
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import openedx.core.djangoapps.xmodule_django.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_comment_common', '0004_auto_20161117_1209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseDiscussionSettings',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(help_text=b'Which course are these settings associated with?', unique=True, max_length=255, db_index=True)),
|
||||
('always_divide_inline_discussions', models.BooleanField(default=False)),
|
||||
('_divided_discussions', models.TextField(null=True, db_column=b'divided_discussions', blank=True)),
|
||||
('division_scheme', models.CharField(default=b'none', max_length=20, choices=[(b'none', b'None'), (b'cohort', b'Cohort'), (b'enrollment_track', b'Enrollment Track')])),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
@@ -162,3 +163,30 @@ class ForumsConfig(ConfigurationModel):
|
||||
def __unicode__(self):
|
||||
"""Simple representation so the admin screen looks less ugly."""
|
||||
return u"ForumsConfig: timeout={}".format(self.connection_timeout)
|
||||
|
||||
|
||||
class CourseDiscussionSettings(models.Model):
|
||||
course_id = CourseKeyField(
|
||||
unique=True,
|
||||
max_length=255,
|
||||
db_index=True,
|
||||
help_text="Which course are these settings associated with?",
|
||||
)
|
||||
always_divide_inline_discussions = models.BooleanField(default=False)
|
||||
_divided_discussions = models.TextField(db_column='divided_discussions', null=True, blank=True) # JSON list
|
||||
|
||||
COHORT = 'cohort'
|
||||
ENROLLMENT_TRACK = 'enrollment_track'
|
||||
NONE = 'none'
|
||||
ASSIGNMENT_TYPE_CHOICES = ((NONE, 'None'), (COHORT, 'Cohort'), (ENROLLMENT_TRACK, 'Enrollment Track'))
|
||||
division_scheme = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=NONE)
|
||||
|
||||
@property
|
||||
def divided_discussions(self):
|
||||
"""Jsonify the divided_discussions"""
|
||||
return json.loads(self._divided_discussions)
|
||||
|
||||
@divided_discussions.setter
|
||||
def divided_discussions(self, value):
|
||||
"""Un-Jsonify the divided_discussions"""
|
||||
self._divided_discussions = json.dumps(value)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from django_comment_common.models import Role
|
||||
from models import CourseDiscussionSettings
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from openedx.core.djangoapps.course_groups.cohorts import CourseCohortsSettings
|
||||
from student.models import CourseEnrollment, User
|
||||
from utils import get_course_discussion_settings, set_course_discussion_settings
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class RoleAssignmentTest(TestCase):
|
||||
"""
|
||||
Basic checks to make sure our Roles get assigned and unassigned as students
|
||||
@@ -55,3 +64,73 @@ class RoleAssignmentTest(TestCase):
|
||||
# )
|
||||
# self.assertNotIn(student_role, self.student_user.roles.all())
|
||||
# self.assertIn(student_role, another_student.roles.all())
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class CourseDiscussionSettingsTest(ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CourseDiscussionSettingsTest, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
def test_get_course_discussion_settings(self):
|
||||
discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.NONE, discussion_settings.division_scheme)
|
||||
self.assertEqual([], discussion_settings.divided_discussions)
|
||||
self.assertFalse(discussion_settings.always_divide_inline_discussions)
|
||||
|
||||
def test_get_course_discussion_settings_legacy_settings(self):
|
||||
self.course.cohort_config = {
|
||||
'cohorted': True,
|
||||
'always_cohort_inline_discussions': True,
|
||||
'cohorted_discussions': ['foo']
|
||||
}
|
||||
modulestore().update_item(self.course, ModuleStoreEnum.UserID.system)
|
||||
discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.COHORT, discussion_settings.division_scheme)
|
||||
self.assertEqual(['foo'], discussion_settings.divided_discussions)
|
||||
self.assertTrue(discussion_settings.always_divide_inline_discussions)
|
||||
|
||||
def test_get_course_discussion_settings_cohort_settings(self):
|
||||
CourseCohortsSettings.objects.get_or_create(
|
||||
course_id=self.course.id,
|
||||
defaults={
|
||||
'is_cohorted': True,
|
||||
'always_cohort_inline_discussions': True,
|
||||
'cohorted_discussions': ['foo', 'bar']
|
||||
}
|
||||
)
|
||||
discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.COHORT, discussion_settings.division_scheme)
|
||||
self.assertEqual(['foo', 'bar'], discussion_settings.divided_discussions)
|
||||
self.assertTrue(discussion_settings.always_divide_inline_discussions)
|
||||
|
||||
def test_set_course_discussion_settings(self):
|
||||
set_course_discussion_settings(
|
||||
course_key=self.course.id,
|
||||
divided_discussions=['cohorted_topic'],
|
||||
division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK,
|
||||
always_divide_inline_discussions=True,
|
||||
)
|
||||
discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.ENROLLMENT_TRACK, discussion_settings.division_scheme)
|
||||
self.assertEqual(['cohorted_topic'], discussion_settings.divided_discussions)
|
||||
self.assertTrue(discussion_settings.always_divide_inline_discussions)
|
||||
|
||||
def test_invalid_data_types(self):
|
||||
exception_msg_template = "Incorrect field type for `{}`. Type must be `{}`"
|
||||
fields = [
|
||||
{'name': 'division_scheme', 'type': str},
|
||||
{'name': 'always_divide_inline_discussions', 'type': bool},
|
||||
{'name': 'divided_discussions', 'type': list}
|
||||
]
|
||||
invalid_value = 3.14
|
||||
|
||||
for field in fields:
|
||||
with self.assertRaises(ValueError) as value_error:
|
||||
set_course_discussion_settings(self.course.id, **{field['name']: invalid_value})
|
||||
|
||||
self.assertEqual(
|
||||
value_error.exception.message,
|
||||
exception_msg_template.format(field['name'], field['type'].__name__)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,10 @@ from django_comment_common.models import (
|
||||
FORUM_ROLE_STUDENT,
|
||||
Role
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_legacy_discussion_settings
|
||||
from request_cache.middleware import request_cached
|
||||
|
||||
from .models import CourseDiscussionSettings
|
||||
|
||||
|
||||
class ThreadContext(object):
|
||||
@@ -91,3 +95,47 @@ def are_permissions_roles_seeded(course_id):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@request_cached
|
||||
def get_course_discussion_settings(course_key):
|
||||
try:
|
||||
course_discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key)
|
||||
except CourseDiscussionSettings.DoesNotExist:
|
||||
legacy_discussion_settings = get_legacy_discussion_settings(course_key)
|
||||
course_discussion_settings, _ = CourseDiscussionSettings.objects.get_or_create(
|
||||
course_id=course_key,
|
||||
defaults={
|
||||
'always_divide_inline_discussions': legacy_discussion_settings['always_cohort_inline_discussions'],
|
||||
'divided_discussions': legacy_discussion_settings['cohorted_discussions'],
|
||||
'division_scheme': CourseDiscussionSettings.COHORT if legacy_discussion_settings['is_cohorted']
|
||||
else CourseDiscussionSettings.NONE
|
||||
}
|
||||
)
|
||||
|
||||
return course_discussion_settings
|
||||
|
||||
|
||||
def set_course_discussion_settings(course_key, **kwargs):
|
||||
"""
|
||||
Set discussion settings for a course.
|
||||
|
||||
Arguments:
|
||||
course_key: CourseKey
|
||||
always_divide_inline_discussions (bool): If inline discussions should always be divided.
|
||||
divided_discussions (list): List of discussion ids.
|
||||
division_scheme (str): `CourseDiscussionSettings.NONE`, `CourseDiscussionSettings.COHORT`,
|
||||
or `CourseDiscussionSettings.ENROLLMENT_TRACK`
|
||||
|
||||
Returns:
|
||||
A CourseDiscussionSettings object.
|
||||
"""
|
||||
fields = {'division_scheme': str, 'always_divide_inline_discussions': bool, 'divided_discussions': list}
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
for field, field_type in fields.items():
|
||||
if field in kwargs:
|
||||
if not isinstance(kwargs[field], field_type):
|
||||
raise ValueError("Incorrect field type for `{}`. Type must be `{}`".format(field, field_type.__name__))
|
||||
setattr(course_discussion_settings, field, kwargs[field])
|
||||
course_discussion_settings.save()
|
||||
return course_discussion_settings
|
||||
|
||||
@@ -133,7 +133,7 @@ class PartitionService(object):
|
||||
if self._cache and (cache_key in self._cache):
|
||||
return self._cache[cache_key]
|
||||
|
||||
user_partition = self._get_user_partition(user_partition_id)
|
||||
user_partition = self.get_user_partition(user_partition_id)
|
||||
if user_partition is None:
|
||||
raise ValueError(
|
||||
"Configuration problem! No user_partition with id {0} "
|
||||
@@ -148,7 +148,7 @@ class PartitionService(object):
|
||||
|
||||
return group_id
|
||||
|
||||
def _get_user_partition(self, user_partition_id):
|
||||
def get_user_partition(self, user_partition_id):
|
||||
"""
|
||||
Look for a user partition with a matching id in the course's partitions.
|
||||
Note that this method can return an inactive user partition.
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
this.retrieveFollowed = function() {
|
||||
return DiscussionThreadListView.prototype.retrieveFollowed.apply(self, arguments);
|
||||
};
|
||||
this.chooseCohort = function() {
|
||||
return DiscussionThreadListView.prototype.chooseCohort.apply(self, arguments);
|
||||
this.chooseGroup = function() {
|
||||
return DiscussionThreadListView.prototype.chooseGroup.apply(self, arguments);
|
||||
};
|
||||
this.chooseFilter = function() {
|
||||
return DiscussionThreadListView.prototype.chooseFilter.apply(self, arguments);
|
||||
@@ -85,7 +85,7 @@
|
||||
'click .forum-nav-thread-link': 'threadSelected',
|
||||
'click .forum-nav-load-more-link': 'loadMorePages',
|
||||
'change .forum-nav-filter-main-control': 'chooseFilter',
|
||||
'change .forum-nav-filter-cohort-control': 'chooseCohort'
|
||||
'change .forum-nav-filter-cohort-control': 'chooseGroup'
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.initialize = function(options) {
|
||||
@@ -194,7 +194,7 @@
|
||||
edx.HtmlUtils.append(
|
||||
this.$el,
|
||||
this.template({
|
||||
isCohorted: this.courseSettings.get('is_cohorted'),
|
||||
isDiscussionDivisionEnabled: this.courseSettings.get('is_discussion_division_enabled'),
|
||||
isPrivilegedUser: DiscussionUtil.isPrivilegedUser()
|
||||
})
|
||||
);
|
||||
@@ -404,7 +404,7 @@
|
||||
return $(elem).data('discussion-id');
|
||||
}).get();
|
||||
this.retrieveDiscussions(discussionIds);
|
||||
return this.$('.forum-nav-filter-cohort').toggle($item.data('cohorted') === true);
|
||||
return this.$('.forum-nav-filter-cohort').toggle($item.data('divided') === true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -413,7 +413,7 @@
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
DiscussionThreadListView.prototype.chooseCohort = function() {
|
||||
DiscussionThreadListView.prototype.chooseGroup = function() {
|
||||
this.group_id = this.$('.forum-nav-filter-cohort-control :selected').val();
|
||||
return this.retrieveFirstPage();
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
'[data-discussion-id="' + this.getCurrentTopicId() + '"]'
|
||||
));
|
||||
} else if ($general.length > 0) {
|
||||
this.setTopic($general);
|
||||
this.setTopic($general.first());
|
||||
} else {
|
||||
this.setTopic(this.$('.post-topic option').first());
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
threadTypeTemplate;
|
||||
context = _.clone(this.course_settings.attributes);
|
||||
_.extend(context, {
|
||||
cohort_options: this.getCohortOptions(),
|
||||
group_options: this.getGroupOptions(),
|
||||
is_commentable_divided: this.is_commentable_divided,
|
||||
mode: this.mode,
|
||||
startHeader: this.startHeader,
|
||||
@@ -84,15 +84,15 @@
|
||||
return this.mode === 'tab';
|
||||
};
|
||||
|
||||
NewPostView.prototype.getCohortOptions = function() {
|
||||
NewPostView.prototype.getGroupOptions = function() {
|
||||
var userGroupId;
|
||||
if (this.course_settings.get('is_cohorted') && DiscussionUtil.isPrivilegedUser()) {
|
||||
if (this.course_settings.get('is_discussion_division_enabled') && DiscussionUtil.isPrivilegedUser()) {
|
||||
userGroupId = $('#discussion-container').data('user-group-id');
|
||||
return _.map(this.course_settings.get('cohorts'), function(cohort) {
|
||||
return _.map(this.course_settings.get('groups'), function(group) {
|
||||
return {
|
||||
value: cohort.id,
|
||||
text: cohort.name,
|
||||
selected: cohort.id === userGroupId
|
||||
value: group.id,
|
||||
text: group.name,
|
||||
selected: group.id === userGroupId
|
||||
};
|
||||
});
|
||||
} else {
|
||||
@@ -112,7 +112,7 @@
|
||||
};
|
||||
|
||||
NewPostView.prototype.toggleGroupDropdown = function($target) {
|
||||
if ($target.data('cohorted')) {
|
||||
if ($target.data('divided')) {
|
||||
$('.js-group-select').prop('disabled', false);
|
||||
return $('.group-selector-wrapper').removeClass('disabled');
|
||||
} else {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
' <li' +
|
||||
' class="forum-nav-browse-menu-item"' +
|
||||
' data-discussion-id="child"' +
|
||||
' data-cohorted="false"' +
|
||||
' data-divided="false"' +
|
||||
' >' +
|
||||
' <a href="#" class="forum-nav-browse-title">Child</a>' +
|
||||
' </li>' +
|
||||
@@ -70,7 +70,7 @@
|
||||
' <li' +
|
||||
' class="forum-nav-browse-menu-item"' +
|
||||
' data-discussion-id="sibling"' +
|
||||
' data-cohorted="false"' +
|
||||
' data-divided="false"' +
|
||||
' >' +
|
||||
' <a href="#" class="forum-nav-browse-title">Sibling</a>' +
|
||||
' </li>' +
|
||||
@@ -79,7 +79,7 @@
|
||||
' <li' +
|
||||
' class="forum-nav-browse-menu-item"' +
|
||||
' data-discussion-id="other"' +
|
||||
' data-cohorted="true"' +
|
||||
' data-divided="true"' +
|
||||
' >' +
|
||||
' <a href="#" class="forum-nav-browse-title">Other Category</a>' +
|
||||
' </li>' +
|
||||
@@ -95,11 +95,11 @@
|
||||
' <option value="flagged">Flagged</option>' +
|
||||
' </select>' +
|
||||
' </label>' +
|
||||
' <% if (isCohorted && isPrivilegedUser) { %>' +
|
||||
' <% if (isDiscussionDivisionEnabled && isPrivilegedUser) { %>' +
|
||||
' <label class="forum-nav-filter-cohort">' +
|
||||
' <span class="sr">Cohort:</span>' +
|
||||
' <span class="sr">Group:</span>' +
|
||||
' <select class="forum-nav-filter-cohort-control">' +
|
||||
' <option value="">in all cohorts</option>' +
|
||||
' <option value="">in all groups</option>' +
|
||||
' <option value="1">Cohort1</option>' +
|
||||
' <option value="2">Cohort2</option>' +
|
||||
' </select>' +
|
||||
@@ -164,7 +164,7 @@
|
||||
collection: this.discussion,
|
||||
el: $('#fixture-element'),
|
||||
courseSettings: new DiscussionCourseSettings({
|
||||
is_cohorted: true
|
||||
is_discussion_division_enabled: true
|
||||
})
|
||||
});
|
||||
return this.view.render();
|
||||
@@ -199,7 +199,7 @@
|
||||
collection: discussion,
|
||||
showThreadPreview: true,
|
||||
courseSettings: new DiscussionCourseSettings({
|
||||
is_cohorted: true
|
||||
is_discussion_division_enabled: true
|
||||
})
|
||||
},
|
||||
props
|
||||
@@ -233,7 +233,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('cohort selector', function() {
|
||||
describe('group selector', function() {
|
||||
it('should not be visible to students', function() {
|
||||
return expect(this.view.$('.forum-nav-filter-cohort-control:visible')).not.toExist();
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
return expect(group_disabled).toEqual(true);
|
||||
}
|
||||
};
|
||||
describe('cohort selector', function() {
|
||||
describe('group selector', function() {
|
||||
beforeEach(function() {
|
||||
this.course_settings = new DiscussionCourseSettings({
|
||||
category_map: {
|
||||
@@ -53,8 +53,8 @@
|
||||
},
|
||||
allow_anonymous: false,
|
||||
allow_anonymous_to_peers: false,
|
||||
is_cohorted: true,
|
||||
cohorts: [
|
||||
is_discussion_division_enabled: true,
|
||||
groups: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Cohort1'
|
||||
@@ -75,15 +75,15 @@
|
||||
it('is not visible to students', function() {
|
||||
return checkVisibility(this.view, false, false, true);
|
||||
});
|
||||
it('allows TAs to see the cohort selector when the topic is cohorted', function() {
|
||||
it('allows TAs to see the group selector when the topic is divided', function() {
|
||||
DiscussionSpecHelper.makeTA();
|
||||
return checkVisibility(this.view, true, false, true);
|
||||
});
|
||||
it('allows moderators to see the cohort selector when the topic is cohorted', function() {
|
||||
it('allows moderators to see the group selector when the topic is divided', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
return checkVisibility(this.view, true, false, true);
|
||||
});
|
||||
it('only enables the cohort selector when applicable', function() {
|
||||
it('only enables the group selector when applicable', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
checkVisibility(this.view, true, false, true);
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
$('.post-topic').trigger('change');
|
||||
return checkVisibility(this.view, true, false, false);
|
||||
});
|
||||
it('allows the user to make a cohort selection', function() {
|
||||
it('allows the user to make a group selection', function() {
|
||||
var expectedGroupId,
|
||||
self = this;
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
@@ -116,23 +116,23 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('always cohort inline discussions ', function() {
|
||||
describe('always divide inline discussions ', function() {
|
||||
beforeEach(function() {
|
||||
this.course_settings = new DiscussionCourseSettings({
|
||||
'category_map': {
|
||||
'children': [],
|
||||
'entries': {}
|
||||
category_map: {
|
||||
children: [],
|
||||
entries: {}
|
||||
},
|
||||
'allow_anonymous': false,
|
||||
'allow_anonymous_to_peers': false,
|
||||
'is_cohorted': true,
|
||||
'cohorts': [
|
||||
allow_anonymous: false,
|
||||
allow_anonymous_to_peers: false,
|
||||
is_discussion_division_enabled: true,
|
||||
groups: [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Cohort1'
|
||||
id: 1,
|
||||
name: 'Cohort1'
|
||||
}, {
|
||||
'id': 2,
|
||||
'name': 'Cohort2'
|
||||
id: 2,
|
||||
name: 'Cohort2'
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -143,12 +143,12 @@
|
||||
mode: 'tab'
|
||||
});
|
||||
});
|
||||
it('disables the cohort menu if it is set false', function() {
|
||||
it('disables the group menu if it is set false', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.is_commentable_divided = false;
|
||||
return checkVisibility(this.view, true, true, true);
|
||||
});
|
||||
it('enables the cohort menu if it is set true', function() {
|
||||
it('enables the group menu if it is set true', function() {
|
||||
DiscussionSpecHelper.makeModerator();
|
||||
this.view.is_commentable_divided = true;
|
||||
return checkVisibility(this.view, true, false, true);
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
is_cohorted: true,
|
||||
is_discussion_division_enabled: true,
|
||||
allow_anonymous: false,
|
||||
allow_anonymous_to_peers: false
|
||||
},
|
||||
@@ -170,7 +170,7 @@
|
||||
' <li' +
|
||||
' class="forum-nav-browse-menu-item"' +
|
||||
' data-discussion-id="child"' +
|
||||
' data-cohorted="false"' +
|
||||
' data-divided="false"' +
|
||||
' >' +
|
||||
' <a href="#" class="forum-nav-browse-title">Child</a>' +
|
||||
' </li>' +
|
||||
@@ -178,7 +178,7 @@
|
||||
' <li' +
|
||||
' class="forum-nav-browse-menu-item"' +
|
||||
' data-discussion-id="sibling"' +
|
||||
' data-cohorted="false"' +
|
||||
' data-divided="false"' +
|
||||
' >' +
|
||||
' <a href="#" class="forum-nav-browse-title">Sibling</a>' +
|
||||
' </li>' +
|
||||
@@ -187,7 +187,7 @@
|
||||
' <li' +
|
||||
' class="forum-nav-browse-menu-item"' +
|
||||
' data-discussion-id="other"' +
|
||||
' data-cohorted="true"' +
|
||||
' data-divided="true"' +
|
||||
' >' +
|
||||
' <a href="#" class="forum-nav-browse-title">Other Category</a>' +
|
||||
' </li>' +
|
||||
@@ -203,11 +203,11 @@
|
||||
' <option value="flagged">Flagged</option>' +
|
||||
' </select>' +
|
||||
' </label>' +
|
||||
' <% if (isCohorted && isPrivilegedUser) { %>' +
|
||||
' <% if (isDiscussionDivisionEnabled && isPrivilegedUser) { %>' +
|
||||
' <label class="forum-nav-filter-cohort">' +
|
||||
' <span class="sr">Cohort:</span>' +
|
||||
' <span class="sr">Group:</span>' +
|
||||
' <select class="forum-nav-filter-cohort-control">' +
|
||||
' <option value="">in all cohorts</option>' +
|
||||
' <option value="">in all groups</option>' +
|
||||
' <option value="1">Cohort1</option>' +
|
||||
' <option value="2">Cohort2</option>' +
|
||||
' </select>' +
|
||||
|
||||
@@ -1 +1 @@
|
||||
<option class="topic-title" data-discussion-id="<%- id %>" data-cohorted="<%- is_divided %>"><%- text %></option>
|
||||
<option class="topic-title" data-discussion-id="<%- id %>" data-divided="<%- is_divided %>"><%- text %></option>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<% } %>
|
||||
<ul class="post-errors" style="display: none"></ul>
|
||||
<div class="forum-new-post-form-wrapper"></div>
|
||||
<% if (cohort_options) { %>
|
||||
<% if (group_options) { %>
|
||||
<div class="post-field group-selector-wrapper <% if (!is_commentable_divided) { print('disabled'); } %>">
|
||||
<label class="field-label">
|
||||
<span class="field-label-text">
|
||||
@@ -17,12 +17,12 @@
|
||||
<%- gettext("Visible to") %>
|
||||
</span>
|
||||
<div class="field-help" id="field_help_visible_to">
|
||||
<%- gettext("Discussion admins, moderators, and TAs can make their posts visible to all students or specify a single cohort.") %>
|
||||
<%- gettext("Discussion admins, moderators, and TAs can make their posts visible to all students or specify a single group.") %>
|
||||
</div>
|
||||
<div class="field-input">
|
||||
<select aria-describedby="field_help_visible_to" class="post-topic field-input js-group-select" name="group_id" <% if (!is_commentable_divided) { print("disabled"); } %>>
|
||||
<option value=""><%- gettext("All Groups") %></option>
|
||||
<% _.each(cohort_options, function(opt) { %>
|
||||
<% _.each(group_options, function(opt) { %>
|
||||
<option value="<%- opt.value %>" <% if (opt.selected) { print("selected"); } %>><%- opt.text %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
|
||||
@@ -41,10 +41,10 @@ define(
|
||||
thread_pages: [],
|
||||
contentInfo: null,
|
||||
courseSettings: {
|
||||
is_cohorted: false,
|
||||
is_discussion_division_enabled: false,
|
||||
allow_anonymous: false,
|
||||
allow_anonymous_to_peers: false,
|
||||
cohorts: [],
|
||||
groups: [],
|
||||
category_map: {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,6 +125,7 @@ def make_mock_thread_data(
|
||||
group_id=None,
|
||||
group_name=None,
|
||||
commentable_id=None,
|
||||
is_commentable_divided=None,
|
||||
):
|
||||
data_commentable_id = (
|
||||
commentable_id or course.discussion_topics.get('General', {}).get('id') or "dummy_commentable_id"
|
||||
@@ -145,6 +146,8 @@ def make_mock_thread_data(
|
||||
}
|
||||
if group_id is not None:
|
||||
thread_data['group_name'] = group_name
|
||||
if is_commentable_divided is not None:
|
||||
thread_data['is_commentable_divided'] = is_commentable_divided
|
||||
if num_children is not None:
|
||||
thread_data["children"] = [{
|
||||
"id": "dummy_comment_id_{}".format(i),
|
||||
@@ -429,7 +432,10 @@ class SingleCohortedThreadTestCase(CohortedTestCase):
|
||||
self.mock_text = "dummy content"
|
||||
self.mock_thread_id = "test_thread_id"
|
||||
mock_request.side_effect = make_mock_request_impl(
|
||||
course=self.course, text=self.mock_text, thread_id=self.mock_thread_id, group_id=self.student_cohort.id
|
||||
course=self.course, text=self.mock_text,
|
||||
thread_id=self.mock_thread_id,
|
||||
group_id=self.student_cohort.id,
|
||||
commentable_id="cohorted_topic",
|
||||
)
|
||||
|
||||
def test_ajax(self, mock_request):
|
||||
@@ -453,11 +459,13 @@ class SingleCohortedThreadTestCase(CohortedTestCase):
|
||||
response_data["content"],
|
||||
make_mock_thread_data(
|
||||
course=self.course,
|
||||
commentable_id="cohorted_topic",
|
||||
text=self.mock_text,
|
||||
thread_id=self.mock_thread_id,
|
||||
num_children=1,
|
||||
group_id=self.student_cohort.id,
|
||||
group_name=self.student_cohort.name
|
||||
group_name=self.student_cohort.name,
|
||||
is_commentable_divided=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -30,17 +30,14 @@ from web_fragments.fragment import Fragment
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.views.views import CourseTabView
|
||||
from openedx.core.djangoapps.course_groups.cohorts import (
|
||||
is_course_cohorted,
|
||||
get_course_cohorts,
|
||||
)
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
|
||||
from courseware.access import has_access
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from django_comment_common.utils import ThreadContext
|
||||
from django_comment_common.utils import ThreadContext, get_course_discussion_settings
|
||||
|
||||
from django_comment_client.permissions import has_permission, get_team
|
||||
from django_comment_client.utils import (
|
||||
merge_dict,
|
||||
@@ -48,6 +45,8 @@ from django_comment_client.utils import (
|
||||
strip_none,
|
||||
add_courseware_context,
|
||||
get_group_id_for_comments_service,
|
||||
course_discussion_division_enabled,
|
||||
get_group_names_by_id,
|
||||
is_commentable_divided,
|
||||
get_group_id_for_user,
|
||||
)
|
||||
@@ -83,11 +82,15 @@ def make_course_settings(course, user):
|
||||
Generate a JSON-serializable model for course settings, which will be used to initialize a
|
||||
DiscussionCourseSettings object on the client.
|
||||
"""
|
||||
course_discussion_settings = get_course_discussion_settings(course.id)
|
||||
group_names_by_id = get_group_names_by_id(course_discussion_settings)
|
||||
return {
|
||||
'is_cohorted': is_course_cohorted(course.id),
|
||||
'is_discussion_division_enabled': course_discussion_division_enabled(course_discussion_settings),
|
||||
'allow_anonymous': course.allow_anonymous,
|
||||
'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
|
||||
'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)],
|
||||
'groups': [
|
||||
{"id": str(group_id), "name": group_name} for group_id, group_name in group_names_by_id.iteritems()
|
||||
],
|
||||
'category_map': utils.get_discussion_category_map(course, user)
|
||||
}
|
||||
|
||||
@@ -346,10 +349,11 @@ def _find_thread(request, course, discussion_id, thread_id):
|
||||
if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id):
|
||||
return None
|
||||
|
||||
# verify that the thread belongs to the requesting student's cohort
|
||||
# verify that the thread belongs to the requesting student's group
|
||||
is_moderator = has_permission(request.user, "see_all_cohorts", course.id)
|
||||
if is_commentable_divided(course.id, discussion_id) and not is_moderator:
|
||||
user_group_id = get_group_id_for_user(request.user, course.id)
|
||||
course_discussion_settings = get_course_discussion_settings(course.id)
|
||||
if is_commentable_divided(course.id, discussion_id, course_discussion_settings) and not is_moderator:
|
||||
user_group_id = get_group_id_for_user(request.user, course_discussion_settings)
|
||||
if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id:
|
||||
return None
|
||||
|
||||
@@ -424,7 +428,8 @@ def _create_discussion_board_context(request, course_key, discussion_id=None, th
|
||||
add_courseware_context(threads, course, user)
|
||||
|
||||
with newrelic_function_trace("get_cohort_info"):
|
||||
user_group_id = get_group_id_for_user(user, course_key)
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
user_group_id = get_group_id_for_user(user, course_discussion_settings)
|
||||
|
||||
context.update({
|
||||
'root_url': root_url,
|
||||
@@ -434,12 +439,12 @@ def _create_discussion_board_context(request, course_key, discussion_id=None, th
|
||||
'thread_pages': thread_pages,
|
||||
'annotated_content_info': annotated_content_info,
|
||||
'is_moderator': has_permission(user, "see_all_cohorts", course_key),
|
||||
'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
|
||||
'groups': course_settings["groups"], # still needed to render _thread_list_template
|
||||
'user_group_id': user_group_id, # read from container in NewPostView
|
||||
'sort_preference': cc_user.default_sort_key,
|
||||
'category_map': course_settings["category_map"],
|
||||
'course_settings': course_settings,
|
||||
'is_commentable_divided': is_commentable_divided(course_key, discussion_id)
|
||||
'is_commentable_divided': is_commentable_divided(course_key, discussion_id, course_discussion_settings)
|
||||
})
|
||||
return context
|
||||
|
||||
@@ -501,7 +506,8 @@ def user_profile(request, course_key, user_id):
|
||||
).order_by("name").values_list("name", flat=True).distinct()
|
||||
|
||||
with newrelic_function_trace("get_cohort_info"):
|
||||
user_group_id = get_group_id_for_user(request.user, course_key)
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
user_group_id = get_group_id_for_user(request.user, course_discussion_settings)
|
||||
|
||||
context = _create_base_discussion_view_context(request, course_key)
|
||||
context.update({
|
||||
|
||||
@@ -43,6 +43,8 @@ from django_comment_common.signals import (
|
||||
comment_voted,
|
||||
comment_deleted,
|
||||
)
|
||||
from django_comment_common.utils import get_course_discussion_settings
|
||||
|
||||
from django_comment_client.utils import get_accessible_discussion_xblocks, is_commentable_divided, get_group_id_for_user
|
||||
from lms.djangoapps.discussion_api.pagination import DiscussionAPIPagination
|
||||
from lms.lib.comment_client.comment import Comment
|
||||
@@ -109,12 +111,13 @@ def _get_thread_and_context(request, thread_id, retrieve_kwargs=None):
|
||||
course_key = CourseKey.from_string(cc_thread["course_id"])
|
||||
course = _get_course(course_key, request.user)
|
||||
context = get_context(course, request, cc_thread)
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
if (
|
||||
not context["is_requester_privileged"] and
|
||||
cc_thread["group_id"] and
|
||||
is_commentable_divided(course.id, cc_thread["commentable_id"])
|
||||
is_commentable_divided(course.id, cc_thread["commentable_id"], course_discussion_settings)
|
||||
):
|
||||
requester_group_id = get_group_id_for_user(request.user, course.id)
|
||||
requester_group_id = get_group_id_for_user(request.user, course_discussion_settings)
|
||||
if requester_group_id is not None and cc_thread["group_id"] != requester_group_id:
|
||||
raise ThreadNotFoundError("Thread not found.")
|
||||
return cc_thread, context
|
||||
@@ -546,7 +549,7 @@ def get_thread_list(
|
||||
"user_id": unicode(request.user.id),
|
||||
"group_id": (
|
||||
None if context["is_requester_privileged"] else
|
||||
get_group_id_for_user(request.user, course.id)
|
||||
get_group_id_for_user(request.user, get_course_discussion_settings(course.id))
|
||||
),
|
||||
"page": page,
|
||||
"per_page": page_size,
|
||||
@@ -828,12 +831,13 @@ def create_thread(request, thread_data):
|
||||
|
||||
context = get_context(course, request)
|
||||
_check_initializable_thread_fields(thread_data, context)
|
||||
discussion_settings = get_course_discussion_settings(course_key)
|
||||
if (
|
||||
"group_id" not in thread_data and
|
||||
is_commentable_divided(course_key, thread_data.get("topic_id"))
|
||||
is_commentable_divided(course_key, thread_data.get("topic_id"), discussion_settings)
|
||||
):
|
||||
thread_data = thread_data.copy()
|
||||
thread_data["group_id"] = get_group_id_for_user(user, course_key)
|
||||
thread_data["group_id"] = get_group_id_for_user(user, discussion_settings)
|
||||
serializer = ThreadSerializer(data=thread_data, context=context)
|
||||
actions_form = ThreadActionsForm(thread_data)
|
||||
if not (serializer.is_valid() and actions_form.is_valid()):
|
||||
|
||||
@@ -77,7 +77,7 @@ def get_editable_fields(cc_content, context):
|
||||
ret |= {"following", "read"}
|
||||
if _is_author_or_privileged(cc_content, context):
|
||||
ret |= {"topic_id", "type", "title"}
|
||||
if context["is_requester_privileged"] and context["course"].is_cohorted:
|
||||
if context["is_requester_privileged"] and context["discussion_division_enabled"]:
|
||||
ret |= {"group_id"}
|
||||
|
||||
# Comment fields
|
||||
|
||||
@@ -17,6 +17,7 @@ from discussion_api.permissions import (
|
||||
)
|
||||
from discussion_api.render import render_body
|
||||
from django_comment_client.utils import is_comment_too_deep
|
||||
from django_comment_common.utils import get_course_discussion_settings
|
||||
from django_comment_common.models import (
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
FORUM_ROLE_COMMUNITY_TA,
|
||||
@@ -27,7 +28,7 @@ from lms.lib.comment_client.comment import Comment
|
||||
from lms.lib.comment_client.thread import Thread
|
||||
from lms.lib.comment_client.user import User as CommentClientUser
|
||||
from lms.lib.comment_client.utils import CommentClientRequestError
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_names
|
||||
from lms.djangoapps.django_comment_client.utils import course_discussion_division_enabled, get_group_names_by_id
|
||||
|
||||
|
||||
def get_context(course, request, thread=None):
|
||||
@@ -52,12 +53,13 @@ def get_context(course, request, thread=None):
|
||||
requester = request.user
|
||||
cc_requester = CommentClientUser.from_django_user(requester).retrieve()
|
||||
cc_requester["course_id"] = course.id
|
||||
course_discussion_settings = get_course_discussion_settings(course.id)
|
||||
return {
|
||||
"course": course,
|
||||
"request": request,
|
||||
"thread": thread,
|
||||
# For now, the only groups are cohorts
|
||||
"group_ids_to_names": get_cohort_names(course),
|
||||
"discussion_division_enabled": course_discussion_division_enabled(course_discussion_settings),
|
||||
"group_ids_to_names": get_group_names_by_id(course_discussion_settings),
|
||||
"is_requester_privileged": requester.id in staff_user_ids or requester.id in ta_user_ids,
|
||||
"staff_user_ids": staff_user_ids,
|
||||
"ta_user_ids": ta_user_ids,
|
||||
|
||||
@@ -55,6 +55,7 @@ from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
from openedx.core.lib.exceptions import CourseNotFoundError, PageNotFoundError
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -591,6 +592,8 @@ class GetThreadListTest(ForumsEnableMixin, CommentsServiceMockMixin, UrlResetMix
|
||||
self.request.user = self.user
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
self.author = UserFactory.create()
|
||||
self.course.cohort_config = {"cohorted": False}
|
||||
modulestore().update_item(self.course, ModuleStoreEnum.UserID.test)
|
||||
self.cohort = CohortFactory.create(course_id=self.course.id)
|
||||
|
||||
def get_thread_list(
|
||||
@@ -662,6 +665,8 @@ class GetThreadListTest(ForumsEnableMixin, CommentsServiceMockMixin, UrlResetMix
|
||||
})
|
||||
|
||||
def test_thread_content(self):
|
||||
self.course.cohort_config = {"cohorted": True}
|
||||
modulestore().update_item(self.course, ModuleStoreEnum.UserID.test)
|
||||
source_threads = [
|
||||
make_minimal_cs_thread({
|
||||
"id": "test_thread_id_0",
|
||||
|
||||
@@ -25,6 +25,7 @@ def _get_context(requester_id, is_requester_privileged, is_cohorted=False, threa
|
||||
"cc_requester": User(id=requester_id),
|
||||
"is_requester_privileged": is_requester_privileged,
|
||||
"course": CourseFactory(cohort_config={"cohorted": is_cohorted}),
|
||||
"discussion_division_enabled": is_cohorted,
|
||||
"thread": thread,
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ from lms.lib.comment_client.comment import Comment
|
||||
from lms.lib.comment_client.thread import Thread
|
||||
from student.tests.factories import UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
@@ -209,6 +211,8 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, SharedModuleStoreTe
|
||||
self.assertEqual(serialized["pinned"], False)
|
||||
|
||||
def test_group(self):
|
||||
self.course.cohort_config = {"cohorted": True}
|
||||
modulestore().update_item(self.course, ModuleStoreEnum.UserID.test)
|
||||
cohort = CohortFactory.create(course_id=self.course.id)
|
||||
serialized = self.serialize(self.make_cs_content({"group_id": cohort.id}))
|
||||
self.assertEqual(serialized["group_id"], cohort.id)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from django_comment_common.models import CourseDiscussionSettings
|
||||
from django_comment_common.utils import set_course_discussion_settings
|
||||
from lms.djangoapps.teams.tests.factories import CourseTeamFactory
|
||||
|
||||
|
||||
@@ -94,11 +99,22 @@ class CohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
|
||||
|
||||
def test_cohorted_topic_moderator_with_invalid_group_id(self, mock_request):
|
||||
invalid_id = self.student_cohort.id + self.moderator_cohort.id
|
||||
try:
|
||||
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
except ValueError:
|
||||
pass # In mock request mode, server errors are not captured
|
||||
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
def test_cohorted_topic_enrollment_track_invalid_group_id(self, mock_request):
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
|
||||
set_course_discussion_settings(
|
||||
course_key=self.course.id,
|
||||
divided_discussions=['cohorted_topic'],
|
||||
division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK,
|
||||
always_divide_inline_discussions=True,
|
||||
)
|
||||
|
||||
invalid_id = -1000
|
||||
response = self.call_view(mock_request, "cohorted_topic", self.moderator, invalid_id)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
|
||||
class NonCohortedTopicGroupIdTestMixin(GroupIdAssertionMixin):
|
||||
|
||||
@@ -17,13 +17,16 @@ from django_comment_client.tests.unicode import UnicodeTestMixin
|
||||
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
|
||||
import django_comment_client.utils as utils
|
||||
from lms.lib.comment_client.utils import perform_request, CommentClientMaintenanceError
|
||||
from django_comment_common.models import ForumsConfig
|
||||
from django_comment_common.models import ForumsConfig, CourseDiscussionSettings
|
||||
from django_comment_common.utils import get_course_discussion_settings, set_course_discussion_settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from courseware.tests.factories import InstructorFactory
|
||||
from courseware.tabs import get_course_tab_list
|
||||
from openedx.core.djangoapps.course_groups import cohorts
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts, topic_name_to_id
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts, topic_name_to_id, CohortFactory
|
||||
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
|
||||
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
||||
from openedx.core.djangoapps.util.testing import ContentGroupTestCase
|
||||
@@ -374,7 +377,6 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
)
|
||||
# Courses get a default discussion topic on creation, so remove it
|
||||
self.course.discussion_topics = {}
|
||||
self.course.save()
|
||||
self.discussion_num = 0
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.maxDiff = None # pylint: disable=invalid-name
|
||||
@@ -426,34 +428,31 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
|
||||
check_cohorted_topics([]) # default (empty) cohort config
|
||||
|
||||
set_course_cohort_settings(course_key=self.course.id, is_cohorted=False, cohorted_discussions=[])
|
||||
set_discussion_division_settings(self.course.id, enable_cohorts=False)
|
||||
check_cohorted_topics([])
|
||||
|
||||
set_course_cohort_settings(course_key=self.course.id, is_cohorted=True, cohorted_discussions=[])
|
||||
set_discussion_division_settings(self.course.id, enable_cohorts=True)
|
||||
check_cohorted_topics([])
|
||||
|
||||
set_course_cohort_settings(
|
||||
course_key=self.course.id,
|
||||
is_cohorted=True,
|
||||
cohorted_discussions=["Topic_B", "Topic_C"],
|
||||
always_cohort_inline_discussions=False,
|
||||
set_discussion_division_settings(
|
||||
self.course.id,
|
||||
enable_cohorts=True,
|
||||
divided_discussions=["Topic_B", "Topic_C"]
|
||||
)
|
||||
check_cohorted_topics(["Topic_B", "Topic_C"])
|
||||
|
||||
set_course_cohort_settings(
|
||||
course_key=self.course.id,
|
||||
is_cohorted=True,
|
||||
cohorted_discussions=["Topic_A", "Some_Other_Topic"],
|
||||
always_cohort_inline_discussions=False,
|
||||
set_discussion_division_settings(
|
||||
self.course.id,
|
||||
enable_cohorts=True,
|
||||
divided_discussions=["Topic_A", "Some_Other_Topic"]
|
||||
)
|
||||
check_cohorted_topics(["Topic_A"])
|
||||
|
||||
# unlikely case, but make sure it works.
|
||||
set_course_cohort_settings(
|
||||
course_key=self.course.id,
|
||||
is_cohorted=False,
|
||||
cohorted_discussions=["Topic_A"],
|
||||
always_cohort_inline_discussions=False,
|
||||
set_discussion_division_settings(
|
||||
self.course.id,
|
||||
enable_cohorts=False,
|
||||
divided_discussions=["Topic_A"]
|
||||
)
|
||||
check_cohorted_topics([])
|
||||
|
||||
@@ -481,7 +480,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_inline_with_always_cohort_inline_discussion_flag(self):
|
||||
self.create_discussion("Chapter", "Discussion")
|
||||
set_course_cohort_settings(course_key=self.course.id, is_cohorted=True, always_cohort_inline_discussions=True)
|
||||
set_discussion_division_settings(self.course.id, enable_cohorts=True, always_divide_inline_discussions=True)
|
||||
|
||||
self.assert_category_map_equals(
|
||||
{
|
||||
@@ -505,7 +504,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_inline_without_always_cohort_inline_discussion_flag(self):
|
||||
self.create_discussion("Chapter", "Discussion")
|
||||
set_course_cohort_settings(course_key=self.course.id, is_cohorted=True)
|
||||
set_discussion_division_settings(self.course.id, enable_cohorts=True)
|
||||
|
||||
self.assert_category_map_equals(
|
||||
{
|
||||
@@ -566,7 +565,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion")
|
||||
self.create_discussion("Chapter 3 / Section 1", "Discussion")
|
||||
|
||||
def check_cohorted(is_divided):
|
||||
def check_divided(is_divided):
|
||||
|
||||
self.assert_category_map_equals(
|
||||
{
|
||||
@@ -652,15 +651,15 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
# empty / default config
|
||||
check_cohorted(False)
|
||||
check_divided(False)
|
||||
|
||||
# explicitly disabled cohorting
|
||||
set_course_cohort_settings(course_key=self.course.id, is_cohorted=False)
|
||||
check_cohorted(False)
|
||||
set_discussion_division_settings(self.course.id, enable_cohorts=False)
|
||||
check_divided(False)
|
||||
|
||||
# explicitly enabled cohorting with inline discusssions also cohorted.
|
||||
set_course_cohort_settings(course_key=self.course.id, is_cohorted=True, always_cohort_inline_discussions=True)
|
||||
check_cohorted(True)
|
||||
# explicitly enable courses divided by Cohort with inline discusssions also divided.
|
||||
set_discussion_division_settings(self.course.id, enable_cohorts=True, always_divide_inline_discussions=True)
|
||||
check_divided(True)
|
||||
|
||||
def test_tree_with_duplicate_targets(self):
|
||||
self.create_discussion("Chapter 1", "Discussion A")
|
||||
@@ -730,7 +729,6 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_self_paced_start_date_filter(self):
|
||||
self.course.self_paced = True
|
||||
self.course.save()
|
||||
|
||||
now = datetime.datetime.now()
|
||||
later = datetime.datetime.max
|
||||
@@ -898,7 +896,6 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase):
|
||||
|
||||
def test_sort_alpha(self):
|
||||
self.course.discussion_sort_alpha = True
|
||||
self.course.save()
|
||||
self.create_discussion("Chapter", "Discussion D")
|
||||
self.create_discussion("Chapter", "Discussion A")
|
||||
self.create_discussion("Chapter", "Discussion E")
|
||||
@@ -1325,7 +1322,7 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase):
|
||||
course,
|
||||
is_cohorted=True,
|
||||
discussion_topics=["General", "Feedback"],
|
||||
cohorted_discussions=["Feedback"]
|
||||
divided_discussions=["Feedback"]
|
||||
)
|
||||
|
||||
self.assertTrue(cohorts.is_course_cohorted(course.id))
|
||||
@@ -1349,7 +1346,7 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase):
|
||||
course,
|
||||
is_cohorted=True,
|
||||
discussion_topics=["General", "Feedback"],
|
||||
cohorted_discussions=["Feedback", "random_inline"]
|
||||
divided_discussions=["Feedback", "random_inline"]
|
||||
)
|
||||
self.assertFalse(
|
||||
utils.is_commentable_divided(course.id, to_id("random")),
|
||||
@@ -1362,8 +1359,8 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase):
|
||||
course,
|
||||
is_cohorted=True,
|
||||
discussion_topics=["General", "Feedback"],
|
||||
cohorted_discussions=["Feedback", "random_inline"],
|
||||
always_cohort_inline_discussions=False
|
||||
divided_discussions=["Feedback", "random_inline"],
|
||||
always_divide_inline_discussions=False
|
||||
)
|
||||
self.assertFalse(
|
||||
utils.is_commentable_divided(course.id, to_id("random")),
|
||||
@@ -1383,7 +1380,7 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase):
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
self.assertFalse(cohorts.is_course_cohorted(course.id))
|
||||
|
||||
config_course_cohorts(course, is_cohorted=True, always_cohort_inline_discussions=True)
|
||||
config_course_cohorts(course, is_cohorted=True, always_divide_inline_discussions=True)
|
||||
team = CourseTeamFactory(course_id=course.id)
|
||||
|
||||
# Verify that team discussions are not cohorted, but other discussions are
|
||||
@@ -1391,6 +1388,205 @@ class IsCommentableDividedTestCase(ModuleStoreTestCase):
|
||||
self.assertFalse(utils.is_commentable_divided(course.id, team.discussion_topic_id))
|
||||
self.assertTrue(utils.is_commentable_divided(course.id, "random"))
|
||||
|
||||
def test_is_commentable_divided_cohorts(self):
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
set_discussion_division_settings(
|
||||
course.id,
|
||||
enable_cohorts=True,
|
||||
divided_discussions=[],
|
||||
always_divide_inline_discussions=True,
|
||||
division_scheme=CourseDiscussionSettings.NONE,
|
||||
)
|
||||
|
||||
# Although Cohorts are enabled, discussion division is explicitly disabled.
|
||||
self.assertFalse(utils.is_commentable_divided(course.id, "random"))
|
||||
|
||||
# Now set the discussion division scheme.
|
||||
set_discussion_division_settings(
|
||||
course.id,
|
||||
enable_cohorts=True,
|
||||
divided_discussions=[],
|
||||
always_divide_inline_discussions=True,
|
||||
division_scheme=CourseDiscussionSettings.COHORT,
|
||||
)
|
||||
self.assertTrue(utils.is_commentable_divided(course.id, "random"))
|
||||
|
||||
def test_is_commentable_divided_enrollment_track(self):
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
set_discussion_division_settings(
|
||||
course.id,
|
||||
divided_discussions=[],
|
||||
always_divide_inline_discussions=True,
|
||||
division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK,
|
||||
)
|
||||
|
||||
# Although division scheme is set to ENROLLMENT_TRACK, divided returns
|
||||
# False because there is only a single enrollment mode.
|
||||
self.assertFalse(utils.is_commentable_divided(course.id, "random"))
|
||||
|
||||
# Now create 2 explicit course modes.
|
||||
CourseModeFactory.create(course_id=course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory.create(course_id=course.id, mode_slug=CourseMode.VERIFIED)
|
||||
self.assertTrue(utils.is_commentable_divided(course.id, "random"))
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class GroupIdForUserTestCase(ModuleStoreTestCase):
|
||||
""" Test the get_group_id_for_user method. """
|
||||
|
||||
def setUp(self):
|
||||
super(GroupIdForUserTestCase, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
|
||||
self.test_user = UserFactory.create()
|
||||
CourseEnrollmentFactory.create(
|
||||
mode=CourseMode.VERIFIED, user=self.test_user, course_id=self.course.id
|
||||
)
|
||||
self.test_cohort = CohortFactory(
|
||||
course_id=self.course.id,
|
||||
name='Test Cohort',
|
||||
users=[self.test_user]
|
||||
)
|
||||
|
||||
def test_discussion_division_disabled(self):
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.NONE, course_discussion_settings.division_scheme)
|
||||
self.assertIsNone(utils.get_group_id_for_user(self.test_user, course_discussion_settings))
|
||||
|
||||
def test_discussion_division_by_cohort(self):
|
||||
set_discussion_division_settings(
|
||||
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
|
||||
)
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.COHORT, course_discussion_settings.division_scheme)
|
||||
self.assertEqual(
|
||||
self.test_cohort.id,
|
||||
utils.get_group_id_for_user(self.test_user, course_discussion_settings)
|
||||
)
|
||||
|
||||
def test_discussion_division_by_enrollment_track(self):
|
||||
set_discussion_division_settings(
|
||||
self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK
|
||||
)
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(CourseDiscussionSettings.ENROLLMENT_TRACK, course_discussion_settings.division_scheme)
|
||||
self.assertEqual(
|
||||
-2, # Verified has group ID 2, and we negate that value to ensure unique IDs
|
||||
utils.get_group_id_for_user(self.test_user, course_discussion_settings)
|
||||
)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class CourseDiscussionDivisionEnabledTestCase(ModuleStoreTestCase):
|
||||
""" Test the course_discussion_division_enabled method. """
|
||||
|
||||
def setUp(self):
|
||||
super(CourseDiscussionDivisionEnabledTestCase, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
|
||||
self.test_cohort = CohortFactory(
|
||||
course_id=self.course.id,
|
||||
name='Test Cohort',
|
||||
users=[]
|
||||
)
|
||||
|
||||
def test_discussion_division_disabled(self):
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertFalse(utils.course_discussion_division_enabled(course_discussion_settings))
|
||||
|
||||
def test_discussion_division_by_cohort(self):
|
||||
set_discussion_division_settings(
|
||||
self.course.id, enable_cohorts=False, division_scheme=CourseDiscussionSettings.COHORT
|
||||
)
|
||||
# Because cohorts are disabled, discussion division is not enabled.
|
||||
self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
|
||||
# Now enable cohorts, which will cause discussions to be divided.
|
||||
set_discussion_division_settings(
|
||||
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
|
||||
)
|
||||
self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
|
||||
|
||||
def test_discussion_division_by_enrollment_track(self):
|
||||
set_discussion_division_settings(
|
||||
self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK
|
||||
)
|
||||
# Only a single enrollment track exists, so discussion division is not enabled.
|
||||
self.assertFalse(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
|
||||
|
||||
# Now create a second CourseMode, which will cause discussions to be divided.
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
|
||||
self.assertTrue(utils.course_discussion_division_enabled(get_course_discussion_settings(self.course.id)))
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class GroupNameTestCase(ModuleStoreTestCase):
|
||||
""" Test the get_group_name and get_group_names_by_id methods. """
|
||||
|
||||
def setUp(self):
|
||||
super(GroupNameTestCase, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
|
||||
self.test_cohort_1 = CohortFactory(
|
||||
course_id=self.course.id,
|
||||
name='Cohort 1',
|
||||
users=[]
|
||||
)
|
||||
self.test_cohort_2 = CohortFactory(
|
||||
course_id=self.course.id,
|
||||
name='Cohort 2',
|
||||
users=[]
|
||||
)
|
||||
|
||||
def test_discussion_division_disabled(self):
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual({}, utils.get_group_names_by_id(course_discussion_settings))
|
||||
self.assertIsNone(utils.get_group_name(-1000, course_discussion_settings))
|
||||
|
||||
def test_discussion_division_by_cohort(self):
|
||||
set_discussion_division_settings(
|
||||
self.course.id, enable_cohorts=True, division_scheme=CourseDiscussionSettings.COHORT
|
||||
)
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(
|
||||
{
|
||||
self.test_cohort_1.id: self.test_cohort_1.name,
|
||||
self.test_cohort_2.id: self.test_cohort_2.name
|
||||
},
|
||||
utils.get_group_names_by_id(course_discussion_settings)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.test_cohort_2.name,
|
||||
utils.get_group_name(self.test_cohort_2.id, course_discussion_settings)
|
||||
)
|
||||
# Test also with a group_id that doesn't exist.
|
||||
self.assertIsNone(
|
||||
utils.get_group_name(-1000, course_discussion_settings)
|
||||
)
|
||||
|
||||
def test_discussion_division_by_enrollment_track(self):
|
||||
set_discussion_division_settings(
|
||||
self.course.id, division_scheme=CourseDiscussionSettings.ENROLLMENT_TRACK
|
||||
)
|
||||
course_discussion_settings = get_course_discussion_settings(self.course.id)
|
||||
self.assertEqual(
|
||||
{
|
||||
-1: "audit course",
|
||||
-2: "verified course"
|
||||
},
|
||||
utils.get_group_names_by_id(course_discussion_settings)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"verified course",
|
||||
utils.get_group_name(-2, course_discussion_settings)
|
||||
)
|
||||
# Test also with a group_id that doesn't exist.
|
||||
self.assertIsNone(
|
||||
utils.get_group_name(-1000, course_discussion_settings)
|
||||
)
|
||||
|
||||
|
||||
class PermissionsTestCase(ModuleStoreTestCase):
|
||||
"""Test utils functionality related to forums "abilities" (permissions)"""
|
||||
@@ -1497,3 +1693,21 @@ class ClientConfigurationTestCase(TestCase):
|
||||
|
||||
result = perform_request('GET', 'http://www.google.com')
|
||||
self.assertEqual(result, {})
|
||||
|
||||
|
||||
def set_discussion_division_settings(
|
||||
course_key, enable_cohorts=False, always_divide_inline_discussions=False,
|
||||
divided_discussions=[], division_scheme=CourseDiscussionSettings.COHORT
|
||||
):
|
||||
"""
|
||||
Convenience method for setting cohort enablement and discussion settings.
|
||||
COHORT is the default division_scheme, as no other schemes were supported at
|
||||
the time that the unit tests were originally written.
|
||||
"""
|
||||
set_course_discussion_settings(
|
||||
course_key=course_key,
|
||||
divided_discussions=divided_discussions,
|
||||
division_scheme=division_scheme,
|
||||
always_divide_inline_discussions=always_divide_inline_discussions,
|
||||
)
|
||||
set_course_cohorted(course_key, enable_cohorts)
|
||||
|
||||
@@ -14,20 +14,22 @@ import pystache_custom as pystache
|
||||
from opaque_keys.edx.locations import i4xEncoder
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
|
||||
from django_comment_common.models import Role, FORUM_ROLE_STUDENT
|
||||
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
|
||||
from django_comment_client.permissions import check_permissions_by_view, has_permission, get_team
|
||||
from django_comment_client.settings import MAX_COMMENT_DEPTH
|
||||
from django_comment_client.constants import TYPE_ENTRY, TYPE_SUBCATEGORY
|
||||
from django_comment_common.models import Role, FORUM_ROLE_STUDENT, CourseDiscussionSettings
|
||||
from django_comment_common.utils import get_course_discussion_settings
|
||||
from edxmako import lookup_template
|
||||
|
||||
from courseware import courses
|
||||
from courseware.access import has_access
|
||||
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
||||
from openedx.core.djangoapps.course_groups.cohorts import (
|
||||
get_course_cohort_settings, get_cohort_by_id, get_cohort_id, is_course_cohorted
|
||||
is_course_cohorted, get_cohort_id, get_cohort_names
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from request_cache.middleware import request_cached
|
||||
from student.roles import GlobalStaff
|
||||
|
||||
@@ -267,7 +269,7 @@ def get_discussion_category_map(course, user, divided_only_if_explicit=False, ex
|
||||
course: Course for which to get the ids.
|
||||
user: User to check for access.
|
||||
divided_only_if_explicit (bool): If True, inline topics are marked is_divided only if they are
|
||||
explicitly listed in course_cohort_settings.discussion_topics.
|
||||
explicitly listed in CourseDiscussionSettings.discussion_topics.
|
||||
|
||||
Example:
|
||||
>>> example = {
|
||||
@@ -310,7 +312,9 @@ def get_discussion_category_map(course, user, divided_only_if_explicit=False, ex
|
||||
|
||||
xblocks = get_accessible_discussion_xblocks(course, user)
|
||||
|
||||
course_cohort_settings = get_course_cohort_settings(course.id)
|
||||
discussion_settings = get_course_discussion_settings(course.id)
|
||||
discussion_division_enabled = course_discussion_division_enabled(discussion_settings)
|
||||
divided_discussion_ids = discussion_settings.divided_discussions
|
||||
|
||||
for xblock in xblocks:
|
||||
discussion_id = xblock.discussion_id
|
||||
@@ -357,13 +361,13 @@ def get_discussion_category_map(course, user, divided_only_if_explicit=False, ex
|
||||
node[level]["start_date"] = category_start_date
|
||||
|
||||
divide_all_inline_discussions = ( # pylint: disable=invalid-name
|
||||
not divided_only_if_explicit and course_cohort_settings.always_cohort_inline_discussions
|
||||
not divided_only_if_explicit and discussion_settings.always_divide_inline_discussions
|
||||
)
|
||||
dupe_counters = defaultdict(lambda: 0) # counts the number of times we see each title
|
||||
for entry in entries:
|
||||
is_entry_divided = (
|
||||
course_cohort_settings.is_cohorted and (
|
||||
divide_all_inline_discussions or entry["id"] in course_cohort_settings.cohorted_discussions
|
||||
discussion_division_enabled and (
|
||||
divide_all_inline_discussions or entry["id"] in divided_discussion_ids
|
||||
)
|
||||
)
|
||||
|
||||
@@ -387,7 +391,7 @@ def get_discussion_category_map(course, user, divided_only_if_explicit=False, ex
|
||||
"sort_key": entry.get("sort_key", topic),
|
||||
"start_date": datetime.now(UTC()),
|
||||
"is_divided": (
|
||||
course_cohort_settings.is_cohorted and entry["id"] in course_cohort_settings.cohorted_discussions
|
||||
discussion_division_enabled and entry["id"] in divided_discussion_ids
|
||||
)
|
||||
}
|
||||
|
||||
@@ -652,7 +656,7 @@ def add_courseware_context(content_list, course, user, id_map=None):
|
||||
content.update({"courseware_url": url, "courseware_title": title})
|
||||
|
||||
|
||||
def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None):
|
||||
def prepare_content(content, course_key, is_staff=False, discussion_division_enabled=None):
|
||||
"""
|
||||
This function is used to pre-process thread and comment models in various
|
||||
ways before adding them to the HTTP response. This includes fixing empty
|
||||
@@ -666,7 +670,9 @@ def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None
|
||||
content (dict): A thread or comment.
|
||||
course_key (CourseKey): The course key of the course.
|
||||
is_staff (bool): Whether the user is a staff member.
|
||||
course_is_cohorted (bool): Whether the course is cohorted.
|
||||
discussion_division_enabled (bool): Whether division of course discussions is enabled.
|
||||
Note that callers of this method do not need to provide this value (it defaults to None)--
|
||||
it is calculated and then passed to recursive calls of this method.
|
||||
"""
|
||||
fields = [
|
||||
'id', 'title', 'body', 'course_id', 'anonymous', 'anonymous_to_peers',
|
||||
@@ -707,23 +713,27 @@ def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None
|
||||
else:
|
||||
del endorsement["user_id"]
|
||||
|
||||
if course_is_cohorted is None:
|
||||
course_is_cohorted = is_course_cohorted(course_key)
|
||||
if discussion_division_enabled is None:
|
||||
discussion_division_enabled = course_discussion_division_enabled(get_course_discussion_settings(course_key))
|
||||
|
||||
for child_content_key in ["children", "endorsed_responses", "non_endorsed_responses"]:
|
||||
if child_content_key in content:
|
||||
children = [
|
||||
prepare_content(child, course_key, is_staff, course_is_cohorted=course_is_cohorted)
|
||||
prepare_content(child, course_key, is_staff, discussion_division_enabled=discussion_division_enabled)
|
||||
for child in content[child_content_key]
|
||||
]
|
||||
content[child_content_key] = children
|
||||
|
||||
if course_is_cohorted:
|
||||
if discussion_division_enabled:
|
||||
# Augment the specified thread info to include the group name if a group id is present.
|
||||
if content.get('group_id') is not None:
|
||||
content['group_name'] = get_cohort_by_id(course_key, content.get('group_id')).name
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
content['group_name'] = get_group_name(content.get('group_id'), course_discussion_settings)
|
||||
content['is_commentable_divided'] = is_commentable_divided(
|
||||
course_key, content['commentable_id'], course_discussion_settings
|
||||
)
|
||||
else:
|
||||
# Remove any cohort information that might remain if the course had previously been cohorted.
|
||||
# Remove any group information that might remain if the course had previously been divided.
|
||||
content.pop('group_id', None)
|
||||
|
||||
return content
|
||||
@@ -741,7 +751,8 @@ def get_group_id_for_comments_service(request, course_key, commentable_id=None):
|
||||
Raises:
|
||||
ValueError if the requested group_id is invalid
|
||||
"""
|
||||
if commentable_id is None or is_commentable_divided(course_key, commentable_id):
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
if commentable_id is None or is_commentable_divided(course_key, commentable_id, course_discussion_settings):
|
||||
if request.method == "GET":
|
||||
requested_group_id = request.GET.get('group_id')
|
||||
elif request.method == "POST":
|
||||
@@ -749,27 +760,35 @@ def get_group_id_for_comments_service(request, course_key, commentable_id=None):
|
||||
if has_permission(request.user, "see_all_cohorts", course_key):
|
||||
if not requested_group_id:
|
||||
return None
|
||||
try:
|
||||
group_id = int(requested_group_id)
|
||||
get_cohort_by_id(course_key, group_id)
|
||||
except CourseUserGroup.DoesNotExist:
|
||||
raise ValueError
|
||||
group_id = int(requested_group_id)
|
||||
_verify_group_exists(group_id, course_discussion_settings)
|
||||
else:
|
||||
# regular users always query with their own id.
|
||||
group_id = get_group_id_for_user(request.user, course_key)
|
||||
group_id = get_group_id_for_user(request.user, course_discussion_settings)
|
||||
return group_id
|
||||
else:
|
||||
# Never pass a group_id to the comments service for a non-cohorted
|
||||
# Never pass a group_id to the comments service for a non-divided
|
||||
# commentable
|
||||
return None
|
||||
|
||||
|
||||
def get_group_id_for_user(user, course_key):
|
||||
def get_group_id_for_user(user, course_discussion_settings):
|
||||
"""
|
||||
This method will be modified to consider Enrollment Tracks.
|
||||
Currently pass-through to get_cohort_id.
|
||||
Given a user, return the group_id for that user according to the course_discussion_settings.
|
||||
If discussions are not divided, this method will return None.
|
||||
It will also return None if the user is in no group within the specified division_scheme.
|
||||
"""
|
||||
return get_cohort_id(user, course_key)
|
||||
division_scheme = _get_course_division_scheme(course_discussion_settings)
|
||||
if division_scheme == CourseDiscussionSettings.COHORT:
|
||||
return get_cohort_id(user, course_discussion_settings.course_id)
|
||||
elif division_scheme == CourseDiscussionSettings.ENROLLMENT_TRACK:
|
||||
partition_service = PartitionService(course_discussion_settings.course_id)
|
||||
group_id = partition_service.get_user_group_id_for_partition(user, ENROLLMENT_TRACK_PARTITION_ID)
|
||||
# We negate the group_ids from dynamic partitions so that they will not conflict
|
||||
# with cohort IDs (which are an auto-incrementing integer field, starting at 1).
|
||||
return -1 * group_id if group_id is not None else None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def is_comment_too_deep(parent):
|
||||
@@ -786,11 +805,13 @@ def is_comment_too_deep(parent):
|
||||
)
|
||||
|
||||
|
||||
def is_commentable_divided(course_key, commentable_id):
|
||||
def is_commentable_divided(course_key, commentable_id, course_discussion_settings=None):
|
||||
"""
|
||||
Args:
|
||||
course_key: CourseKey
|
||||
commentable_id: string
|
||||
course_discussion_settings: CourseDiscussionSettings model instance (optional). If not
|
||||
supplied, it will be retrieved via the course_key.
|
||||
|
||||
Returns:
|
||||
Bool: is this commentable divided, meaning that learners are divided into
|
||||
@@ -799,28 +820,118 @@ def is_commentable_divided(course_key, commentable_id):
|
||||
Raises:
|
||||
Http404 if the course doesn't exist.
|
||||
"""
|
||||
course = courses.get_course_by_id(course_key)
|
||||
course_cohort_settings = get_course_cohort_settings(course_key)
|
||||
if not course_discussion_settings:
|
||||
course_discussion_settings = get_course_discussion_settings(course_key)
|
||||
|
||||
if not course_cohort_settings.is_cohorted or get_team(commentable_id):
|
||||
course = courses.get_course_by_id(course_key)
|
||||
|
||||
if not course_discussion_division_enabled(course_discussion_settings) or get_team(commentable_id):
|
||||
# this is the easy case :)
|
||||
ans = False
|
||||
elif (
|
||||
commentable_id in course.top_level_discussion_topic_ids or
|
||||
course_cohort_settings.always_cohort_inline_discussions is False
|
||||
commentable_id in course.top_level_discussion_topic_ids or
|
||||
course_discussion_settings.always_divide_inline_discussions is False
|
||||
):
|
||||
# top level discussions have to be manually configured as cohorted
|
||||
# top level discussions have to be manually configured as divided
|
||||
# (default is not).
|
||||
# Same thing for inline discussions if the default is explicitly set to False in settings
|
||||
ans = commentable_id in course_cohort_settings.cohorted_discussions
|
||||
ans = commentable_id in course_discussion_settings.divided_discussions
|
||||
else:
|
||||
# inline discussions are cohorted by default
|
||||
# inline discussions are divided by default
|
||||
ans = True
|
||||
|
||||
log.debug(u"is_commentable_divided(%s, %s) = {%s}", course_key, commentable_id, ans)
|
||||
return ans
|
||||
|
||||
|
||||
def course_discussion_division_enabled(course_discussion_settings):
|
||||
"""
|
||||
Are discussions divided for the course represented by this instance of
|
||||
course_discussion_settings? This method looks both at
|
||||
course_discussion_settings.division_scheme, and information about the course
|
||||
state itself (For example, are cohorts enabled? And are there multiple
|
||||
enrollment tracks?).
|
||||
|
||||
Args:
|
||||
course_discussion_settings: CourseDiscussionSettings model instance
|
||||
|
||||
Returns: True if discussion division is enabled for the course, else False
|
||||
"""
|
||||
return _get_course_division_scheme(course_discussion_settings) != CourseDiscussionSettings.NONE
|
||||
|
||||
|
||||
def _get_course_division_scheme(course_discussion_settings):
|
||||
division_scheme = course_discussion_settings.division_scheme
|
||||
if (
|
||||
division_scheme == CourseDiscussionSettings.COHORT and
|
||||
not is_course_cohorted(course_discussion_settings.course_id)
|
||||
):
|
||||
division_scheme = CourseDiscussionSettings.NONE
|
||||
elif (
|
||||
division_scheme == CourseDiscussionSettings.ENROLLMENT_TRACK and
|
||||
len(_get_enrollment_track_groups(course_discussion_settings.course_id)) <= 1
|
||||
):
|
||||
division_scheme = CourseDiscussionSettings.NONE
|
||||
return division_scheme
|
||||
|
||||
|
||||
def get_group_name(group_id, course_discussion_settings):
|
||||
"""
|
||||
Given a specified comments_service group_id, returns the learner-facing
|
||||
name of the Group. If no such Group exists for the specified group_id
|
||||
(taking into account the division_scheme and course specified by course_discussion_settings),
|
||||
returns None.
|
||||
Args:
|
||||
group_id: the group_id as used by the comments_service code
|
||||
course_discussion_settings: CourseDiscussionSettings model instance
|
||||
|
||||
Returns: learner-facing name of the Group, or None if no such group exists
|
||||
"""
|
||||
group_names_by_id = get_group_names_by_id(course_discussion_settings)
|
||||
return group_names_by_id[group_id] if group_id in group_names_by_id else None
|
||||
|
||||
|
||||
def get_group_names_by_id(course_discussion_settings):
|
||||
"""
|
||||
Creates of a dict of group_id to learner-facing group names, for the division_scheme
|
||||
in use as specified by course_discussion_settings.
|
||||
Args:
|
||||
course_discussion_settings: CourseDiscussionSettings model instance
|
||||
|
||||
Returns: dict of group_id to learner-facing group names. If no division_scheme
|
||||
is in use, returns an empty dict.
|
||||
"""
|
||||
division_scheme = _get_course_division_scheme(course_discussion_settings)
|
||||
course_key = course_discussion_settings.course_id
|
||||
if division_scheme == CourseDiscussionSettings.COHORT:
|
||||
return get_cohort_names(courses.get_course_by_id(course_key))
|
||||
elif division_scheme == CourseDiscussionSettings.ENROLLMENT_TRACK:
|
||||
# We negate the group_ids from dynamic partitions so that they will not conflict
|
||||
# with cohort IDs (which are an auto-incrementing integer field, starting at 1).
|
||||
return {-1 * group.id: group.name for group in _get_enrollment_track_groups(course_key)}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def _get_enrollment_track_groups(course_key):
|
||||
"""
|
||||
Helper method that returns an array of the Groups in the EnrollmentTrackUserPartition for the given course.
|
||||
If no such partition exists on the course, an empty array is returned.
|
||||
"""
|
||||
partition_service = PartitionService(course_key)
|
||||
partition = partition_service.get_user_partition(ENROLLMENT_TRACK_PARTITION_ID)
|
||||
return partition.groups if partition else []
|
||||
|
||||
|
||||
def _verify_group_exists(group_id, course_discussion_settings):
|
||||
"""
|
||||
Helper method that verifies the given group_id corresponds to a Group in the
|
||||
division scheme being used. If it does not, a ValueError will be raised.
|
||||
"""
|
||||
if get_group_name(group_id, course_discussion_settings) is None:
|
||||
raise ValueError
|
||||
|
||||
|
||||
def is_discussion_enabled(course_id):
|
||||
"""
|
||||
Return True if discussions are enabled; else False
|
||||
|
||||
@@ -67,7 +67,7 @@ from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from certificates.models import CertificateStatuses
|
||||
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
|
||||
from openedx.core.lib.xblock_utils import grade_histogram
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
@@ -2701,7 +2701,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
cohorted, and does not when the course is not cohorted.
|
||||
"""
|
||||
url = reverse('get_students_features', kwargs={'course_id': unicode(self.course.id)})
|
||||
set_course_cohort_settings(self.course.id, is_cohorted=is_cohorted)
|
||||
set_course_cohorted(self.course.id, is_cohorted)
|
||||
|
||||
response = self.client.post(url, {})
|
||||
res_json = json.loads(response.content)
|
||||
|
||||
@@ -20,7 +20,7 @@ from openedx.core.djangolib.markup import HTML
|
||||
class="forum-nav-browse-menu-item"
|
||||
data-discussion-id='${entries[entry]["id"]}'
|
||||
id='${entries[entry]["id"]}'
|
||||
data-cohorted="${str(entries[entry]['is_divided']).lower()}"
|
||||
data-divided="${str(entries[entry]['is_divided']).lower()}"
|
||||
role="option"
|
||||
>
|
||||
% if entry:
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
%endif
|
||||
</select>
|
||||
## safe-lint: disable=python-parse-error,python-wrap-html
|
||||
</label>${"<% if (isCohorted && isPrivilegedUser) { %>" | n, decode.utf8}<label class="forum-nav-filter-cohort">
|
||||
## Translators: This labels a cohort menu in forum navigation
|
||||
<span class="sr">${_("Cohort:")}</span>
|
||||
</label>${"<% if (isDiscussionDivisionEnabled && isPrivilegedUser) { %>" | n, decode.utf8}<label class="forum-nav-filter-cohort">
|
||||
## Translators: This labels a group menu in forum navigation
|
||||
<span class="sr">${_("Group:")}</span>
|
||||
<select class="forum-nav-filter-cohort-control">
|
||||
<option value="">${_("in all cohorts")}</option>
|
||||
## cohorts is not iterable sometimes because inline discussions xmodule doesn't pass it
|
||||
%for c in (cohorts or []):
|
||||
<option value="${c['id']}">${c['name']}</option>
|
||||
<option value="">${_("in all groups")}</option>
|
||||
## groups is not iterable sometimes because inline discussions xmodule doesn't pass it
|
||||
%for group in (groups or []):
|
||||
<option value="${group['id']}">${group['name']}</option>
|
||||
%endfor
|
||||
</select>
|
||||
## safe-lint: disable=python-parse-error,python-wrap-html
|
||||
|
||||
@@ -118,7 +118,21 @@ def is_course_cohorted(course_key):
|
||||
Raises:
|
||||
Http404 if the course doesn't exist.
|
||||
"""
|
||||
return get_course_cohort_settings(course_key).is_cohorted
|
||||
return _get_course_cohort_settings(course_key).is_cohorted
|
||||
|
||||
|
||||
def set_course_cohorted(course_key, cohorted):
|
||||
"""
|
||||
Given a course course and a boolean, sets whether or not the course is cohorted.
|
||||
|
||||
Raises:
|
||||
Value error if `cohorted` is not a boolean
|
||||
"""
|
||||
if not isinstance(cohorted, bool):
|
||||
raise ValueError("Cohorted must be a boolean")
|
||||
course_cohort_settings = _get_course_cohort_settings(course_key)
|
||||
course_cohort_settings.is_cohorted = cohorted
|
||||
course_cohort_settings.save()
|
||||
|
||||
|
||||
def get_cohort_id(user, course_key, use_cached=False):
|
||||
@@ -130,22 +144,6 @@ def get_cohort_id(user, course_key, use_cached=False):
|
||||
return None if cohort is None else cohort.id
|
||||
|
||||
|
||||
def get_cohorted_commentables(course_key):
|
||||
"""
|
||||
Given a course_key return a set of strings representing cohorted commentables.
|
||||
"""
|
||||
|
||||
course_cohort_settings = get_course_cohort_settings(course_key)
|
||||
|
||||
if not course_cohort_settings.is_cohorted:
|
||||
# this is the easy case :)
|
||||
ans = set()
|
||||
else:
|
||||
ans = set(course_cohort_settings.cohorted_discussions)
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
COHORT_CACHE_NAMESPACE = u"cohorts.get_cohort"
|
||||
|
||||
|
||||
@@ -213,8 +211,7 @@ def get_cohort(user, course_key, assign=True, use_cached=False):
|
||||
|
||||
# First check whether the course is cohorted (users shouldn't be in a cohort
|
||||
# in non-cohorted courses, but settings can change after course starts)
|
||||
course_cohort_settings = get_course_cohort_settings(course_key)
|
||||
if not course_cohort_settings.is_cohorted:
|
||||
if not is_course_cohorted(course_key):
|
||||
return cache.setdefault(cache_key, None)
|
||||
|
||||
# If course is cohorted, check if the user already has a cohort.
|
||||
@@ -277,11 +274,7 @@ def migrate_cohort_settings(course):
|
||||
"""
|
||||
cohort_settings, created = CourseCohortsSettings.objects.get_or_create(
|
||||
course_id=course.id,
|
||||
defaults={
|
||||
'is_cohorted': course.is_cohorted,
|
||||
'cohorted_discussions': list(course.cohorted_discussions),
|
||||
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
|
||||
}
|
||||
defaults=_get_cohort_settings_from_modulestore(course)
|
||||
)
|
||||
|
||||
# Add the new and update the existing cohorts
|
||||
@@ -507,43 +500,20 @@ def is_last_random_cohort(user_group):
|
||||
return len(random_cohorts) == 1 and random_cohorts[0].name == user_group.name
|
||||
|
||||
|
||||
def set_course_cohort_settings(course_key, **kwargs):
|
||||
"""
|
||||
Set cohort settings for a course.
|
||||
|
||||
Arguments:
|
||||
course_key: CourseKey
|
||||
is_cohorted (bool): If the course should be cohorted.
|
||||
always_cohort_inline_discussions (bool): If inline discussions should always be cohorted.
|
||||
cohorted_discussions (list): List of discussion ids.
|
||||
|
||||
Returns:
|
||||
A CourseCohortSettings object.
|
||||
|
||||
Raises:
|
||||
Http404 if course_key is invalid.
|
||||
"""
|
||||
fields = {'is_cohorted': bool, 'always_cohort_inline_discussions': bool, 'cohorted_discussions': list}
|
||||
course_cohort_settings = get_course_cohort_settings(course_key)
|
||||
for field, field_type in fields.items():
|
||||
if field in kwargs:
|
||||
if not isinstance(kwargs[field], field_type):
|
||||
raise ValueError("Incorrect field type for `{}`. Type must be `{}`".format(field, field_type.__name__))
|
||||
setattr(course_cohort_settings, field, kwargs[field])
|
||||
course_cohort_settings.save()
|
||||
return course_cohort_settings
|
||||
|
||||
|
||||
@request_cached
|
||||
def get_course_cohort_settings(course_key):
|
||||
def _get_course_cohort_settings(course_key):
|
||||
"""
|
||||
Return cohort settings for a course.
|
||||
Return cohort settings for a course. NOTE that the only non-deprecated fields in
|
||||
CourseCohortSettings are `course_id` and `is_cohorted`. Other fields should only be used for
|
||||
migration purposes.
|
||||
|
||||
Arguments:
|
||||
course_key: CourseKey
|
||||
|
||||
Returns:
|
||||
A CourseCohortSettings object.
|
||||
A CourseCohortSettings object. NOTE that the only non-deprecated field in
|
||||
CourseCohortSettings are `course_id` and `is_cohorted`. Other fields should only be used
|
||||
for migration purposes.
|
||||
|
||||
Raises:
|
||||
Http404 if course_key is invalid.
|
||||
@@ -554,3 +524,25 @@ def get_course_cohort_settings(course_key):
|
||||
course = courses.get_course_by_id(course_key)
|
||||
course_cohort_settings = migrate_cohort_settings(course)
|
||||
return course_cohort_settings
|
||||
|
||||
|
||||
def get_legacy_discussion_settings(course_key):
|
||||
|
||||
try:
|
||||
course_cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key)
|
||||
return {
|
||||
'is_cohorted': course_cohort_settings.is_cohorted,
|
||||
'cohorted_discussions': course_cohort_settings.cohorted_discussions,
|
||||
'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions
|
||||
}
|
||||
except CourseCohortsSettings.DoesNotExist:
|
||||
course = courses.get_course_by_id(course_key)
|
||||
return _get_cohort_settings_from_modulestore(course)
|
||||
|
||||
|
||||
def _get_cohort_settings_from_modulestore(course):
|
||||
return {
|
||||
'is_cohorted': course.is_cohorted,
|
||||
'cohorted_discussions': list(course.cohorted_discussions),
|
||||
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ class CourseUserGroupPartitionGroup(models.Model):
|
||||
class CourseCohortsSettings(models.Model):
|
||||
"""
|
||||
This model represents cohort settings for courses.
|
||||
The only non-deprecated fields are `is_cohorted` and `course_id`.
|
||||
"""
|
||||
is_cohorted = models.BooleanField(default=False)
|
||||
|
||||
@@ -184,16 +185,23 @@ class CourseCohortsSettings(models.Model):
|
||||
# in reality the default value at the time that cohorting is enabled for a course comes from
|
||||
# course_module.always_cohort_inline_discussions (via `migrate_cohort_settings`).
|
||||
# pylint: disable=invalid-name
|
||||
# DEPRECATED-- DO NOT USE: Instead use `CourseDiscussionSettings.always_divide_inline_discussions`
|
||||
# via `get_course_discussion_settings` or `set_course_discussion_settings`.
|
||||
always_cohort_inline_discussions = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def cohorted_discussions(self):
|
||||
"""Jsonify the cohorted_discussions"""
|
||||
"""
|
||||
DEPRECATED-- DO NOT USE. Instead use `CourseDiscussionSettings.divided_discussions`
|
||||
via `get_course_discussion_settings`.
|
||||
"""
|
||||
return json.loads(self._cohorted_discussions)
|
||||
|
||||
@cohorted_discussions.setter
|
||||
def cohorted_discussions(self, value):
|
||||
"""Un-Jsonify the cohorted_discussions"""
|
||||
"""
|
||||
DEPRECATED-- DO NOT USE. Instead use `CourseDiscussionSettings` via `set_course_discussion_settings`.
|
||||
"""
|
||||
self._cohorted_discussions = json.dumps(value)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from ..cohorts import set_course_cohort_settings
|
||||
from ..cohorts import set_course_cohorted
|
||||
from django_comment_common.models import CourseDiscussionSettings
|
||||
from django_comment_common.utils import set_course_discussion_settings
|
||||
from ..models import CourseUserGroup, CourseCohort, CourseCohortsSettings, CohortMembership
|
||||
|
||||
|
||||
@@ -140,8 +142,8 @@ def config_course_cohorts(
|
||||
auto_cohorts=[],
|
||||
manual_cohorts=[],
|
||||
discussion_topics=[],
|
||||
cohorted_discussions=[],
|
||||
always_cohort_inline_discussions=False
|
||||
divided_discussions=[],
|
||||
always_divide_inline_discussions=False
|
||||
):
|
||||
"""
|
||||
Set discussions and configure cohorts for a course.
|
||||
@@ -153,10 +155,10 @@ def config_course_cohorts(
|
||||
manual_cohorts (list): Names of manual cohorts to create.
|
||||
discussion_topics (list): Discussion topic names. Picks ids and
|
||||
sort_keys automatically.
|
||||
cohorted_discussions: Discussion topics to cohort. Converts the
|
||||
divided_discussions: Discussion topics to divide. Converts the
|
||||
list to use the same ids as discussion topic names.
|
||||
always_cohort_inline_discussions (bool): Whether inline discussions
|
||||
should be cohorted by default.
|
||||
always_divide_inline_discussions (bool): Whether inline discussions
|
||||
should be divided by default.
|
||||
|
||||
Returns:
|
||||
Nothing -- modifies course in place.
|
||||
@@ -165,11 +167,12 @@ def config_course_cohorts(
|
||||
"""Convert name to id."""
|
||||
return topic_name_to_id(course, name)
|
||||
|
||||
set_course_cohort_settings(
|
||||
set_course_cohorted(course.id, is_cohorted)
|
||||
set_course_discussion_settings(
|
||||
course.id,
|
||||
is_cohorted=is_cohorted,
|
||||
cohorted_discussions=[to_id(name) for name in cohorted_discussions],
|
||||
always_cohort_inline_discussions=always_cohort_inline_discussions
|
||||
divided_discussions=[to_id(name) for name in divided_discussions],
|
||||
always_divide_inline_discussions=always_divide_inline_discussions,
|
||||
division_scheme=CourseDiscussionSettings.COHORT,
|
||||
)
|
||||
|
||||
for cohort_name in auto_cohorts:
|
||||
|
||||
@@ -22,8 +22,8 @@ from xmodule.modulestore.tests.factories import ToyCourseFactory
|
||||
from ..models import CourseUserGroup, CourseCohort, CourseUserGroupPartitionGroup
|
||||
from .. import cohorts
|
||||
from ..tests.helpers import (
|
||||
topic_name_to_id, config_course_cohorts, config_course_cohorts_legacy,
|
||||
CohortFactory, CourseCohortFactory, CourseCohortSettingsFactory
|
||||
config_course_cohorts, config_course_cohorts_legacy,
|
||||
CohortFactory, CourseCohortFactory
|
||||
)
|
||||
|
||||
|
||||
@@ -475,44 +475,6 @@ class TestCohorts(ModuleStoreTestCase):
|
||||
{cohort1.id: cohort1.name, cohort2.id: cohort2.name}
|
||||
)
|
||||
|
||||
def test_get_cohorted_commentables(self):
|
||||
"""
|
||||
Make sure cohorts.get_cohorted_commentables() correctly returns a list of strings representing cohorted
|
||||
commentables. Also verify that we can't get the cohorted commentables from a course which does not exist.
|
||||
"""
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
|
||||
self.assertEqual(cohorts.get_cohorted_commentables(course.id), set())
|
||||
|
||||
config_course_cohorts(course, is_cohorted=True)
|
||||
self.assertEqual(cohorts.get_cohorted_commentables(course.id), set())
|
||||
|
||||
config_course_cohorts(
|
||||
course,
|
||||
is_cohorted=True,
|
||||
discussion_topics=["General", "Feedback"],
|
||||
cohorted_discussions=["Feedback"]
|
||||
)
|
||||
self.assertItemsEqual(
|
||||
cohorts.get_cohorted_commentables(course.id),
|
||||
set([topic_name_to_id(course, "Feedback")])
|
||||
)
|
||||
|
||||
config_course_cohorts(
|
||||
course,
|
||||
is_cohorted=True,
|
||||
discussion_topics=["General", "Feedback"],
|
||||
cohorted_discussions=["General", "Feedback"]
|
||||
)
|
||||
self.assertItemsEqual(
|
||||
cohorts.get_cohorted_commentables(course.id),
|
||||
set([topic_name_to_id(course, "General"), topic_name_to_id(course, "Feedback")])
|
||||
)
|
||||
self.assertRaises(
|
||||
Http404,
|
||||
lambda: cohorts.get_cohorted_commentables(SlashSeparatedCourseKey("course", "does_not", "exist"))
|
||||
)
|
||||
|
||||
def test_get_cohort_by_name(self):
|
||||
"""
|
||||
Make sure cohorts.get_cohort_by_name() properly finds a cohort by name for a given course. Also verify that it
|
||||
@@ -672,59 +634,16 @@ class TestCohorts(ModuleStoreTestCase):
|
||||
# Note that the following get() will fail with MultipleObjectsReturned if race condition is not handled.
|
||||
self.assertEqual(first_cohort.users.get(), course_user)
|
||||
|
||||
def test_get_course_cohort_settings(self):
|
||||
def test_set_cohorted_with_invalid_data_type(self):
|
||||
"""
|
||||
Test that cohorts.get_course_cohort_settings is working as expected.
|
||||
Test that cohorts.set_course_cohorted raises exception if argument is not a boolean.
|
||||
"""
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
course_cohort_settings = cohorts.get_course_cohort_settings(course.id)
|
||||
|
||||
self.assertFalse(course_cohort_settings.is_cohorted)
|
||||
self.assertEqual(course_cohort_settings.cohorted_discussions, [])
|
||||
self.assertFalse(course_cohort_settings.always_cohort_inline_discussions)
|
||||
with self.assertRaises(ValueError) as value_error:
|
||||
cohorts.set_course_cohorted(course.id, 'not a boolean')
|
||||
|
||||
def test_update_course_cohort_settings(self):
|
||||
"""
|
||||
Test that cohorts.set_course_cohort_settings is working as expected.
|
||||
"""
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
CourseCohortSettingsFactory(course_id=course.id)
|
||||
|
||||
cohorts.set_course_cohort_settings(
|
||||
course.id,
|
||||
is_cohorted=False,
|
||||
cohorted_discussions=['topic a id', 'topic b id'],
|
||||
always_cohort_inline_discussions=True
|
||||
)
|
||||
|
||||
course_cohort_settings = cohorts.get_course_cohort_settings(course.id)
|
||||
|
||||
self.assertFalse(course_cohort_settings.is_cohorted)
|
||||
self.assertEqual(course_cohort_settings.cohorted_discussions, ['topic a id', 'topic b id'])
|
||||
self.assertTrue(course_cohort_settings.always_cohort_inline_discussions)
|
||||
|
||||
def test_update_course_cohort_settings_with_invalid_data_type(self):
|
||||
"""
|
||||
Test that cohorts.set_course_cohort_settings raises exception if fields have incorrect data type.
|
||||
"""
|
||||
course = modulestore().get_course(self.toy_course_key)
|
||||
CourseCohortSettingsFactory(course_id=course.id)
|
||||
|
||||
exception_msg_tpl = "Incorrect field type for `{}`. Type must be `{}`"
|
||||
fields = [
|
||||
{'name': 'is_cohorted', 'type': bool},
|
||||
{'name': 'always_cohort_inline_discussions', 'type': bool},
|
||||
{'name': 'cohorted_discussions', 'type': list}
|
||||
]
|
||||
|
||||
for field in fields:
|
||||
with self.assertRaises(ValueError) as value_error:
|
||||
cohorts.set_course_cohort_settings(course.id, **{field['name']: ''})
|
||||
|
||||
self.assertEqual(
|
||||
value_error.exception.message,
|
||||
exception_msg_tpl.format(field['name'], field['type'].__name__)
|
||||
)
|
||||
self.assertEqual("Cohorted must be a boolean", value_error.exception.message)
|
||||
|
||||
|
||||
@attr(shard=2)
|
||||
|
||||
@@ -125,7 +125,7 @@ class CohortViewsTestCase(ModuleStoreTestCase):
|
||||
self.course,
|
||||
is_cohorted=True,
|
||||
discussion_topics=discussion_topics,
|
||||
cohorted_discussions=cohorted_discussions
|
||||
divided_discussions=cohorted_discussions
|
||||
)
|
||||
return cohorted_inline_discussions, cohorted_course_wide_discussions
|
||||
|
||||
@@ -315,6 +315,17 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
|
||||
"""
|
||||
config_course_cohorts(self.course, is_cohorted=True)
|
||||
|
||||
response = self.patch_handler(
|
||||
self.course,
|
||||
data={'always_cohort_inline_discussions': ''},
|
||||
expected_response_code=400,
|
||||
handler=course_cohort_settings_handler
|
||||
)
|
||||
self.assertEqual(
|
||||
"Incorrect field type for `{}`. Type must be `{}`".format('always_divide_inline_discussions', bool.__name__),
|
||||
response.get("error")
|
||||
)
|
||||
|
||||
response = self.patch_handler(
|
||||
self.course,
|
||||
data={'is_cohorted': ''},
|
||||
@@ -322,7 +333,7 @@ class CourseCohortSettingsHandlerTestCase(CohortViewsTestCase):
|
||||
handler=course_cohort_settings_handler
|
||||
)
|
||||
self.assertEqual(
|
||||
"Incorrect field type for `{}`. Type must be `{}`".format('is_cohorted', bool.__name__),
|
||||
"Cohorted must be a boolean",
|
||||
response.get("error")
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Views related to course groups functionality.
|
||||
import logging
|
||||
import re
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.paginator import EmptyPage, Paginator
|
||||
@@ -14,13 +15,13 @@ from django.http import Http404, HttpResponseBadRequest
|
||||
from django.utils.translation import ugettext
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods, require_POST
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from django_comment_common.models import CourseDiscussionSettings
|
||||
from django_comment_common.utils import get_course_discussion_settings, set_course_discussion_settings
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY
|
||||
from lms.djangoapps.django_comment_client.utils import get_discussion_categories_ids, get_discussion_category_map
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from util.json_request import JsonResponse, expect_json
|
||||
|
||||
from . import cohorts
|
||||
@@ -63,20 +64,20 @@ def unlink_cohort_partition_group(cohort):
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _get_course_cohort_settings_representation(course, course_cohort_settings):
|
||||
def _get_course_cohort_settings_representation(course, is_cohorted, course_discussion_settings):
|
||||
"""
|
||||
Returns a JSON representation of a course cohort settings.
|
||||
"""
|
||||
cohorted_course_wide_discussions, cohorted_inline_discussions = get_cohorted_discussions(
|
||||
course, course_cohort_settings
|
||||
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
|
||||
course, course_discussion_settings
|
||||
)
|
||||
|
||||
return {
|
||||
'id': course_cohort_settings.id,
|
||||
'is_cohorted': course_cohort_settings.is_cohorted,
|
||||
'cohorted_inline_discussions': cohorted_inline_discussions,
|
||||
'cohorted_course_wide_discussions': cohorted_course_wide_discussions,
|
||||
'always_cohort_inline_discussions': course_cohort_settings.always_cohort_inline_discussions,
|
||||
'id': course_discussion_settings.id,
|
||||
'is_cohorted': is_cohorted,
|
||||
'cohorted_inline_discussions': divided_inline_discussions,
|
||||
'cohorted_course_wide_discussions': divided_course_wide_discussions,
|
||||
'always_cohort_inline_discussions': course_discussion_settings.always_divide_inline_discussions,
|
||||
}
|
||||
|
||||
|
||||
@@ -97,23 +98,23 @@ def _get_cohort_representation(cohort, course):
|
||||
}
|
||||
|
||||
|
||||
def get_cohorted_discussions(course, course_settings):
|
||||
def get_divided_discussions(course, discussion_settings):
|
||||
"""
|
||||
Returns the course-wide and inline cohorted discussion ids separately.
|
||||
Returns the course-wide and inline divided discussion ids separately.
|
||||
"""
|
||||
cohorted_course_wide_discussions = []
|
||||
cohorted_inline_discussions = []
|
||||
divided_course_wide_discussions = []
|
||||
divided_inline_discussions = []
|
||||
|
||||
course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
|
||||
all_discussions = get_discussion_categories_ids(course, None, include_all=True)
|
||||
|
||||
for cohorted_discussion_id in course_settings.cohorted_discussions:
|
||||
if cohorted_discussion_id in course_wide_discussions:
|
||||
cohorted_course_wide_discussions.append(cohorted_discussion_id)
|
||||
elif cohorted_discussion_id in all_discussions:
|
||||
cohorted_inline_discussions.append(cohorted_discussion_id)
|
||||
for divided_discussion_id in discussion_settings.divided_discussions:
|
||||
if divided_discussion_id in course_wide_discussions:
|
||||
divided_course_wide_discussions.append(divided_discussion_id)
|
||||
elif divided_discussion_id in all_discussions:
|
||||
divided_inline_discussions.append(divided_discussion_id)
|
||||
|
||||
return cohorted_course_wide_discussions, cohorted_inline_discussions
|
||||
return divided_course_wide_discussions, divided_inline_discussions
|
||||
|
||||
|
||||
@require_http_methods(("GET", "PATCH"))
|
||||
@@ -131,11 +132,12 @@ def course_cohort_settings_handler(request, course_key_string):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course = get_course_with_access(request.user, 'staff', course_key)
|
||||
cohort_settings = cohorts.get_course_cohort_settings(course_key)
|
||||
is_cohorted = cohorts.is_course_cohorted(course_key)
|
||||
discussion_settings = get_course_discussion_settings(course_key)
|
||||
|
||||
if request.method == 'PATCH':
|
||||
cohorted_course_wide_discussions, cohorted_inline_discussions = get_cohorted_discussions(
|
||||
course, cohort_settings
|
||||
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
|
||||
course, discussion_settings
|
||||
)
|
||||
|
||||
settings_to_change = {}
|
||||
@@ -144,16 +146,16 @@ def course_cohort_settings_handler(request, course_key_string):
|
||||
settings_to_change['is_cohorted'] = request.json.get('is_cohorted')
|
||||
|
||||
if 'cohorted_course_wide_discussions' in request.json or 'cohorted_inline_discussions' in request.json:
|
||||
cohorted_course_wide_discussions = request.json.get(
|
||||
'cohorted_course_wide_discussions', cohorted_course_wide_discussions
|
||||
divided_course_wide_discussions = request.json.get(
|
||||
'cohorted_course_wide_discussions', divided_course_wide_discussions
|
||||
)
|
||||
cohorted_inline_discussions = request.json.get(
|
||||
'cohorted_inline_discussions', cohorted_inline_discussions
|
||||
divided_inline_discussions = request.json.get(
|
||||
'cohorted_inline_discussions', divided_inline_discussions
|
||||
)
|
||||
settings_to_change['cohorted_discussions'] = cohorted_course_wide_discussions + cohorted_inline_discussions
|
||||
settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions
|
||||
|
||||
if 'always_cohort_inline_discussions' in request.json:
|
||||
settings_to_change['always_cohort_inline_discussions'] = request.json.get(
|
||||
settings_to_change['always_divide_inline_discussions'] = request.json.get(
|
||||
'always_cohort_inline_discussions'
|
||||
)
|
||||
|
||||
@@ -161,14 +163,19 @@ def course_cohort_settings_handler(request, course_key_string):
|
||||
return JsonResponse({"error": unicode("Bad Request")}, 400)
|
||||
|
||||
try:
|
||||
cohort_settings = cohorts.set_course_cohort_settings(
|
||||
course_key, **settings_to_change
|
||||
)
|
||||
if 'is_cohorted' in settings_to_change:
|
||||
is_cohorted = settings_to_change['is_cohorted']
|
||||
cohorts.set_course_cohorted(course_key, is_cohorted)
|
||||
del settings_to_change['is_cohorted']
|
||||
settings_to_change['division_scheme'] = CourseDiscussionSettings.COHORT if is_cohorted \
|
||||
else CourseDiscussionSettings.NONE
|
||||
if settings_to_change:
|
||||
discussion_settings = set_course_discussion_settings(course_key, **settings_to_change)
|
||||
except ValueError as err:
|
||||
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
|
||||
return JsonResponse({"error": unicode(err)}, 400)
|
||||
|
||||
return JsonResponse(_get_course_cohort_settings_representation(course, cohort_settings))
|
||||
return JsonResponse(_get_course_cohort_settings_representation(course, is_cohorted, discussion_settings))
|
||||
|
||||
|
||||
@require_http_methods(("GET", "PUT", "POST", "PATCH"))
|
||||
|
||||
@@ -12,7 +12,7 @@ from course_modes.models import CourseMode
|
||||
from student.models import CourseEnrollment
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.verified_track_content.models import VerifiedTrackCohortedCourse
|
||||
from xmodule.partitions.partitions import NoSuchUserPartitionGroupError, Group, UserPartition
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
|
||||
|
||||
# These IDs must be less than 100 so that they do not overlap with Groups in
|
||||
|
||||
@@ -18,7 +18,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from ..models import VerifiedTrackCohortedCourse, DEFAULT_VERIFIED_COHORT_NAME
|
||||
from ..tasks import sync_cohort_with_mode
|
||||
from openedx.core.djangoapps.course_groups.cohorts import (
|
||||
set_course_cohort_settings, add_cohort, CourseCohort, DEFAULT_COHORT_NAME
|
||||
set_course_cohorted, add_cohort, CourseCohort, DEFAULT_COHORT_NAME
|
||||
)
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestMoveToVerified(SharedModuleStoreTestCase):
|
||||
|
||||
def _enable_cohorting(self):
|
||||
""" Turn on cohorting in the course. """
|
||||
set_course_cohort_settings(self.course.id, is_cohorted=True)
|
||||
set_course_cohorted(self.course.id, True)
|
||||
|
||||
def _create_verified_cohort(self, name=DEFAULT_VERIFIED_COHORT_NAME):
|
||||
""" Create a verified cohort. """
|
||||
|
||||
Reference in New Issue
Block a user