Add support for user partitioning based on cohort.
JIRA: TNL-710 IMPORTANT: this commit converts the course_groups package to using migrations. When deploying to an existing openedx instance, migration 0001 may fail with an error indicating that the CourseUserGroup table already exists. If this happens, running the 0001 migration first, with the --fake option, is recommended. After performing this step, remaining migrations should work as expected.
This commit is contained in:
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
LMS: Add support for user partitioning based on cohort. TNL-710
|
||||
|
||||
Platform: Add base support for cohorted group configurations. TNL-649
|
||||
|
||||
Common: Add configurable reset button to units
|
||||
|
||||
@@ -575,7 +575,7 @@ INSTALLED_APPS = (
|
||||
'contentstore',
|
||||
'course_creators',
|
||||
'student', # misleading name due to sharing with lms
|
||||
'course_groups', # not used in cms (yet), but tests run
|
||||
'openedx.core.djangoapps.course_groups', # not used in cms (yet), but tests run
|
||||
|
||||
# Tracking
|
||||
'track',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -394,7 +394,7 @@ CREATE TABLE `auth_permission` (
|
||||
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
|
||||
KEY `auth_permission_e4470c6e` (`content_type_id`),
|
||||
CONSTRAINT `content_type_id_refs_id_728de91f` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=391 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=415 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -666,7 +666,7 @@ CREATE TABLE `course_groups_courseusergroup` (
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
`group_type` varchar(20) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`,`course_id`),
|
||||
UNIQUE KEY `course_groups_courseusergroup_name_63f7511804c52f38_uniq` (`name`,`course_id`),
|
||||
KEY `course_groups_courseusergroup_ff48d8e5` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
@@ -678,11 +678,26 @@ CREATE TABLE `course_groups_courseusergroup_users` (
|
||||
`courseusergroup_id` int(11) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `courseusergroup_id` (`courseusergroup_id`,`user_id`),
|
||||
UNIQUE KEY `course_groups_courseus_courseusergroup_id_46691806058983eb_uniq` (`courseusergroup_id`,`user_id`),
|
||||
KEY `course_groups_courseusergroup_users_caee1c64` (`courseusergroup_id`),
|
||||
KEY `course_groups_courseusergroup_users_fbfc09f1` (`user_id`),
|
||||
CONSTRAINT `courseusergroup_id_refs_id_d26180aa` FOREIGN KEY (`courseusergroup_id`) REFERENCES `course_groups_courseusergroup` (`id`),
|
||||
CONSTRAINT `user_id_refs_id_bf33b47a` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
CONSTRAINT `user_id_refs_id_779390fdbf33b47a` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `courseusergroup_id_refs_id_43167b76d26180aa` FOREIGN KEY (`courseusergroup_id`) REFERENCES `course_groups_courseusergroup` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_groups_courseusergrouppartitiongroup`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `course_groups_courseusergrouppartitiongroup` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`course_user_group_id` int(11) NOT NULL,
|
||||
`partition_id` int(11) NOT NULL,
|
||||
`group_id` int(11) NOT NULL,
|
||||
`created_at` datetime NOT NULL,
|
||||
`updated_at` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `course_user_group_id` (`course_user_group_id`),
|
||||
CONSTRAINT `course_user_group_id_refs_id_48edc507145383c4` FOREIGN KEY (`course_user_group_id`) REFERENCES `course_groups_courseusergroup` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_modes_coursemode`;
|
||||
@@ -887,8 +902,8 @@ CREATE TABLE `django_admin_log` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `django_admin_log_fbfc09f1` (`user_id`),
|
||||
KEY `django_admin_log_e4470c6e` (`content_type_id`),
|
||||
CONSTRAINT `content_type_id_refs_id_288599e6` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`),
|
||||
CONSTRAINT `user_id_refs_id_c8665aa` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
CONSTRAINT `user_id_refs_id_c8665aa` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `content_type_id_refs_id_288599e6` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_comment_client_permission`;
|
||||
@@ -950,7 +965,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `app_label` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=130 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=138 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1609,6 +1624,30 @@ CREATE TABLE `shoppingcart_couponredemption` (
|
||||
CONSTRAINT `user_id_refs_id_c543b145e9b8167` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_courseregcodeitem`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `shoppingcart_courseregcodeitem` (
|
||||
`orderitem_ptr_id` int(11) NOT NULL,
|
||||
`course_id` varchar(128) NOT NULL,
|
||||
`mode` varchar(50) NOT NULL,
|
||||
PRIMARY KEY (`orderitem_ptr_id`),
|
||||
KEY `shoppingcart_courseregcodeitem_ff48d8e5` (`course_id`),
|
||||
KEY `shoppingcart_courseregcodeitem_4160619e` (`mode`),
|
||||
CONSTRAINT `orderitem_ptr_id_refs_id_2ea4d9d5a466f07f` FOREIGN KEY (`orderitem_ptr_id`) REFERENCES `shoppingcart_orderitem` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_courseregcodeitemannotation`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `shoppingcart_courseregcodeitemannotation` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`course_id` varchar(128) NOT NULL,
|
||||
`annotation` longtext,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `course_id` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_courseregistrationcode`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -1632,6 +1671,31 @@ CREATE TABLE `shoppingcart_courseregistrationcode` (
|
||||
CONSTRAINT `invoice_id_refs_id_6e8c54da995f0ae8` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_donation`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `shoppingcart_donation` (
|
||||
`orderitem_ptr_id` int(11) NOT NULL,
|
||||
`donation_type` varchar(32) NOT NULL,
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`orderitem_ptr_id`),
|
||||
KEY `shoppingcart_donation_ff48d8e5` (`course_id`),
|
||||
CONSTRAINT `orderitem_ptr_id_refs_id_28d47c0cb7138a4b` FOREIGN KEY (`orderitem_ptr_id`) REFERENCES `shoppingcart_orderitem` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_donationconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `shoppingcart_donationconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `shoppingcart_donationconfiguration_16905482` (`changed_by_id`),
|
||||
CONSTRAINT `changed_by_id_refs_id_28af5c44b4a26b7f` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_invoice`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -1680,6 +1744,13 @@ CREATE TABLE `shoppingcart_order` (
|
||||
`bill_to_cardtype` varchar(32) NOT NULL,
|
||||
`processor_reply_dump` longtext NOT NULL,
|
||||
`refunded_time` datetime,
|
||||
`company_name` varchar(255),
|
||||
`company_contact_name` varchar(255),
|
||||
`company_contact_email` varchar(255),
|
||||
`recipient_name` varchar(255),
|
||||
`recipient_email` varchar(255),
|
||||
`customer_reference_number` varchar(63),
|
||||
`order_type` varchar(32) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `shoppingcart_order_fbfc09f1` (`user_id`),
|
||||
CONSTRAINT `user_id_refs_id_a4b0342e1195673` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
@@ -1702,6 +1773,8 @@ CREATE TABLE `shoppingcart_orderitem` (
|
||||
`refund_requested_time` datetime,
|
||||
`service_fee` decimal(30,2) NOT NULL,
|
||||
`list_price` decimal(30,2),
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `shoppingcart_orderitem_8337030b` (`order_id`),
|
||||
KEY `shoppingcart_orderitem_fbfc09f1` (`user_id`),
|
||||
@@ -1816,7 +1889,7 @@ CREATE TABLE `south_migrationhistory` (
|
||||
`migration` varchar(255) NOT NULL,
|
||||
`applied` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=180 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=188 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `splash_splashconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1903,6 +1976,20 @@ CREATE TABLE `student_courseenrollmentallowed` (
|
||||
KEY `student_courseenrollmentallowed_3216ff68` (`created`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `student_dashboardconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `student_dashboardconfiguration` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`recent_enrollment_time_delta` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `student_dashboardconfiguration_16905482` (`changed_by_id`),
|
||||
CONSTRAINT `changed_by_id_refs_id_31b94d88eec78c18` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `student_loginfailures`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2083,6 +2170,38 @@ CREATE TABLE `submissions_submission` (
|
||||
CONSTRAINT `student_item_id_refs_id_1df1d83e00b5cccc` FOREIGN KEY (`student_item_id`) REFERENCES `submissions_studentitem` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `survey_surveyanswer`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `survey_surveyanswer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`form_id` int(11) NOT NULL,
|
||||
`field_name` varchar(255) NOT NULL,
|
||||
`field_value` varchar(1024) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `survey_surveyanswer_fbfc09f1` (`user_id`),
|
||||
KEY `survey_surveyanswer_1d0aabf2` (`form_id`),
|
||||
KEY `survey_surveyanswer_7e1499` (`field_name`),
|
||||
CONSTRAINT `form_id_refs_id_5a119e5cf4c79f29` FOREIGN KEY (`form_id`) REFERENCES `survey_surveyform` (`id`),
|
||||
CONSTRAINT `user_id_refs_id_74dcdfa0e0ad4b5e` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `survey_surveyform`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `survey_surveyform` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`form` longtext NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `track_trackinglog`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
|
||||
Binary file not shown.
@@ -6,11 +6,7 @@ from django.http import Http404
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from mock import patch, Mock, ANY, call
|
||||
from nose.tools import assert_true # pylint: disable=no-name-in-module
|
||||
|
||||
from course_groups.models import CourseUserGroup
|
||||
from courseware.courses import UserNotEnrolled
|
||||
from django_comment_client.forum import views
|
||||
from django_comment_client.tests.group_id import (
|
||||
CohortedTopicGroupIdTestMixin,
|
||||
@@ -25,6 +21,15 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from courseware.tests.modulestore_config import TEST_DATA_DIR
|
||||
from courseware.courses import UserNotEnrolled
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
from mock import patch, Mock, ANY, call
|
||||
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
|
||||
TEST_DATA_MONGO_MODULESTORE = mixed_store_config(TEST_DATA_DIR, {}, include_xml=False)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
@@ -11,7 +11,12 @@ import newrelic.agent
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from courseware.courses import get_course_with_access
|
||||
from course_groups.cohorts import is_course_cohorted, get_cohort_id, get_course_cohorts, is_commentable_cohorted
|
||||
from openedx.core.djangoapps.course_groups.cohorts import (
|
||||
is_course_cohorted,
|
||||
get_cohort_id,
|
||||
get_course_cohorts,
|
||||
is_commentable_cohorted
|
||||
)
|
||||
from courseware.access import has_access
|
||||
|
||||
from django_comment_client.permissions import cached_has_permission
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from course_groups.models import CourseUserGroup
|
||||
|
||||
|
||||
class GroupIdAssertionMixin(object):
|
||||
def _data_or_params_cs_request(self, mock_request):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
|
||||
from course_groups.models import CourseUserGroup
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from django_comment_common.models import Role
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
|
||||
@@ -17,8 +17,8 @@ from django_comment_client.permissions import check_permissions_by_view, cached_
|
||||
from edxmako import lookup_template
|
||||
import pystache_custom as pystache
|
||||
|
||||
from course_groups.cohorts import get_cohort_by_id, get_cohort_id, is_commentable_cohorted, is_course_cohorted
|
||||
from course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_id, get_cohort_id, is_commentable_cohorted, is_course_cohorted
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from opaque_keys.edx.locations import i4xEncoder
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -17,8 +17,8 @@ from instructor_analytics.basic import (
|
||||
sale_record_features, sale_order_record_features, enrolled_students_features, course_registration_features,
|
||||
coupon_codes_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
||||
)
|
||||
from course_groups.tests.helpers import CohortFactory
|
||||
from course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from courseware.tests.factories import InstructorFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.contrib.auth.models import User
|
||||
from django.http import Http404
|
||||
from rest_framework import serializers
|
||||
|
||||
from course_groups.cohorts import is_course_cohorted
|
||||
from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted
|
||||
from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from django_comment_common.models import Role, Permission
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
|
||||
@@ -1438,7 +1438,7 @@ INSTALLED_APPS = (
|
||||
'open_ended_grading',
|
||||
'psychometrics',
|
||||
'licenses',
|
||||
'course_groups',
|
||||
'openedx.core.djangoapps.course_groups',
|
||||
'bulk_email',
|
||||
|
||||
# External auth (OpenID, shib)
|
||||
|
||||
12
lms/urls.py
12
lms/urls.py
@@ -355,21 +355,21 @@ if settings.COURSEWARE_ENABLED:
|
||||
|
||||
# Cohorts management
|
||||
url(r'^courses/{}/cohorts$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'course_groups.views.list_cohorts', name="cohorts"),
|
||||
'openedx.core.djangoapps.course_groups.views.list_cohorts', name="cohorts"),
|
||||
url(r'^courses/{}/cohorts/add$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'course_groups.views.add_cohort',
|
||||
'openedx.core.djangoapps.course_groups.views.add_cohort',
|
||||
name="add_cohort"),
|
||||
url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'course_groups.views.users_in_cohort',
|
||||
'openedx.core.djangoapps.course_groups.views.users_in_cohort',
|
||||
name="list_cohort"),
|
||||
url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)/add$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'course_groups.views.add_users_to_cohort',
|
||||
'openedx.core.djangoapps.course_groups.views.add_users_to_cohort',
|
||||
name="add_to_cohort"),
|
||||
url(r'^courses/{}/cohorts/(?P<cohort_id>[0-9]+)/delete$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'course_groups.views.remove_user_from_cohort',
|
||||
'openedx.core.djangoapps.course_groups.views.remove_user_from_cohort',
|
||||
name="remove_from_cohort"),
|
||||
url(r'^courses/{}/cohorts/debug$'.format(settings.COURSE_KEY_PATTERN),
|
||||
'course_groups.views.debug_cohort_mgmt',
|
||||
'openedx.core.djangoapps.course_groups.views.debug_cohort_mgmt',
|
||||
name="debug_cohort_mgmt"),
|
||||
|
||||
# Open Ended Notifications
|
||||
|
||||
@@ -14,7 +14,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
|
||||
from .models import CourseUserGroup, CourseUserGroupPartitionGroup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -373,3 +373,17 @@ def add_user_to_cohort(cohort, username_or_email):
|
||||
)
|
||||
cohort.users.add(user)
|
||||
return (user, previous_cohort_name)
|
||||
|
||||
|
||||
def get_partition_group_id_for_cohort(cohort):
|
||||
"""
|
||||
Get the ids of the partition and group to which this cohort has been linked
|
||||
as a tuple of (int, int).
|
||||
|
||||
If the cohort has not been linked to any partition/group, both values in the
|
||||
tuple will be None.
|
||||
"""
|
||||
res = CourseUserGroupPartitionGroup.objects.filter(course_user_group=cohort)
|
||||
if len(res):
|
||||
return res[0].partition_id, res[0].group_id
|
||||
return None, None
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- 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 'CourseUserGroup'
|
||||
db.create_table('course_groups_courseusergroup', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
('group_type', self.gf('django.db.models.fields.CharField')(max_length=20)),
|
||||
))
|
||||
db.send_create_signal('course_groups', ['CourseUserGroup'])
|
||||
|
||||
# Adding unique constraint on 'CourseUserGroup', fields ['name', 'course_id']
|
||||
db.create_unique('course_groups_courseusergroup', ['name', 'course_id'])
|
||||
|
||||
# Adding M2M table for field users on 'CourseUserGroup'
|
||||
db.create_table('course_groups_courseusergroup_users', (
|
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('courseusergroup', models.ForeignKey(orm['course_groups.courseusergroup'], null=False)),
|
||||
('user', models.ForeignKey(orm['auth.user'], null=False))
|
||||
))
|
||||
db.create_unique('course_groups_courseusergroup_users', ['courseusergroup_id', 'user_id'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'CourseUserGroup', fields ['name', 'course_id']
|
||||
db.delete_unique('course_groups_courseusergroup', ['name', 'course_id'])
|
||||
|
||||
# Deleting model 'CourseUserGroup'
|
||||
db.delete_table('course_groups_courseusergroup')
|
||||
|
||||
# Removing M2M table for field users on 'CourseUserGroup'
|
||||
db.delete_table('course_groups_courseusergroup_users')
|
||||
|
||||
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.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']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['course_groups']
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- 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 'CourseUserGroupPartitionGroup'
|
||||
db.create_table('course_groups_courseusergrouppartitiongroup', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('course_user_group', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['course_groups.CourseUserGroup'], unique=True)),
|
||||
('partition_id', self.gf('django.db.models.fields.IntegerField')()),
|
||||
('group_id', self.gf('django.db.models.fields.IntegerField')()),
|
||||
('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('course_groups', ['CourseUserGroupPartitionGroup'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseUserGroupPartitionGroup'
|
||||
db.delete_table('course_groups_courseusergrouppartitiongroup')
|
||||
|
||||
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.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']
|
||||
@@ -35,3 +35,17 @@ class CourseUserGroup(models.Model):
|
||||
COHORT = 'cohort'
|
||||
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
|
||||
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
|
||||
|
||||
|
||||
class CourseUserGroupPartitionGroup(models.Model):
|
||||
"""
|
||||
"""
|
||||
course_user_group = models.OneToOneField(CourseUserGroup)
|
||||
partition_id = models.IntegerField(
|
||||
help_text="contains the id of a cohorted partition in this course"
|
||||
)
|
||||
group_id = models.IntegerField(
|
||||
help_text="contains the id of a specific group within the cohorted partition"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
75
openedx/core/djangoapps/course_groups/partition_scheme.py
Normal file
75
openedx/core/djangoapps/course_groups/partition_scheme.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Provides a UserPartition driver for cohorts.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from .cohorts import get_cohort, get_partition_group_id_for_cohort
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CohortPartitionScheme(object):
|
||||
"""
|
||||
This scheme uses lms cohorts (CourseUserGroups) and cohort-partition
|
||||
mappings (CourseUserGroupPartitionGroup) to map lms users into Partition
|
||||
Groups.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_group_for_user(cls, course_id, user, user_partition, track_function=None):
|
||||
"""
|
||||
Returns the Group from the specified user partition to which the user
|
||||
is assigned, via their cohort membership and any mappings from cohorts
|
||||
to partitions / groups that might exist.
|
||||
|
||||
If the user has not yet been assigned to a cohort, an assignment *might*
|
||||
be created on-the-fly, as determined by the course's cohort config.
|
||||
Any such side-effects will be triggered inside the call to
|
||||
cohorts.get_cohort().
|
||||
|
||||
If the user has no cohort mapping, or there is no (valid) cohort ->
|
||||
partition group mapping found, the function returns None.
|
||||
"""
|
||||
cohort = get_cohort(user, course_id)
|
||||
if cohort is None:
|
||||
# student doesn't have a cohort
|
||||
return None
|
||||
|
||||
partition_id, group_id = get_partition_group_id_for_cohort(cohort)
|
||||
if partition_id is None:
|
||||
# cohort isn't mapped to any partition group.
|
||||
return None
|
||||
|
||||
if partition_id != user_partition.id:
|
||||
# if we have a match but the partition doesn't match the requested
|
||||
# one it means the mapping is invalid. the previous state of the
|
||||
# partition configuration may have been modified.
|
||||
log.warn(
|
||||
"partition mismatch in CohortPartitionScheme: %r",
|
||||
{
|
||||
"requested_partition_id": user_partition.id,
|
||||
"found_partition_id": partition_id,
|
||||
"found_group_id": group_id,
|
||||
"cohort_id": cohort.id,
|
||||
}
|
||||
)
|
||||
# fail silently
|
||||
return None
|
||||
|
||||
group = user_partition.get_group(group_id)
|
||||
if group is None:
|
||||
# if we have a match but the group doesn't exist in the partition,
|
||||
# it means the mapping is invalid. the previous state of the
|
||||
# partition configuration may have been modified.
|
||||
log.warn(
|
||||
"group not found in CohortPartitionScheme: %r",
|
||||
{
|
||||
"requested_partition_id": user_partition.id,
|
||||
"requested_group_id": group_id,
|
||||
"cohort_id": cohort.id,
|
||||
}
|
||||
)
|
||||
# fail silently
|
||||
return None
|
||||
|
||||
return group
|
||||
@@ -3,11 +3,12 @@ Helper methods for testing cohorts.
|
||||
"""
|
||||
from factory import post_generation, Sequence
|
||||
from factory.django import DjangoModelFactory
|
||||
from course_groups.models import CourseUserGroup
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from ..models import CourseUserGroup
|
||||
|
||||
|
||||
class CohortFactory(DjangoModelFactory):
|
||||
"""
|
||||
@@ -1,24 +1,31 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import IntegrityError
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from mock import call, patch
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from course_groups import cohorts
|
||||
from course_groups.models import CourseUserGroup
|
||||
from course_groups.tests.helpers import topic_name_to_id, config_course_cohorts, CohortFactory
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, mixed_store_config
|
||||
|
||||
from ..models import CourseUserGroup, CourseUserGroupPartitionGroup
|
||||
from .. import cohorts
|
||||
from ..tests.helpers import topic_name_to_id, config_course_cohorts, CohortFactory
|
||||
|
||||
# NOTE: running this with the lms.envs.test config works without
|
||||
# manually overriding the modulestore. However, running with
|
||||
# cms.envs.test doesn't.
|
||||
@patch("course_groups.cohorts.tracker")
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
|
||||
|
||||
|
||||
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
|
||||
class TestCohortSignals(TestCase):
|
||||
def setUp(self):
|
||||
self.course_key = SlashSeparatedCourseKey("dummy", "dummy", "dummy")
|
||||
@@ -446,7 +453,7 @@ class TestCohorts(TestCase):
|
||||
lambda: cohorts.get_cohort_by_id(course.id, cohort.id)
|
||||
)
|
||||
|
||||
@patch("course_groups.cohorts.tracker")
|
||||
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
|
||||
def test_add_cohort(self, mock_tracker):
|
||||
"""
|
||||
Make sure cohorts.add_cohort() properly adds a cohort to a course and handles
|
||||
@@ -469,7 +476,7 @@ class TestCohorts(TestCase):
|
||||
lambda: cohorts.add_cohort(SlashSeparatedCourseKey("course", "does_not", "exist"), "My Cohort")
|
||||
)
|
||||
|
||||
@patch("course_groups.cohorts.tracker")
|
||||
@patch("openedx.core.djangoapps.course_groups.cohorts.tracker")
|
||||
def test_add_user_to_cohort(self, mock_tracker):
|
||||
"""
|
||||
Make sure cohorts.add_user_to_cohort() properly adds a user to a cohort and
|
||||
@@ -525,3 +532,127 @@ class TestCohorts(TestCase):
|
||||
User.DoesNotExist,
|
||||
lambda: cohorts.add_user_to_cohort(first_cohort, "non_existent_username")
|
||||
)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestCohortsAndPartitionGroups(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Regenerate a test course and cohorts for each test
|
||||
"""
|
||||
self.test_course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.course = modulestore().get_course(self.test_course_key)
|
||||
|
||||
self.first_cohort = CohortFactory(course_id=self.course.id, name="FirstCohort")
|
||||
self.second_cohort = CohortFactory(course_id=self.course.id, name="SecondCohort")
|
||||
|
||||
self.partition_id = 1
|
||||
self.group1_id = 10
|
||||
self.group2_id = 20
|
||||
|
||||
def _link_cohort_partition_group(self, cohort, partition_id, group_id):
|
||||
"""
|
||||
Utility to create cohort -> partition group assignments in the database.
|
||||
"""
|
||||
link = CourseUserGroupPartitionGroup(
|
||||
course_user_group=cohort,
|
||||
partition_id=partition_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
link.save()
|
||||
return link
|
||||
|
||||
def test_get_partition_group_id_for_cohort(self):
|
||||
"""
|
||||
Basic test of the partition_group_id accessor function
|
||||
"""
|
||||
# api should return nothing for an unmapped cohort
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
(None, None),
|
||||
)
|
||||
# create a link for the cohort in the db
|
||||
link = self._link_cohort_partition_group(
|
||||
self.first_cohort,
|
||||
self.partition_id,
|
||||
self.group1_id
|
||||
)
|
||||
# api should return the specified partition and group
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
(self.partition_id, self.group1_id)
|
||||
)
|
||||
# delete the link in the db
|
||||
link.delete()
|
||||
# api should return nothing again
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
(None, None),
|
||||
)
|
||||
|
||||
def test_multiple_cohorts(self):
|
||||
"""
|
||||
Test that multiple cohorts can be linked to the same partition group
|
||||
"""
|
||||
self._link_cohort_partition_group(
|
||||
self.first_cohort,
|
||||
self.partition_id,
|
||||
self.group1_id,
|
||||
)
|
||||
self._link_cohort_partition_group(
|
||||
self.second_cohort,
|
||||
self.partition_id,
|
||||
self.group1_id,
|
||||
)
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
(self.partition_id, self.group1_id),
|
||||
)
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.second_cohort),
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
)
|
||||
|
||||
def test_multiple_partition_groups(self):
|
||||
"""
|
||||
Test that a cohort cannot be mapped to more than one partition group
|
||||
"""
|
||||
self._link_cohort_partition_group(
|
||||
self.first_cohort,
|
||||
self.partition_id,
|
||||
self.group1_id,
|
||||
)
|
||||
with self.assertRaisesRegexp(IntegrityError, 'not unique'):
|
||||
self._link_cohort_partition_group(
|
||||
self.first_cohort,
|
||||
self.partition_id,
|
||||
self.group2_id,
|
||||
)
|
||||
|
||||
def test_delete_cascade(self):
|
||||
"""
|
||||
Test that cohort -> partition group links are automatically deleted
|
||||
when their parent cohort is deleted.
|
||||
"""
|
||||
self._link_cohort_partition_group(
|
||||
self.first_cohort,
|
||||
self.partition_id,
|
||||
self.group1_id
|
||||
)
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
(self.partition_id, self.group1_id)
|
||||
)
|
||||
# delete the link
|
||||
self.first_cohort.delete()
|
||||
# api should return nothing at that point
|
||||
self.assertEqual(
|
||||
cohorts.get_partition_group_id_for_cohort(self.first_cohort),
|
||||
(None, None),
|
||||
)
|
||||
# link should no longer exist because of delete cascade
|
||||
with self.assertRaises(CourseUserGroupPartitionGroup.DoesNotExist):
|
||||
CourseUserGroupPartitionGroup.objects.get(
|
||||
course_user_group_id=self.first_cohort.id
|
||||
)
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Test the partitions and partitions service
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
import django.test
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition, UserPartitionError
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
from xmodule.modulestore.tests.django_utils import mixed_store_config
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from ..partition_scheme import CohortPartitionScheme
|
||||
from ..models import CourseUserGroupPartitionGroup
|
||||
from ..cohorts import add_user_to_cohort
|
||||
from .helpers import CohortFactory, config_course_cohorts
|
||||
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestCohortPartitionScheme(django.test.TestCase):
|
||||
"""
|
||||
Test the logic for linking a user to a partition group based on their cohort.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Regenerate a course with cohort configuration, partition and groups,
|
||||
and a student for each test.
|
||||
"""
|
||||
self.course_key = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
config_course_cohorts(modulestore().get_course(self.course_key), [], cohorted=True)
|
||||
|
||||
self.groups = [Group(10, 'Group 10'), Group(20, 'Group 20')]
|
||||
self.user_partition = UserPartition(
|
||||
0,
|
||||
'Test Partition',
|
||||
'for testing purposes',
|
||||
self.groups,
|
||||
scheme=CohortPartitionScheme
|
||||
)
|
||||
self.student = UserFactory.create()
|
||||
|
||||
def link_cohort_partition_group(self, cohort, partition, group):
|
||||
"""
|
||||
Utility for creating cohort -> partition group links
|
||||
"""
|
||||
CourseUserGroupPartitionGroup(
|
||||
course_user_group=cohort,
|
||||
partition_id=partition.id,
|
||||
group_id=group.id,
|
||||
).save()
|
||||
|
||||
def unlink_cohort_partition_group(self, cohort):
|
||||
"""
|
||||
Utility for removing cohort -> partition group links
|
||||
"""
|
||||
CourseUserGroupPartitionGroup.objects.filter(course_user_group=cohort).delete()
|
||||
|
||||
def assert_student_in_group(self, group, partition=None):
|
||||
"""
|
||||
Utility for checking that our test student comes up as assigned to the
|
||||
specified partition (or, if None, no partition at all)
|
||||
"""
|
||||
self.assertEqual(
|
||||
CohortPartitionScheme.get_group_for_user(
|
||||
self.course_key,
|
||||
self.student,
|
||||
partition or self.user_partition,
|
||||
),
|
||||
group
|
||||
)
|
||||
|
||||
def test_student_cohort_assignment(self):
|
||||
"""
|
||||
Test that the CohortPartitionScheme continues to return the correct
|
||||
group for a student as the student is moved in and out of different
|
||||
cohorts.
|
||||
"""
|
||||
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)
|
||||
|
||||
# link first cohort to group 0 in the partition
|
||||
self.link_cohort_partition_group(
|
||||
first_cohort,
|
||||
self.user_partition,
|
||||
self.groups[0],
|
||||
)
|
||||
# link second cohort to to group 1 in the partition
|
||||
self.link_cohort_partition_group(
|
||||
second_cohort,
|
||||
self.user_partition,
|
||||
self.groups[1],
|
||||
)
|
||||
self.assert_student_in_group(self.groups[0])
|
||||
|
||||
# move student from first cohort to second cohort
|
||||
add_user_to_cohort(second_cohort, self.student.username)
|
||||
self.assert_student_in_group(self.groups[1])
|
||||
|
||||
# move the student out of the cohort
|
||||
second_cohort.users.remove(self.student)
|
||||
self.assert_student_in_group(None)
|
||||
|
||||
def test_cohort_partition_group_assignment(self):
|
||||
"""
|
||||
Test that the CohortPartitionScheme returns the correct group for a
|
||||
student in a cohort when the cohort link is created / moved / deleted.
|
||||
"""
|
||||
test_cohort = CohortFactory(course_id=self.course_key)
|
||||
|
||||
# assign user to cohort (but cohort isn't linked to a partition group yet)
|
||||
add_user_to_cohort(test_cohort, self.student.username)
|
||||
# scheme should not yet find any link
|
||||
self.assert_student_in_group(None)
|
||||
|
||||
# link cohort to group 0
|
||||
self.link_cohort_partition_group(
|
||||
test_cohort,
|
||||
self.user_partition,
|
||||
self.groups[0],
|
||||
)
|
||||
# now the scheme should find a link
|
||||
self.assert_student_in_group(self.groups[0])
|
||||
|
||||
# link cohort to group 1 (first unlink it from group 0)
|
||||
self.unlink_cohort_partition_group(test_cohort)
|
||||
self.link_cohort_partition_group(
|
||||
test_cohort,
|
||||
self.user_partition,
|
||||
self.groups[1],
|
||||
)
|
||||
# scheme should pick up the link
|
||||
self.assert_student_in_group(self.groups[1])
|
||||
|
||||
# unlink cohort from anywhere
|
||||
self.unlink_cohort_partition_group(
|
||||
test_cohort,
|
||||
)
|
||||
# scheme should now return nothing
|
||||
self.assert_student_in_group(None)
|
||||
|
||||
def setup_student_in_group_0(self):
|
||||
"""
|
||||
Utility to set up a cohort, add our student to the cohort, and link
|
||||
the cohort to self.groups[0]
|
||||
"""
|
||||
test_cohort = CohortFactory(course_id=self.course_key)
|
||||
|
||||
# link cohort to group 0
|
||||
self.link_cohort_partition_group(
|
||||
test_cohort,
|
||||
self.user_partition,
|
||||
self.groups[0],
|
||||
)
|
||||
# place student into cohort
|
||||
add_user_to_cohort(test_cohort, self.student.username)
|
||||
# check link is correct
|
||||
self.assert_student_in_group(self.groups[0])
|
||||
|
||||
def test_partition_changes_nondestructive(self):
|
||||
"""
|
||||
If the name of a user partition is changed, or a group is added to the
|
||||
partition, links from cohorts do not break.
|
||||
|
||||
If the name of a group is changed, links from cohorts do not break.
|
||||
"""
|
||||
self.setup_student_in_group_0()
|
||||
|
||||
# to simulate a non-destructive configuration change on the course, create
|
||||
# a new partition with the same id and scheme but with groups renamed and
|
||||
# a group added
|
||||
new_groups = [Group(10, 'New Group 10'), Group(20, 'New Group 20'), Group(30, 'New Group 30')]
|
||||
new_user_partition = UserPartition(
|
||||
0, # same id
|
||||
'Different Partition',
|
||||
'dummy',
|
||||
new_groups,
|
||||
scheme=CohortPartitionScheme,
|
||||
)
|
||||
# the link should still work
|
||||
self.assert_student_in_group(new_groups[0], new_user_partition)
|
||||
|
||||
def test_missing_group(self):
|
||||
"""
|
||||
If the group is deleted (or its id is changed), there's no referential
|
||||
integrity enforced, so any references from cohorts to that group will be
|
||||
lost. A warning should be logged when links are found from cohorts to
|
||||
groups that no longer exist.
|
||||
"""
|
||||
self.setup_student_in_group_0()
|
||||
|
||||
# to simulate a destructive change on the course, create a new partition
|
||||
# with the same id, but different group ids.
|
||||
new_user_partition = UserPartition(
|
||||
0, # same id
|
||||
'Another Partition',
|
||||
'dummy',
|
||||
[Group(11, 'Not Group 10'), Group(21, 'Not Group 20')], # different ids
|
||||
scheme=CohortPartitionScheme,
|
||||
)
|
||||
# the partition will be found since it has the same id, but the group
|
||||
# ids aren't present anymore, so the scheme returns None (and logs a
|
||||
# warning)
|
||||
with patch('openedx.core.djangoapps.course_groups.partition_scheme.log') as mock_log:
|
||||
self.assert_student_in_group(None, new_user_partition)
|
||||
self.assertTrue(mock_log.warn.called)
|
||||
self.assertRegexpMatches(mock_log.warn.call_args[0][0], 'group not found')
|
||||
|
||||
def test_missing_partition(self):
|
||||
"""
|
||||
If the user partition is deleted (or its id is changed), there's no
|
||||
referential integrity enforced, so any references from cohorts to that
|
||||
partition's groups will be lost. A warning should be logged when links
|
||||
are found from cohorts to partitions that do not exist.
|
||||
"""
|
||||
self.setup_student_in_group_0()
|
||||
|
||||
# to simulate another destructive change on the course, create a new
|
||||
# partition with a different id, but using the same groups.
|
||||
new_user_partition = UserPartition(
|
||||
1, # different id
|
||||
'Moved Partition',
|
||||
'dummy',
|
||||
[Group(10, 'Group 10'), Group(20, 'Group 20')], # same ids
|
||||
scheme=CohortPartitionScheme,
|
||||
)
|
||||
# the partition will not be found even though the group ids match, so the
|
||||
# scheme returns None (and logs a warning).
|
||||
with patch('openedx.core.djangoapps.course_groups.partition_scheme.log') as mock_log:
|
||||
self.assert_student_in_group(None, new_user_partition)
|
||||
self.assertTrue(mock_log.warn.called)
|
||||
self.assertRegexpMatches(mock_log.warn.call_args[0][0], 'partition mismatch')
|
||||
|
||||
|
||||
class TestExtension(django.test.TestCase):
|
||||
"""
|
||||
Ensure that the scheme extension is correctly plugged in (via entry point
|
||||
in setup.py)
|
||||
"""
|
||||
|
||||
def test_get_scheme(self):
|
||||
self.assertEqual(UserPartition.get_scheme('cohort'), CohortPartitionScheme)
|
||||
with self.assertRaisesRegexp(UserPartitionError, 'Unrecognized scheme'):
|
||||
UserPartition.get_scheme('other')
|
||||
@@ -4,25 +4,23 @@ Tests for course group views
|
||||
from collections import namedtuple
|
||||
import json
|
||||
|
||||
from collections import namedtuple
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import Http404
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from course_groups.cohorts import (
|
||||
get_cohort, CohortAssignmentType, get_cohort_by_name, DEFAULT_COHORT_NAME
|
||||
)
|
||||
from course_groups.models import CourseUserGroup
|
||||
from course_groups.tests.helpers import config_course_cohorts, CohortFactory
|
||||
from course_groups.views import (
|
||||
list_cohorts, add_cohort, users_in_cohort, add_users_to_cohort, remove_user_from_cohort
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from ..models import CourseUserGroup
|
||||
from ..views import list_cohorts, add_cohort, users_in_cohort, add_users_to_cohort, remove_user_from_cohort
|
||||
from ..cohorts import get_cohort, CohortAssignmentType, get_cohort_by_name, DEFAULT_COHORT_NAME
|
||||
from .helpers import config_course_cohorts, CohortFactory
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
@@ -7,7 +7,7 @@ Stores global metadata using the UserPreference model, and per-course metadata u
|
||||
UserCourseTag model.
|
||||
"""
|
||||
|
||||
from user_api.models import UserCourseTag
|
||||
from ..models import UserCourseTag
|
||||
|
||||
# Scopes
|
||||
# (currently only allows per-course tags. Can be expanded to support
|
||||
|
||||
@@ -3,6 +3,7 @@ Test the user api's partition extensions.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from mock import patch
|
||||
from unittest import TestCase
|
||||
|
||||
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme, UserPartitionError
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -105,3 +106,15 @@ class TestRandomUserPartitionScheme(PartitionTestCase):
|
||||
# Now, get a new group using the same call
|
||||
new_group = RandomUserPartitionScheme.get_group_for_user(self.MOCK_COURSE_ID, self.user, user_partition)
|
||||
self.assertEqual(old_group.id, new_group.id)
|
||||
|
||||
|
||||
class TestExtension(TestCase):
|
||||
"""
|
||||
Ensure that the scheme extension is correctly plugged in (via entry point
|
||||
in setup.py)
|
||||
"""
|
||||
|
||||
def test_get_scheme(self):
|
||||
self.assertEqual(UserPartition.get_scheme('random'), RandomUserPartitionScheme)
|
||||
with self.assertRaisesRegexp(UserPartitionError, 'Unrecognized scheme'):
|
||||
UserPartition.get_scheme('other')
|
||||
|
||||
@@ -888,7 +888,7 @@ class RegistrationViewTest(ApiTestCase):
|
||||
no_extra_fields_setting = {}
|
||||
|
||||
with simulate_running_pipeline(
|
||||
"user_api.views.third_party_auth.pipeline",
|
||||
"openedx.core.djangoapps.user_api.views.third_party_auth.pipeline",
|
||||
"google-oauth2", email="bob@example.com",
|
||||
fullname="Bob", username="Bob123"
|
||||
):
|
||||
|
||||
@@ -27,7 +27,7 @@ from edxmako.shortcuts import marketing_link
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from .api import account as account_api, profile as profile_api
|
||||
from .helpers import FormDescription, shim_student_view, require_post_params
|
||||
from .models import UserPreference
|
||||
from .models import UserPreference, UserProfile
|
||||
from .serializers import UserSerializer, UserPreferenceSerializer
|
||||
|
||||
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,12 +13,14 @@ setup(
|
||||
# be reorganized to be a more conventional Python tree.
|
||||
packages=[
|
||||
"openedx.core.djangoapps.user_api",
|
||||
"openedx.core.djangoapps.course_groups",
|
||||
"lms",
|
||||
"cms",
|
||||
],
|
||||
entry_points={
|
||||
'openedx.user_partition_scheme': [
|
||||
'random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme',
|
||||
'cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme',
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user