Commit 31a5bc25 by Xavier Antoviaque

Merge pull request #258 from aboudreault/cohorts-rebase

Cohorts rebase
parents 6abc5a05 f75f5534
...@@ -291,6 +291,36 @@ def is_user_in_cohort(cohort, user_id, group_type=CourseUserGroup.COHORT): ...@@ -291,6 +291,36 @@ def is_user_in_cohort(cohort, user_id, group_type=CourseUserGroup.COHORT):
group_type=group_type).exists() group_type=group_type).exists()
def remove_user_from_cohort(cohort, username_or_email):
"""
Look up the given user, and if successful, remove them to the specified cohort.
Arguments:
cohort: CourseUserGroup
username_or_email: string. Treated as email if has '@'
Returns:
User object that has been removed from the cohort
Raises:
User.DoesNotExist if can't find user.
ValueError if user is not in this cohort.
"""
user = get_user_by_username_or_email(username_or_email)
course_cohorts = CourseUserGroup.objects.filter(
course_id=cohort.course_id,
users__id=user.id,
group_type=CourseUserGroup.COHORT,
)
if course_cohorts.exists():
cohort.users.remove(user)
return user
def get_course_cohort_names(course_key): def get_course_cohort_names(course_key):
""" """
Return a list of the cohort names in a course. Return a list of the cohort names in a course.
......
from django.core.management.base import BaseCommand, CommandError
from django.core.exceptions import MultipleObjectsReturned
from optparse import make_option
from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
from course_groups.models import CourseUserGroup
from course_groups.cohorts import (
get_cohort,
get_cohort_by_name,
add_cohort,
add_user_to_cohort,
remove_user_from_cohort
)
from student.models import CourseEnrollment
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--fix',
action='store_true',
dest='fix',
default=False,
help='Apply possible fixes automatically'),
)
help = 'Revert the multiple cohorts feature.'
def handle(self, *args, **options):
self.stdout.write('### Checking CourseUserGroup group types\n')
error = False
for course_group in CourseUserGroup.objects.all():
if course_group.group_type != CourseUserGroup.COHORT:
if options['fix']:
self.stdout.write(
'Fixed: CourseUserGroup with an invalid group_type found: {} (type: {})\n'.format(
course_group.name, course_group.group_type)
)
course_group.group_type = CourseUserGroup.COHORT
course_group.save()
else:
error = True
self.stdout.write(
'CourseUserGroup with an invalid group_type found: {} (type: {})\n'.format(
course_group.name, course_group.group_type)
)
if not error:
self.stdout.write('Ok.\n')
self.stdout.write('\n### Checking user cohorts\n')
error = False
users = User.objects.all()
_courses = modulestore().get_courses()
courses = [c for c in _courses if isinstance(c, CourseDescriptor)]
# for each course, check if users are in atleast and only 1 cohort
for course in courses:
for user in users:
if not CourseEnrollment.is_enrolled(user, course.id):
continue
try:
CourseUserGroup.objects.get(course_id=course.id,
users__id=user.id)
except CourseUserGroup.DoesNotExist:
if options['fix']:
# create a "default_cohort" is it doesn't already exist
try:
default_cohort = get_cohort_by_name(course.id, CourseUserGroup.default_cohort_name)
except CourseUserGroup.DoesNotExist:
default_cohort = add_cohort(course.id, CourseUserGroup.default_cohort_name)
self.stdout.write('Default cohort "{}" created for course "{}"'.format(
default_cohort.name, course.display_name)
)
add_user_to_cohort(default_cohort, user.username)
self.stdout.write(
'Fixed: User "{}" is not in a cohort in course "{}". Added in "{}" cohort\n'.format(
user.username, course.display_name, default_cohort.name)
)
else:
error = True
self.stdout.write(
'User "{}" is not in a cohort in course "{}".\n'.format(
user.username, course.display_name)
)
except MultipleObjectsReturned:
self.stdout.write(
'User "{}" is in multiple cohorts in course "{}".\n'.format(
user.username, course.display_name)
)
if options['fix']:
user_cohorts = CourseUserGroup.objects.filter(course_id=course.id,
users__id=user.id).all()
user_cohort = user_cohorts[0]
for cohort in user_cohorts[1:]:
remove_user_from_cohort(cohort, user.username)
self.stdout.write("User '{}' has been removed from cohort '{}' in course '{}'.\n".format(
user.username, cohort.name, course.display_name)
)
self.stdout.write("User '{}' is now only in cohort '{}' in course '{}'.\n".format(
user.username, cohort.name, course.display_name)
)
else:
error = True
if not error:
self.stdout.write('Ok.\n')
self.stdout.write('\nTo fix issues, run the script with the "--fix" option.\n')
...@@ -13,6 +13,8 @@ class CourseUserGroup(models.Model): ...@@ -13,6 +13,8 @@ class CourseUserGroup(models.Model):
which may be treated specially. For example, a user can be in at most one cohort per which may be treated specially. For example, a user can be in at most one cohort per
course, and cohorts are used to split up the forums by group. course, and cohorts are used to split up the forums by group.
""" """
default_cohort_name = "default_cohort"
class Meta: class Meta:
unique_together = (('name', 'course_id'), ) unique_together = (('name', 'course_id'), )
......
...@@ -228,7 +228,7 @@ def remove_user_from_cohort(request, course_key_string, cohort_id): ...@@ -228,7 +228,7 @@ def remove_user_from_cohort(request, course_key_string, cohort_id):
cohort = cohorts.get_cohort_by_id(course_key, cohort_id) cohort = cohorts.get_cohort_by_id(course_key, cohort_id)
try: try:
user = User.objects.get(username=username) user = User.objects.get(username=username)
cohort.users.remove(user) cohorts.remove_user_from_cohort(cohort, user.username)
return json_http_response({'success': True}) return json_http_response({'success': True})
except User.DoesNotExist: except User.DoesNotExist:
log.debug('no user') log.debug('no user')
......
...@@ -18,6 +18,12 @@ from rest_framework.response import Response ...@@ -18,6 +18,12 @@ from rest_framework.response import Response
from courseware import grades, module_render from courseware import grades, module_render
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.views import get_module_for_descriptor, save_child_position, get_current_child from courseware.views import get_module_for_descriptor, save_child_position, get_current_child
from course_groups.models import CourseUserGroup
from course_groups.cohorts import (
get_cohort_by_name,
add_cohort,
add_user_to_cohort
)
from django_comment_common.models import Role, FORUM_ROLE_MODERATOR from django_comment_common.models import Role, FORUM_ROLE_MODERATOR
from gradebook.models import StudentGradebook from gradebook.models import StudentGradebook
from instructor.access import revoke_access, update_forum_role from instructor.access import revoke_access, update_forum_role
...@@ -716,6 +722,15 @@ class UsersCoursesList(SecureAPIView): ...@@ -716,6 +722,15 @@ class UsersCoursesList(SecureAPIView):
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
base_uri = generate_base_uri(request) base_uri = generate_base_uri(request)
course_enrollment = CourseEnrollment.enroll(user, course_key) course_enrollment = CourseEnrollment.enroll(user, course_key)
# Ensure the user is in a cohort. Add it explicitly in the default_cohort
try:
default_cohort = get_cohort_by_name(course_id, CourseUserGroup.default_cohort_name)
except CourseUserGroup.DoesNotExist:
default_cohort = add_cohort(course_id, CourseUserGroup.default_cohort_name)
add_user_to_cohort(default_cohort, user.username)
log.debug('User "{}" has been automatically added in cohort "{}" for course "{}"'.format(
user.username, default_cohort.name, course_descriptor.display_name)
)
response_data['uri'] = '{}/{}'.format(base_uri, course_id) response_data['uri'] = '{}/{}'.format(base_uri, course_id)
response_data['id'] = unicode(course_key) response_data['id'] = unicode(course_key)
response_data['name'] = course_descriptor.display_name response_data['name'] = course_descriptor.display_name
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from django.core.exceptions import ValidationError
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -34,7 +35,8 @@ class Workgroup(TimeStampedModel): ...@@ -34,7 +35,8 @@ class Workgroup(TimeStampedModel):
""" """
name = models.CharField(max_length=255, null=True, blank=True) name = models.CharField(max_length=255, null=True, blank=True)
project = models.ForeignKey(Project, related_name="workgroups") project = models.ForeignKey(Project, related_name="workgroups")
users = models.ManyToManyField(User, related_name="workgroups", blank=True, null=True) users = models.ManyToManyField(User, related_name="workgroups",
through="WorkgroupUser", blank=True, null=True)
groups = models.ManyToManyField(Group, related_name="workgroups", blank=True, null=True) groups = models.ManyToManyField(Group, related_name="workgroups", blank=True, null=True)
@property @property
...@@ -53,6 +55,35 @@ class Workgroup(TimeStampedModel): ...@@ -53,6 +55,35 @@ class Workgroup(TimeStampedModel):
workgroup_name workgroup_name
) )
def add_user(self, user):
workgroup_user = WorkgroupUser(workgroup=self, user=user)
workgroup_user.save()
def remove_user(self, user):
workgroup_user = WorkgroupUser.objects.get(workgroup=self, user=user)
workgroup_user.delete()
class WorkgroupUser(models.Model):
"""A Folder to store some data between a client and its insurance"""
workgroup = models.ForeignKey(Workgroup, null=False)
user = models.ForeignKey(User, null=False)
class Meta:
db_table = 'projects_workgroup_users'
def clean(self):
# Ensure the user is not already assigned to a workgroup for this course
existing_workgroups = Workgroup.objects.filter(users=self.user).filter(project__course_id=self.workgroup.project.course_id)
if len(existing_workgroups):
raise ValidationError('User {} is already assigned to a workgroup for this course'.format(self.user.username))
def save(self, **kwargs):
self.clean()
return super(WorkgroupUser, self).save(**kwargs)
class WorkgroupReview(TimeStampedModel): class WorkgroupReview(TimeStampedModel):
""" """
Model representing the Workgroup Review concept. A Workgroup Review is Model representing the Workgroup Review concept. A Workgroup Review is
......
...@@ -84,7 +84,7 @@ class PeerReviewsApiTests(TestCase): ...@@ -84,7 +84,7 @@ class PeerReviewsApiTests(TestCase):
name="Test Workgroup", name="Test Workgroup",
project=self.test_project, project=self.test_project,
) )
self.test_workgroup.users.add(self.test_peer_user) self.test_workgroup.add_user(self.test_peer_user)
self.test_workgroup.save() self.test_workgroup.save()
self.client = SecureClient() self.client = SecureClient()
......
...@@ -58,7 +58,7 @@ class ProjectsApiTests(TestCase): ...@@ -58,7 +58,7 @@ class ProjectsApiTests(TestCase):
name="Test Workgroup", name="Test Workgroup",
project=self.test_project, project=self.test_project,
) )
self.test_workgroup.users.add(self.test_user) self.test_workgroup.add_user(self.test_user)
self.test_workgroup.save() self.test_workgroup.save()
self.client = SecureClient() self.client = SecureClient()
......
...@@ -77,7 +77,7 @@ class SubmissionReviewsApiTests(TestCase): ...@@ -77,7 +77,7 @@ class SubmissionReviewsApiTests(TestCase):
name="Test Workgroup", name="Test Workgroup",
project=self.test_project, project=self.test_project,
) )
self.test_workgroup.users.add(self.test_user) self.test_workgroup.add_user(self.test_user)
self.test_workgroup.save() self.test_workgroup.save()
self.test_submission = WorkgroupSubmission.objects.create( self.test_submission = WorkgroupSubmission.objects.create(
......
...@@ -77,7 +77,7 @@ class WorkgroupReviewsApiTests(TestCase): ...@@ -77,7 +77,7 @@ class WorkgroupReviewsApiTests(TestCase):
name="Test Workgroup", name="Test Workgroup",
project=self.test_project, project=self.test_project,
) )
self.test_workgroup.users.add(self.test_user) self.test_workgroup.add_user(self.test_user)
self.test_workgroup.save() self.test_workgroup.save()
self.test_submission = WorkgroupSubmission.objects.create( self.test_submission = WorkgroupSubmission.objects.create(
......
...@@ -64,7 +64,7 @@ class SubmissionsApiTests(TestCase): ...@@ -64,7 +64,7 @@ class SubmissionsApiTests(TestCase):
name="Test Workgroup", name="Test Workgroup",
project=self.test_project, project=self.test_project,
) )
self.test_workgroup.users.add(self.test_user) self.test_workgroup.add_user(self.test_user)
self.test_workgroup.save() self.test_workgroup.save()
self.client = SecureClient() self.client = SecureClient()
......
...@@ -182,7 +182,7 @@ class WorkgroupsApiTests(TestCase): ...@@ -182,7 +182,7 @@ class WorkgroupsApiTests(TestCase):
response.data['id'], response.data['id'],
self.test_workgroup_name self.test_workgroup_name
) )
cohort = get_cohort_by_name(self.test_course.id, cohort_name, CourseUserGroup.WORKGROUP) cohort = get_cohort_by_name(self.test_course.id, cohort_name)
self.assertIsNotNone(cohort) self.assertIsNotNone(cohort)
def test_workgroups_detail_get(self): def test_workgroups_detail_get(self):
...@@ -276,9 +276,9 @@ class WorkgroupsApiTests(TestCase): ...@@ -276,9 +276,9 @@ class WorkgroupsApiTests(TestCase):
response.data['id'], response.data['id'],
self.test_workgroup_name self.test_workgroup_name
) )
cohort = get_cohort_by_name(self.test_course.id, cohort_name, CourseUserGroup.WORKGROUP) cohort = get_cohort_by_name(self.test_course.id, cohort_name)
self.assertIsNotNone(cohort) self.assertIsNotNone(cohort)
self.assertTrue(is_user_in_cohort(cohort, self.test_user.id, CourseUserGroup.WORKGROUP)) self.assertTrue(is_user_in_cohort(cohort, self.test_user.id))
def test_workgroups_users_post_preexisting_workgroup(self): def test_workgroups_users_post_preexisting_workgroup(self):
...@@ -360,15 +360,15 @@ class WorkgroupsApiTests(TestCase): ...@@ -360,15 +360,15 @@ class WorkgroupsApiTests(TestCase):
) )
# now let's remove existing cohort users # now let's remove existing cohort users
cohort = get_cohort_by_name(self.test_course.id, cohort_name, CourseUserGroup.WORKGROUP) cohort = get_cohort_by_name(self.test_course.id, cohort_name)
self.assertTrue(is_user_in_cohort(cohort, self.test_user.id, CourseUserGroup.WORKGROUP)) self.assertTrue(is_user_in_cohort(cohort, self.test_user.id))
remove_user_from_cohort(cohort, self.test_user.username, CourseUserGroup.WORKGROUP) remove_user_from_cohort(cohort, self.test_user.username)
self.assertFalse(is_user_in_cohort(cohort, self.test_user.id, CourseUserGroup.WORKGROUP)) self.assertFalse(is_user_in_cohort(cohort, self.test_user.id))
# delete cohort # delete cohort
delete_empty_cohort(self.test_course.id, cohort_name, CourseUserGroup.WORKGROUP) delete_empty_cohort(self.test_course.id, cohort_name)
self.assertEqual(0, len(get_course_cohort_names(self.test_course.id, CourseUserGroup.WORKGROUP))) self.assertEqual(0, len(get_course_cohort_names(self.test_course.id)))
# add a 2nd user and make sure a discussion cohort was created and users were backfilled # add a 2nd user and make sure a discussion cohort was created and users were backfilled
test_uri = '{}{}/'.format(self.test_workgroups_uri, str(response.data['id'])) test_uri = '{}{}/'.format(self.test_workgroups_uri, str(response.data['id']))
...@@ -378,10 +378,10 @@ class WorkgroupsApiTests(TestCase): ...@@ -378,10 +378,10 @@ class WorkgroupsApiTests(TestCase):
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
# now inspect cohort and assert that things are as we anticipate (i.e. both users are in there) # now inspect cohort and assert that things are as we anticipate (i.e. both users are in there)
cohort = get_cohort_by_name(self.test_course.id, cohort_name, CourseUserGroup.WORKGROUP) cohort = get_cohort_by_name(self.test_course.id, cohort_name)
self.assertIsNotNone(cohort) self.assertIsNotNone(cohort)
self.assertTrue(is_user_in_cohort(cohort, self.test_user.id, CourseUserGroup.WORKGROUP)) self.assertTrue(is_user_in_cohort(cohort, self.test_user.id))
self.assertTrue(is_user_in_cohort(cohort, self.test_user2.id, CourseUserGroup.WORKGROUP)) self.assertTrue(is_user_in_cohort(cohort, self.test_user2.id))
def test_workgroups_users_delete(self): def test_workgroups_users_delete(self):
data = { data = {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
""" WORKGROUPS API VIEWS """ """ WORKGROUPS API VIEWS """
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist, ValidationError
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action, link from rest_framework.decorators import action, link
...@@ -22,6 +22,8 @@ from opaque_keys.edx.keys import CourseKey, UsageKey ...@@ -22,6 +22,8 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore import Location, InvalidLocationError from xmodule.modulestore import Location, InvalidLocationError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from course_groups.cohorts import (add_cohort, add_user_to_cohort, get_cohort_by_name,
remove_user_from_cohort)
from .models import Project, Workgroup, WorkgroupSubmission from .models import Project, Workgroup, WorkgroupSubmission
from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview from .models import WorkgroupReview, WorkgroupSubmissionReview, WorkgroupPeerReview
...@@ -111,6 +113,20 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -111,6 +113,20 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
serializer_class = WorkgroupSerializer serializer_class = WorkgroupSerializer
model = Workgroup model = Workgroup
def create(self, request):
"""
Create a new workgroup and its cohort.
"""
response = super(WorkgroupsViewSet, self).create(request)
if response.status_code == status.HTTP_201_CREATED:
# create the workgroup cohort
workgroup = self.object
course_descriptor, course_key, course_content = _get_course(self.request, self.request.user, workgroup.project.course_id) # pylint: disable=W0612
add_cohort(course_key, workgroup.cohort_name)
return response
@action(methods=['get', 'post']) @action(methods=['get', 'post'])
def groups(self, request, pk): def groups(self, request, pk):
""" """
...@@ -134,7 +150,6 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -134,7 +150,6 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
workgroup = self.get_object() workgroup = self.get_object()
workgroup.groups.add(group) workgroup.groups.add(group)
workgroup.save() workgroup.save()
print workgroup.groups.all()
return Response({}, status=status.HTTP_201_CREATED) return Response({}, status=status.HTTP_201_CREATED)
@action(methods=['get', 'post', 'delete']) @action(methods=['get', 'post', 'delete'])
...@@ -160,21 +175,31 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -160,21 +175,31 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
workgroup = self.get_object() workgroup = self.get_object()
# Ensure the user is not already assigned to a workgroup for this project
existing_workgroups = Workgroup.objects.filter(users=user).filter(project=workgroup.project)
if len(existing_workgroups):
message = 'User {} already assigned to a workgroup for this project'.format(user_id)
return Response({"detail": message}, status.HTTP_400_BAD_REQUEST)
# Ensure the user is not already assigned to a project for this course # Ensure the user is not already assigned to a project for this course
existing_projects = Project.objects.filter(course_id=workgroup.project.course_id).filter(workgroups__users__id=user.id) existing_projects = Project.objects.filter(course_id=workgroup.project.course_id).filter(workgroups__users__id=user.id)
if len(existing_projects): if len(existing_projects):
message = 'User {} already assigned to a project for this course'.format(user_id) message = 'User {} already assigned to a project for this course'.format(user_id)
return Response({"detail": message}, status.HTTP_400_BAD_REQUEST) return Response({"detail": message}, status.HTTP_400_BAD_REQUEST)
workgroup.users.add(user) try:
workgroup.add_user(user)
except ValidationError as e:
return Response({"detail": unicode(e)}, status.HTTP_400_BAD_REQUEST)
workgroup.save() workgroup.save()
# add user to the workgroup cohort, create it if it doesn't exist (for cases where there is a legacy
# workgroup)
course_descriptor, course_key, course_content = _get_course(self.request, user, workgroup.project.course_id) # pylint: disable=W0612
try:
cohort = get_cohort_by_name(course_key, workgroup.cohort_name)
add_user_to_cohort(cohort, user.username)
except ObjectDoesNotExist:
# This use case handles cases where a workgroup might have been created before
# the notion of a cohorted discussion. So we need to backfill in the data
cohort = add_cohort(course_key, workgroup.cohort_name)
for workgroup_user in workgroup.users.all():
add_user_to_cohort(cohort, workgroup_user.username)
return Response({}, status=status.HTTP_201_CREATED) return Response({}, status=status.HTTP_201_CREATED)
else: else:
user_id = request.DATA.get('id') user_id = request.DATA.get('id')
...@@ -184,7 +209,11 @@ class WorkgroupsViewSet(viewsets.ModelViewSet): ...@@ -184,7 +209,11 @@ class WorkgroupsViewSet(viewsets.ModelViewSet):
message = 'User {} does not exist'.format(user_id) message = 'User {} does not exist'.format(user_id)
return Response({"detail": message}, status.HTTP_400_BAD_REQUEST) return Response({"detail": message}, status.HTTP_400_BAD_REQUEST)
workgroup = self.get_object() workgroup = self.get_object()
workgroup.users.remove(user) course_descriptor, course_key, course_content = _get_course(self.request, user, workgroup.project.course_id) # pylint: disable=W0612
cohort = get_cohort_by_name(course_key,
workgroup.cohort_name)
workgroup.remove_user(user)
remove_user_from_cohort(cohort, user.username)
return Response({}, status=status.HTTP_204_NO_CONTENT) return Response({}, status=status.HTTP_204_NO_CONTENT)
@link() @link()
......
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