roles.py 11.2 KB
Newer Older
1 2 3 4 5 6 7
"""
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

8
from django.contrib.auth.models import User
9
import logging
Julia Hansbrough committed
10

Julia Hansbrough committed
11
from student.models import CourseAccessRole
12
from xmodule_django.models import CourseKeyField
13 14


15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
log = logging.getLogger(__name__)

# A list of registered access roles.
REGISTERED_ACCESS_ROLES = {}


def register_access_role(cls):
    """
    Decorator that allows access roles to be registered within the roles module and referenced by their
    string values.

    Assumes that the decorated class has a "ROLE" attribute, defining its type.

    """
    try:
        role_name = getattr(cls, 'ROLE')
        REGISTERED_ACCESS_ROLES[role_name] = cls
    except AttributeError:
        log.exception(u"Unable to register Access Role with attribute 'ROLE'.")
    return cls


37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
class RoleCache(object):
    """
    A cache of the CourseAccessRoles held by a particular user
    """
    def __init__(self, user):
        self._roles = set(
            CourseAccessRole.objects.filter(user=user).all()
        )

    def has_role(self, role, course_id, org):
        """
        Return whether this RoleCache contains a role with the specified role, course_id, and org
        """
        return any(
            access_role.role == role and
            access_role.course_id == course_id and
            access_role.org == org
            for access_role in self._roles
        )


58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
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:
102
            if (user.is_authenticated() and user.is_active):
103 104
                user.is_staff = True
                user.save()
105 106 107

    def remove_users(self, *users):
        for user in users:
108
            # don't check is_authenticated nor is_active on purpose
109 110 111 112 113 114 115
            user.is_staff = False
            user.save()

    def users_with_role(self):
        raise Exception("This operation is un-indexed, and shouldn't be used")


116
class RoleBase(AccessRole):
117
    """
118
    Roles by type (e.g., instructor, beta_user) and optionally org, course_key
119
    """
120
    def __init__(self, role_name, org='', course_key=None):
121
        """
122 123 124 125 126 127
        Create role from required role_name w/ optional org and course_key. You may just provide a role
        name if it's a global role (not constrained to an org or course). Provide org if constrained to
        an org. Provide org and course if constrained to a course. Although, you should use the subclasses
        for all of these.
        """
        super(RoleBase, self).__init__()
128

129 130 131
        self.org = org
        self.course_key = course_key
        self._role_name = role_name
132 133 134 135 136

    def has_user(self, user):
        """
        Return whether the supplied django user has access to this role.
        """
137
        if not (user.is_authenticated() and user.is_active):
138 139
            return False

140
        # pylint: disable=protected-access
141
        if not hasattr(user, '_roles'):
142 143 144
            # Cache a list of tuples identifying the particular roles that a user has
            # Stored as tuples, rather than django models, to make it cheaper to construct objects for comparison
            user._roles = RoleCache(user)
145

146
        return user._roles.has_role(self._role_name, self.course_key, self.org)
147 148 149 150 151

    def add_users(self, *users):
        """
        Add the supplied django users to this role.
        """
152 153
        # silently ignores anonymous and inactive users so that any that are
        # legit get updated.
Julia Hansbrough committed
154
        from student.models import CourseAccessRole
155
        for user in users:
156 157 158 159 160
            if user.is_authenticated and user.is_active and not self.has_user(user):
                entry = CourseAccessRole(user=user, role=self._role_name, course_id=self.course_key, org=self.org)
                entry.save()
                if hasattr(user, '_roles'):
                    del user._roles
161 162 163 164 165

    def remove_users(self, *users):
        """
        Remove the supplied django users from this role.
        """
166 167 168 169
        entries = CourseAccessRole.objects.filter(
            user__in=users, role=self._role_name, org=self.org, course_id=self.course_key
        )
        entries.delete()
170
        for user in users:
171 172
            if hasattr(user, '_roles'):
                del user._roles
173 174 175 176 177

    def users_with_role(self):
        """
        Return a django QuerySet for all of the users with this role
        """
178 179 180
        # Org roles don't query by CourseKey, so use CourseKeyField.Empty for that query
        if self.course_key is None:
            self.course_key = CourseKeyField.Empty
181 182 183 184 185 186
        entries = User.objects.filter(
            courseaccessrole__role=self._role_name,
            courseaccessrole__org=self.org,
            courseaccessrole__course_id=self.course_key
        )
        return entries
187 188


189
class CourseRole(RoleBase):
190 191 192
    """
    A named role in a particular course
    """
193
    def __init__(self, role, course_key):
194
        """
195 196
        Args:
            course_key (CourseKey)
197
        """
198 199 200 201 202 203 204 205
        super(CourseRole, self).__init__(role, course_key.org, course_key)

    @classmethod
    def course_group_already_exists(self, course_key):
        return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists()


class OrgRole(RoleBase):
206
    """
207
    A named role in a particular org independent of course
