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)
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 ")