Beispiel #1
0
class RestrictedCourse(models.Model):
    """Course with access restrictions.

    Restricted courses can block users at two points:

    1) When enrolling in a course.
    2) When attempting to access a course the user is already enrolled in.

    The second case can occur when new restrictions
    are put into place; for example, when new countries
    are embargoed.

    Restricted courses can be configured to display
    messages to users when they are blocked.
    These displayed on pages served by the embargo app.

    """
    ENROLL_MSG_KEY_CHOICES = tuple([
        (msg_key, msg.description)
        for msg_key, msg in ENROLL_MESSAGES.iteritems()
    ])

    COURSEWARE_MSG_KEY_CHOICES = tuple([
        (msg_key, msg.description)
        for msg_key, msg in COURSEWARE_MESSAGES.iteritems()
    ])

    course_key = CourseKeyField(
        max_length=255, db_index=True, unique=True,
        help_text=ugettext_lazy(u"The course key for the restricted course.")
    )

    enroll_msg_key = models.CharField(
        max_length=255,
        choices=ENROLL_MSG_KEY_CHOICES,
        default='default',
        help_text=ugettext_lazy(u"The message to show when a user is blocked from enrollment.")
    )

    access_msg_key = models.CharField(
        max_length=255,
        choices=COURSEWARE_MSG_KEY_CHOICES,
        default='default',
        help_text=ugettext_lazy(u"The message to show when a user is blocked from accessing a course.")
    )

    def __unicode__(self):
        return unicode(self.course_key)