208
    """
209 210
    def __init__(self, role, org):
        super(OrgRole, self).__init__(role, org)
211 212


213
@register_access_role
214 215
class CourseStaffRole(CourseRole):
    """A Staff member of a course"""
216
    ROLE = 'staff'
217

218
    def __init__(self, *args, **kwargs):
219
        super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs)
220 221


222
@register_access_role
223 224
class CourseInstructorRole(CourseRole):
    """A course Instructor"""
225
    ROLE = 'instructor'
226

227
    def __init__(self, *args, **kwargs):
228
        super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs)
229 230


231
@register_access_role
232
class CourseFinanceAdminRole(CourseRole):
stephensanchez committed
233
    """A course staff member with privileges to review financial data."""
234 235 236 237
    ROLE = 'finance_admin'

    def __init__(self, *args, **kwargs):
        super(CourseFinanceAdminRole, self).__init__(self.ROLE, *args, **kwargs)
stephensanchez committed
238 239


240
@register_access_role
stephensanchez committed
241 242 243 244 245 246
class CourseSalesAdminRole(CourseRole):
    """A course staff member with privileges to perform sales operations. """
    ROLE = 'sales_admin'

    def __init__(self, *args, **kwargs):
        super(CourseSalesAdminRole, self).__init__(self.ROLE, *args, **kwargs)
247

248

249
@register_access_role
250 251
class CourseBetaTesterRole(CourseRole):
    """A course Beta Tester"""
252
    ROLE = 'beta_testers'
253

254
    def __init__(self, *args, **kwargs):
255
        super(CourseBetaTesterRole, self).__init__(self.ROLE, *args, **kwargs)
256 257


258
@register_access_role
259 260 261 262 263 264 265 266 267 268 269
class LibraryUserRole(CourseRole):
    """
    A user who can view a library and import content from it, but not edit it.
    Used in Studio only.
    """
    ROLE = 'library_user'

    def __init__(self, *args, **kwargs):
        super(LibraryUserRole, self).__init__(self.ROLE, *args, **kwargs)


cewing committed
270 271 272
class CourseCcxCoachRole(CourseRole):
    """A CCX Coach"""
    ROLE = 'ccx_coach'
273 274

    def __init__(self, *args, **kwargs):
cewing committed
275
        super(CourseCcxCoachRole, self).__init__(self.ROLE, *args, **kwargs)
276 277


278 279 280 281 282 283 284
class OrgStaffRole(OrgRole):
    """An organization staff member"""
    def __init__(self, *args, **kwargs):
        super(OrgStaffRole, self).__init__('staff', *args, **kwargs)


class OrgInstructorRole(OrgRole):
285
    """An organization instructor"""
286
    def __init__(self, *args, **kwargs):
287
        super(OrgInstructorRole, self).__init__('instructor', *args, **kwargs)
288 289


290 291 292 293 294 295 296 297 298 299 300
class OrgLibraryUserRole(OrgRole):
    """
    A user who can view any libraries in an org and import content from them, but not edit them.
    Used in Studio only.
    """
    ROLE = LibraryUserRole.ROLE

    def __init__(self, *args, **kwargs):
        super(OrgLibraryUserRole, self).__init__(self.ROLE, *args, **kwargs)


301
@register_access_role
302
class CourseCreatorRole(RoleBase):
303 304 305 306 307
    """
    This is the group of people who have permission to create new courses (we may want to eventually
    make this an org based role).
    """
    ROLE = "course_creator_group"
308

309
    def __init__(self, *args, **kwargs):
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
        super(CourseCreatorRole, self).__init__(self.ROLE, *args, **kwargs)


class UserBasedRole(object):
    """
    Backward mapping: given a user, manipulate the courses and roles
    """
    def __init__(self, user, role):
        """
        Create a UserBasedRole accessor: for a given user and role (e.g., "instructor")
        """
        self.user = user
        self.role = role

    def has_course(self, course_key):
        """
        Return whether the role's user has the configured role access to the passed course
        """
        if not (self.user.is_authenticated() and self.user.is_active):
            return False

        # pylint: disable=protected-access
        if not hasattr(self.user, '_roles'):
333
            self.user._roles = RoleCache(self.user)
334

335
        return self.user._roles.has_role(self.role, course_key, course_key.org)
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368

    def add_course(self, *course_keys):
        """
        Grant this object's user the object's role for the supplied courses
        """
        if self.user.is_authenticated and self.user.is_active:
            for course_key in course_keys:
                entry = CourseAccessRole(user=self.user, role=self.role, course_id=course_key, org=course_key.org)
                entry.save()
            if hasattr(self.user, '_roles'):
                del self.user._roles
        else:
            raise ValueError("user is not active. Cannot grant access to courses")

    def remove_courses(self, *course_keys):
        """
        Remove the supplied courses from this user's configured role.
        """
        entries = CourseAccessRole.objects.filter(user=self.user, role=self.role, course_id__in=course_keys)
        entries.delete()
        if hasattr(self.user, '_roles'):
            del self.user._roles

    def courses_with_role(self):
        """
        Return a django QuerySet for all of the courses with this user x role. You can access
        any of these properties on each result record:
        * user (will be self.user--thus uninteresting)
        * org
        * course_id
        * role (will be self.role--thus uninteresting)
        """
        return CourseAccessRole.objects.filter(role=self.role, user=self.user)