Commit 3d5aa2b5 by Calen Pennington

Merge pull request #1575 from cpennington/hotfix-memory-leaks

Improve memory profile during course grading runs
parents 6d6391db 061a46be
......@@ -4,9 +4,17 @@ from student.models import (User, UserProfile, Registration,
PendingEmailChange, UserStanding,
)
from course_modes.models import CourseMode
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group, AnonymousUser
from datetime import datetime
from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence
from factory import (
DjangoModelFactory,
Factory,
LazyAttribute,
post_generation,
PostGenerationMethodCall,
Sequence,
SubFactory,
)
from uuid import uuid4
from pytz import UTC
......@@ -16,8 +24,10 @@ from pytz import UTC
class GroupFactory(DjangoModelFactory):
FACTORY_FOR = Group
FACTORY_DJANGO_GET_OR_CREATE = ('name', )
name = Sequence(u'group{0}'.format)
name = u'staff_MITx/999/Robot_Super_Course'
class UserStandingFactory(DjangoModelFactory):
......@@ -30,9 +40,10 @@ class UserStandingFactory(DjangoModelFactory):
class UserProfileFactory(DjangoModelFactory):
FACTORY_FOR = UserProfile
FACTORY_DJANGO_GET_OR_CREATE = ('user', )
user = None
name = u'Robot Test'
name = LazyAttribute(u'{0.user.first_name} {0.user.last_name}'.format)
level_of_education = None
gender = u'm'
mailing_address = None
......@@ -59,6 +70,7 @@ class RegistrationFactory(DjangoModelFactory):
class UserFactory(DjangoModelFactory):
FACTORY_FOR = User
FACTORY_DJANGO_GET_OR_CREATE = ('email', 'username')
username = Sequence(u'robot{0}'.format)
email = Sequence(u'robot+test+{0}@edx.org'.format)
......@@ -82,6 +94,21 @@ class UserFactory(DjangoModelFactory):
else:
return None
@post_generation
def groups(self, create, extracted, **kwargs):
if extracted is None:
return
if isinstance(extracted, basestring):
extracted = [extracted]
for group_name in extracted:
self.groups.add(GroupFactory.simple_generate(create, name=group_name))
class AnonymousUserFactory(Factory):
FACTORY_FOR = AnonymousUser
class AdminFactory(UserFactory):
is_staff = True
......
......@@ -28,7 +28,7 @@ from celery.states import SUCCESS, FAILURE, RETRY
from celery.exceptions import RetryTaskError
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.urlresolvers import reverse
......@@ -36,8 +36,8 @@ from bulk_email.models import (
CourseEmail, Optout, CourseEmailTemplate,
SEND_TO_MYSELF, SEND_TO_ALL, TO_OPTIONS,
)
from courseware.access import _course_staff_group_name, _course_instructor_group_name
from courseware.courses import get_course, course_image_url
from courseware.roles import CourseStaffRole, CourseInstructorRole
from instructor_task.models import InstructorTask
from instructor_task.subtasks import (
SubtaskStatus,
......@@ -106,12 +106,8 @@ def _get_recipient_queryset(user_id, to_option, course_id, course_location):
if to_option == SEND_TO_MYSELF:
recipient_qset = User.objects.filter(id=user_id)
else:
staff_grpname = _course_staff_group_name(course_location)
staff_group, _ = Group.objects.get_or_create(name=staff_grpname)
staff_qset = staff_group.user_set.all()
instructor_grpname = _course_instructor_group_name(course_location)
instructor_group, _ = Group.objects.get_or_create(name=instructor_grpname)
instructor_qset = instructor_group.user_set.all()
staff_qset = CourseStaffRole(course_location).users_with_role()
instructor_qset = CourseInstructorRole(course_location).users_with_role()
recipient_qset = staff_qset | instructor_qset
if to_option == SEND_TO_ALL:
# We also require students to have activated their accounts to
......
......@@ -50,10 +50,10 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
# Create staff
self.staff = [StaffFactory(self.course) for _ in xrange(STAFF_COUNT)]
self.staff = [StaffFactory(course=self.course.location) for _ in xrange(STAFF_COUNT)]
# Create students
self.students = [UserFactory() for _ in xrange(STUDENT_COUNT)]
......
......@@ -25,7 +25,7 @@ class TestWikiAccessBase(ModuleStoreTestCase):
self.wiki = get_or_create_root()
self.course_math101 = CourseFactory.create(org='org', number='math101', display_name='Course')
self.course_math101_staff = [InstructorFactory(self.course_math101), StaffFactory(self.course_math101)]
self.course_math101_staff = [InstructorFactory(course=self.course_math101.location), StaffFactory(course=self.course_math101.location)]
wiki_math101 = self.create_urlpath(self.wiki, course_wiki_slug(self.course_math101))
wiki_math101_page = self.create_urlpath(wiki_math101, 'Child')
......@@ -44,9 +44,9 @@ class TestWikiAccess(TestWikiAccessBase):
super(TestWikiAccess, self).setUp()
self.course_310b = CourseFactory.create(org='org', number='310b', display_name='Course')
self.course_310b_staff = [InstructorFactory(self.course_310b), StaffFactory(self.course_310b)]
self.course_310b_staff = [InstructorFactory(course=self.course_310b.location), StaffFactory(course=self.course_310b.location)]
self.course_310b_ = CourseFactory.create(org='org', number='310b_', display_name='Course')
self.course_310b__staff = [InstructorFactory(self.course_310b_), StaffFactory(self.course_310b_)]
self.course_310b__staff = [InstructorFactory(course=self.course_310b_.location), StaffFactory(course=self.course_310b_.location)]
self.wiki_310b = self.create_urlpath(self.wiki, course_wiki_slug(self.course_310b))
self.wiki_310b_ = self.create_urlpath(self.wiki, course_wiki_slug(self.course_310b_))
......@@ -105,7 +105,7 @@ class TestWikiAccessForNumericalCourseNumber(TestWikiAccessBase):
super(TestWikiAccessForNumericalCourseNumber, self).setUp()
self.course_200 = CourseFactory.create(org='org', number='200', display_name='Course')
self.course_200_staff = [InstructorFactory(self.course_200), StaffFactory(self.course_200)]
self.course_200_staff = [InstructorFactory(course=self.course_200.location), StaffFactory(course=self.course_200.location)]
wiki_200 = self.create_urlpath(self.wiki, course_wiki_slug(self.course_200))
wiki_200_page = self.create_urlpath(wiki_200, 'Child')
......@@ -126,7 +126,7 @@ class TestWikiAccessForOldFormatCourseStaffGroups(TestWikiAccessBase):
self.course_math101c = CourseFactory.create(org='org', number='math101c', display_name='Course')
Group.objects.get_or_create(name='instructor_math101c')
self.course_math101c_staff = [InstructorFactory(self.course_math101c), StaffFactory(self.course_math101c)]
self.course_math101c_staff = [InstructorFactory(course=self.course_math101c.location), StaffFactory(course=self.course_math101c.location)]
wiki_math101c = self.create_urlpath(self.wiki, course_wiki_slug(self.course_math101c))
wiki_math101c_page = self.create_urlpath(wiki_math101c, 'Child')
......
......@@ -18,19 +18,16 @@ from external_auth.models import ExternalAuthMap
from courseware.masquerade import is_masquerading_as_student
from django.utils.timezone import UTC
from student.models import CourseEnrollment
from courseware.roles import (
GlobalStaff, CourseStaffRole, CourseInstructorRole,
OrgStaffRole, OrgInstructorRole, CourseBetaTesterRole
)
DEBUG_ACCESS = False
log = logging.getLogger(__name__)
class CourseContextRequired(Exception):
"""
Raised when a course_context is required to determine permissions
"""
pass
def debug(*args, **kwargs):
# to avoid overly verbose output, this is off by default
if DEBUG_ACCESS:
......@@ -91,22 +88,6 @@ def has_access(user, obj, action, course_context=None):
.format(type(obj)))
def get_access_group_name(obj, action):
'''
Returns group name for user group which has "action" access to the given object.
Used in managing access lists.
'''
if isinstance(obj, CourseDescriptor):
return _get_access_group_name_course_desc(obj, action)
# Passing an unknown object here is a coding error, so rather than
# returning a default, complain.
raise TypeError("Unknown object type in get_access_group_name(): '{0}'"
.format(type(obj)))
# ================ Implementation helpers ================================
def _has_access_course_desc(user, course, action):
"""
......@@ -214,20 +195,6 @@ def _has_access_course_desc(user, course, action):
return _dispatch(checkers, action, user, course)
def _get_access_group_name_course_desc(course, action):
'''
Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor'
'''
if action == 'staff':
return _course_staff_group_name(course.location)
elif action == 'instructor':
return _course_instructor_group_name(course.location)
return []
def _has_access_error_desc(user, descriptor, action, course_context):
"""
Only staff should see error descriptors.
......@@ -340,7 +307,7 @@ def _has_access_string(user, perm, action, course_context):
if perm != 'global':
debug("Deny: invalid permission '%s'", perm)
return False
return _has_global_staff_access(user)
return GlobalStaff().has_user(user)
checkers = {
'staff': check_staff
......@@ -370,139 +337,6 @@ def _dispatch(table, action, user, obj):
type(obj), action))
def _does_course_group_name_exist(name):
return len(Group.objects.filter(name=name)) > 0
def _course_org_staff_group_name(location, course_context=None):
"""
Get the name of the staff group for an organization which corresponds
to the organization in the course id.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which
the location occurs.
Required if location doesn't have category 'course'
"""
loc = Location(location)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
return 'staff_%s' % course_id.split('/')[0]
def group_names_for(role, location, course_context=None):
"""Returns the group names for a given role with this location. Plural
because it will return both the name we expect now as well as the legacy
group name we support for backwards compatibility. This should not check
the DB for existence of a group (like some of its callers do) because that's
a DB roundtrip, and we expect this might be invoked many times as we crawl
an XModule tree."""
loc = Location(location)
legacy_group_name = '{0}_{1}'.format(role, loc.course)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
group_name = '{0}_{1}'.format(role, course_id)
return [group_name, legacy_group_name]
group_names_for_staff = partial(group_names_for, 'staff')
group_names_for_instructor = partial(group_names_for, 'instructor')
def _course_staff_group_name(location, course_context=None):
"""
Get the name of the staff group for a location in the context of a course run.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which the location occurs.
Required if location doesn't have category 'course'
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
"""
loc = Location(location)
group_name, legacy_group_name = group_names_for_staff(location, course_context)
if _does_course_group_name_exist(legacy_group_name):
return legacy_group_name
return group_name
def _course_org_instructor_group_name(location, course_context=None):
"""
Get the name of the instructor group for an organization which corresponds
to the organization in the course id.
location: something that can passed to Location
course_context: A course_id that specifies the course run in which
the location occurs.
Required if location doesn't have category 'course'
"""
loc = Location(location)
if loc.category == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
return 'instructor_%s' % course_id.split('/')[0]
def _course_instructor_group_name(location, course_context=None):
"""
Get the name of the instructor group for a location, in the context of a course run.
A course instructor has all staff privileges, but also can manage list of course staff (add, remove, list).
location: something that can passed to Location.
course_context: A course_id that specifies the course run in which the location occurs.
Required if location doesn't have category 'course'
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
using course_id rather than just the course number. So first check to see if the group name exists
"""
loc = Location(location)
group_name, legacy_group_name = group_names_for_instructor(location, course_context)
if _does_course_group_name_exist(legacy_group_name):
return legacy_group_name
return group_name
def course_beta_test_group_name(location):
"""
Get the name of the beta tester group for a location. Right now, that's
beta_testers_COURSE.
location: something that can passed to Location.
"""
return 'beta_testers_{0}'.format(Location(location).course)
# nosetests thinks that anything with _test_ in the name is a test.
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
course_beta_test_group_name.__test__ = False
def _has_global_staff_access(user):
if user.is_staff:
debug("Allow: user.is_staff")
return True
else:
debug("Deny: not user.is_staff")
return False
def _adjust_start_date_for_beta_testers(user, descriptor):
"""
If user is in a beta test group, adjust the start date by the appropriate number of
......@@ -530,11 +364,8 @@ def _adjust_start_date_for_beta_testers(user, descriptor):
# bail early if no beta testing is set up
return descriptor.start
user_groups = [g.name for g in user.groups.all()]
beta_group = course_beta_test_group_name(descriptor.location)
if beta_group in user_groups:
debug("Adjust start time: user in group %s", beta_group)
if CourseBetaTesterRole(descriptor.location).has_user(user):
debug("Adjust start time: user in beta role for %s", descriptor)
delta = timedelta(descriptor.days_early_for_beta)
effective = descriptor.start - delta
return effective
......@@ -572,32 +403,34 @@ def _has_access_to_location(user, location, access_level, course_context):
if is_masquerading_as_student(user):
return False
if user.is_staff:
if GlobalStaff().has_user(user):
debug("Allow: user.is_staff")
return True
# If not global staff, is the user in the Auth group for this class?
user_groups = [g.name for g in user.groups.all()]
if access_level not in ('staff', 'instructor'):
log.debug("Error in access._has_access_to_location access_level=%s unknown", access_level)
debug("Deny: unknown access level")
return False
staff_access = (
CourseStaffRole(location, course_context).has_user(user) or
OrgStaffRole(location).has_user(user)
)
if access_level == 'staff':
staff_groups = group_names_for_staff(location, course_context) + \
[_course_org_staff_group_name(location, course_context)]
for staff_group in staff_groups:
if staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
if staff_access and access_level == 'staff':
debug("Allow: user has course staff access")
return True
debug("Deny: user not in groups %s", staff_groups)
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
instructor_groups = group_names_for_instructor(location, course_context) + \
[_course_org_instructor_group_name(location, course_context)]
for instructor_group in instructor_groups:
if instructor_group in user_groups:
debug("Allow: user in group %s", instructor_group)
instructor_access = (
CourseInstructorRole(location, course_context).has_user(user) or
OrgInstructorRole(location).has_user(user)
)
if instructor_access and access_level in ('staff', 'instructor'):
debug("Allow: user has course instructor access")
return True
debug("Deny: user not in groups %s", instructor_groups)
else:
log.debug("Error in access._has_access_to_location access_level=%s unknown" % access_level)
debug("Deny: user did not have correct access")
return False
......
"""
Classes used to model the roles used in the courseware. Each role is responsible for checking membership,
adding users, removing users, and listing members
"""
from abc import ABCMeta, abstractmethod
from functools import partial
from django.contrib.auth.models import User, Group
from xmodule.modulestore import Location
class CourseContextRequired(Exception):
"""
Raised when a course_context is required to determine permissions
"""
pass
class AccessRole(object):
"""
Object representing a role with particular access to a resource
"""
__metaclass__ = ABCMeta
@abstractmethod
def has_user(self, user): # pylint: disable=unused-argument
"""
Return whether the supplied django user has access to this role.
"""
return False
@abstractmethod
def add_users(self, *users):
"""
Add the role to the supplied django users.
"""
pass
@abstractmethod
def remove_users(self, *users):
"""
Remove the role from the supplied django users.
"""
pass
@abstractmethod
def users_with_role(self):
"""
Return a django QuerySet for all of the users with this role
"""
return User.objects.none()
class GlobalStaff(AccessRole):
"""
The global staff role
"""
def has_user(self, user):
return user.is_staff
def add_users(self, *users):
for user in users:
user.is_staff = True
user.save()
def remove_users(self, *users):
for user in users:
user.is_staff = False
user.save()
def users_with_role(self):
raise Exception("This operation is un-indexed, and shouldn't be used")
class GroupBasedRole(AccessRole):
"""
A role based on membership to any of a set of groups.
"""
def __init__(self, group_names):
"""
Create a GroupBasedRole from a list of group names
The first element of `group_names` will be the preferred group
to use when adding a user to this Role.
If a user is a member of any of the groups in the list, then
they will be consider a member of the Role
"""
self._group_names = [name.lower() for name in group_names]
def has_user(self, user):
"""
Return whether the supplied django user has access to this role.
"""
# pylint: disable=protected-access
if not user.is_authenticated():
return False
if not hasattr(user, '_groups'):
user._groups = set(name.lower() for name in user.groups.values_list('name', flat=True))
return len(user._groups.intersection(self._group_names)) > 0
def add_users(self, *users):
"""
Add the supplied django users to this role.
"""
group, _ = Group.objects.get_or_create(name=self._group_names[0])
group.user_set.add(*users)
for user in users:
if hasattr(user, '_groups'):
del user._groups
def remove_users(self, *users):
"""
Remove the supplied django users from this role.
"""
group, _ = Group.objects.get_or_create(name=self._group_names[0])
group.user_set.remove(*users)
for user in users:
if hasattr(user, '_groups'):
del user._groups
def users_with_role(self):
"""
Return a django QuerySet for all of the users with this role
"""
return User.objects.filter(groups__name__in=self._group_names)
class CourseRole(GroupBasedRole):
"""
A named role in a particular course
"""
def __init__(self, role, location, course_context=None):
# pylint: disable=no-member
loc = Location(location)
legacy_group_name = '{0}_{1}'.format(role, loc.course)
if loc.category.lower() == 'course':
course_id = loc.course_id
else:
if course_context is None:
raise CourseContextRequired()
course_id = course_context
group_name = '{0}_{1}'.format(role, course_id)
super(CourseRole, self).__init__([group_name, legacy_group_name])
class OrgRole(GroupBasedRole):
"""
A named role in a particular org
"""
def __init__(self, role, location):
# pylint: disable=no-member
location = Location(location)
super(OrgRole, self).__init__(['{}_{}'.format(role, location.org)])
class CourseStaffRole(CourseRole):
"""A Staff member of a course"""
def __init__(self, *args, **kwargs):
super(CourseStaffRole, self).__init__('staff', *args, **kwargs)
class CourseInstructorRole(CourseRole):
"""A course Instructor"""
def __init__(self, *args, **kwargs):
super(CourseInstructorRole, self).__init__('instructor', *args, **kwargs)
class CourseBetaTesterRole(CourseRole):
"""A course Beta Tester"""
def __init__(self, *args, **kwargs):
super(CourseBetaTesterRole, self).__init__('beta_testers', *args, **kwargs)
class OrgStaffRole(OrgRole):
"""An organization staff member"""
def __init__(self, *args, **kwargs):
super(OrgStaffRole, self).__init__('staff', *args, **kwargs)
class OrgInstructorRole(OrgRole):
"""An organization staff member"""
def __init__(self, *args, **kwargs):
super(OrgInstructorRole, self).__init__('staff', *args, **kwargs)
from datetime import datetime
import json
from functools import partial
from factory import DjangoModelFactory, SubFactory
from student.tests.factories import UserFactory as StudentUserFactory
from student.tests.factories import GroupFactory as StudentGroupFactory
from factory import DjangoModelFactory, SubFactory, post_generation
# Imported to re-export
# pylint: disable=unused-import
from student.tests.factories import UserFactory # Imported to re-export
from student.tests.factories import GroupFactory # Imported to re-export
from student.tests.factories import CourseEnrollmentAllowedFactory # Imported to re-export
from student.tests.factories import RegistrationFactory # Imported to re-export
# pylint: enable=unused-import
from student.tests.factories import UserProfileFactory as StudentUserProfileFactory
from student.tests.factories import CourseEnrollmentAllowedFactory as StudentCourseEnrollmentAllowedFactory
from student.tests.factories import RegistrationFactory as StudentRegistrationFactory
from courseware.models import StudentModule, XModuleUserStateSummaryField
from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField
from instructor.access import allow_access
from courseware.roles import CourseInstructorRole, CourseStaffRole
from xmodule.modulestore import Location
from pytz import UTC
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
class UserProfileFactory(StudentUserProfileFactory):
name = 'Robot Studio'
courseware = 'course.xml'
class RegistrationFactory(StudentRegistrationFactory):
pass
class UserFactory(StudentUserFactory):
email = 'robot@edx.org'
last_name = 'Tester'
last_login = datetime.now(UTC)
date_joined = datetime.now(UTC)
def InstructorFactory(course): # pylint: disable=invalid-name
class InstructorFactory(UserFactory):
"""
Given a course object, returns a User object with instructor
Given a course Location, returns a User object with instructor
permissions for `course`.
"""
user = StudentUserFactory.create(last_name="Instructor")
allow_access(course, user, "instructor")
return user
last_name = "Instructor"
@post_generation
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for a course instructor user")
CourseInstructorRole(extracted).add_users(self)
def StaffFactory(course): # pylint: disable=invalid-name
class StaffFactory(UserFactory):
"""
Given a course object, returns a User object with staff
Given a course Location, returns a User object with staff
permissions for `course`.
"""
user = StudentUserFactory.create(last_name="Staff")
allow_access(course, user, "staff")
return user
class GroupFactory(StudentGroupFactory):
name = 'test_group'
last_name = "Staff"
class CourseEnrollmentAllowedFactory(StudentCourseEnrollmentAllowedFactory):
pass
@post_generation
def course(self, create, extracted, **kwargs):
if extracted is None:
raise ValueError("Must specify a course location for a course staff user")
CourseStaffRole(extracted).add_users(self)
class StudentModuleFactory(DjangoModelFactory):
......
import courseware.access as access
import datetime
from mock import Mock
from django.test import TestCase
from django.test.utils import override_settings
from courseware.tests.factories import UserFactory, CourseEnrollmentAllowedFactory, StaffFactory, InstructorFactory
from student.tests.factories import AnonymousUserFactory
from xmodule.modulestore import Location
import courseware.access as access
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
from .factories import CourseEnrollmentAllowedFactory
import datetime
import pytz
# pylint: disable=protected-access
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class AccessTestCase(TestCase):
"""
Tests for the various access controls on the student dashboard
"""
def test__has_global_staff_access(self):
u = Mock(is_staff=False)
self.assertFalse(access._has_global_staff_access(u))
u = Mock(is_staff=True)
self.assertTrue(access._has_global_staff_access(u))
def setUp(self):
self.course = Location('i4x://edX/toy/course/2012_Fall')
self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course=self.course)
self.course_instructor = InstructorFactory(course=self.course)
def test__has_access_to_location(self):
location = Location('i4x://edX/toy/course/2012_Fall')
self.assertFalse(access._has_access_to_location(None, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.anonymous_user, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.anonymous_user, self.course, 'instructor', None))
self.assertTrue(access._has_access_to_location(self.global_staff, self.course, 'staff', None))
self.assertTrue(access._has_access_to_location(self.global_staff, self.course, 'instructor', None))
self.assertFalse(access._has_access_to_location(None, location,
'staff', None))
u = Mock()
u.is_authenticated.return_value = False
self.assertFalse(access._has_access_to_location(u, location,
'staff', None))
u = Mock(is_staff=True)
self.assertTrue(access._has_access_to_location(u, location,
'instructor', None))
# A user has staff access if they are in the staff group
u = Mock(is_staff=False)
g = Mock()
g.name = 'staff_edX/toy/2012_Fall'
u.groups.all.return_value = [g]
self.assertTrue(access._has_access_to_location(u, location,
'staff', None))
# A user has staff access if they are in the instructor group
g.name = 'instructor_edX/toy/2012_Fall'
self.assertTrue(access._has_access_to_location(u, location,
'staff', None))
# A user has instructor access if they are in the instructor group
g.name = 'instructor_edX/toy/2012_Fall'
self.assertTrue(access._has_access_to_location(u, location,
'instructor', None))
# A user does not have staff access if they are
self.assertTrue(access._has_access_to_location(self.course_staff, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.course_staff, self.course, 'instructor', None))
# A user has staff and instructor access if they are in the instructor group
self.assertTrue(access._has_access_to_location(self.course_instructor, self.course, 'staff', None))
self.assertTrue(access._has_access_to_location(self.course_instructor, self.course, 'instructor', None))
# A user does not have staff or instructor access if they are
# not in either the staff or the the instructor group
g.name = 'student_only'
self.assertFalse(access._has_access_to_location(u, location,
'staff', None))
# A user does not have instructor access if they are
# not in the instructor group
g.name = 'student_only'
self.assertFalse(access._has_access_to_location(u, location,
'instructor', None))
self.assertFalse(access._has_access_to_location(self.student, self.course, 'staff', None))
self.assertFalse(access._has_access_to_location(self.student, self.course, 'instructor', None))
def test__has_access_string(self):
u = Mock(is_staff=True)
......
......@@ -12,11 +12,11 @@ import json
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from courseware.access import _course_staff_group_name
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from courseware.roles import CourseStaffRole
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from lms.lib.xblock.runtime import quote_slashes
......@@ -42,9 +42,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
self.activate_user(self.instructor)
def make_instructor(course):
group_name = _course_staff_group_name(course.location)
g = Group.objects.create(name=group_name)
g.user_set.add(User.objects.get(email=self.instructor))
CourseStaffRole(course.location).add_users(User.objects.get(email=self.instructor))
make_instructor(self.graded_course)
......
"""
Tests of courseware.roles
"""
from django.test import TestCase
from xmodule.modulestore import Location
from courseware.tests.factories import UserFactory, StaffFactory, InstructorFactory
from student.tests.factories import AnonymousUserFactory
from courseware.roles import GlobalStaff, CourseRole
class RolesTestCase(TestCase):
"""
Tests of courseware.roles
"""
def setUp(self):
self.course = Location('i4x://edX/toy/course/2012_Fall')
self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True)
self.course_staff = StaffFactory(course=self.course)
self.course_instructor = InstructorFactory(course=self.course)
def test_global_staff(self):
self.assertFalse(GlobalStaff().has_user(self.student))
self.assertFalse(GlobalStaff().has_user(self.course_staff))
self.assertFalse(GlobalStaff().has_user(self.course_instructor))
self.assertTrue(GlobalStaff().has_user(self.global_staff))
def test_group_name_case_insensitive(self):
uppercase_loc = "i4x://ORG/COURSE/course/NAME"
lowercase_loc = uppercase_loc.lower()
lowercase_group = "role_org/course/name"
uppercase_group = lowercase_group.upper()
lowercase_user = UserFactory(groups=lowercase_group)
uppercase_user = UserFactory(groups=uppercase_group)
self.assertTrue(CourseRole("role", lowercase_loc).has_user(lowercase_user))
self.assertTrue(CourseRole("role", uppercase_loc).has_user(lowercase_user))
self.assertTrue(CourseRole("role", lowercase_loc).has_user(uppercase_user))
self.assertTrue(CourseRole("role", uppercase_loc).has_user(uppercase_user))
......@@ -3,19 +3,19 @@ import pytz
from mock import patch
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from courseware.access import (has_access, _course_staff_group_name,
course_beta_test_group_name, settings as access_settings)
from courseware.access import has_access
from courseware.roles import CourseBetaTesterRole, CourseInstructorRole, CourseStaffRole, GlobalStaff
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from helpers import LoginEnrollmentTestCase, check_for_get_code
from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
......@@ -173,9 +173,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
email, password = self.ACCOUNT_INFO[1]
# Make the instructor staff in self.course
group_name = _course_staff_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=email))
CourseInstructorRole(self.course.location).add_users(User.objects.get(email=email))
self.login(email, password)
......@@ -206,7 +204,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
for url in urls:
check_for_get_code(self, 200, url)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
@patch.dict('courseware.access.settings.MITX_FEATURES', {'DISABLE_START_DATES': False})
def test_dark_launch_enrolled_student(self):
"""
Make sure that before course start, students can't access course
......@@ -237,7 +235,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self._check_non_staff_light(self.test_course)
self._check_non_staff_dark(self.test_course)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
@patch.dict('courseware.access.settings.MITX_FEATURES', {'DISABLE_START_DATES': False})
def test_dark_launch_instructor(self):
"""
Make sure that before course start instructors can access the
......@@ -253,9 +251,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.test_course = self.update_course(self.test_course, test_course_data)
# Make the instructor staff in self.course
group_name = _course_staff_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=instructor_email))
CourseStaffRole(self.course.location).add_users(User.objects.get(email=instructor_email))
self.logout()
self.login(instructor_email, instructor_password)
# Enroll in the classes---can't see courseware otherwise.
......@@ -267,7 +264,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self._check_non_staff_dark(self.test_course)
self._check_staff(self.course)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
@patch.dict('courseware.access.settings.MITX_FEATURES', {'DISABLE_START_DATES': False})
def test_dark_launch_staff(self):
"""
Make sure that before course start staff can access
......@@ -295,7 +292,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self._check_staff(self.course)
self._check_staff(self.test_course)
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
@patch.dict('courseware.access.settings.MITX_FEATURES', {'DISABLE_START_DATES': False})
def test_enrollment_period(self):
"""
Check that enrollment periods work.
......@@ -323,25 +320,22 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertTrue(self.enroll(self.test_course))
# Make the instructor staff in the self.course
group_name = _course_staff_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=instructor_email))
instructor_role = CourseInstructorRole(self.course.location)
instructor_role.add_users(User.objects.get(email=instructor_email))
self.logout()
self.login(instructor_email, instructor_password)
self.assertTrue(self.enroll(self.course))
# now make the instructor global staff, but not in the instructor group
group.user_set.remove(User.objects.get(email=instructor_email))
instructor = User.objects.get(email=instructor_email)
instructor.is_staff = True
instructor.save()
instructor_role.remove_users(User.objects.get(email=instructor_email))
GlobalStaff().add_users(User.objects.get(email=instructor_email))
# unenroll and try again
self.unenroll(self.course)
self.assertTrue(self.enroll(self.course))
@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False})
@patch.dict('courseware.access.settings.MITX_FEATURES', {'DISABLE_START_DATES': False})
def test_beta_period(self):
"""
Check that beta-test access works.
......@@ -366,9 +360,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertFalse(has_access(student_user, self.course, 'load'))
# now add the student to the beta test group
group_name = course_beta_test_group_name(self.course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(student_user)
CourseBetaTesterRole(self.course.location).add_users(student_user)
# now the student should see it
self.assertTrue(has_access(student_user, self.course, 'load'))
......@@ -10,13 +10,18 @@ TO DO sync instructor and staff flags
"""
import logging
from django.contrib.auth.models import Group
from courseware.access import (get_access_group_name,
course_beta_test_group_name)
from django_comment_common.models import Role
from courseware.roles import CourseBetaTesterRole, CourseInstructorRole, CourseStaffRole
log = logging.getLogger(__name__)
ROLES = {
'beta': CourseBetaTesterRole,
'instructor': CourseInstructorRole,
'staff': CourseStaffRole,
}
def list_with_level(course, level):
"""
......@@ -26,16 +31,7 @@ def list_with_level(course, level):
There could be other levels specific to the course.
If there is no Group for that course-level, returns an empty list
"""
if level == 'beta':
grpname = course_beta_test_group_name(course.location)
else:
grpname = get_access_group_name(course, level)
try:
return Group.objects.get(name=grpname).user_set.all()
except Group.DoesNotExist:
log.info("list_with_level called with non-existant group named {}".format(grpname))
return []
return ROLES[level](course.location).users_with_role()
def allow_access(course, user, level):
......@@ -66,18 +62,15 @@ def _change_access(course, user, level, action):
NOTE: will create a group if it does not yet exist.
"""
if level == 'beta':
grpname = course_beta_test_group_name(course.location)
elif level in ['instructor', 'staff']:
grpname = get_access_group_name(course, level)
else:
try:
role = ROLES[level](course.location)
except KeyError:
raise ValueError("unrecognized level '{}'".format(level))
group, _ = Group.objects.get_or_create(name=grpname)
if action == 'allow':
user.groups.add(group)
role.add_users(user)
elif action == 'revoke':
user.groups.remove(group)
role.remove_users(user)
else:
raise ValueError("unrecognized action '{}'".format(action))
......
......@@ -3,15 +3,14 @@ Test instructor.access
"""
from nose.tools import raises
from django.contrib.auth.models import Group
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from django.test.utils import override_settings
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from courseware.roles import CourseBetaTesterRole, CourseStaffRole
from courseware.access import get_access_group_name
from django_comment_common.models import (Role,
FORUM_ROLE_MODERATOR)
from instructor.access import (allow_access,
......@@ -51,39 +50,29 @@ class TestInstructorAccessAllow(ModuleStoreTestCase):
def test_allow(self):
user = UserFactory()
allow_access(self.course, user, 'staff')
group = Group.objects.get(
name=get_access_group_name(self.course, 'staff')
)
self.assertIn(user, group.user_set.all())
self.assertTrue(CourseStaffRole(self.course.location).has_user(user))
def test_allow_twice(self):
user = UserFactory()
allow_access(self.course, user, 'staff')
allow_access(self.course, user, 'staff')
group = Group.objects.get(
name=get_access_group_name(self.course, 'staff')
)
self.assertIn(user, group.user_set.all())
self.assertTrue(CourseStaffRole(self.course.location).has_user(user))
def test_allow_beta(self):
""" Test allow beta against list beta. """
user = UserFactory()
allow_access(self.course, user, 'beta')
self.assertIn(user, list_with_level(self.course, 'beta'))
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(user))
@raises(ValueError)
def test_allow_badlevel(self):
user = UserFactory()
allow_access(self.course, user, 'robot-not-a-level')
group = Group.objects.get(name=get_access_group_name(self.course, 'robot-not-a-level'))
self.assertIn(user, group.user_set.all())
@raises(Exception)
def test_allow_noneuser(self):
user = None
allow_access(self.course, user, 'staff')
group = Group.objects.get(name=get_access_group_name(self.course, 'staff'))
self.assertIn(user, group.user_set.all())
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -102,32 +91,22 @@ class TestInstructorAccessRevoke(ModuleStoreTestCase):
def test_revoke(self):
user = self.staff[0]
revoke_access(self.course, user, 'staff')
group = Group.objects.get(
name=get_access_group_name(self.course, 'staff')
)
self.assertNotIn(user, group.user_set.all())
self.assertFalse(CourseStaffRole(self.course.location).has_user(user))
def test_revoke_twice(self):
user = self.staff[0]
revoke_access(self.course, user, 'staff')
group = Group.objects.get(
name=get_access_group_name(self.course, 'staff')
)
self.assertNotIn(user, group.user_set.all())
self.assertFalse(CourseStaffRole(self.course.location).has_user(user))
def test_revoke_beta(self):
user = self.beta_testers[0]
revoke_access(self.course, user, 'beta')
self.assertNotIn(user, list_with_level(self.course, 'beta'))
self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(user))
@raises(ValueError)
def test_revoke_badrolename(self):
user = UserFactory()
revoke_access(self.course, user, 'robot-not-a-level')
group = Group.objects.get(
name=get_access_group_name(self.course, 'robot-not-a-level')
)
self.assertNotIn(user, group.user_set.all())
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......
......@@ -183,7 +183,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Ensure that a staff member can't access instructor endpoints.
"""
staff_member = StaffFactory(self.course)
staff_member = StaffFactory(course=self.course.location)
CourseEnrollment.enroll(staff_member, self.course.id)
self.client.login(username=staff_member.username, password='test')
# Try to promote to forums admin - not working
......@@ -212,7 +212,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Ensure that an instructor member can access all endpoints.
"""
inst = InstructorFactory(self.course)
inst = InstructorFactory(course=self.course.location)
CourseEnrollment.enroll(inst, self.course.id)
self.client.login(username=inst.username, password='test')
......@@ -249,7 +249,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
self.enrolled_student = UserFactory()
......@@ -376,7 +376,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
"""
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
self.other_instructor = UserFactory()
......@@ -513,7 +513,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
"""
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
self.students = [UserFactory() for _ in xrange(6)]
......@@ -650,7 +650,7 @@ class TestInstructorAPIRegradeTask(ModuleStoreTestCase, LoginEnrollmentTestCase)
"""
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
self.student = UserFactory()
......@@ -794,7 +794,7 @@ class TestInstructorSendEmail(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
test_subject = u'\u1234 test subject'
test_message = u'\u6824 test message'
......@@ -916,7 +916,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
self.student = UserFactory()
......@@ -1047,7 +1047,7 @@ class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCa
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(self.course)
self.instructor = InstructorFactory(course=self.course.location)
self.client.login(username=self.instructor.username, password='test')
@patch.object(instructor.views.api.requests, 'get')
......
......@@ -11,16 +11,15 @@ Notes for running by hand:
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from courseware.roles import CourseStaffRole
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
import xmodule.modulestore.django
from mock import patch
......@@ -46,10 +45,8 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
self.activate_user(self.instructor)
def make_instructor(course):
""" Create an instructor for the course. """
group_name = _course_staff_group_name(course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=self.instructor))
""" Create an instructor for the course."""
CourseStaffRole(course.location).add_users(User.objects.get(email=self.instructor))
make_instructor(self.toy)
......@@ -68,4 +65,3 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas
self.assertEqual(response['Content-Type'], 'text/csv')
body = response.content.replace('\r', '')
self.assertEqual(body, '"User ID","Anonymized user ID"\n"2","42"\n')
......@@ -11,16 +11,15 @@ Notes for running by hand:
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from courseware.access import _course_staff_group_name
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from courseware.roles import CourseStaffRole
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
import xmodule.modulestore.django
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
......@@ -44,9 +43,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme
def make_instructor(course):
""" Create an instructor for the course. """
group_name = _course_staff_group_name(course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=self.instructor))
CourseStaffRole(course.location).add_users(User.objects.get(email=self.instructor))
make_instructor(self.toy)
......
......@@ -195,7 +195,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
course = self.course
# Create activated, but not enrolled, user
UserFactory.create(username="student3_0", email="student3_0@test.com")
UserFactory.create(username="student3_0", email="student3_0@test.com", first_name='Autoenrolled')
url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'})
......@@ -215,12 +215,12 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
)
self.assertEqual(
mail.outbox[0].body,
"Dear Robot Test\n\nYou have been enrolled in Robot Super Course "
"Dear Autoenrolled Test\n\nYou have been enrolled in Robot Super Course "
"at edx.org by a member of the course staff. "
"The course should now appear on your edx.org dashboard.\n\n"
"To start accessing course materials, please visit "
"https://edx.org/courses/MITx/999/Robot_Super_Course\n\n"
"----\nThis email was automatically sent from edx.org to Robot Test"
"----\nThis email was automatically sent from edx.org to Autoenrolled Test"
)
self.assertEqual(
......
......@@ -6,16 +6,16 @@ Unit tests for instructor dashboard forum administration
from django.test.utils import override_settings
# Need access to internal func to put users in the right group
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
from django_comment_client.utils import has_forum_access
from courseware.access import _course_staff_group_name
from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from courseware.roles import CourseStaffRole
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -54,9 +54,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest
self.activate_user(self.student)
self.activate_user(self.instructor)
group_name = _course_staff_group_name(self.toy.location)
g = Group.objects.create(name=group_name)
g.user_set.add(User.objects.get(email=self.instructor))
CourseStaffRole(self.toy.location).add_users(User.objects.get(email=self.instructor))
self.logout()
self.login(self.instructor, self.password)
......
......@@ -15,7 +15,7 @@ from requests.status_codes import codes
from StringIO import StringIO
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User
from django.http import HttpResponse
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
......@@ -32,9 +32,9 @@ from xmodule.html_module import HtmlDescriptor
from bulk_email.models import CourseEmail, CourseAuthorization
from courseware import grades
from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
from courseware.access import has_access
from courseware.courses import get_course_with_access, get_cms_course_link
from courseware.roles import CourseStaffRole, CourseInstructorRole, CourseBetaTesterRole
from courseware.models import StudentModule
from django_comment_common.models import (Role,
FORUM_ROLE_ADMINISTRATOR,
......@@ -50,12 +50,11 @@ from instructor_task.api import (get_running_instructor_tasks,
submit_reset_problem_attempts_for_all_students,
submit_bulk_course_email)
from instructor_task.views import get_task_completion_info
from mitxmako.shortcuts import render_to_response
from mitxmako.shortcuts import render_to_response, render_to_string
from psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user
from student.views import course_from_id
import track.views
from mitxmako.shortcuts import render_to_string
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from django.utils.translation import ugettext as _u
......@@ -137,35 +136,6 @@ def instructor_dashboard(request, course_id):
writer.writerow(encoded_row)
return response
def get_staff_group(course):
"""Get or create the staff access group"""
return get_group(course, 'staff')
def get_instructor_group(course):
"""Get or create the instructor access group"""
return get_group(course, 'instructor')
def get_group(course, groupname):
"""Get or create an access group"""
grpname = get_access_group_name(course, groupname)
try:
group = Group.objects.get(name=grpname)
except Group.DoesNotExist:
group = Group(name=grpname) # create the group
group.save()
return group
def get_beta_group(course):
"""
Get the group for beta testers of course.
"""
# Not using get_group because there is no access control action called
# 'beta', so adding it to get_access_group_name doesn't really make
# sense.
name = course_beta_test_group_name(course.location)
(group, _) = Group.objects.get_or_create(name=name)
return group
def get_module_url(urlname):
"""
Construct full URL for a module from its urlname.
......@@ -496,58 +466,34 @@ def instructor_dashboard(request, course_id):
# Admin
elif 'List course staff' in action:
group = get_staff_group(course)
msg += 'Staff group = {0}'.format(group.name)
datatable = _group_members_table(group, "List of Staff", course_id)
role = CourseStaffRole(course.location)
datatable = _role_members_table(role, "List of Staff", course_id)
track.views.server_track(request, "list-staff", {}, page="idashboard")
elif 'List course instructors' in action and request.user.is_staff:
group = get_instructor_group(course)
msg += 'Instructor group = {0}'.format(group.name)
log.debug('instructor grp={0}'.format(group.name))
uset = group.user_set.all()
datatable = {'header': ['Username', 'Full name']}
datatable['data'] = [[x.username, x.profile.name] for x in uset]
datatable['title'] = 'List of Instructors in course {0}'.format(course_id)
elif 'List course instructors' in action and GlobalStaff().has_user(request.user):
role = CourseInstructorRole(course.location)
datatable = _role_members_table(role, "List of Instructors", course_id)
track.views.server_track(request, "list-instructors", {}, page="idashboard")
elif action == 'Add course staff':
uname = request.POST['staffuser']
group = get_staff_group(course)
msg += add_user_to_group(request, uname, group, 'staff', 'staff')
role = CourseStaffRole(course.location)
msg += add_user_to_role(request, uname, role, 'staff', 'staff')
elif action == 'Add instructor' and request.user.is_staff:
uname = request.POST['instructor']
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None
if user is not None:
group = get_instructor_group(course)
msg += '<font color="green">Added {0} to instructor group = {1}</font>'.format(user, group.name)
log.debug('staffgrp={0}'.format(group.name))
user.groups.add(group)
track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard")
role = CourseInstructorRole(course.location)
msg += add_user_to_role(request, uname, role, 'instructor', 'instructor')
elif action == 'Remove course staff':
uname = request.POST['staffuser']
group = get_staff_group(course)
msg += remove_user_from_group(request, uname, group, 'staff', 'staff')
role = CourseStaffRole(course.location)
msg += remove_user_from_role(request, uname, role, 'staff', 'staff')
elif action == 'Remove instructor' and request.user.is_staff:
uname = request.POST['instructor']
try:
user = User.objects.get(username=uname)
except User.DoesNotExist:
msg += '<font color="red">Error: unknown username "{0}"</font>'.format(uname)
user = None
if user is not None:
group = get_instructor_group(course)
msg += '<font color="green">Removed {0} from instructor group = {1}</font>'.format(user, group.name)
log.debug('instructorgrp={0}'.format(group.name))
user.groups.remove(group)
track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard")
role = CourseInstructorRole(course.location)
msg += remove_user_from_role(request, uname, role, 'instructor', 'instructor')
#----------------------------------------
# DataDump
......@@ -605,25 +551,24 @@ def instructor_dashboard(request, course_id):
# Group management
elif 'List beta testers' in action:
group = get_beta_group(course)
msg += 'Beta test group = {0}'.format(group.name)
datatable = _group_members_table(group, "List of beta_testers", course_id)
role = CourseBetaTesterRole(course.location)
datatable = _role_members_table(role, "List of Beta Testers", course_id)
track.views.server_track(request, "list-beta-testers", {}, page="idashboard")
elif action == 'Add beta testers':
users = request.POST['betausers']
log.debug("users: {0!r}".format(users))
group = get_beta_group(course)
role = CourseBetaTesterRole(course.location)
for username_or_email in split_by_comma_and_whitespace(users):
msg += "<p>{0}</p>".format(
add_user_to_group(request, username_or_email, group, 'beta testers', 'beta-tester'))
add_user_to_role(request, username_or_email, role, 'beta testers', 'beta-tester'))
elif action == 'Remove beta testers':
users = request.POST['betausers']
group = get_beta_group(course)
role = CourseBetaTesterRole(course.location)
for username_or_email in split_by_comma_and_whitespace(users):
msg += "<p>{0}</p>".format(
remove_user_from_group(request, username_or_email, group, 'beta testers', 'beta-tester'))
remove_user_from_role(request, username_or_email, role, 'beta testers', 'beta-tester'))
#----------------------------------------
# forum administration
......@@ -1016,12 +961,12 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove):
return msg
def _group_members_table(group, title, course_id):
def _role_members_table(role, title, course_id):
"""
Return a data table of usernames and names of users in group_name.
Arguments:
group -- a django group.
role -- a courseware.roles.AccessRole
title -- a descriptive title to show the user
Returns:
......@@ -1030,76 +975,107 @@ def _group_members_table(group, title, course_id):
'data': [[username, name] for all users]
'title': "{title} in course {course}"
"""
uset = group.user_set.all()
uset = role.users_with_role()
datatable = {'header': ['Username', 'Full name']}
datatable['data'] = [[x.username, x.profile.name] for x in uset]
datatable['title'] = '{0} in course {1}'.format(title, course_id)
return datatable
def _add_or_remove_user_group(request, username_or_email, group, group_title, event_name, do_add):
def _user_from_name_or_email(username_or_email):
"""
Implementation for both add and remove functions, to get rid of shared code. do_add is bool that determines which
to do.
Return the `django.contrib.auth.User` with the supplied username or email.
If `username_or_email` contains an `@` it is treated as an email, otherwise
it is treated as the username
"""
user = None
username_or_email = strip_if_string(username_or_email)
try:
if '@' in username_or_email:
user = User.objects.get(email=username_or_email)
else:
user = User.objects.get(username=username_or_email)
except User.DoesNotExist:
msg = u'<font color="red">Error: unknown username or email "{0}"</font>'.format(username_or_email)
user = None
if user is not None:
action = "Added" if do_add else "Removed"
prep = "to" if do_add else "from"
msg = '<font color="green">{action} {0} {prep} {1} group = {2}</font>'.format(user, group_title, group.name,
action=action, prep=prep)
if do_add:
user.groups.add(group)
return User.objects.get(email=username_or_email)
else:
user.groups.remove(group)
event = "add" if do_add else "remove"
track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": unicode(user), "event": event}, page="idashboard")
return msg
return User.objects.get(username=username_or_email)
def add_user_to_group(request, username_or_email, group, group_title, event_name):
def add_user_to_role(request, username_or_email, role, group_title, event_name):
"""
Look up the given user by username (if no '@') or email (otherwise), and add them to group.
Arguments:
request: django request--used for tracking log
username_or_email: who to add. Decide if it's an email by presense of an '@'
group: django group object
group: A group name
group_title: what to call this group in messages to user--e.g. "beta-testers".
event_name: what to call this event when logging to tracking logs.
Returns:
html to insert in the message field
"""
return _add_or_remove_user_group(request, username_or_email, group, group_title, event_name, True)
username_or_email = strip_if_string(username_or_email)
try:
user = _user_from_name_or_email(username_or_email)
except User.DoesNotExist:
return u'<font color="red">Error: unknown username or email "{0}"</font>'.format(username_or_email)
role.add_users(user)
# Deal with historical event names
if event_name in ('staff', 'beta-tester'):
track.views.server_track(
request,
"add-or-remove-user-group",
{
"event_name": event_name,
"user": unicode(user),
"event": "add"
},
page="idashboard"
)
else:
track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard")
return '<font color="green">Added {0} to {1}</font>'.format(user, group_title)
def remove_user_from_group(request, username_or_email, group, group_title, event_name):
def remove_user_from_role(request, username_or_email, role, group_title, event_name):
"""
Look up the given user by username (if no '@') or email (otherwise), and remove them from group.
Look up the given user by username (if no '@') or email (otherwise), and remove them from the supplied role.
Arguments:
request: django request--used for tracking log
username_or_email: who to remove. Decide if it's an email by presense of an '@'
group: django group object
role: A courseware.roles.AccessRole
group_title: what to call this group in messages to user--e.g. "beta-testers".
event_name: what to call this event when logging to tracking logs.
Returns:
html to insert in the message field
"""
return _add_or_remove_user_group(request, username_or_email, group, group_title, event_name, False)
username_or_email = strip_if_string(username_or_email)
try:
user = _user_from_name_or_email(username_or_email)
except User.DoesNotExist:
return u'<font color="red">Error: unknown username or email "{0}"</font>'.format(username_or_email)
role.remove_users(user)
# Deal with historical event names
if event_name in ('staff', 'beta-tester'):
track.views.server_track(
request,
"add-or-remove-user-group",
{
"event_name": event_name,
"user": unicode(user),
"event": "remove"
},
page="idashboard"
)
else:
track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard")
return '<font color="green">Removed {0} from {1}</font>'.format(user, group_title)
def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False):
......
......@@ -8,7 +8,7 @@ import json
import logging
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from mock import MagicMock, patch, Mock
......@@ -22,11 +22,11 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
from xmodule.tests import test_util_open_ended
from courseware.access import _course_staff_group_name
from courseware.tests import factories
from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code, check_for_post_code
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from lms.lib.xblock.runtime import LmsModuleSystem
from courseware.roles import CourseStaffRole
from mitxmako.shortcuts import render_to_string
from student.models import unique_id_for_user
......@@ -52,9 +52,7 @@ def make_instructor(course, user_email):
"""
Makes a given user an instructor in a course.
"""
group_name = _course_staff_group_name(course.location)
group = Group.objects.create(name=group_name)
group.user_set.add(User.objects.get(email=user_email))
CourseStaffRole(course.location).add_users(User.objects.get(email=user_email))
class StudentProblemListMockQuery(object):
......
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