Exemple #1
0
def _get_thread_replies(request, thread, offset=1, limit=None):
    '''
    Get and sort the replies for a thread.
    By default, offset = 1 to skip the original message.
    '''
    if not thread:
        raise Http404

    if "sort" in request.GET and request.GET["sort"] == "date":
        sort_mode = "date"
        emails = thread.emails
    else:
        sort_mode = "thread"
        emails = thread.emails_by_reply

    # XXX: Storm-specific
    emails = emails.find()
    emails.config(offset=offset, limit=limit)

    emails = list(emails)
    for email in emails:
        # Extract all the votes for this message
        set_message_votes(email, request.user)
        if sort_mode == "thread":
            email.level = email.thread_depth - 1  # replies start ragged left
            if email.level > 5:
                email.level = 5

    return emails
Exemple #2
0
def _get_thread_replies(request, thread, offset=1, limit=None):
    '''
    Get and sort the replies for a thread.
    By default, offset = 1 to skip the original message.
    '''
    if not thread:
        raise Http404

    if "sort" in request.GET and request.GET["sort"] == "date":
        sort_mode = "date"
        emails = thread.emails
    else:
        sort_mode = "thread"
        emails = thread.emails_by_reply

    # XXX: Storm-specific
    emails = emails.find()
    emails.config(offset=offset, limit=limit)

    emails = list(emails)
    for email in emails:
        # Extract all the votes for this message
        set_message_votes(email, request.user)
        if sort_mode == "thread":
            email.level = email.thread_depth - 1 # replies start ragged left
            if email.level > 5:
                email.level = 5

    return emails
Exemple #3
0
def vote(request, mlist_fqdn, message_id_hash):
    """ Add a rating to a given message identified by messageid. """
    if request.method != 'POST':
        raise SuspiciousOperation

    if not request.user.is_authenticated():
        return HttpResponse('You must be logged in to vote',
                            content_type="text/plain",
                            status=403)

    store = get_store(request)
    message = store.get_message_by_hash_from_list(mlist_fqdn, message_id_hash)
    if message is None:
        raise Http404

    value = int(request.POST['vote'])
    if value not in [-1, 0, 1]:
        raise SuspiciousOperation

    # Checks if the user has already voted for a this message.
    try:
        v = Rating.objects.get(user=request.user,
                               messageid=message_id_hash,
                               list_address=mlist_fqdn)
        if v.vote == value:
            return HttpResponse("You've already cast this vote",
                                content_type="text/plain",
                                status=403)
    except Rating.DoesNotExist:
        if value != 0:
            v = Rating(list_address=mlist_fqdn,
                       messageid=message_id_hash,
                       vote=value)
            v.user = request.user
        else:
            return HttpResponse("There is no vote to cancel",
                                content_type="text/plain",
                                status=500)

    if value == 0:
        v.delete()
    else:
        v.vote = value
        v.save()

    # Extract all the votes for this message to refresh it
    set_message_votes(message, request.user)
    t = loader.get_template('messages/like_form.html')
    html = t.render(
        RequestContext(request, {
            "object": message,
            "message_id_hash": message_id_hash,
        }))

    result = {
        "like": message.likes,
        "dislike": message.dislikes,
        "html": html,
    }
    return HttpResponse(json.dumps(result), mimetype='application/javascript')
Exemple #4
0
def search(request, page=1):
    """ Returns messages corresponding to a query """
    results_per_page = 10
    store = get_store(request)
    query = request.GET.get("query")
    mlist_fqdn = request.GET.get("list")
    sort_mode = request.GET.get('sort')

    if mlist_fqdn is None:
        mlist = None
    else:
        mlist = store.get_list(mlist_fqdn)
        if mlist is None:
            raise Http404("No archived mailing-list by that name.")

    if not query:
        return render(
            request, "search_results.html", {
                'mlist': mlist,
                "query": query or "",
                'messages': [],
                'total': 0,
                'sort_mode': sort_mode,
            })

    try:
        page_num = int(request.GET.get('page', "1"))
    except ValueError:
        page_num = 1

    sortedby = None
    reverse = False
    if sort_mode == "date-asc":
        sortedby = "date"
    elif sort_mode == "date-desc":
        sortedby = "date"
        reverse = True

    query_result = store.search(query,
                                mlist_fqdn,
                                page_num,
                                results_per_page,
                                sortedby=sortedby,
                                reverse=reverse)
    total = query_result["total"]
    messages = query_result["results"]
    for message in messages:
        set_message_votes(message, request.user)

    paginator = SearchPaginator(messages, 10, total)
    messages = paginate(messages, page_num, paginator=paginator)

    context = {
        'mlist': mlist,
        "query": query,
        'messages': messages,
        'total': total,
        'sort_mode': sort_mode,
    }
    return render(request, "search_results.html", context)
