Add initial reply / reply-all support for messages.
This commit is contained in:
parent
415fc439b7
commit
46923d40da
|
@ -21,9 +21,9 @@
|
|||
% endif
|
||||
</div>
|
||||
|
||||
% if not _focus_rendered and (fieldset.focus == field or fieldset.focus is True):
|
||||
% if not _focus_rendered and (fieldset.focus is True or fieldset.focus is field):
|
||||
% if not field.is_readonly() and getattr(field.renderer, 'needs_focus', True):
|
||||
<script language="javascript" type="text/javascript">
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
% if hasattr(field.renderer, 'focus_name'):
|
||||
$('#${field.renderer.focus_name}').focus();
|
||||
|
|
|
@ -6,12 +6,20 @@
|
|||
${h.javascript_link(request.static_url('tailbone:static/js/lib/tag-it.min.js'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.tagit.css'))}
|
||||
<script type="text/javascript">
|
||||
|
||||
% if initial_recipient_names is not Undefined:
|
||||
var initial_recipients = {
|
||||
% for i, (uuid, name) in enumerate(initial_recipient_names.iteritems(), 1):
|
||||
'${uuid}': "${name.replace('"', '\\""')|n}"${',' if i < len(initial_recipient_names) else ''}
|
||||
% endfor
|
||||
};
|
||||
% endif
|
||||
|
||||
$(function() {
|
||||
|
||||
var recipients = $('.recipients ul');
|
||||
var recipients = $('.recipients input');
|
||||
|
||||
recipients.tagit({
|
||||
fieldName: 'Message--recipients',
|
||||
autocomplete: {
|
||||
delay: 0,
|
||||
minLength: 2,
|
||||
|
@ -29,7 +37,11 @@
|
|||
var label = ui.tagLabel.split(',');
|
||||
var value = label.shift();
|
||||
$(ui.tag).find('.tagit-hidden-field').val(value);
|
||||
$(ui.tag).find('.tagit-label').text(label.join());
|
||||
if (label.length) {
|
||||
$(ui.tag).find('.tagit-label').text(label.join());
|
||||
} else {
|
||||
$(ui.tag).find('.tagit-label').text(initial_recipients[value]);
|
||||
}
|
||||
},
|
||||
removeConfirmation: true
|
||||
});
|
||||
|
@ -41,7 +53,8 @@
|
|||
</script>
|
||||
<style type="text/css">
|
||||
|
||||
.field-wrapper.subject .field input[type="text"] {
|
||||
.field-wrapper.subject .field input[type="text"],
|
||||
.field-wrapper.body .field textarea {
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
margin-bottom: 15px;
|
||||
padding-top: 15px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.message-body p {
|
||||
margin-bottom: 15px;
|
||||
|
@ -43,6 +43,8 @@
|
|||
<%def name="message_tools()">
|
||||
% if recipient:
|
||||
<div class="message-tools">
|
||||
${h.link_to("Reply", url('messages.reply', uuid=instance.uuid), class_='button')}
|
||||
${h.link_to("Reply to All", url('messages.reply_all', uuid=instance.uuid), class_='button')}
|
||||
% if recipient.status == rattail.enum.MESSAGE_STATUS_INBOX:
|
||||
${h.link_to("Move to Archive", url('messages.move', uuid=instance.uuid) + '?dest=archive', class_='button')}
|
||||
% else:
|
||||
|
|
|
@ -32,8 +32,8 @@ from sqlalchemy import orm
|
|||
from edbob.util import prettify
|
||||
|
||||
import formalchemy
|
||||
from pyramid import httpexceptions
|
||||
from pyramid.renderers import get_renderer, render_to_response
|
||||
from pyramid.httpexceptions import HTTPException, HTTPFound, HTTPNotFound
|
||||
|
||||
from tailbone import forms
|
||||
from tailbone.views import View
|
||||
|
@ -107,9 +107,9 @@ class MasterView(View):
|
|||
form.save()
|
||||
instance = form.fieldset.model
|
||||
self.after_create(instance)
|
||||
self.request.session.flash("{0} {1} has been created.".format(
|
||||
self.request.session.flash("{} {} has been created.".format(
|
||||
self.get_model_title(), instance))
|
||||
return HTTPFound(location=self.get_action_url('view', instance))
|
||||
return self.redirect(self.get_action_url('view', instance))
|
||||
return self.render_to_response('create', {'form': form})
|
||||
|
||||
def view(self):
|
||||
|
@ -136,7 +136,7 @@ class MasterView(View):
|
|||
self.after_edit(instance)
|
||||
self.request.session.flash("{0} {1} has been updated.".format(
|
||||
self.get_model_title(), self.get_instance_title(instance)))
|
||||
return HTTPFound(location=self.get_action_url('view', instance))
|
||||
return self.redirect(self.get_action_url('view', instance))
|
||||
return self.render_to_response('edit', {'instance': instance,
|
||||
'instance_title': self.get_instance_title(instance),
|
||||
'form': form})
|
||||
|
@ -160,7 +160,7 @@ class MasterView(View):
|
|||
|
||||
# Let derived classes prep for (or cancel) deletion.
|
||||
result = self.before_delete(instance)
|
||||
if isinstance(result, HTTPException):
|
||||
if isinstance(result, httpexceptions.HTTPException):
|
||||
return result
|
||||
|
||||
self.delete_instance(instance)
|
||||
|
@ -308,7 +308,7 @@ class MasterView(View):
|
|||
"""
|
||||
Convenience method to return a HTTP 302 response.
|
||||
"""
|
||||
return HTTPFound(location=url)
|
||||
return httpexceptions.HTTPFound(location=url)
|
||||
|
||||
##############################
|
||||
# Grid Stuff
|
||||
|
@ -500,7 +500,7 @@ class MasterView(View):
|
|||
key = self.request.matchdict[self.get_model_key()]
|
||||
instance = self.Session.query(self.model_class).get(key)
|
||||
if not instance:
|
||||
raise HTTPNotFound()
|
||||
raise httpexceptions.HTTPNotFound()
|
||||
return instance
|
||||
|
||||
def get_instance_title(self, instance):
|
||||
|
@ -605,6 +605,13 @@ class MasterView(View):
|
|||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
"""
|
||||
Provide default configuration for a master view.
|
||||
"""
|
||||
return cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _defaults(cls, config):
|
||||
"""
|
||||
Provide default configuration for a master view.
|
||||
"""
|
||||
|
|
|
@ -26,11 +26,14 @@ Message Views
|
|||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import pytz
|
||||
|
||||
from rattail import enum
|
||||
from rattail.db import model
|
||||
from rattail.time import localtime
|
||||
|
||||
import formalchemy
|
||||
from formalchemy.helpers import hidden_field
|
||||
from formalchemy.helpers import text_field
|
||||
from pyramid import httpexceptions
|
||||
from webhelpers.html import tags, HTML
|
||||
|
||||
|
@ -66,7 +69,7 @@ class RecipientsField(formalchemy.Field):
|
|||
def sync(self):
|
||||
if not self.is_readonly():
|
||||
message = self.parent.model
|
||||
for uuid in set(self._deserialize()):
|
||||
for uuid in self._deserialize():
|
||||
user = Session.query(model.User).get(uuid)
|
||||
if user:
|
||||
message.add_recipient(user, status=enum.MESSAGE_STATUS_INBOX)
|
||||
|
@ -75,7 +78,15 @@ class RecipientsField(formalchemy.Field):
|
|||
class RecipientsFieldRenderer(formalchemy.FieldRenderer):
|
||||
|
||||
def render(self, **kwargs):
|
||||
return HTML.tag('ul')
|
||||
uuids = self.value
|
||||
value = ','.join(uuids) if uuids else ''
|
||||
return text_field(self.name, value=value, maxlength=self.length, **kwargs)
|
||||
|
||||
def deserialize(self):
|
||||
value = self.params.getone(self.name).split(',')
|
||||
value = [uuid.strip() for uuid in value]
|
||||
value = set([uuid for uuid in value if uuid])
|
||||
return value
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
recipients = self.raw_value
|
||||
|
@ -109,6 +120,8 @@ class MessagesView(MasterView):
|
|||
editable = False
|
||||
deletable = False
|
||||
checkboxes = True
|
||||
replying = False
|
||||
reply_header_sent_format = '%a %d %b %Y at %I:%M %p'
|
||||
|
||||
def get_index_url(self):
|
||||
# not really used, but necessary to make certain other code happy
|
||||
|
@ -168,39 +181,108 @@ class MessagesView(MasterView):
|
|||
return {'data-uuid': recip.uuid}
|
||||
return {}
|
||||
|
||||
def make_form(self, instance, **kwargs):
|
||||
form = super(MessagesView, self).make_form(instance, **kwargs)
|
||||
if self.creating:
|
||||
form.cancel_url = self.request.get_referrer(default=self.request.route_url('messages.inbox'))
|
||||
return form
|
||||
|
||||
def configure_fieldset(self, fs):
|
||||
fs.sender.set(label="From")
|
||||
|
||||
if self.creating:
|
||||
fs.append(RecipientsField('recipients', label="To", renderer=RecipientsFieldRenderer))
|
||||
fs.configure(include=[
|
||||
fs.sender.with_renderer(forms.renderers.UserFieldRenderer).readonly(),
|
||||
fs.recipients,
|
||||
fs.subject,
|
||||
fs.body.textarea(size='50x10'),
|
||||
fs.body.textarea(size='50x15'),
|
||||
])
|
||||
else:
|
||||
message = fs.model
|
||||
message.sender = self.request.user
|
||||
|
||||
if self.replying:
|
||||
old_message = self.get_instance()
|
||||
message.subject = "Re: {}".format(old_message.subject)
|
||||
message.body = self.get_reply_body(old_message)
|
||||
if self.replying == 'all':
|
||||
value = [(r.recipient_uuid, r.recipient.person.display_name)
|
||||
for r in old_message.recipients
|
||||
if r.recipient.active]
|
||||
value = dict(value)
|
||||
value.pop(self.request.user.uuid, None)
|
||||
value = sorted(value.iteritems(), key=lambda r: r[1])
|
||||
value = [r[0] for r in value]
|
||||
if old_message.sender is not self.request.user and old_message.sender.active:
|
||||
value.insert(0, old_message.sender_uuid)
|
||||
fs.recipients.set(value=value)
|
||||
else:
|
||||
fs.recipients.set(value=[old_message.sender_uuid])
|
||||
fs.focus = fs.body
|
||||
|
||||
elif self.viewing:
|
||||
fs.configure(include=[
|
||||
fs.sender.with_renderer(SenderFieldRenderer).label("From"),
|
||||
fs.sender.with_renderer(SenderFieldRenderer),
|
||||
fs.recipients.with_renderer(RecipientsFieldRenderer).label("To"),
|
||||
fs.sent.with_renderer(forms.renderers.DateTimeFieldRenderer(self.rattail_config)),
|
||||
fs.subject,
|
||||
fs.body.textarea(size='50x10'),
|
||||
])
|
||||
if self.viewing:
|
||||
del fs.body # ..really?
|
||||
|
||||
def before_create(self, form):
|
||||
message = form.fieldset.model
|
||||
message.sender = self.request.user
|
||||
def get_reply_header(self, message):
|
||||
sent = pytz.utc.localize(message.sent)
|
||||
sent = localtime(self.rattail_config, sent)
|
||||
sent = sent.strftime(self.reply_header_sent_format)
|
||||
return "On {}, {} wrote:".format(sent, message.sender.person.display_name)
|
||||
|
||||
def get_reply_body(self, message):
|
||||
"""
|
||||
Given an original message, this method should return the default body
|
||||
value for a "reply" message, i.e. with ">" prefixes etc.
|
||||
"""
|
||||
header = self.get_reply_header(message)
|
||||
lines = message.body.split('\n')
|
||||
if lines and lines[0]:
|
||||
lines.insert(0, '')
|
||||
lines = ['', '', '', header] + ["> {}".format(line) for line in lines]
|
||||
return '\n'.join(lines)
|
||||
|
||||
def get_recipient(self, message):
|
||||
for recip in message.recipients:
|
||||
if recip.recipient is self.request.user:
|
||||
return recip
|
||||
|
||||
def template_kwargs_create(self, **kwargs):
|
||||
if self.replying:
|
||||
message = self.get_instance()
|
||||
kwargs['original_message'] = message
|
||||
names = {message.sender_uuid: message.sender.person.display_name}
|
||||
if self.replying == 'all':
|
||||
for recip in message.recipients:
|
||||
if recip.recipient is not message.sender:
|
||||
names[recip.recipient_uuid] = recip.recipient.person.display_name
|
||||
kwargs['initial_recipient_names'] = names
|
||||
return kwargs
|
||||
|
||||
def template_kwargs_view(self, **kwargs):
|
||||
message = kwargs['instance']
|
||||
return {'message': message,
|
||||
'recipient': self.get_recipient(message)}
|
||||
|
||||
def reply(self):
|
||||
"""
|
||||
Reply to a message, i.e. create a new one with first sender as recipient.
|
||||
"""
|
||||
self.replying = True
|
||||
return self.create()
|
||||
|
||||
def reply_all(self):
|
||||
"""
|
||||
Reply-all to a message, i.e. create a new one with all original
|
||||
recipients listed again in the new message.
|
||||
"""
|
||||
self.replying = 'all'
|
||||
return self.create()
|
||||
|
||||
def move(self):
|
||||
"""
|
||||
Move a message, either to the archive or back to the inbox.
|
||||
|
@ -243,6 +325,30 @@ class MessagesView(MasterView):
|
|||
route = 'messages.{}'.format('archive' if dest == 'inbox' else 'inbox')
|
||||
return self.redirect(self.request.route_url(route))
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
"""
|
||||
Extra default config for message views.
|
||||
"""
|
||||
|
||||
# reply
|
||||
config.add_route('messages.reply', '/messages/{uuid}/reply')
|
||||
config.add_view(cls, attr='reply', route_name='messages.reply')
|
||||
|
||||
# reply-all
|
||||
config.add_route('messages.reply_all', '/messages/{uuid}/reply-all')
|
||||
config.add_view(cls, attr='reply_all', route_name='messages.reply_all')
|
||||
|
||||
# move (single)
|
||||
config.add_route('messages.move', '/messages/{uuid}/move')
|
||||
config.add_view(cls, attr='move', route_name='messages.move')
|
||||
|
||||
# move bulk
|
||||
config.add_route('messages.move_bulk', '/messages/move-bulk')
|
||||
config.add_view(cls, attr='move_bulk', route_name='messages.move_bulk')
|
||||
|
||||
cls._defaults(config)
|
||||
|
||||
|
||||
class InboxView(MessagesView):
|
||||
"""
|
||||
|
@ -324,14 +430,6 @@ def includeme(config):
|
|||
config.add_view(ArchiveView, attr='index', route_name='messages.archive',
|
||||
permission='messages.list')
|
||||
|
||||
# move (single)
|
||||
config.add_route('messages.move', '/messages/{uuid}/move')
|
||||
config.add_view(MessagesView, attr='move', route_name='messages.move')
|
||||
|
||||
# move bulk
|
||||
config.add_route('messages.move_bulk', '/messages/move-bulk')
|
||||
config.add_view(MessagesView, attr='move_bulk', route_name='messages.move_bulk')
|
||||
|
||||
# sent
|
||||
config.add_route('messages.sent', '/messages/sent/')
|
||||
config.add_view(SentView, attr='index', route_name='messages.sent',
|
||||
|
|
Loading…
Reference in a new issue