Beispiel #2
0
class RestrictedCourse(models.Model):
    """Course with access restrictions.

    Restricted courses can block users at two points:

    1) When enrolling in a course.
    2) When attempting to access a course the user is already enrolled in.

    The second case can occur when new restrictions
    are put into place; for example, when new countries
    are embargoed.

    Restricted courses can be configured to display
    messages to users when they are blocked.
    These displayed on pages served by the embargo app.

    """
    COURSE_LIST_CACHE_KEY = 'embargo.restricted_courses'
    MESSAGE_URL_CACHE_KEY = 'embargo.message_url_path.{access_point}.{course_key}'

    ENROLL_MSG_KEY_CHOICES = tuple([
        (msg_key, msg.description)
        for msg_key, msg in ENROLL_MESSAGES.iteritems()
    ])

    COURSEWARE_MSG_KEY_CHOICES = tuple([
        (msg_key, msg.description)
        for msg_key, msg in COURSEWARE_MESSAGES.iteritems()
    ])

    course_key = CourseKeyField(
        max_length=255,
        db_index=True,
        unique=True,
        help_text=ugettext_lazy(u"The course key for the restricted course."))

    enroll_msg_key = models.CharField(
        max_length=255,
        choices=ENROLL_MSG_KEY_CHOICES,
        default='default',
        help_text=ugettext_lazy(
            u"The message to show when a user is blocked from enrollment."))

    access_msg_key = models.CharField(
        max_length=255,
        choices=COURSEWARE_MSG_KEY_CHOICES,
        default='default',
        help_text=ugettext_lazy(
            u"The message to show when a user is blocked from accessing a course."
        ))

    disable_access_check = models.BooleanField(
        default=False,
        help_text=ugettext_lazy(
            u"Allow users who enrolled in an allowed country "
            u"to access restricted courses from excluded countries."))

    @classmethod
    def is_restricted_course(cls, course_id):
        """
        Check if the course is in restricted list

        Args:
            course_id (str): course_id to look for

        Returns:
            Boolean
            True if course is in restricted course list.
        """
        return unicode(course_id) in cls._get_restricted_courses_from_cache()

    @classmethod
    def is_disabled_access_check(cls, course_id):
        """
        Check if the course is in restricted list has disabled_access_check

        Args:
            course_id (str): course_id to look for

        Returns:
            Boolean
            disabled_access_check attribute of restricted course
        """

        # checking is_restricted_course method also here to make sure course exists in the list otherwise in case of
        # no course found it will throw the key not found error on 'disable_access_check'
        return (cls.is_restricted_course(unicode(course_id))
                and cls._get_restricted_courses_from_cache().get(
                    unicode(course_id))["disable_access_check"])

    @classmethod
    def _get_restricted_courses_from_cache(cls):
        """
        Cache all restricted courses and returns the dict of course_keys and disable_access_check that are restricted
        """
        restricted_courses = cache.get(cls.COURSE_LIST_CACHE_KEY)
        if restricted_courses is None:
            restricted_courses = {
                unicode(course.course_key): {
                    'disable_access_check': course.disable_access_check
                }
                for course in RestrictedCourse.objects.all()
            }
            cache.set(cls.COURSE_LIST_CACHE_KEY, restricted_courses)
        return restricted_courses

    def snapshot(self):
        """Return a snapshot of all access rules for this course.

        This is useful for recording an audit trail of rule changes.
        The returned dictionary is JSON-serializable.

        Returns:
            dict

        Example Usage:
        >>> restricted_course.snapshot()
        {
            'enroll_msg': 'default',
            'access_msg': 'default',
            'country_rules': [
                {'country': 'IR', 'rule_type': 'blacklist'},
                {'country': 'CU', 'rule_type': 'blacklist'}
            ]
        }

        """
        country_rules_for_course = (
            CountryAccessRule.objects).select_related('country').filter(
                restricted_course=self)

        return {
            'enroll_msg':
            self.enroll_msg_key,
            'access_msg':
            self.access_msg_key,
            'country_rules': [{
                'country': unicode(rule.country.country),
                'rule_type': rule.rule_type
            } for rule in country_rules_for_course]
        }

    def message_key_for_access_point(self, access_point):
        """Determine which message to show the user.

        The message can be configured per-course and depends
        on how the user is trying to access the course
        (trying to enroll or accessing courseware).

        Arguments:
            access_point (str): Either "courseware" or "enrollment"

        Returns:
            str: The message key.  If the access point is not valid,
                returns None instead.

        """
        if access_point == 'enrollment':
            return self.enroll_msg_key
        elif access_point == 'courseware':
            return self.access_msg_key

    def __unicode__(self):
        return unicode(self.course_key)

    @classmethod
    def message_url_path(cls, course_key, access_point):
        """Determine the URL path for the message explaining why the user was blocked.

        This is configured per-course.  See `RestrictedCourse` in the `embargo.models`
        module for more details.

        Arguments:
            course_key (CourseKey): The location of the course.
            access_point (str): How the user was trying to access the course.
                Can be either "enrollment" or "courseware".

        Returns:
            unicode: The URL path to a page explaining why the user was blocked.

        Raises:
            InvalidAccessPoint: Raised if access_point is not a supported value.

        """
        if access_point not in ['enrollment', 'courseware']:
            raise InvalidAccessPoint(access_point)

        # First check the cache to see if we already have
        # a URL for this (course_key, access_point) tuple
        cache_key = cls.MESSAGE_URL_CACHE_KEY.format(access_point=access_point,
                                                     course_key=course_key)
        url = cache.get(cache_key)

        # If there's a cache miss, we'll need to retrieve the message
        # configuration from the database
        if url is None:
            url = cls._get_message_url_path_from_db(course_key, access_point)
            cache.set(cache_key, url)

        return url

    @classmethod
    def _get_message_url_path_from_db(cls, course_key, access_point):
        """Retrieve the "blocked" message from the database.

        Arguments:
            course_key (CourseKey): The location of the course.
            access_point (str): How the user was trying to access the course.
                Can be either "enrollment" or "courseware".

        Returns:
            unicode: The URL path to a page explaining why the user was blocked.

        """
        # Fallback in case we're not able to find a message path
        # Presumably if the caller is requesting a URL, the caller
        # has already determined that the user should be blocked.
        # We use generic messaging unless we find something more specific,
        # but *always* return a valid URL path.
        default_path = reverse('embargo_blocked_message',
                               kwargs={
                                   'access_point': 'courseware',
                                   'message_key': 'default'
                               })

        # First check whether this is a restricted course.
        # The list of restricted courses is cached, so this does
        # not require a database query.
        if not cls.is_restricted_course(course_key):
            return default_path

        # Retrieve the message key from the restricted course
        # for this access point, then determine the URL.
        try:
            course = cls.objects.get(course_key=course_key)
            msg_key = course.message_key_for_access_point(access_point)
            return reverse('embargo_blocked_message',
                           kwargs={
                               'access_point': access_point,
                               'message_key': msg_key
                           })
        except cls.DoesNotExist:
            # This occurs only if there's a race condition
            # between cache invalidation and database access.
            return default_path

    @classmethod
    def invalidate_cache_for_course(cls, course_key):
        """Invalidate the caches for the restricted course. """
        cache.delete(cls.COURSE_LIST_CACHE_KEY)
        log.info("Invalidated cached list of restricted courses.")

        for access_point in ['enrollment', 'courseware']:
            msg_cache_key = cls.MESSAGE_URL_CACHE_KEY.format(
                access_point=access_point, course_key=course_key)
            cache.delete(msg_cache_key)
        log.info("Invalidated cached messaging URLs ")