Exemple #5
0
def vote(request, mlist_fqdn, message_id_hash):
    """ Add a rating to a given message identified by messageid. """
    if request.method != 'POST':
        raise SuspiciousOperation

    if not request.user.is_authenticated():
        return HttpResponse('You must be logged in to vote',
                            content_type="text/plain", status=403)

    store = get_store(request)
    message = store.get_message_by_hash_from_list(mlist_fqdn, message_id_hash)
    if message is None:
        raise Http404

    value = int(request.POST['vote'])
    if value not in [-1, 0, 1]:
        raise SuspiciousOperation

    # Checks if the user has already voted for a this message.
    try:
        v = Rating.objects.get(user=request.user, messageid=message_id_hash,
                               list_address=mlist_fqdn)
        if v.vote == value:
            return HttpResponse("You've already cast this vote",
                                content_type="text/plain", status=403)
    except Rating.DoesNotExist:
        if value != 0:
            v = Rating(list_address=mlist_fqdn, messageid=message_id_hash,
                       vote=value)
            v.user = request.user
        else:
            return HttpResponse("There is no vote to cancel",
                                content_type="text/plain", status=500)

    if value == 0:
        v.delete()
    else:
        v.vote = value
        v.save()

    # Invalidate the cache for the thread and user votes
    cache.delete("list:%s:thread:%s:votes"
                 % (mlist_fqdn, message.thread_id))
    if message.user_id:
        cache.delete("user:%s:list:%s:votes" % (message.user_id, mlist_fqdn))

    # Extract all the votes for this message to refresh it
    set_message_votes(message, request.user)
    t = loader.get_template('messages/like_form.html')
    html = t.render(RequestContext(request, {
            "object": message,
            "message_id_hash": message_id_hash,
            }))

    result = { "like": message.likes, "dislike": message.dislikes,
               "html": html, }
    return HttpResponse(json.dumps(result),
                        content_type='application/javascript')
Exemple #6
0
def search(request, page=1):
    """ Returns messages corresponding to a query """
    results_per_page = 10
    store = get_store(request)
    query = request.GET.get("query")
    mlist_fqdn = request.GET.get("list")
    sort_mode = request.GET.get('sort')

    if mlist_fqdn is None:
        mlist = None
    else:
        mlist = store.get_list(mlist_fqdn)
        if mlist is None:
            raise Http404("No archived mailing-list by that name.")

    if not query:
        return render(request, "search_results.html", {
            'mlist' : mlist,
            "query": query or "",
            'messages': [],
            'total': 0,
            'sort_mode': sort_mode,
        })

    try:
        page_num = int(request.GET.get('page', "1"))
    except ValueError:
        page_num = 1

    sortedby = None
    reverse = False
    if sort_mode == "date-asc":
        sortedby = "date"
    elif sort_mode == "date-desc":
        sortedby = "date"
        reverse = True

    query_result = store.search(query, mlist_fqdn, page_num, results_per_page,
                                sortedby=sortedby, reverse=reverse)
    total = query_result["total"]
    messages = query_result["results"]
    for message in messages:
        set_message_votes(message, request.user)

    paginator = SearchPaginator(messages, 10, total)
    messages = paginate(messages, page_num, paginator=paginator)

    context = {
        'mlist' : mlist,
        "query": query,
        'messages': messages,
        'total': total,
        'sort_mode': sort_mode,
    }
    return render(request, "search_results.html", context)
Exemple #7
0
def posts(request, user_id):
    store = get_store(request)
    mlist_fqdn = request.GET.get("list")
    sort_mode = request.GET.get('sort')
    if mlist_fqdn is None:
        mlist = None
        return HttpResponse("Not implemented yet", status=500)
    else:
        mlist = store.get_list(mlist_fqdn)
        if mlist is None:
            raise Http404("No archived mailing-list by that name.")
        if not is_mlist_authorized(request, mlist):
            return render(request, "errors/private.html", {
                            "mlist": mlist,
                          }, status=403)

    # Get the user's full name
    try:
        client = mailmanclient.Client('%s/3.0' %
                    settings.MAILMAN_REST_SERVER,
                    settings.MAILMAN_API_USER,
                    settings.MAILMAN_API_PASS)
        mm_user = client.get_user(user_id)
    except HTTPError:
        raise Http404("No user with this ID: %s" % user_id)
    except mailmanclient.MailmanConnectionError:
        fullname = None
    else:
        fullname = mm_user.display_name
    if not fullname:
        fullname = store.get_sender_name(user_id)

    # Get the messages and paginate them
    messages = store.get_messages_by_user_id(user_id, mlist_fqdn)
    try:
        page_num = int(request.GET.get('page', "1"))
    except ValueError:
        page_num = 1
    messages = paginate(messages, page_num)

    for message in messages:
        set_message_votes(message, request.user)

    context = {
        'user_id': user_id,
        'mlist' : mlist,
        'messages': messages,
        'fullname': fullname,
    }
    return render(request, "user_posts.html", context)
Exemple #8
0
def index(request, mlist_fqdn, message_id_hash):
    '''
    Displays a single message identified by its message_id_hash (derived from
    message_id)
    '''
    store = get_store(request)
    message = store.get_message_by_hash_from_list(mlist_fqdn, message_id_hash)
    if message is None:
        raise Http404
    message.sender_email = message.sender_email.strip()
    set_message_votes(message, request.user)
    mlist = store.get_list(mlist_fqdn)

    context = {
        'mlist' : mlist,
        'message': message,
        'message_id_hash' : message_id_hash,
        'months_list': get_months(store, mlist.name),
        'reply_form': ReplyForm(),
    }
    return render(request, "message.html", context)
Exemple #9
0
def index(request, mlist_fqdn, message_id_hash):
    '''
    Displays a single message identified by its message_id_hash (derived from
    message_id)
    '''
    store = get_store(request)
    message = store.get_message_by_hash_from_list(mlist_fqdn, message_id_hash)
    if message is None:
        raise Http404
    message.sender_email = message.sender_email.strip()
    set_message_votes(message, request.user)
    mlist = store.get_list(mlist_fqdn)

    context = {
        'mlist': mlist,
        'message': message,
        'message_id_hash': message_id_hash,
        'months_list': get_months(store, mlist.name),
        'reply_form': ReplyForm(),
    }
    return render(request, "message.html", context)
Exemple #10
0
def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
    ''' Displays all the email for a given thread identifier '''
    store = get_store(request)
    thread = store.get_thread(mlist_fqdn, threadid)
    if not thread:
        raise Http404
    prev_thread, next_thread = store.get_thread_neighbors(mlist_fqdn, threadid)

    sort_mode = request.GET.get("sort", "thread")
    set_message_votes(thread.starting_email, request.user)

    # Tags
    tag_form = AddTagForm()
    try:
        tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn)
    except Tag.DoesNotExist:
        tags = []

    # Favorites
    fav_action = "add"
    if request.user.is_authenticated():
        try:
            Favorite.objects.get(list_address=mlist_fqdn, threadid=threadid,
                                 user=request.user)
        except Favorite.DoesNotExist:
            pass
        else:
            fav_action = "rm"

    # Category
    category, category_form = get_category_widget(request, thread.category)

    # Extract relative dates
    today = datetime.date.today()
    days_old = today - thread.starting_email.date.date()
    days_inactive = today - thread.last_email.date.date()

    mlist = store.get_list(mlist_fqdn)
    subject = stripped_subject(mlist, thread.starting_email.subject)

    # Last view
    last_view = None
    if request.user.is_authenticated():
        last_view_obj, created = LastView.objects.get_or_create(
                list_address=mlist_fqdn, threadid=threadid, user=request.user)
        if not created:
            last_view = last_view_obj.view_date
            last_view_obj.save() # update timestamp
    # get the number of unread messages
    if last_view is None:
        if request.user.is_authenticated():
            unread_count = len(thread)
        else:
            unread_count = 0
    else:
        # XXX: Storm-specific
        unread_count = thread.replies_after(last_view).count()

    # Flash messages
    flash_messages = []
    flash_msg = request.GET.get("msg")
    if flash_msg:
        flash_msg = { "type": FLASH_MESSAGES[flash_msg][0],
                      "msg": FLASH_MESSAGES[flash_msg][1] }
        flash_messages.append(flash_msg)

    # TODO: eventually move to a middleware ?
    # http://djangosnippets.org/snippets/1865/
    is_bot = True
    user_agent = request.META.get('HTTP_USER_AGENT', None)
    if user_agent:
        is_bot = robot_detection.is_robot(user_agent)

    context = {
        'mlist': mlist,
        'threadid': threadid,
        'subject': subject,
        'tags': tags,
        'addtag_form': tag_form,
        'month': thread.date_active,
        'first_mail': thread.starting_email,
        'neighbors': (prev_thread, next_thread),
        'months_list': get_months(store, mlist.name),
        'days_inactive': days_inactive.days,
        'days_old': days_old.days,
        'sort_mode': sort_mode,
        'fav_action': fav_action,
        'reply_form': ReplyForm(),
        'is_bot': is_bot,
        'num_comments': len(thread),
        'participants': thread.participants,
        'last_view': last_view,
        'unread_count': unread_count,
        'category_form': category_form,
        'category': category,
        'flash_messages': flash_messages,
    }
    context["participants"].sort(key=lambda x: x[0].lower())

    if is_bot:
        # Don't rely on AJAX to load the replies
        # The limit is a safety measure, don't let a bot kill the DB
        context["replies"] = _get_thread_replies(request, thread, limit=1000)

    return render(request, "thread.html", context)
Exemple #11
0
def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
    ''' Displays all the email for a given thread identifier '''
    search_form = SearchForm(auto_id=False)
    store = get_store(request)
    thread = store.get_thread(mlist_fqdn, threadid)
    if not thread:
        raise Http404
    prev_thread, next_thread = store.get_thread_neighbors(mlist_fqdn, threadid)

    sort_mode = request.GET.get("sort", "thread")
    set_message_votes(thread.starting_email, request.user)

    from_url = reverse("thread", kwargs={"mlist_fqdn":mlist_fqdn,
                                         "threadid":threadid})
    # Tags
    tag_form = AddTagForm(initial={'from_url' : from_url})
    try:
        tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn)
    except Tag.DoesNotExist:
        tags = []

    # Favorites
    fav_action = "add"
    if request.user.is_authenticated():
        try:
            Favorite.objects.get(list_address=mlist_fqdn, threadid=threadid,
                                 user=request.user)
        except Favorite.DoesNotExist:
            pass
        else:
            fav_action = "rm"

    # Extract relative dates
    today = datetime.date.today()
    days_old = today - thread.starting_email.date.date()
    days_inactive = today - thread.last_email.date.date()

    mlist = store.get_list(mlist_fqdn)
    subject = stripped_subject(mlist, thread.starting_email.subject)

    # TODO: eventually move to a middleware ?
    # http://djangosnippets.org/snippets/1865/
    is_bot = True
    user_agent = request.META.get('HTTP_USER_AGENT', None)
    if user_agent:
        is_bot = robot_detection.is_robot(user_agent)

    context = {
        'mlist': mlist,
        'threadid': threadid,
        'subject': subject,
        'tags': tags,
        'search_form': search_form,
        'addtag_form': tag_form,
        'month': thread.date_active,
        'first_mail': thread.starting_email,
        'neighbors': (prev_thread, next_thread),
        'months_list': get_months(store, mlist.name),
        'days_inactive': days_inactive.days,
        'days_old': days_old.days,
        'sort_mode': sort_mode,
        'fav_action': fav_action,
        'reply_form': ReplyForm(),
        'is_bot': is_bot,
        'participants': thread.participants,
    }
    context["participants"].sort(key=lambda x: x[0].lower())

    if is_bot:
        # Don't rely on AJAX to load the replies
        context["replies"] = _get_thread_replies(request, thread)

    return render(request, "thread.html", context)
