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