Mercurial > public > sg101
changeset 425:76ba9478ebbd
Initial beta-test commit of a revamped, jquery ui tab-based PM system. This is for #211.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Tue, 26 Apr 2011 00:16:35 +0000 |
parents | 8df6e9edac22 |
children | c8148cf11a79 |
files | gpp/messages/models.py gpp/messages/static/css/messages.css gpp/messages/static/js/tabbed_messages.js gpp/messages/urls.py gpp/messages/views2.py gpp/settings.py gpp/templates/messages/compose_tab.html gpp/templates/messages/inbox_tab.html gpp/templates/messages/options_tab.html gpp/templates/messages/outbox_tab.html gpp/templates/messages/pagination.html gpp/templates/messages/tabbed_base.html gpp/templates/messages/trash_tab.html |
diffstat | 13 files changed, 720 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- a/gpp/messages/models.py Sun Apr 24 03:54:40 2011 +0000 +++ b/gpp/messages/models.py Tue Apr 26 00:16:35 2011 +0000 @@ -14,17 +14,17 @@ def inbox(self, user): return self.filter(receiver=user, - receiver_delete_date__isnull=True) + receiver_delete_date__isnull=True).select_related('sender') def outbox(self, user): return self.filter(sender=user, - sender_delete_date__isnull=True) + sender_delete_date__isnull=True).select_related('receiver') def trash(self, user): return self.filter( Q(sender=user, sender_delete_date__isnull=False) | Q(receiver=user, receiver_delete_date__isnull=False) - ) + ).select_related() def unread_count(self, user): return self.filter(receiver=user, read_date__isnull=True).count()
--- a/gpp/messages/static/css/messages.css Sun Apr 24 03:54:40 2011 +0000 +++ b/gpp/messages/static/css/messages.css Tue Apr 26 00:16:35 2011 +0000 @@ -11,7 +11,8 @@ border: 1px solid black; border-spacing: 0px; border-collapse: collapse; - margin-left: 1em; + margin: 1em auto; + width: 96%; } table.messages th { @@ -64,3 +65,12 @@ form.messages-button { display: inline; } +.unread { + font-weight: bold; +} +.replied_to { + font-style: italic; +} +.pagination { + text-align: right; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/messages/static/js/tabbed_messages.js Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,165 @@ +$(document).ready(function() { + $tabs = $('#tabs').tabs({ + selected: initialTab, + select: function(event, ui) { + $(ui.panel).html(''); + }, + load: function(event, ui) { + selectedTab = ui; + if (doReply && ui.index == 1) + { + doReply = false; + var msg = msgCache[$msgDialog.msgId]; + $('#id_receiver').val(msg.sender); + $('#id_subject').val(msg.re_subject); + $('#id_message').val(msg.re_content); + } + }, + ajaxOptions: { + error: function(xhr, status, index, anchor) { + $(anchor.hash).html( + "Oops, we couldn't load this tab. We'll try to fix this as soon as possible."); + } + } + }); + $msgDialog = $('#msgDialog').dialog({ + autoOpen: false, + width: 460, + buttons: [ + { + text: "Reply", + click: function() { + doReply = true; + $(this).dialog('close'); + $tabs.tabs("select", 1); + } + }, + { + text: "Close", + click: function() { + $(this).dialog('close'); + } + } + ] + }); +}); + +var $tabs = 0; +var $msgDialog = 0; +var msgCache = {}; +var doReply = false; +var selectedTab = 0; + +function showMsg(link, id) { + $msgDialog.msgId = id; // create a msgId attribute on the dialog + var msg = msgCache[id]; + + // mark as read if necessary + if (username == msg.receiver) { + $(link).removeClass('unread'); + } + + $msgDialog.html(msg.content); + var title = 'PM From ' + msg.sender + ' To ' + msg.receiver + '<br /> ' + msg.subject; + $msgDialog.dialog('option', 'title', title); + $msgDialog.dialog('open'); +} + +function msgShow(link, id) { + if (msgCache[id]) { + showMsg(link, id); + return; + } + $.ajax({ + url: '/messages/beta/message/', + type: 'POST', + data: { + msg_id : id + }, + dataType: 'json', + success: function (data, textStatus) { + msgCache[id] = data; + showMsg(link, id); + }, + error: function (xhr, textStatus, ex) { + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + } + }); +} + +function submitOptions(form) { + $.ajax({ + url: '/messages/beta/options-tab/', + type: 'POST', + data: $(form).serialize(), + dataType: 'html', + success: function (data, textStatus) { + $(selectedTab.panel).html(data); + }, + error: function (xhr, textStatus, ex) { + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + } + }); + return false; +} + +function messageSubmit(form) { + $.ajax({ + url: '/messages/beta/compose-tab/', + type: 'POST', + data: $(form).serialize(), + dataType: 'html', + success: function (data, textStatus) { + $('#ui-tabs-1').html(data); + }, + error: function (xhr, textStatus, ex) { + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + } + }); + return false; +} + +function tabMasterCheckClick(box, name) { + var state = $(box).attr('checked'); + $('input[name="' + name + '"]').each(function() { + this.checked = state; + }); +} + +function bulkMsgAction(form, action) { + if (confirm("Really " + action + " checked messages?")) { + $.ajax({ + url: '/messages/beta/bulk/', + type: 'POST', + data: $(form).serialize(), + dataType: 'text', + success: function (data, textStatus) { + $tabs.tabs("load", selectedTab.index); + }, + error: function (xhr, textStatus, ex) { + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + } + }); + } + return false; +} + +function ajaxPageFetch(link) { + $.ajax({ + url: link.href, + type: 'GET', + dataType: 'html', + success: function (data, textStatus) { + $(selectedTab.panel).html(data); + }, + error: function (xhr, textStatus, ex) { + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + } + }); + return false; +}
--- a/gpp/messages/urls.py Sun Apr 24 03:54:40 2011 +0000 +++ b/gpp/messages/urls.py Tue Apr 26 00:16:35 2011 +0000 @@ -2,6 +2,7 @@ from django.conf.urls.defaults import * from django.views.generic import RedirectView + urlpatterns = patterns('messages.views', url(r'^inbox/$', 'inbox', name='messages-inbox'), url(r'^outbox/$', 'outbox', name='messages-outbox'), @@ -21,3 +22,35 @@ (r'^$', RedirectView.as_view(url='inbox/')), ) +urlpatterns += patterns('messages.views2', + url(r'^beta/$', + 'index', + name='messages-beta_index'), + url(r'^beta/(inbox|compose|outbox|trash|options)/$', + 'index', + name='messages-beta_index_named'), + url(r'^beta/inbox-tab/$', + 'inbox', + name='messages-beta_inbox'), + url(r'^beta/outbox-tab/$', + 'outbox', + name='messages-beta_outbox'), + url(r'^beta/trash-tab/$', + 'trash', + name='messages-beta_trash'), + url(r'^beta/message/$', + 'message', + name='messages-beta_message'), + url(r'^beta/options-tab/$', + 'options', + name='messages-beta_options'), + url(r'^beta/compose-tab/$', + 'compose', + name='messages-beta_compose'), + url(r'^beta/compose-tab/([\w.@+-]{1,30})/$', + 'compose', + name='messages-beta_compose_to'), + url(r'^beta/bulk/$', + 'bulk', + name='messages-beta_bulk'), +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/messages/views2.py Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,287 @@ +""" +Views for the messages application. + +""" +import datetime + +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.core.paginator import Paginator, EmptyPage, InvalidPage +from django.core.urlresolvers import reverse +from django.http import HttpResponse +from django.http import HttpResponseForbidden +from django.http import HttpResponseNotAllowed +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +import django.utils.simplejson as json + +from messages.models import Message, Options +from messages.forms import OptionsForm, ComposeForm +from messages.utils import reply_subject, quote_message + + +MSGS_PER_PAGE = 20 + +TAB_INDICES = { + 'inbox': 0, + 'compose': 1, + 'outbox': 2, + 'trash': 3, + 'options': 4, +} + + +def _get_page(request): + try: + n = int(request.GET.get('page', '1')) + except ValueError: + n = 1 + return n + + +@login_required +def index(request, tab=None): + """ + This function displays the base tabbed private messages view. + + """ + tab_index = TAB_INDICES[tab] if tab else 0 + return render(request, 'messages/tabbed_base.html', { + 'tab': tab_index, + }) + + +def inbox(request): + """ + Returns the inbox for the user. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + + msg_list = Message.objects.inbox(request.user) + paginator = Paginator(msg_list, MSGS_PER_PAGE) + try: + msgs = paginator.page(_get_page(request)) + except EmptyPage, InvalidPage: + msgs = paginator.page(paginator.num_pages) + + return render(request, 'messages/inbox_tab.html', { + 'msgs': msgs, + 'url': reverse('messages-beta_inbox'), + }) + + +def outbox(request): + """ + Returns the outbox for the user. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + + msg_list = Message.objects.outbox(request.user) + paginator = Paginator(msg_list, MSGS_PER_PAGE) + try: + msgs = paginator.page(_get_page(request)) + except EmptyPage, InvalidPage: + msgs = paginator.page(paginator.num_pages) + + return render(request, 'messages/outbox_tab.html', { + 'msgs': msgs, + 'url': reverse('messages-beta_outbox'), + }) + + +def trash(request): + """ + Returns the trash for the user. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + + msg_list = Message.objects.trash(request.user) + paginator = Paginator(msg_list, MSGS_PER_PAGE) + try: + msgs = paginator.page(_get_page(request)) + except EmptyPage, InvalidPage: + msgs = paginator.page(paginator.num_pages) + + return render(request, 'messages/trash_tab.html', { + 'msgs': msgs, + 'url': reverse('messages-beta_trash'), + }) + + +def message(request): + """ + This view function retrieves a message and returns it as a JSON object. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + if request.method != 'POST': + return HttpResponseNotAllowed(['POST']) + + msg_id = request.POST.get('msg_id') + msg = get_object_or_404(Message.objects.select_related(), pk=msg_id) + if msg.sender != request.user and msg.receiver != request.user: + return HttpResponseForbidden() + + if msg.receiver == request.user and msg.read_date is None: + msg.read_date = datetime.datetime.now() + msg.save() + + msg_dict = dict(subject=msg.subject, + sender=msg.sender.username, + receiver=msg.receiver.username, + content=msg.html, + re_subject=reply_subject(msg.subject), + re_content=quote_message(msg.sender.username, msg.send_date, + msg.message)) + + result = json.dumps(msg_dict, ensure_ascii=False) + return HttpResponse(result, content_type='application/json') + + +def options(request): + """ + This view handles the displaying and changing of private message options. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + + if request.method == "POST": + options = Options.objects.for_user(request.user) + form = OptionsForm(request.POST, instance=options, prefix='opts') + if form.is_valid(): + form.save() + messages.success(request, 'Options saved.') + else: + options = Options.objects.for_user(request.user) + form = OptionsForm(instance=options, prefix='opts') + + return render(request, 'messages/options_tab.html', { + 'form': form, + }) + + +def compose(request, receiver=None): + """ + Process or prepare the compose form to create a new private message. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + + if request.method == "POST": + compose_form = ComposeForm(request.user, request.POST) + if compose_form.is_valid(): + compose_form.save(sender=request.user) + messages.success(request, 'Message sent.') + return HttpResponseRedirect(reverse('messages-beta_index_named', args=['compose'])) + else: + if receiver is not None: + form_data = {'receiver': receiver} + compose_form = ComposeForm(request.user, initial=form_data) + else: + compose_form = ComposeForm(request.user) + + return render(request, 'messages/compose_tab.html', { + 'compose_form': compose_form, + }) + + +def _only_integers(slist): + """ + Accepts a list of strings. Returns a list of integers consisting of only + those elements from the original list that could be converted to integers + + """ + result = [] + for s in slist: + try: + n = int(s) + except ValueError: + pass + else: + result.append(n) + return result + + +def _delete_msgs(user, msg_ids): + """ + Deletes the messages given by the list of msg_ids. For this to succeed, the + user has to be either the sender or receiver on each message. + + """ + msg_ids = _only_integers(msg_ids) + msgs = Message.objects.filter(id__in=msg_ids) + + for msg in msgs: + if msg.sender == user: + if (msg.receiver_delete_date is not None or + msg.read_date is None): + # Both parties deleted the message or receiver hasn't read it + # yet, we can delete it now + msg.delete() + else: + # receiver still has it in inbox + msg.sender_delete_date = datetime.datetime.now() + msg.save() + + elif msg.receiver == user: + if msg.sender_delete_date is not None: + # both parties deleted the message, we can delete it now + msg.delete() + else: + # sender still has it in the outbox + msg.receiver_delete_date = datetime.datetime.now() + msg.save() + + +def _undelete_msgs(user, msg_ids): + """ + Attempts to "undelete" the messages given by the msg_ids list. + This will only succeed if the user is either the sender or receiver. + + """ + msg_ids = _only_integers(msg_ids) + msgs = Message.objects.filter(id__in=msg_ids) + for msg in msgs: + if msg.sender == user: + msg.sender_delete_date = None + msg.save() + elif msg.receiver == user: + msg.receiver_delete_date = None + msg.save() + + +def bulk(request): + """ + This view processes messages in bulk. Arrays of message ids are expected in + the POST query dict: inbox_ids and outbox_ids will be deleted; trash_ids will + be undeleted. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden() + if request.method != 'POST': + return HttpResponseNotAllowed(['POST']) + + delete_ids = [] + if 'inbox_ids' in request.POST: + delete_ids.extend(request.POST.getlist('inbox_ids')) + if 'outbox_ids' in request.POST: + delete_ids.extend(request.POST.getlist('outbox_ids')) + + if len(delete_ids): + _delete_msgs(request.user, delete_ids) + + if 'trash_ids' in request.POST: + _undelete_msgs(request.user, request.POST.getlist('trash_ids')) + + return HttpResponse('');
--- a/gpp/settings.py Sun Apr 24 03:54:40 2011 +0000 +++ b/gpp/settings.py Tue Apr 26 00:16:35 2011 +0000 @@ -278,13 +278,13 @@ # should also be used by developers when creating form media classes. GPP_THIRD_PARTY_JS = { 'jquery': ( - 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js', + 'http://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js', ), 'jquery-jeditable': ( 'js/jquery.jeditable.mini.js', ), 'jquery-ui': ( - 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.min.js', + 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.12/jquery-ui.min.js', ), 'markitup': ( 'js/markitup/jquery.markitup.pack.js', @@ -297,7 +297,7 @@ } GPP_THIRD_PARTY_CSS = { 'jquery-ui': ( - 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/themes/redmond/jquery-ui.css', + 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.12/themes/redmond/jquery-ui.css', ), 'markitup': ( 'js/markitup/skins/markitup/style.css',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/compose_tab.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,23 @@ +{% load url from future %} +{% load core_tags %} +{% load script_tags %} +{% script_tags 'markitup' %} +{% if messages %} +<ul class="user-messages"> + {% for msg in messages %} + <li{% if msg.tags %} class="{{ msg.tags }}"{% endif %}>{{ msg }}</li> + {% endfor %} +</ul> +{% endif %} +<form action="{% url 'messages-beta_compose' %}" method="post">{% csrf_token %} +<table> +{{ compose_form.as_table }} +<tr> + <td> </td> + <td> + {% comment_dialogs %} + <input type="submit" name="submit_button" value="Send" /> + </td> +</tr> +</table> +</form>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/inbox_tab.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,40 @@ +{% load url from future %} +{% if messages %} +<ul class="user-messages"> + {% for msg in messages %} + <li{% if msg.tags %} class="{{ msg.tags }}"{% endif %}>{{ msg }}</li> + {% endfor %} +</ul> +{% endif %} +{% if msgs.object_list %} + <form action="." onsubmit="return bulkMsgAction(this, 'delete');"> + <table class="messages"> + <tr> + <th>From</th> + <th>Subject</th> + <th>Date</th> + <th><input type="checkbox" onclick="tabMasterCheckClick(this, 'inbox_ids');" /></th> + </tr> + {% for msg in msgs.object_list %} + <tr> + <td><a href="{% url 'bio.views.view_profile' msg.sender.username %}"> + {{ msg.sender.username }}</a></td> + <td> + <a href="#" onclick="msgShow(this, {{ msg.id }}); return false;" + class="{% if msg.unread %}unread {% endif %}{% if msg.replied_to %}replied_to{% endif %}">{{ msg.subject }}</a> + </td> + <td>{{ msg.send_date|date:"M j, Y g:i A" }}</td> + <td><input type="checkbox" name="inbox_ids" value="{{ msg.id }}" /></td> + </tr> + {% endfor %} + <tr><td colspan="4"><input type="submit" value="Delete Checked Messages" /></td></tr> + </table> + </form> + {% include "messages/pagination.html" %} + <ul> + <li>Messages in <strong>bold</strong> are unread.</li> + <li>Messages in <em>italics</em> have been replied to.</li> + </ul> +{% else %} + <p><em>Your Inbox is empty.</em></p> +{% endif %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/options_tab.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,18 @@ +{% if messages %} +<ul id="message-opts-messages" class="user-messages"> + {% for msg in messages %} + <li{% if msg.tags %} class="{{ msg.tags }}"{% endif %}>{{ msg }}</li> + {% endfor %} +</ul> +{% endif %} +<form action="." method="post" onsubmit="return submitOptions(this);">{% csrf_token %} +<table> +{{ form.as_table }} +<tr> + <td> </td> + <td> + <input type="submit" name="submit_button" value="Save" /> + </td> +</tr> +</table> +</form>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/outbox_tab.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,42 @@ +{% load url from future %} +{% if messages %} +<ul class="user-messages"> + {% for msg in messages %} + <li{% if msg.tags %} class="{{ msg.tags }}"{% endif %}>{{ msg }}</li> + {% endfor %} +</ul> +{% endif %} +{% if msgs.object_list %} + <form action="." onsubmit="return bulkMsgAction(this, 'delete');"> + <table class="messages"> + <tr> + <th>To</th> + <th>Subject</th> + <th>Sent</th> + <th>Received</th> + <th><input type="checkbox" onclick="tabMasterCheckClick(this, 'outbox_ids');" /></th> + </tr> + {% for msg in msgs.object_list %} + <tr> + <td><a href="{% url 'bio.views.view_profile' msg.receiver.username %}"> + {{ msg.receiver.username }}</a></td> + <td> + <a href="#" onclick="msgShow(this, {{ msg.id }}); return false;" + class="{% if msg.unread %}unread {% endif %}{% if msg.replied_to %}replied_to{% endif %}">{{ msg.subject }}</a> + </td> + <td>{{ msg.send_date|date:"M j, Y g:i A" }}</td> + <td>{% if msg.unread %}<em>Unread</em>{% else %}{{ msg.read_date|date:"M j, Y g:i A" }}{% endif %}</td> + <td><input type="checkbox" name="outbox_ids" value="{{ msg.id }}" /></td> + </tr> + {% endfor %} + <tr><td colspan="5"><input type="submit" value="Delete Checked Messages" /></td></tr> + </table> + </form> + {% include "messages/pagination.html" %} + <ul> + <li>Messages in <strong>bold</strong> are unread.</li> + <li>Messages in <em>italics</em> have been replied to.</li> + </ul> +{% else %} + <p><em>Your Outbox is empty.</em></p> +{% endif %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/pagination.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,15 @@ +<div class="pagination"> +<span class="step-links"> +{% if msgs.has_previous %} + <a href="{{ url }}?page={{ msgs.previous_page_number }}" onclick="return ajaxPageFetch(this);">« Previous</a> +{% endif %} + +<span class="current"> + Page {{ msgs.number }} of {{ msgs.paginator.num_pages }}. +</span> + +{% if msgs.has_next %} + <a href="{{ url }}?page={{ msgs.next_page_number }}" onclick="return ajaxPageFetch(this);">Next »</a> +{% endif %} + </span> +</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/tabbed_base.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,37 @@ +{% extends 'base.html' %} +{% load url from future %} +{% load script_tags %} +{% block custom_css %} +<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/messages.css" /> +{% endblock %} +{% block custom_js %} +{% script_tags 'jquery-ui' %} +<script type="text/javascript"> +//<![CDATA[ + var initialTab = {{ tab }}; + var username = "{{ request.user.username }}"; +//]]> +</script> +<script type="text/javascript" src="{{ STATIC_URL }}js/tabbed_messages.js"></script> +{% endblock %} +{% block content %} +<h2>Your Private Messages (Beta)</h2> +<p> +This is an experimental version of the SG101 private message system. Don't worry, if this +isn't working for you, you can always go back to the +<a href="{% url 'messages-inbox' %}">current system</a>. Please leave any feedback on this +change in forums. Thanks! +</p> + +<div id="tabs"> + <ul> + <li><a href="{% url 'messages-beta_inbox' %}">Inbox</a></li> + <li><a href="{% url 'messages-beta_compose' %}">Compose</a></li> + <li><a href="{% url 'messages-beta_outbox' %}">Outbox</a></li> + <li><a href="{% url 'messages-beta_trash' %}">Trash</a></li> + <li><a href="{% url 'messages-beta_options' %}">Options</a></li> + </ul> +</div> + +<div id="msgDialog"></div> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/messages/trash_tab.html Tue Apr 26 00:16:35 2011 +0000 @@ -0,0 +1,43 @@ +{% load url from future %} +{% if messages %} +<ul class="user-messages"> + {% for msg in messages %} + <li{% if msg.tags %} class="{{ msg.tags }}"{% endif %}>{{ msg }}</li> + {% endfor %} +</ul> +{% endif %} +{% if msgs.object_list %} + <form action="." onsubmit="return bulkMsgAction(this, 'undelete');"> + <table class="messages"> + <tr> + <th>From</th> + <th>To</th> + <th>Subject</th> + <th>Date</th> + <th><input type="checkbox" onclick="tabMasterCheckClick(this, 'trash_ids');" /></th> + </tr> + {% for msg in msgs.object_list %} + <tr> + <td><a href="{% url 'bio.views.view_profile' msg.sender.username %}"> + {{ msg.sender.username }}</a></td> + <td><a href="{% url 'bio.views.view_profile' msg.receiver.username %}"> + {{ msg.receiver.username }}</a></td> + <td> + <a href="#" onclick="msgShow(this, {{ msg.id }}); return false;" + class="{% if msg.unread %}unread {% endif %}{% if msg.replied_to %}replied_to{% endif %}">{{ msg.subject }}</a> + </td> + <td>{{ msg.send_date|date:"M j, Y g:i:s A T" }}</td> + <td><input type="checkbox" name="trash_ids" value="{{ msg.id }}" /></td> + </tr> + {% endfor %} + <tr><td colspan="5"><input type="submit" value="Undelete Checked Messages" /></td></tr> + </table> + </form> + {% include "messages/pagination.html" %} + <ul> + <li>Messages in <strong>bold</strong> are unread.</li> + <li>Messages in <em>italics</em> have been replied to.</li> + </ul> +{% else %} + <p><em>Your Trash is empty.</em></p> +{% endif %}