Exemple #12
0
def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
    ''' Displays all the email for a given thread identifier '''
    store = get_store(request)
    thread = store.get_thread(mlist_fqdn, threadid)
    if not thread:
        raise Http404
    prev_thread, next_thread = store.get_thread_neighbors(mlist_fqdn, threadid)

    sort_mode = request.GET.get("sort", "thread")
    set_message_votes(thread.starting_email, request.user)

    # Tags
    tag_form = AddTagForm()
    try:
        tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn)
    except Tag.DoesNotExist:
        tags = []

    # Favorites
    fav_action = "add"
    if request.user.is_authenticated():
        try:
            Favorite.objects.get(list_address=mlist_fqdn,
                                 threadid=threadid,
                                 user=request.user)
        except Favorite.DoesNotExist:
            pass
        else:
            fav_action = "rm"

    # Category
    category, category_form = get_category_widget(request, thread.category)

    # Extract relative dates
    today = datetime.date.today()
    days_old = today - thread.starting_email.date.date()
    days_inactive = today - thread.last_email.date.date()

    mlist = store.get_list(mlist_fqdn)
    subject = stripped_subject(mlist, thread.starting_email.subject)

    # Last view
    last_view = None
    if request.user.is_authenticated():
        last_view_obj, created = LastView.objects.get_or_create(
            list_address=mlist_fqdn, threadid=threadid, user=request.user)
        if not created:
            last_view = last_view_obj.view_date
            last_view_obj.save()  # update timestamp
    # get the number of unread messages
    if last_view is None:
        if request.user.is_authenticated():
            unread_count = len(thread)
        else:
            unread_count = 0
    else:
        # XXX: Storm-specific
        unread_count = thread.replies_after(last_view).count()

    # Flash messages
    flash_messages = []
    flash_msg = request.GET.get("msg")
    if flash_msg:
        flash_msg = {
            "type": FLASH_MESSAGES[flash_msg][0],
            "msg": FLASH_MESSAGES[flash_msg][1]
        }
        flash_messages.append(flash_msg)

    # TODO: eventually move to a middleware ?
    # http://djangosnippets.org/snippets/1865/
    is_bot = True
    user_agent = request.META.get('HTTP_USER_AGENT', None)
    if user_agent:
        is_bot = robot_detection.is_robot(user_agent)

    context = {
        'mlist': mlist,
        'threadid': threadid,
        'subject': subject,
        'tags': tags,
        'addtag_form': tag_form,
        'month': thread.date_active,
        'first_mail': thread.starting_email,
        'neighbors': (prev_thread, next_thread),
        'months_list': get_months(store, mlist.name),
        'days_inactive': days_inactive.days,
        'days_old': days_old.days,
        'sort_mode': sort_mode,
        'fav_action': fav_action,
        'reply_form': ReplyForm(),
        'is_bot': is_bot,
        'num_comments': len(thread),
        'participants': thread.participants,
        'last_view': last_view,
        'unread_count': unread_count,
        'category_form': category_form,
        'category': category,
        'flash_messages': flash_messages,
    }
    context["participants"].sort(key=lambda x: x[0].lower())

    if is_bot:
        # Don't rely on AJAX to load the replies
        # The limit is a safety measure, don't let a bot kill the DB
        context["replies"] = _get_thread_replies(request, thread, limit=1000)

    return render(request, "thread.html", context)
