Commit 0ebd8c29 by muhammad-ammar Committed by Usman Khalid

Allow Instructor To Rename Cohorts And Set Cohort Assignment Method

TNL-181
parent ec6cc04b
......@@ -6,7 +6,7 @@ Instructor (2) dashboard page.
from bok_choy.page_object import PageObject
from .course_page import CoursePage
import os
from bok_choy.promise import EmptyPromise
from bok_choy.promise import EmptyPromise, Promise
from ...tests.helpers import select_option_by_text, get_selected_option_text, get_options
......@@ -101,6 +101,7 @@ class MembershipPageCohortManagementSection(PageObject):
content_group_selector_css = 'select.input-cohort-group-association'
no_content_group_button_css = '.cohort-management-details-association-course input.radio-no'
select_content_group_button_css = '.cohort-management-details-association-course input.radio-yes'
assignment_type_buttons_css = '.cohort-management-assignment-type-settings input'
def is_browser_on_page(self):
return self.q(css='.cohort-management.membership-section').present
......@@ -115,7 +116,12 @@ class MembershipPageCohortManagementSection(PageObject):
"""
Returns the available options in the cohort dropdown, including the initial "Select a cohort".
"""
return self.q(css=self._bounded_selector("#cohort-select option"))
def check_func():
"""Promise Check Function"""
query = self.q(css=self._bounded_selector("#cohort-select option"))
return len(query) > 0, query
return Promise(check_func, "Waiting for cohort selector to populate").fulfill()
def _cohort_name(self, label):
"""
......@@ -129,6 +135,41 @@ class MembershipPageCohortManagementSection(PageObject):
"""
return int(label.split(' (')[1].split(')')[0])
def save_cohort_settings(self):
"""
Click on Save button shown after click on Settings tab or when we add a new cohort.
"""
self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click()
@property
def is_assignment_settings_disabled(self):
"""
Check if assignment settings are disabled.
"""
attributes = self.q(css=self._bounded_selector('.cohort-management-assignment-type-settings')).attrs('class')
if 'is-disabled' in attributes[0].split():
return True
return False
@property
def assignment_settings_message(self):
"""
Return assignment settings disabled message in case of default cohort.
"""
query = self.q(css=self._bounded_selector('.copy-error'))
if query.present:
return query.text[0]
else:
return ''
@property
def cohort_name_in_header(self):
"""
Return cohort name as shown in cohort header.
"""
return self._cohort_name(self.q(css=self._bounded_selector(".group-header-title .title-value")).text[0])
def get_cohorts(self):
"""
Returns, as a list, the names of the available cohorts in the drop-down, filtering out "Select a cohort".
......@@ -142,10 +183,6 @@ class MembershipPageCohortManagementSection(PageObject):
"""
Returns the name of the selected cohort.
"""
EmptyPromise(
lambda: len(self._get_cohort_options().results) > 0,
"Waiting for cohort selector to populate"
).fulfill()
return self._cohort_name(
self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0]
)
......@@ -162,10 +199,6 @@ class MembershipPageCohortManagementSection(PageObject):
"""
Selects the given cohort in the drop-down.
"""
EmptyPromise(
lambda: cohort_name in self.get_cohorts(),
"Waiting for cohort selector to populate"
).fulfill()
# Note: can't use Select to select by text because the count is also included in the displayed text.
self._get_cohort_options().filter(
lambda el: self._cohort_name(el.text) == cohort_name
......@@ -176,7 +209,25 @@ class MembershipPageCohortManagementSection(PageObject):
"Waiting to confirm cohort has been selected"
).fulfill()
def add_cohort(self, cohort_name, content_group=None):
def set_cohort_name(self, cohort_name):
"""
Set Cohort Name.
"""
textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0]
textinput.clear()
textinput.send_keys(cohort_name)
def set_assignment_type(self, assignment_type):
"""
Set assignment type for selected cohort.
Arguments:
assignment_type (str): Should be 'random' or 'manual'
"""
css = self._bounded_selector(self.assignment_type_buttons_css)
self.q(css=css).filter(lambda el: el.get_attribute('value') == assignment_type).first.click()
def add_cohort(self, cohort_name, content_group=None, assignment_type=None):
"""
Adds a new manual cohort with the specified name.
If a content group should also be associated, the name of the content group should be specified.
......@@ -187,9 +238,15 @@ class MembershipPageCohortManagementSection(PageObject):
create_buttons.results[len(create_buttons.results) - 1].click()
textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0]
textinput.send_keys(cohort_name)
# Manual assignment type will be selected by default for a new cohort
# if we are not setting the assignment type explicitly
if assignment_type:
self.set_assignment_type(assignment_type)
if content_group:
self._select_associated_content_group(content_group)
self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click()
self.save_cohort_settings()
def get_cohort_group_setup(self):
"""
......@@ -200,6 +257,12 @@ class MembershipPageCohortManagementSection(PageObject):
def select_edit_settings(self):
self.q(css=self._bounded_selector(".action-edit")).first.click()
def select_manage_settings(self):
"""
Click on Manage Students Tab under cohort management section.
"""
self.q(css=self._bounded_selector(".tab-manage_students")).first.click()
def add_students_to_selected_cohort(self, users):
"""
Adds a list of users (either usernames or email addresses) to the currently selected cohort.
......@@ -245,23 +308,6 @@ class MembershipPageCohortManagementSection(PageObject):
return None
return get_selected_option_text(self.q(css=self._bounded_selector(self.content_group_selector_css)))
def verify_cohort_content_group_selected(self, content_group=None):
"""
Waits for the expected content_group (or none) to show as selected for
cohort associated content group.
"""
if content_group:
self.wait_for(
lambda: unicode(
self.q(css='select.input-cohort-group-association option:checked').text[0]
) == content_group,
"Cohort group has been selected."
)
else:
self.wait_for_element_visibility(
'.cohort-management-details-association-course input.radio-no:checked',
'Radio button "No content group" has been selected.')
def set_cohort_associated_content_group(self, content_group=None, select_settings=True):
"""
Sets the content group associated with the cohort currently being edited.
......@@ -274,8 +320,7 @@ class MembershipPageCohortManagementSection(PageObject):
self.q(css=self._bounded_selector(self.no_content_group_button_css)).first.click()
else:
self._select_associated_content_group(content_group)
self.q(css=self._bounded_selector("div.form-actions .action-save")).first.click()
self.verify_cohort_content_group_selected(content_group)
self.save_cohort_settings()
def _select_associated_content_group(self, content_group):
"""
......
......@@ -73,7 +73,7 @@ class CohortTestMixin(object):
Adds a cohort by name, returning its ID.
"""
url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/'
data = json.dumps({"name": cohort_name})
data = json.dumps({"name": cohort_name, 'assignment_type': 'manual'})
response = course_fixture.session.post(url, data=data, headers=course_fixture.headers)
self.assertTrue(response.ok, "Failed to create cohort")
return response.json()['id']
......
......@@ -6,14 +6,17 @@ var edx = edx || {};
edx.groups = edx.groups || {};
edx.groups.CohortEditorView = Backbone.View.extend({
events : {
'click .wrapper-tabs .tab': 'selectTab',
'click .tab-content-settings .action-save': 'saveSettings',
'click .tab-content-settings .action-cancel': 'cancelSettings',
'submit .cohort-management-group-add-form': 'addStudents'
},
initialize: function(options) {
this.template = _.template($('#cohort-editor-tpl').text());
this.groupHeaderTemplate = _.template($('#cohort-group-header-tpl').text());
this.cohorts = options.cohorts;
this.contentGroups = options.contentGroups;
this.context = options.context;
......@@ -26,9 +29,9 @@ var edx = edx || {};
render: function() {
this.$el.html(this.template({
cohort: this.model,
studioAdvancedSettingsUrl: this.context.studioAdvancedSettingsUrl
cohort: this.model
}));
this.renderGroupHeader();
this.cohortFormView = new CohortFormView({
model: this.model,
contentGroups: this.contentGroups,
......@@ -39,6 +42,13 @@ var edx = edx || {};
return this;
},
renderGroupHeader: function() {
this.$('.cohort-management-group-header').html(this.groupHeaderTemplate({
cohort: this.model,
studioAdvancedSettingsUrl: this.context.studioAdvancedSettingsUrl
}));
},
selectTab: function(event) {
var tabElement = $(event.currentTarget),
tabName = tabElement.data('tab');
......@@ -53,13 +63,20 @@ var edx = edx || {};
saveSettings: function(event) {
var cohortFormView = this.cohortFormView;
var self = this;
event.preventDefault();
cohortFormView.saveForm()
.done(function() {
self.renderGroupHeader();
cohortFormView.showMessage(gettext('Saved cohort'));
});
},
cancelSettings: function(event) {
event.preventDefault();
this.render();
},
setCohort: function(cohort) {
this.model = cohort;
this.render();
......
......@@ -35,12 +35,22 @@ var edx = edx || {};
render: function() {
this.$el.html(this.template({
cohort: this.model,
isDefaultCohort: this.isDefault(this.model.get('name')),
contentGroups: this.contentGroups,
studioGroupConfigurationsUrl: this.context.studioGroupConfigurationsUrl
}));
return this;
},
isDefault: function(name) {
var cohorts = this.model.collection;
if (_.isUndefined(cohorts)) {
return false;
}
var randomModels = cohorts.where({assignment_type:'random'});
return (randomModels.length === 1) && (randomModels[0].get('name') === name);
},
onRadioButtonChange: function(event) {
var target = $(event.currentTarget),
groupsEnabled = target.val() === 'yes';
......@@ -77,7 +87,11 @@ var edx = edx || {};
getUpdatedCohortName: function() {
var cohortName = this.$('.cohort-name').val();
return cohortName ? cohortName.trim() : this.model.get('name');
return cohortName ? cohortName.trim() : '';
},
getAssignmentType: function() {
return this.$('input[name="cohort-assignment-type"]:checked').val();
},
showMessage: function(message, type, details) {
......@@ -109,18 +123,21 @@ var edx = edx || {};
cohort = this.model,
saveOperation = $.Deferred(),
isUpdate = !_.isUndefined(this.model.id),
fieldData, selectedContentGroup, errorMessages, showErrorMessage;
fieldData, selectedContentGroup, selectedAssignmentType, errorMessages, showErrorMessage;
showErrorMessage = function(message, details) {
self.showMessage(message, 'error', details);
};
this.removeNotification();
selectedContentGroup = this.getSelectedContentGroup();
selectedAssignmentType = this.getAssignmentType();
fieldData = {
name: this.getUpdatedCohortName(),
group_id: selectedContentGroup ? selectedContentGroup.id : null,
user_partition_id: selectedContentGroup ? selectedContentGroup.get('user_partition_id') : null
user_partition_id: selectedContentGroup ? selectedContentGroup.get('user_partition_id') : null,
assignment_type: selectedAssignmentType
};
errorMessages = this.validate(fieldData);
if (errorMessages.length > 0) {
showErrorMessage(
isUpdate ? gettext("The cohort cannot be saved") : gettext("The cohort cannot be added"),
......@@ -129,7 +146,7 @@ var edx = edx || {};
saveOperation.reject();
} else {
cohort.save(
fieldData, {patch: isUpdate}
fieldData, {patch: isUpdate, wait: true}
).done(function(result) {
cohort.id = result.id;
self.render(); // re-render to remove any now invalid error messages
......
......@@ -567,6 +567,12 @@
}
}
.cohort-management-assignment-type-settings {
&.is-disabled {
opacity: 0.50;
}
}
// cohort
.cohort-management-group-header {
padding: $baseline;
......
<section class="cohort-management-settings has-tabs">
<header class="cohort-management-group-header">
<h3 class="group-header-title" tabindex="-1">
<span class="title-value"><%- cohort.get('name') %></span>
<span class="group-count"><%-
interpolate(
ngettext('(contains %(student_count)s student)', '(contains %(student_count)s students)', cohort.get('user_count')),
{ student_count: cohort.get('user_count') },
true
)
%></span>
</h3>
<div class="cohort-management-group-setup">
<div class="setup-value">
<% if (cohort.get('assignment_type') == "none") { %>
<%- gettext("Students are added to this cohort only when you provide their email addresses or usernames on this page.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohort_config.html#assign-students-to-cohort-groups-manually" class="incontext-help action-secondary action-help"><%= gettext("What does this mean?") %></a>
<% } else { %>
<%- gettext("Students are added to this cohort automatically.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohorts_overview.html#all-automated-assignment" class="incontext-help action-secondary action-help"><%- gettext("What does this mean?") %></a>
<% } %>
</div>
<div class="setup-actions">
<% if (studioAdvancedSettingsUrl !== "None") { %>
<a href="<%= studioAdvancedSettingsUrl %>" class="action-secondary action-edit"><%- gettext("Edit settings in Studio") %></a>
<% } %>
</div>
</div>
</header>
<header class="cohort-management-group-header"></header>
<ul class="wrapper-tabs">
<li class="tab tab-manage_students is-selected" data-tab="manage_students"><a href="#"><span class="sr"><%- gettext('Selected tab') %> </span><%- gettext("Manage Students") %></a></li>
......
......@@ -6,23 +6,6 @@
<div class="tab-content is-visible new-cohort-form">
<% } %>
<div class="form-fields">
<%
// Don't allow renaming of existing cohorts yet as it doesn't interact well with
// the course's advanced setting for auto cohorting.
if (isNewCohort) {
%>
<div class="form-field">
<div class="cohort-management-settings-form-name field field-text">
<label for="cohort-name" class="form-label">
<%- gettext('Cohort Name') %> *
<span class="sr"><%- gettext('(Required Field)')%></span>
</label>
<input type="text" name="cohort-name" value="<%- cohort ? cohort.get('name') : '' %>" class="input cohort-name"
id="cohort-name"
placeholder="<%- gettext("Enter the name of the cohort") %>" required="required" />
</div>
</div>
<% } %>
<%
var foundSelected = false;
......@@ -30,8 +13,44 @@
var selectedUserPartitionId = cohort.get('user_partition_id');
var hasSelectedContentGroup = selectedContentGroupId != null;
var hasContentGroups = contentGroups.length > 0;
var assignment_type = cohort.get('assignment_type');
var cohort_name = cohort.get('name');
var cohort_name_value = isNewCohort ? '' : cohort_name;
var placeholder_value = isNewCohort ? gettext('Enter the name of the cohort') : '';
%>
<div class="form-field">
<div class="cohort-management-settings-form-name field field-text">
<label for="cohort-name" class="form-label">
<%- gettext('Cohort Name') %> *
<span class="sr"><%- gettext('(Required Field)')%></span>
</label>
<input name="cohort-name" value="<%- cohort_name_value %>" class="input cohort-name" id="cohort-name"
placeholder="<%- placeholder_value %>" required="required" type="text">
</div>
<hr class="divider divider-lv1">
<% if (isDefaultCohort) { %>
<div class="cohort-management-assignment-type-settings field field-radio is-disabled" aria-disabled="true">
<% } else { %>
<div class="cohort-management-assignment-type-settings field field-radio">
<% } %>
<h4 class="form-label">
<%- gettext('Students in this cohort are:') %>
</h4>
<label>
<input type="radio" class="type-random" name="cohort-assignment-type" value="random" <%- assignment_type == 'random' ? 'checked="checked"' : '' %>/> <%- gettext("Randomly Assigned") %>
</label>
<label>
<input type="radio" class="type-manual" name="cohort-assignment-type" value="manual" <%- assignment_type == 'manual' || isNewCohort ? 'checked="checked"' : '' %>/> <%- gettext("Manually Assigned") %>
</label>
</div>
<% if (isDefaultCohort) { %>
<p class="copy-error">
<i class="icon fa fa-exclamation-triangle" aria-hidden="true"></i>
<%- gettext("There must be one cohort to which students can be randomly assigned.") %>
</p>
<% } %>
<hr class="divider divider-lv1">
<div class="cohort-management-details-association-course field field-radio">
<h4 class="form-label">
<%- gettext('Associated Content Group') %>
......@@ -119,12 +138,9 @@
<div class="form-actions <% if (isNewCohort) { %>new-cohort-form<% } %>">
<button class="form-submit button action-primary action-save">
<i class="icon fa fa-plus" aria-hidden="true"></i>
<%- gettext('Save') %>
</button>
<% if (isNewCohort) { %>
<a href="" class="form-cancel action-secondary action-cancel"><%- gettext('Cancel') %></a>
<% } %>
<a href="" class="form-cancel action-secondary action-cancel"><%- gettext('Cancel') %></a>
</div>
</form>
</div>
<h3 class="group-header-title" tabindex="-1">
<span class="title-value"><%- cohort.get('name') %></span>
<span class="group-count"><%-
interpolate(
ngettext('(contains %(student_count)s student)', '(contains %(student_count)s students)', cohort.get('user_count')),
{ student_count: cohort.get('user_count') },
true
)
%></span>
</h3>
<div class="cohort-management-group-setup">
<div class="setup-value">
<% if (cohort.get('assignment_type') == "manual") { %>
<%- gettext("Students are added to this cohort only when you provide their email addresses or usernames on this page.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohort_config.html#assign-students-to-cohort-groups-manually" class="incontext-help action-secondary action-help"><%= gettext("What does this mean?") %></a>
<% } else { %>
<%- gettext("Students are added to this cohort automatically.") %>
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/cohorts/cohorts_overview.html#all-automated-assignment" class="incontext-help action-secondary action-help"><%- gettext("What does this mean?") %></a>
<% } %>
</div>
<div class="setup-actions">
<% if (studioAdvancedSettingsUrl !== "None") { %>
<a href="<%= studioAdvancedSettingsUrl %>" class="action-secondary action-edit"><%- gettext("Edit settings in Studio") %></a>
<% } %>
</div>
</div>
\ No newline at end of file
......@@ -66,7 +66,7 @@
## Include Underscore templates
<%block name="header_extras">
% for template_name in ["cohorts", "cohort-editor", "cohort-selector", "cohort-form", "notification"]:
% for template_name in ["cohorts", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
</script>
......
......@@ -5,6 +5,7 @@ forums, and to the cohort admin views.
import logging
import random
import json
from django.db import transaction
from django.db.models.signals import post_save, m2m_changed
......@@ -15,7 +16,7 @@ from django.utils.translation import ugettext as _
from courseware import courses
from eventtracking import tracker
from student.models import get_user_by_username_or_email
from .models import CourseUserGroup, CourseUserGroupPartitionGroup
from .models import CourseUserGroup, CourseCohort, CourseCohortsSettings, CourseUserGroupPartitionGroup
log = logging.getLogger(__name__)
......@@ -82,31 +83,6 @@ def _cohort_membership_changed(sender, **kwargs):
DEFAULT_COHORT_NAME = "Default Group"
class CohortAssignmentType(object):
"""
The various types of rule-based cohorts
"""
# No automatic rules are applied to this cohort; users must be manually added.
NONE = "none"
# One of (possibly) multiple cohorts to which users are randomly assigned.
# Note: The 'default' cohort is included in this category iff it exists and
# there are no other random groups. (Also see Note 2 above.)
RANDOM = "random"
@staticmethod
def get(cohort, course):
"""
Returns the assignment type of the given cohort for the given course
"""
if cohort.name in course.auto_cohort_groups:
return CohortAssignmentType.RANDOM
elif len(course.auto_cohort_groups) == 0 and cohort.name == DEFAULT_COHORT_NAME:
return CohortAssignmentType.RANDOM
else:
return CohortAssignmentType.NONE
# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
# if and when that's fixed, it's a good idea to have a local generator to avoid any other
# code that messes with the global random module.
......@@ -237,47 +213,74 @@ def get_cohort(user, course_key, assign=True):
if not assign:
return None
choices = course.auto_cohort_groups
if len(choices) > 0:
# Randomly choose one of the auto_cohort_groups, creating it if needed.
group_name = local_random().choice(choices)
cohorts = get_course_cohorts(course, assignment_type=CourseCohort.RANDOM)
if cohorts:
cohort = local_random().choice(cohorts)
else:
# Use the "default cohort".
group_name = DEFAULT_COHORT_NAME
cohort = CourseCohort.create(
cohort_name=DEFAULT_COHORT_NAME,
course_id=course_key,
assignment_type=CourseCohort.RANDOM
).course_user_group
group, __ = CourseUserGroup.objects.get_or_create(
course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=group_name
user.course_groups.add(cohort)
return cohort
def migrate_cohort_settings(course):
"""
Migrate all the cohort settings associated with this course from modulestore to mysql.
After that we will never touch modulestore for any cohort related settings.
"""
course_id = course.location.course_key
cohort_settings, created = CourseCohortsSettings.objects.get_or_create(
course_id=course_id,
defaults={
'is_cohorted': course.is_cohorted,
'cohorted_discussions': json.dumps(list(course.cohorted_discussions)),
'always_cohort_inline_discussions': course.always_cohort_inline_discussions
}
)
user.course_groups.add(group)
return group
# Add the new and update the existing cohorts
if created:
# Update the manual cohorts already present in CourseUserGroup
manual_cohorts = CourseUserGroup.objects.filter(
course_id=course_id,
group_type=CourseUserGroup.COHORT
).exclude(name__in=course.auto_cohort_groups)
for cohort in manual_cohorts:
CourseCohort.create(course_user_group=cohort)
for group_name in course.auto_cohort_groups:
CourseCohort.create(cohort_name=group_name, course_id=course_id, assignment_type=CourseCohort.RANDOM)
return cohort_settings
def get_course_cohorts(course):
def get_course_cohorts(course, assignment_type=None):
"""
Get a list of all the cohorts in the given course. This will include auto cohorts,
regardless of whether or not the auto cohorts include any users.
Arguments:
course: the course for which cohorts should be returned
assignment_type: cohort assignment type
Returns:
A list of CourseUserGroup objects. Empty if there are no cohorts. Does
A list of CourseUserGroup objects. Empty if there are no cohorts. Does
not check whether the course is cohorted.
"""
# Ensure all auto cohorts are created.
for group_name in course.auto_cohort_groups:
CourseUserGroup.objects.get_or_create(
course_id=course.location.course_key,
group_type=CourseUserGroup.COHORT,
name=group_name
)
# Migrate cohort settings for this course
migrate_cohort_settings(course)
return list(CourseUserGroup.objects.filter(
query_set = CourseUserGroup.objects.filter(
course_id=course.location.course_key,
group_type=CourseUserGroup.COHORT
))
)
query_set = query_set.filter(cohort__assignment_type=assignment_type) if assignment_type else query_set
return list(query_set)
### Helpers for cohort management views
......@@ -297,7 +300,7 @@ def get_cohort_by_name(course_key, name):
def get_cohort_by_id(course_key, cohort_id):
"""
Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
it isn't present. Uses the course_key for extra validation...
it isn't present. Uses the course_key for extra validation.
"""
return CourseUserGroup.objects.get(
course_id=course_key,
......@@ -306,15 +309,13 @@ def get_cohort_by_id(course_key, cohort_id):
)
def add_cohort(course_key, name):
def add_cohort(course_key, name, assignment_type):
"""
Add a cohort to a course. Raises ValueError if a cohort of the same name already
exists.
"""
log.debug("Adding cohort %s to %s", name, course_key)
if CourseUserGroup.objects.filter(course_id=course_key,
group_type=CourseUserGroup.COHORT,
name=name).exists():
if is_cohort_exists(course_key, name):
raise ValueError(_("You cannot create two cohorts with the same name"))
try:
......@@ -322,11 +323,11 @@ def add_cohort(course_key, name):
except Http404:
raise ValueError("Invalid course_key")
cohort = CourseUserGroup.objects.create(
course_id=course.id,
group_type=CourseUserGroup.COHORT,
name=name
)
cohort = CourseCohort.create(
cohort_name=name, course_id=course.id,
assignment_type=assignment_type
).course_user_group
tracker.emit(
"edx.cohort.creation_requested",
{"cohort_name": cohort.name, "cohort_id": cohort.id}
......@@ -334,6 +335,13 @@ def add_cohort(course_key, name):
return cohort
def is_cohort_exists(course_key, name):
"""
Check if a cohort already exists.
"""
return CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.COHORT, name=name).exists()
def add_user_to_cohort(cohort, username_or_email):
"""
Look up the given user, and if successful, add them to the specified cohort.
......@@ -396,3 +404,37 @@ def get_group_info_for_cohort(cohort):
if len(res):
return res[0].group_id, res[0].partition_id
return None, None
def set_assignment_type(user_group, assignment_type):
"""
Set assignment type for cohort.
"""
course_cohort = user_group.cohort
if is_default_cohort(user_group) and course_cohort.assignment_type != assignment_type:
raise ValueError(_("There must be one cohort to which students can be randomly assigned."))
course_cohort.assignment_type = assignment_type
course_cohort.save()
def get_assignment_type(user_group):
"""
Get assignment type for cohort.
"""
course_cohort = user_group.cohort
return course_cohort.assignment_type
def is_default_cohort(user_group):
"""
Check if a cohort is default.
"""
random_cohorts = CourseUserGroup.objects.filter(
course_id=user_group.course_id,
group_type=CourseUserGroup.COHORT,
cohort__assignment_type=CourseCohort.RANDOM
)
return len(random_cohorts) == 1 and random_cohorts[0].name == user_group.name
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseCohort'
db.create_table('course_groups_coursecohort', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_user_group', self.gf('django.db.models.fields.related.OneToOneField')(related_name='cohort', unique=True, to=orm['course_groups.CourseUserGroup'])),
('assignment_type', self.gf('django.db.models.fields.CharField')(default='manual', max_length=20)),
))
db.send_create_signal('course_groups', ['CourseCohort'])
# Adding model 'CourseCohortsSettings'
db.create_table('course_groups_coursecohortssettings', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('is_cohorted', self.gf('django.db.models.fields.BooleanField')(default=False)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=255, db_index=True)),
('cohorted_discussions', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('always_cohort_inline_discussions', self.gf('django.db.models.fields.BooleanField')(default=True)),
))
db.send_create_signal('course_groups', ['CourseCohortsSettings'])
def backwards(self, orm):
# Deleting model 'CourseCohort'
db.delete_table('course_groups_coursecohort')
# Deleting model 'CourseCohortsSettings'
db.delete_table('course_groups_coursecohortssettings')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'course_groups.coursecohort': {
'Meta': {'object_name': 'CourseCohort'},
'assignment_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}),
'course_user_group': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'cohort'", 'unique': 'True', 'to': "orm['course_groups.CourseUserGroup']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'course_groups.coursecohortssettings': {
'Meta': {'object_name': 'CourseCohortsSettings'},
'always_cohort_inline_discussions': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'cohorted_discussions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_cohorted': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'course_groups.courseusergroup': {
'Meta': {'unique_together': "(('name', 'course_id'),)", 'object_name': 'CourseUserGroup'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'group_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'related_name': "'course_groups'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
},
'course_groups.courseusergrouppartitiongroup': {
'Meta': {'object_name': 'CourseUserGroupPartitionGroup'},
'course_user_group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['course_groups.CourseUserGroup']", 'unique': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'group_id': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'partition_id': ('django.db.models.fields.IntegerField', [], {}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
}
}
complete_apps = ['course_groups']
\ No newline at end of file
......@@ -36,9 +36,26 @@ class CourseUserGroup(models.Model):
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
@classmethod
def create(cls, name, course_id, group_type=COHORT):
"""
Create a new course user group.
Args:
name: Name of group
course_id: course id
group_type: group type
"""
return cls.objects.get_or_create(
course_id=course_id,
group_type=group_type,
name=name
)
class CourseUserGroupPartitionGroup(models.Model):
"""
Create User Partition Info.
"""
course_user_group = models.OneToOneField(CourseUserGroup)
partition_id = models.IntegerField(
......@@ -49,3 +66,55 @@ class CourseUserGroupPartitionGroup(models.Model):
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class CourseCohortsSettings(models.Model):
"""
This model represents cohort settings for courses.
"""
is_cohorted = models.BooleanField(default=False)
course_id = CourseKeyField(
unique=True,
max_length=255,
db_index=True,
help_text="Which course are these settings associated with?",
)
cohorted_discussions = models.TextField(null=True, blank=True) # JSON list
# pylint: disable=invalid-name
always_cohort_inline_discussions = models.BooleanField(default=True)
class CourseCohort(models.Model):
"""
This model represents cohort related info.
"""
course_user_group = models.OneToOneField(CourseUserGroup, unique=True, related_name='cohort')
RANDOM = 'random'
MANUAL = 'manual'
ASSIGNMENT_TYPE_CHOICES = ((RANDOM, 'Random'), (MANUAL, 'Manual'),)
assignment_type = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=MANUAL)
@classmethod
def create(cls, cohort_name=None, course_id=None, course_user_group=None, assignment_type=MANUAL):
"""
Create a complete(CourseUserGroup + CourseCohort) object.
Args:
cohort_name: Name of the cohort to be created
course_id: Course Id
course_user_group: CourseUserGroup
assignment_type: 'random' or 'manual'
"""
if course_user_group is None:
course_user_group, __ = CourseUserGroup.create(cohort_name, course_id)
course_cohort, __ = cls.objects.get_or_create(
course_user_group=course_user_group,
defaults={'assignment_type': assignment_type}
)
return course_cohort
"""
Helper methods for testing cohorts.
"""
import factory
from factory import post_generation, Sequence
from factory.django import DjangoModelFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from ..models import CourseUserGroup
from ..models import CourseUserGroup, CourseCohort
class CohortFactory(DjangoModelFactory):
......@@ -29,6 +30,16 @@ class CohortFactory(DjangoModelFactory):
self.users.add(*extracted)
class CourseCohortFactory(DjangoModelFactory):
"""
Factory for constructing mock course cohort.
"""
FACTORY_FOR = CourseCohort
course_user_group = factory.SubFactory(CohortFactory)
assignment_type = 'manual'
def topic_name_to_id(course, name):
"""
Given a discussion topic name, return an id for that name (includes
......
......@@ -76,6 +76,7 @@ class TestCohortPartitionScheme(ModuleStoreTestCase):
first_cohort, second_cohort = [
CohortFactory(course_id=self.course_key) for _ in range(2)
]
# place student 0 into first cohort
add_user_to_cohort(first_cohort, self.student.username)
self.assert_student_in_group(None)
......
......@@ -7,7 +7,8 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods
from util.json_request import expect_json, JsonResponse
from django.contrib.auth.decorators import login_required
import json
from django.utils.translation import ugettext
import logging
import re
......@@ -59,13 +60,14 @@ def _get_cohort_representation(cohort, course):
Returns a JSON representation of a cohort.
"""
group_id, partition_id = cohorts.get_group_info_for_cohort(cohort)
assignment_type = cohorts.get_assignment_type(cohort)
return {
'name': cohort.name,
'id': cohort.id,
'user_count': cohort.users.count(),
'assignment_type': cohorts.CohortAssignmentType.get(cohort, course),
'assignment_type': assignment_type,
'user_partition_id': partition_id,
'group_id': group_id
'group_id': group_id,
}
......@@ -102,25 +104,30 @@ def cohort_handler(request, course_key_string, cohort_id=None):
cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
return JsonResponse(_get_cohort_representation(cohort, course))
else:
name = request.json.get('name')
assignment_type = request.json.get('assignment_type')
if not name:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": "Cohort name must be specified."}, 400)
if not assignment_type:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": "Assignment type must be specified."}, 400)
# If cohort_id is specified, update the existing cohort. Otherwise, create a new cohort.
if cohort_id:
cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
name = request.json.get('name')
if name != cohort.name:
if cohorts.CohortAssignmentType.get(cohort, course) == cohorts.CohortAssignmentType.RANDOM:
return JsonResponse(
# Note: error message not translated because it is not exposed to the user (UI prevents).
{"error": "Renaming of random cohorts is not supported at this time."}, 400
)
if cohorts.is_cohort_exists(course_key, name):
err_msg = ugettext("A cohort with the same name already exists.")
return JsonResponse({"error": unicode(err_msg)}, 400)
cohort.name = name
cohort.save()
try:
cohorts.set_assignment_type(cohort, assignment_type)
except ValueError as err:
return JsonResponse({"error": unicode(err)}, 400)
else:
name = request.json.get('name')
if not name:
# Note: error message not translated because it is not exposed to the user (UI prevents this state).
return JsonResponse({"error": "In order to create a cohort, a name must be specified."}, 400)
try:
cohort = cohorts.add_cohort(course_key, name)
cohort = cohorts.add_cohort(course_key, name, assignment_type)
except ValueError as err:
return JsonResponse({"error": unicode(err)}, 400)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment