<reponame>asterisk/reviewboard<filename>reviewboard/reviews/templatetags/reviewtags.py
from django import template
from django.conf import settings
from django.db.models import Q
from django.template import NodeList, TemplateSyntaxError
from django.template.loader import render_to_string
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from djblets.util.decorators import basictag, blocktag
from djblets.util.misc import get_object_or_none
from djblets.util.templatetags.djblets_utils import humanize_list

from reviewboard.accounts.models import Profile
from reviewboard.diffviewer.models import DiffSet
from reviewboard.reviews.models import Comment, Group, ReviewRequest, \
                                       ScreenshotComment


register = template.Library()


@register.tag
@blocktag
def forcomment(context, nodelist, filediff, review=None):
    """
    Loops over a list of comments beloning to a filediff.

    This will populate a special ``comment`` variable for use in the content.
    This is of the type :model:`reviews.Comment`.
    """
    new_nodelist = NodeList()
    context.push()

    if not review:
        comments = filediff.comments.all()
    else:
        comments = filediff.comments.filter(review=review)

    for comment in comments:
        context['comment'] = comment

        for node in nodelist:
            new_nodelist.append(node.render(context))

    context.pop()
    return new_nodelist.render(context)


@register.tag
@blocktag
def ifneatnumber(context, nodelist, rid):
    """
    Returns whether or not the specified number is a "neat" number.
    This is a number with a special property, such as being a
    palindrome or having trailing zeroes.

    If the number is a neat number, the contained content is rendered,
    and two variables, ``milestone`` and ``palindrome`` are defined.
    """
    if rid == None or rid < 1000:
        return ""

    ridstr = str(rid)
    interesting = False

    context.push()
    context['milestone'] = False
    context['palindrome'] = False

    if rid >= 1000:
        trailing = ridstr[1:]
        if trailing == "0" * len(trailing):
            context['milestone'] = True
            interesting = True

    if not interesting:
        if ridstr == ''.join(reversed(ridstr)):
            context['palindrome'] = True
            interesting = True

    if not interesting:
        context.pop()
        return ""

    s = nodelist.render(context)
    context.pop()
    return s


@register.tag
@basictag(takes_context=True)
def commentcounts(context, filediff, interfilediff=None):
    """
    Returns a JSON array of current comments for a filediff, sorted by
    line number.

    Each entry in the array has a dictionary containing the following keys:

      =========== ==================================================
      Key         Description
      =========== ==================================================
      comment_id  The ID of the comment
      text        The text of the comment
      line        The first line number
      num_lines   The number of lines this comment spans
      user        A dictionary containing "username" and "name" keys
                  for the user
      url         The URL to the comment
      localdraft  True if this is the current user's draft comment
      =========== ==================================================
    """
    comment_dict = {}
    user = context.get('user', None)

    if interfilediff:
        query = Comment.objects.filter(filediff=filediff,
                                       interfilediff=interfilediff)
    else:
        query = Comment.objects.filter(filediff=filediff,
                                       interfilediff__isnull=True)

    for comment in query:
        review = get_object_or_none(comment.review)

        if review and (review.public or review.user == user):
            key = (comment.first_line, comment.num_lines)

            comment_dict.setdefault(key, []).append({
                'comment_id': comment.id,
                'text': comment.text,
                'line': comment.first_line,
                'num_lines': comment.num_lines,
                'user': {
                    'username': review.user.username,
                    'name': review.user.get_full_name() or review.user.username,
                },
                #'timestamp': comment.timestamp,
                'url': comment.get_review_url(),
                'localdraft': review.user == user and \
                              not review.public,
            })

    comments_array = []

    for key, value in comment_dict.iteritems():
        comments_array.append({
            'linenum': key[0],
            'num_lines': key[1],
            'comments': value,
        })

    comments_array.sort(cmp=lambda x, y: cmp(x['linenum'], y['linenum'] or
                                         cmp(x['num_lines'], y['num_lines'])))

    return simplejson.dumps(comments_array)


@register.tag
@basictag(takes_context=True)
def screenshotcommentcounts(context, screenshot):
    """
    Returns a JSON array of current comments for a screenshot.

    Each entry in the array has a dictionary containing the following keys:

      =========== ==================================================
      Key         Description
      =========== ==================================================
      text        The text of the comment
      localdraft  True if this is the current user's draft comment
      x           The X location of the comment's region
      y           The Y location of the comment's region
      w           The width of the comment's region
      h           The height of the comment's region
      =========== ==================================================
    """
    comments = {}
    user = context.get('user', None)

    for comment in screenshot.comments.all():
        review = get_object_or_none(comment.review)

        if review and (review.public or review.user == user):
            position = '%dx%d+%d+%d' % (comment.w, comment.h, \
                                        comment.x, comment.y)

            comments.setdefault(position, []).append({
                'id': comment.id,
                'text': comment.text,
                'user': {
                    'username': review.user.username,
                    'name': review.user.get_full_name() or review.user.username,
                },
                'url': comment.get_review_url(),
                'localdraft' : review.user == user and \
                               not review.public,
                'x' : comment.x,
                'y' : comment.y,
                'w' : comment.w,
                'h' : comment.h,
            })

    return simplejson.dumps(comments)


@register.tag
@basictag(takes_context=True)
def reply_list(context, review, comment, context_type, context_id):
    """
    Renders a list of comments of a specified type.

    This is a complex, confusing function accepts lots of inputs in order
    to display replies to a type of object. In each case, the replies will
    be rendered using the template :template:`reviews/review_reply.html`.

    If ``context_type`` is ``"comment"`` or ``"screenshot_comment"``,
    the generated list of replies are to ``comment``.

    If ``context_type`` is ``"body_top"`` or ```"body_bottom"``,
    the generated list of replies are to ``review``. Depending on the
    ``context_type``, these will either be replies to the top of the
    review body or to the bottom.

    The ``context_id`` parameter has to do with the internal IDs used by
    the JavaScript code for storing and categorizing the comments.
    """
    def generate_reply_html(reply, timestamp, text):
        return render_to_string('reviews/review_reply.html', {
            'context_id': context_id,
            'id': reply.id,
            'review': review,
            'timestamp': timestamp,
            'text': text,
            'reply_user': reply.user,
            'draft': not reply.public
        })

    def process_body_replies(queryset, attrname, user):
        if user.is_anonymous():
            queryset = queryset.filter(public=True)
        else:
            queryset = queryset.filter(Q(public=True) | Q(user=user))

        s = ""
        for reply_comment in queryset:
            s += generate_reply_html(reply, reply.timestamp,
                                     getattr(reply, attrname))

        return s

    user = context.get('user', None)
    if user.is_anonymous():
        user = None

    s = ""

    if context_type == "comment" or context_type == "screenshot_comment":
        for reply_comment in comment.public_replies(user):
            s += generate_reply_html(reply_comment.review.get(),
                                     reply_comment.timestamp,
                                     reply_comment.text)
    elif context_type == "body_top" or context_type == "body_bottom":
        q = Q(public=True)

        if user:
            q = q | Q(user=user)

        replies = getattr(review, "%s_replies" % context_type).filter(q)

        for reply in replies:
            s += generate_reply_html(reply, reply.timestamp,
                                     getattr(reply, context_type))

        return s
    else:
        raise TemplateSyntaxError, "Invalid context type passed"

    return s


@register.inclusion_tag('reviews/review_reply_section.html',
                        takes_context=True)
def reply_section(context, review, comment, context_type, context_id):
    """
    Renders a template for displaying a reply.

    This takes the same parameters as :tag:`reply_list`. The template
    rendered by this function, :template:`reviews/review_reply_section.html`,
    is responsible for invoking :tag:`reply_list` and as such passes these
    variables through. It does not make use of them itself.
    """
    if comment != "":
        if type(comment) is ScreenshotComment:
            context_id += 's'
        context_id += str(comment.id)

    return {
        'review': review,
        'comment': comment,
        'context_type': context_type,
        'context_id': context_id,
        'user': context.get('user', None)
    }


@register.inclusion_tag('reviews/dashboard_entry.html', takes_context=True)
def dashboard_entry(context, level, text, view, group=None):
    """
    Renders an entry in the dashboard sidebar.

    This includes the name of the entry and the list of review requests
    associated with it. The entry is rendered by the template
    :template:`reviews/dashboard_entry.html`.
    """
    user = context.get('user', None)
    datagrid = context.get('datagrid', None)
    starred = False
    show_count = True
    count = 0

    if view == 'to-group':
        count = datagrid.counts['groups'].get(group.name, 0)
    elif view == 'watched-groups':
        starred = True
        show_count = False
    elif view in datagrid.counts:
        count = datagrid.counts[view]

        if view == 'starred':
            starred = True
    else:
        raise template.TemplateSyntaxError, \
            "Invalid view type '%s' passed to 'dashboard_entry' tag." % view

    return {
        'MEDIA_URL': settings.MEDIA_URL,
        'MEDIA_SERIAL': settings.MEDIA_SERIAL,
        'level': level,
        'text': text,
        'view': view,
        'group': group,
        'count': count,
        'show_count': show_count,
        'user': user,
        'starred': starred,
        'selected': context.get('view', None) == view and \
                    (not group or context.get('group', None) == group.name),
    }


@register.simple_tag
def reviewer_list(review_request):
    """
    Returns a humanized list of target reviewers in a review request.
    """
    return humanize_list([group.display_name or group.name \
                          for group in review_request.target_groups.all()] + \
                         [user.get_full_name() or user.username \
                          for user  in review_request.target_people.all()])


@register.filter
def bug_url(bug_id, review_request):
    """
    Returns the URL based on a bug number on the specified review request.

    If the repository the review request belongs to doesn't have an
    associated bug tracker, this returns None.
    """
    if (review_request.repository and
        review_request.repository.bug_tracker and
        '%s' in review_request.repository.bug_tracker):
        return review_request.repository.bug_tracker % bug_id

    return None


@register.filter
def diffsets_with_comments(review, current_pair):
    """
    Returns a list of diffsets in the review that contain draft comments.
    """
    if not review:
        return

    diffsets = DiffSet.objects.filter(files__comments__review=review)
    diffsets = diffsets.filter(files__comments__interfilediff__isnull=True)
    diffsets = diffsets.distinct()

    for diffset in diffsets:
        yield {
            'diffset': diffset,
            'is_current': current_pair[0] == diffset and
                          current_pair[1] == None,
        }


@register.filter
def interdiffs_with_comments(review, current_pair):
    """
    Returns a list of interdiffs in the review that contain draft comments.
    """
    if not review:
        return

    diffsets = DiffSet.objects.filter(files__comments__review=review)
    diffsets = diffsets.filter(files__comments__interfilediff__isnull=False)
    diffsets = diffsets.distinct()

    for diffset in diffsets:
        interdiffs = DiffSet.objects.filter(
            files__interdiff_comments__filediff__diffset=diffset).distinct()

        for interdiff in interdiffs:
            yield {
                'diffset': diffset,
                'interdiff': interdiff,
                'is_current': current_pair[0] == diffset and
                              current_pair[1] == interdiff,
            }


@register.filter
def has_comments_in_diffsets_excluding(review, diffset_pair):
    """
    Returns whether or not the specified review has any comments that
    aren't in the specified diffset or interdiff.
    """
    if not review:
        return False

    current_diffset, interdiff = diffset_pair

    # See if there are any diffsets with comments on them in this review.
    q = DiffSet.objects.filter(files__comments__review=review)
    q = q.filter(files__comments__interfilediff__isnull=True).distinct()

    if not interdiff:
        # The user is browsing a standard diffset, so filter it out.
        q = q.exclude(pk=current_diffset.id)

    if q.count() > 0:
        return True

    # See if there are any interdiffs with comments on them in this review.
    q = DiffSet.objects.filter(files__comments__review=review)
    q = q.filter(files__comments__interfilediff__isnull=False)

    if interdiff:
        # The user is browsing an interdiff, so filter it out.
        q = q.exclude(pk=current_diffset.id,
                      files__comments__interfilediff__diffset=interdiff)

    return q.count() > 0


@register.tag
@basictag(takes_context=True)
def star(context, obj):
    """
    Renders the code for displaying a star used for starring items.

    The rendered code should handle click events so that the user can
    toggle the star. The star is rendered by the template
    :template:`reviews/star.html`.

    The passed object must be either a :model:`reviews.ReviewRequest` or
    a :model:`reviews.Group`.
    """
    return render_star(context.get('user', None), obj)


def render_star(user, obj):
    """
    Does the actual work of rendering the star. The star tag is a wrapper
    around this.
    """
    if user.is_anonymous():
        return ""

    profile = None

    if not hasattr(obj, 'starred'):
        try:
            profile = user.get_profile()
        except Profile.DoesNotExist:
            return ""

    if isinstance(obj, ReviewRequest):
        obj_info = {
            'type': 'reviewrequests',
            'id': obj.id
        }

        if hasattr(obj, 'starred'):
            starred = obj.starred
        else:
            starred = \
                profile.starred_review_requests.filter(pk=obj.id).count() > 0
    elif isinstance(obj, Group):
        obj_info = {
            'type': 'groups',
            'id': obj.name
        }

        if hasattr(obj, 'starred'):
            starred = obj.starred
        else:
            starred = \
                profile.starred_groups.filter(pk=obj.id).count() > 0
    else:
        raise template.TemplateSyntaxError, \
            "star tag received an incompatible object type (%s)" % \
            type(obj)

    if starred:
        image_alt = _("Starred")
    else:
        image_alt = _("Click to star")

    return render_to_string('reviews/star.html', {
        'object': obj_info,
        'starred': int(starred),
        'alt': image_alt,
        'user': user,
        'MEDIA_URL': settings.MEDIA_URL,
    })

from django import template
from django.conf import settings
from django.db.models import Q
from django.template import NodeList, TemplateSyntaxError
from django.template.loader import render_to_string
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from djblets.util.decorators import basictag, blocktag
from djblets.util.misc import get_object_or_none
from djblets.util.templatetags.djblets_utils import humanize_list

from reviewboard.accounts.models import Profile
from reviewboard.diffviewer.models import DiffSet
from reviewboard.reviews.models import Comment, Group, ReviewRequest, \
                                       ScreenshotComment


register = template.Library()


@register.tag
@blocktag
def forcomment(context, nodelist, filediff, review=None):
    """
    Loops over a list of comments beloning to a filediff.

    This will populate a special ``comment`` variable for use in the content.
    This is of the type :model:`reviews.Comment`.
    """
    new_nodelist = NodeList()
    context.push()

    if not review:
        comments = filediff.comments.all()
    else:
        comments = filediff.comments.filter(review=review)

    for comment in comments:
        context['comment'] = comment

        for node in nodelist:
            new_nodelist.append(node.render(context))

    context.pop()
    return new_nodelist.render(context)


@register.tag
@blocktag
def ifneatnumber(context, nodelist, rid):
    """
    Returns whether or not the specified number is a "neat" number.
    This is a number with a special property, such as being a
    palindrome or having trailing zeroes.

    If the number is a neat number, the contained content is rendered,
    and two variables, ``milestone`` and ``palindrome`` are defined.
    """
    if rid == None or rid < 1000:
        return ""

    ridstr = str(rid)
    interesting = False

    context.push()
    context['milestone'] = False
    context['palindrome'] = False

    if rid >= 1000:
        trailing = ridstr[1:]
        if trailing == "0" * len(trailing):
            context['milestone'] = True
            interesting = True

    if not interesting:
        if ridstr == ''.join(reversed(ridstr)):
            context['palindrome'] = True
            interesting = True

    if not interesting:
        context.pop()
        return ""

    s = nodelist.render(context)
    context.pop()
    return s


@register.tag
@basictag(takes_context=True)
def commentcounts(context, filediff, interfilediff=None):
    """
    Returns a JSON array of current comments for a filediff, sorted by
    line number.

    Each entry in the array has a dictionary containing the following keys:

      =========== ==================================================
      Key         Description
      =========== ==================================================
      comment_id  The ID of the comment
      text        The text of the comment
      line        The first line number
      num_lines   The number of lines this comment spans
      user        A dictionary containing "username" and "name" keys
                  for the user
      url         The URL to the comment
      localdraft  True if this is the current user's draft comment
      =========== ==================================================
    """
    comment_dict = {}
    user = context.get('user', None)

    if interfilediff:
        query = Comment.objects.filter(filediff=filediff,
                                       interfilediff=interfilediff)
    else:
        query = Comment.objects.filter(filediff=filediff,
                                       interfilediff__isnull=True)

    for comment in query:
        review = get_object_or_none(comment.review)

        if review and (review.public or review.user == user):
            key = (comment.first_line, comment.num_lines)

            comment_dict.setdefault(key, []).append({
                'comment_id': comment.id,
                'text': comment.text,
                'line': comment.first_line,
                'num_lines': comment.num_lines,
                'user': {
                    'username': review.user.username,
                    'name': review.user.get_full_name() or review.user.username,
                },
                #'timestamp': comment.timestamp,
                'url': comment.get_review_url(),
                'localdraft': review.user == user and \
                              not review.public,
            })

    comments_array = []

    for key, value in comment_dict.iteritems():
        comments_array.append({
            'linenum': key[0],
            'num_lines': key[1],
            'comments': value,
        })

    comments_array.sort(cmp=lambda x, y: cmp(x['linenum'], y['linenum'] or
                                         cmp(x['num_lines'], y['num_lines'])))

    return simplejson.dumps(comments_array)


@register.tag
@basictag(takes_context=True)
def screenshotcommentcounts(context, screenshot):
    """
    Returns a JSON array of current comments for a screenshot.

    Each entry in the array has a dictionary containing the following keys:

      =========== ==================================================
      Key         Description
      =========== ==================================================
      text        The text of the comment
      localdraft  True if this is the current user's draft comment
      x           The X location of the comment's region
      y           The Y location of the comment's region
      w           The width of the comment's region
      h           The height of the comment's region
      =========== ==================================================
    """
    comments = {}
    user = context.get('user', None)

    for comment in screenshot.comments.all():
        review = get_object_or_none(comment.review)

        if review and (review.public or review.user == user):
            position = '%dx%d+%d+%d' % (comment.w, comment.h, \
                                        comment.x, comment.y)

            comments.setdefault(position, []).append({
                'id': comment.id,
                'text': comment.text,
                'user': {
                    'username': review.user.username,
                    'name': review.user.get_full_name() or review.user.username,
                },
                'url': comment.get_review_url(),
                'localdraft' : review.user == user and \
                               not review.public,
                'x' : comment.x,
                'y' : comment.y,
                'w' : comment.w,
                'h' : comment.h,
            })

    return simplejson.dumps(comments)


@register.tag
@basictag(takes_context=True)
def reply_list(context, review, comment, context_type, context_id):
    """
    Renders a list of comments of a specified type.

    This is a complex, confusing function accepts lots of inputs in order
    to display replies to a type of object. In each case, the replies will
    be rendered using the template :template:`reviews/review_reply.html`.

    If ``context_type`` is ``"comment"`` or ``"screenshot_comment"``,
    the generated list of replies are to ``comment``.

    If ``context_type`` is ``"body_top"`` or ```"body_bottom"``,
    the generated list of replies are to ``review``. Depending on the
    ``context_type``, these will either be replies to the top of the
    review body or to the bottom.

    The ``context_id`` parameter has to do with the internal IDs used by
    the JavaScript code for storing and categorizing the comments.
    """
    def generate_reply_html(reply, timestamp, text):
        return render_to_string('reviews/review_reply.html', {
            'context_id': context_id,
            'id': reply.id,
            'review': review,
            'timestamp': timestamp,
            'text': text,
            'reply_user': reply.user,
            'draft': not reply.public
        })

    def process_body_replies(queryset, attrname, user):
        if user.is_anonymous():
            queryset = queryset.filter(public=True)
        else:
            queryset = queryset.filter(Q(public=True) | Q(user=user))

        s = ""
        for reply_comment in queryset:
            s += generate_reply_html(reply, reply.timestamp,
                                     getattr(reply, attrname))

        return s

    user = context.get('user', None)
    if user.is_anonymous():
        user = None

    s = ""

    if context_type == "comment" or context_type == "screenshot_comment":
        for reply_comment in comment.public_replies(user):
            s += generate_reply_html(reply_comment.review.get(),
                                     reply_comment.timestamp,
                                     reply_comment.text)
    elif context_type == "body_top" or context_type == "body_bottom":
        q = Q(public=True)

        if user:
            q = q | Q(user=user)

        replies = getattr(review, "%s_replies" % context_type).filter(q)

        for reply in replies:
            s += generate_reply_html(reply, reply.timestamp,
                                     getattr(reply, context_type))

        return s
    else:
        raise TemplateSyntaxError, "Invalid context type passed"

    return s


@register.inclusion_tag('reviews/review_reply_section.html',
                        takes_context=True)
def reply_section(context, review, comment, context_type, context_id):
    """
    Renders a template for displaying a reply.

    This takes the same parameters as :tag:`reply_list`. The template
    rendered by this function, :template:`reviews/review_reply_section.html`,
    is responsible for invoking :tag:`reply_list` and as such passes these
    variables through. It does not make use of them itself.
    """
    if comment != "":
        if type(comment) is ScreenshotComment:
            context_id += 's'
        context_id += str(comment.id)

    return {
        'review': review,
        'comment': comment,
        'context_type': context_type,
        'context_id': context_id,
        'user': context.get('user', None)
    }


@register.inclusion_tag('reviews/dashboard_entry.html', takes_context=True)
def dashboard_entry(context, level, text, view, group=None):
    """
    Renders an entry in the dashboard sidebar.

    This includes the name of the entry and the list of review requests
    associated with it. The entry is rendered by the template
    :template:`reviews/dashboard_entry.html`.
    """
    user = context.get('user', None)
    datagrid = context.get('datagrid', None)
    starred = False
    show_count = True
    count = 0

    if view == 'to-group':
        count = datagrid.counts['groups'].get(group.name, 0)
    elif view == 'watched-groups':
        starred = True
        show_count = False
    elif view in datagrid.counts:
        count = datagrid.counts[view]

        if view == 'starred':
            starred = True
    else:
        raise template.TemplateSyntaxError, \
            "Invalid view type '%s' passed to 'dashboard_entry' tag." % view

    return {
        'MEDIA_URL': settings.MEDIA_URL,
        'MEDIA_SERIAL': settings.MEDIA_SERIAL,
        'level': level,
        'text': text,
        'view': view,
        'group': group,
        'count': count,
        'show_count': show_count,
        'user': user,
        'starred': starred,
        'selected': context.get('view', None) == view and \
                    (not group or context.get('group', None) == group.name),
    }


@register.simple_tag
def reviewer_list(review_request):
    """
    Returns a humanized list of target reviewers in a review request.
    """
    return humanize_list([group.display_name or group.name \
                          for group in review_request.target_groups.all()] + \
                         [user.get_full_name() or user.username \
                          for user  in review_request.target_people.all()])


@register.filter
def bug_url(bug_id, review_request):
    """
    Returns the URL based on a bug number on the specified review request.

    If the repository the review request belongs to doesn't have an
    associated bug tracker, this returns None.
    """
    if (review_request.repository and
        review_request.repository.bug_tracker and
        '%s' in review_request.repository.bug_tracker):
        return review_request.repository.bug_tracker % bug_id

    return None


@register.filter
def diffsets_with_comments(review, current_pair):
    """
    Returns a list of diffsets in the review that contain draft comments.
    """
    if not review:
        return

    diffsets = DiffSet.objects.filter(files__comments__review=review)
    diffsets = diffsets.filter(files__comments__interfilediff__isnull=True)
    diffsets = diffsets.distinct()

    for diffset in diffsets:
        yield {
            'diffset': diffset,
            'is_current': current_pair[0] == diffset and
                          current_pair[1] == None,
        }


@register.filter
def interdiffs_with_comments(review, current_pair):
    """
    Returns a list of interdiffs in the review that contain draft comments.
    """
    if not review:
        return

    diffsets = DiffSet.objects.filter(files__comments__review=review)
    diffsets = diffsets.filter(files__comments__interfilediff__isnull=False)
    diffsets = diffsets.distinct()

    for diffset in diffsets:
        interdiffs = DiffSet.objects.filter(
            files__interdiff_comments__filediff__diffset=diffset).distinct()

        for interdiff in interdiffs:
            yield {
                'diffset': diffset,
                'interdiff': interdiff,
                'is_current': current_pair[0] == diffset and
                              current_pair[1] == interdiff,
            }


@register.filter
def has_comments_in_diffsets_excluding(review, diffset_pair):
    """
    Returns whether or not the specified review has any comments that
    aren't in the specified diffset or interdiff.
    """
    if not review:
        return False

    current_diffset, interdiff = diffset_pair

    # See if there are any diffsets with comments on them in this review.
    q = DiffSet.objects.filter(files__comments__review=review)
    q = q.filter(files__comments__interfilediff__isnull=True).distinct()

    if not interdiff:
        # The user is browsing a standard diffset, so filter it out.
        q = q.exclude(pk=current_diffset.id)

    if q.count() > 0:
        return True

    # See if there are any interdiffs with comments on them in this review.
    q = DiffSet.objects.filter(files__comments__review=review)
    q = q.filter(files__comments__interfilediff__isnull=False)

    if interdiff:
        # The user is browsing an interdiff, so filter it out.
        q = q.exclude(pk=current_diffset.id,
                      files__comments__interfilediff__diffset=interdiff)

    return q.count() > 0


@register.tag
@basictag(takes_context=True)
def star(context, obj):
    """
    Renders the code for displaying a star used for starring items.

    The rendered code should handle click events so that the user can
    toggle the star. The star is rendered by the template
    :template:`reviews/star.html`.

    The passed object must be either a :model:`reviews.ReviewRequest` or
    a :model:`reviews.Group`.
    """
    return render_star(context.get('user', None), obj)


