def anonymous_id_for_user(user, course_id, save=True):
    """
    Return a unique id for a (user, course) pair, suitable for inserting
    into e.g. personalized survey links.

    If user is an `AnonymousUser`, returns `None`

    Keyword arguments:
    save -- Whether the id should be saved in an AnonymousUserId object.
    """
    # This part is for ability to get xblock instance in xblock_noauth handlers, where user is unauthenticated.
    if user.is_anonymous():
        return None

    cached_id = getattr(user, '_anonymous_id', {}).get(course_id)
    if cached_id is not None:
        return cached_id

    # include the secret key as a salt, and to make the ids unique across different LMS installs.
    hasher = hashlib.md5()
    hasher.update(settings.SECRET_KEY)
    hasher.update(unicode(user.id))
    if course_id:
        hasher.update(course_id.to_deprecated_string().encode('utf-8'))
    digest = hasher.hexdigest()

    if not hasattr(user, '_anonymous_id'):
        user._anonymous_id = {}  # pylint: disable=protected-access

    user._anonymous_id[course_id] = digest  # pylint: disable=protected-access

    if save is False:
        return digest

    try:
        anonymous_user_id, __ = AnonymousUserId.objects.get_or_create(
            defaults={'anonymous_user_id': digest},
            user=user,
            course_id=course_id
        )
        if anonymous_user_id.anonymous_user_id != digest:
            log.error(
                u"Stored anonymous user id %r for user %r "
                u"in course %r doesn't match computed id %r",
                user,
                course_id,
                anonymous_user_id.anonymous_user_id,
                digest
            )
    except IntegrityError:
        # Another thread has already created this entry, so
        # continue
        pass

    return digest