Exemple #13
0
def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
    ''' Displays all the email for a given thread identifier '''
    search_form = SearchForm(auto_id=False)
    store = get_store(request)
    thread = store.get_thread(mlist_fqdn, threadid)
    if not thread:
        raise Http404
    prev_thread, next_thread = store.get_thread_neighbors(mlist_fqdn, threadid)

    sort_mode = request.GET.get("sort", "thread")
    set_message_votes(thread.starting_email, request.user)

    from_url = reverse("thread",
                       kwargs={
                           "mlist_fqdn": mlist_fqdn,
                           "threadid": threadid
                       })
    # Tags
    tag_form = AddTagForm(initial={'from_url': from_url})
    try:
        tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn)
    except Tag.DoesNotExist:
        tags = []

    # Favorites
    fav_action = "add"
    if request.user.is_authenticated():
        try:
            Favorite.objects.get(list_address=mlist_fqdn,
                                 threadid=threadid,
                                 user=request.user)
        except Favorite.DoesNotExist:
            pass
        else:
            fav_action = "rm"

    # Extract relative dates
    today = datetime.date.today()
    days_old = today - thread.starting_email.date.date()
    days_inactive = today - thread.last_email.date.date()

    mlist = store.get_list(mlist_fqdn)
    subject = stripped_subject(mlist, thread.starting_email.subject)

    # TODO: eventually move to a middleware ?
    # http://djangosnippets.org/snippets/1865/
    is_bot = True
    user_agent = request.META.get('HTTP_USER_AGENT', None)
    if user_agent:
        is_bot = robot_detection.is_robot(user_agent)

    context = {
        'mlist': mlist,
        'threadid': threadid,
        'subject': subject,
        'tags': tags,
        'search_form': search_form,
        'addtag_form': tag_form,
        'month': thread.date_active,
        'first_mail': thread.starting_email,
        'neighbors': (prev_thread, next_thread),
        'months_list': get_months(store, mlist.name),
        'days_inactive': days_inactive.days,
        'days_old': days_old.days,
        'sort_mode': sort_mode,
        'fav_action': fav_action,
        'reply_form': ReplyForm(),
        'is_bot': is_bot,
        'participants': thread.participants,
    }
    context["participants"].sort(key=lambda x: x[0].lower())

    if is_bot:
        # Don't rely on AJAX to load the replies
        context["replies"] = _get_thread_replies(request, thread)

    return render(request, "thread.html", context)
Exemple #14
0
def thread_index(request, mlist_fqdn, threadid, month=None, year=None):
    ''' Displays all the email for a given thread identifier '''
    search_form = SearchForm(auto_id=False)
    store = get_store(request)
    thread = store.get_thread(mlist_fqdn, threadid)
    if not thread:
        raise Http404
    prev_thread, next_thread = store.get_thread_neighbors(mlist_fqdn, threadid)

    if "sort" in request.GET and request.GET["sort"] == "date":
        sort_mode = "date"
        emails = thread.emails
    else:
        sort_mode = "thread"
        emails = thread.emails_by_reply

    participants = {}
    for email in emails:
        # Extract all the votes for this message
        set_message_votes(email, request.user)

        # Statistics on how many participants and messages this month
        participants[email.sender_name] = email.sender_email

        if sort_mode == "thread":
            email.level = email.thread_depth - 1 # replies start ragged left
            if email.level > 5:
                email.level = 5

    from_url = reverse("thread", kwargs={"mlist_fqdn":mlist_fqdn,
                                         "threadid":threadid})
    # Tags
    tag_form = AddTagForm(initial={'from_url' : from_url})
    try:
        tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn)
    except Tag.DoesNotExist:
        tags = []

    # Favorites
    fav_action = "add"
    if request.user.is_authenticated():
        try:
            Favorite.objects.get(list_address=mlist_fqdn, threadid=threadid,
                                 user=request.user)
        except Favorite.DoesNotExist:
            pass
        else:
            fav_action = "rm"

    # Extract relative dates
    today = datetime.date.today()
    days_old = today - thread.starting_email.date.date()
    days_inactive = today - thread.last_email.date.date()

    mlist = store.get_list(mlist_fqdn)
    subject = stripped_subject(mlist, thread.starting_email.subject)

    context = {
        'mlist' : mlist,
        'threadid' : threadid,
        'subject': subject,
        'tags' : tags,
        'search_form': search_form,
        'addtag_form': tag_form,
        'month': thread.date_active,
        'participants': participants,
        'first_mail': thread.starting_email,
        'replies': list(emails)[1:],
        'neighbors': (prev_thread, next_thread),
        'months_list': get_months(store, mlist.name),
        'days_inactive': days_inactive.days,
        'days_old': days_old.days,
        'sort_mode': sort_mode,
        'fav_action': fav_action,
        'reply_form': ReplyForm(),
    }
    return render(request, "thread.html", context)