def render_star(user, obj):
    """
    Does the actual work of rendering the star. The star tag is a wrapper
    around this.
    """
    if user.is_anonymous():
        return ""

    profile = None

    if not hasattr(obj, 'starred'):
        try:
            profile = user.get_profile()
        except Profile.DoesNotExist:
            return ""

    if isinstance(obj, ReviewRequest):
        obj_info = {
            'type': 'reviewrequests',
            'id': obj.id
        }

        if hasattr(obj, 'starred'):
            starred = obj.starred
        else:
            starred = \
                profile.starred_review_requests.filter(pk=obj.id).count() > 0
    elif isinstance(obj, Group):
        obj_info = {
            'type': 'groups',
            'id': obj.name
        }

        if hasattr(obj, 'starred'):
            starred = obj.starred
        else:
            starred = \
                profile.starred_groups.filter(pk=obj.id).count() > 0
    else:
        raise template.TemplateSyntaxError, \
            "star tag received an incompatible object type (%s)" % \
            type(obj)

    if starred:
        image_alt = _("Starred")
    else:
        image_alt = _("Click to star")

    return render_to_string('reviews/star.html', {
        'object': obj_info,
        'starred': int(starred),
        'alt': image_alt,
        'user': user,
        'MEDIA_URL': settings.MEDIA_URL,
    })
Loops over a list of comments beloning to a filediff.      This will populate a special ``comment`` variable for use in the content.     This is of the type :model:`reviews.Comment`. Returns whether or not the specified number is a "neat" number.     This is a number with a special property, such as being a     palindrome or having trailing zeroes.      If the number is a neat number, the contained content is rendered,     and two variables, ``milestone`` and ``palindrome`` are defined. Returns a JSON array of current comments for a filediff, sorted by     line number.      Each entry in the array has a dictionary containing the following keys:        =========== ==================================================       Key         Description       =========== ==================================================       comment_id  The ID of the comment       text        The text of the comment       line        The first line number       num_lines   The number of lines this comment spans       user        A dictionary containing "username" and "name" keys                   for the user       url         The URL to the comment       localdraft  True if this is the current user's draft comment       =========== ================================================== #'timestamp': comment.timestamp, Returns a JSON array of current comments for a screenshot.      Each entry in the array has a dictionary containing the following keys:        =========== ==================================================       Key         Description       =========== ==================================================       text        The text of the comment       localdraft  True if this is the current user's draft comment       x           The X location of the comment's region       y           The Y location of the comment's region       w           The width of the comment's region       h           The height of the comment's region       =========== ================================================== Renders a list of comments of a specified type.      This is a complex, confusing function accepts lots of inputs in order     to display replies to a type of object. In each case, the replies will     be rendered using the template :template:`reviews/review_reply.html`.      If ``context_type`` is ``"comment"`` or ``"screenshot_comment"``,     the generated list of replies are to ``comment``.      If ``context_type`` is ``"body_top"`` or ```"body_bottom"``,     the generated list of replies are to ``review``. Depending on the     ``context_type``, these will either be replies to the top of the     review body or to the bottom.      The ``context_id`` parameter has to do with the internal IDs used by     the JavaScript code for storing and categorizing the comments. Renders a template for displaying a reply.      This takes the same parameters as :tag:`reply_list`. The template     rendered by this function, :template:`reviews/review_reply_section.html`,     is responsible for invoking :tag:`reply_list` and as such passes these     variables through. It does not make use of them itself. Renders an entry in the dashboard sidebar.      This includes the name of the entry and the list of review requests     associated with it. The entry is rendered by the template     :template:`reviews/dashboard_entry.html`. Returns a humanized list of target reviewers in a review request. Returns the URL based on a bug number on the specified review request.      If the repository the review request belongs to doesn't have an     associated bug tracker, this returns None. Returns a list of diffsets in the review that contain draft comments. Returns a list of interdiffs in the review that contain draft comments. Returns whether or not the specified review has any comments that     aren't in the specified diffset or interdiff. # See if there are any diffsets with comments on them in this review. # The user is browsing a standard diffset, so filter it out. # See if there are any interdiffs with comments on them in this review. # The user is browsing an interdiff, so filter it out. Renders the code for displaying a star used for starring items.      The rendered code should handle click events so that the user can     toggle the star. The star is rendered by the template     :template:`reviews/star.html`.      The passed object must be either a :model:`reviews.ReviewRequest` or     a :model:`reviews.Group`. Does the actual work of rendering the star. The star tag is a wrapper     around this.