Refactor magic recipients field when sending new message.

Uses local cache of user mappings instead of AJAX calls; has just enough
customization hooks to allow for a department/user mapping for MJ...
This commit is contained in:
Lance Edgar 2016-02-11 17:05:56 -06:00
parent ad9cd8be8e
commit c65bc6f229
3 changed files with 86 additions and 66 deletions

View file

@ -7,43 +7,53 @@
${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 recipient_mappings = new Map([
var initial_recipients = { <% last = len(available_recipients) %>
% for i, (uuid, name) in enumerate(initial_recipient_names.iteritems(), 1): % for i, (uuid, entry) in enumerate(sorted(available_recipients.iteritems(), key=lambda r: r[1]), 1):
'${uuid}': "${name.replace('"', '\\""')|n}"${',' if i < len(initial_recipient_names) else ''} ['${uuid}', ${json.dumps(entry)|n}]${',' if i < last else ''}
% endfor % endfor
}; ]);
% endif
$(function() { $(function() {
var recipients = $('.recipients input'); var recipients = $('.recipients input');
recipients.tagit({ recipients.tagit({
autocomplete: { autocomplete: {
delay: 0, delay: 0,
minLength: 2, minLength: 2,
autoFocus: true, autoFocus: true,
removeConfirmation: true,
source: function(request, response) { source: function(request, response) {
$.get('${url('messages.recipients')}', {term: request.term}, response); var term = request.term.toLowerCase();
}, var data = [];
select: function(event, ui) { recipient_mappings.forEach(function(name, uuid) {
recipients.tagit('createTag', ui.item.value + ',' + ui.item.label); if (!name.toLowerCase && name.name) {
// Preventing the tag input to be updated with the chosen value. name = name.name;
return false; }
if (name.toLowerCase().indexOf(term) >= 0) {
data.push({value: uuid, label: name});
}
});
response(data);
} }
}, },
beforeTagAdded: function(event, ui) {
var label = ui.tagLabel.split(','); beforeTagAdded: ${self.before_tag_added()},
var value = label.shift();
$(ui.tag).find('.tagit-hidden-field').val(value); beforeTagRemoved: function(event, ui) {
if (label.length) {
$(ui.tag).find('.tagit-label').text(label.join()); // Unfortunately we're responsible for cleaning up the hidden
} else { // field, since the values there do not match the tag labels.
$(ui.tag).find('.tagit-label').text(initial_recipients[value]); var tags = recipients.tagit('assignedTags');
} var uuid = ui.tag.data('uuid');
}, tags = tags.filter(function(element) {
removeConfirmation: true return element != uuid;
});
recipients.data('ui-tagit')._updateSingleTagsField(tags);
}
}); });
// set focus to recipients field // set focus to recipients field
@ -61,6 +71,18 @@
</style> </style>
</%def> </%def>
<%def name="before_tag_added()">
function(event, ui) {
// Lookup the name in cached mapping, and show that on the tag, instead
// of the UUID. The tagit widget should take care of keeping the
// hidden field in sync for us, still using the UUID.
var uuid = ui.tagLabel;
var name = recipient_mappings.get(uuid);
ui.tag.find('.tagit-label').html(name);
}
</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
% if request.has_perm('messages.list'): % if request.has_perm('messages.list'):
<li>${h.link_to("Go to my Message Inbox", url('messages.inbox'))}</li> <li>${h.link_to("Go to my Message Inbox", url('messages.inbox'))}</li>

View file

@ -63,6 +63,9 @@ class AutocompleteView(View):
""" """
return instance.uuid return instance.uuid
def get_data(self, term):
return self.query(term).all()
def __call__(self): def __call__(self):
""" """
View implementation. View implementation.
@ -73,5 +76,5 @@ class AutocompleteView(View):
term = self.prepare_term(term) term = self.prepare_term(term)
if not term: if not term:
return [] return []
results = self.query(term).all() results = self.get_data(term)
return [{'label': self.display(x), 'value': self.value(x)} for x in results] return [{'label': self.display(x), 'value': self.value(x)} for x in results]

View file

@ -26,6 +26,7 @@ Message Views
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import json
import pytz import pytz
from rattail import enum from rattail import enum
@ -39,7 +40,7 @@ from webhelpers.html import tags, HTML
from tailbone import forms from tailbone import forms
from tailbone.db import Session from tailbone.db import Session
from tailbone.views import MasterView, AutocompleteView from tailbone.views import MasterView
class SubjectFieldRenderer(formalchemy.FieldRenderer): class SubjectFieldRenderer(formalchemy.FieldRenderer):
@ -80,7 +81,7 @@ class RecipientsFieldRenderer(formalchemy.FieldRenderer):
def render(self, **kwargs): def render(self, **kwargs):
uuids = self.value uuids = self.value
value = ','.join(uuids) if uuids else '' value = ','.join(uuids) if uuids else ''
return text_field(self.name, value=value, maxlength=self.length, **kwargs) return text_field(self.name, value=value, **kwargs)
def deserialize(self): def deserialize(self):
value = self.params.getone(self.name).split(',') value = self.params.getone(self.name).split(',')
@ -150,8 +151,8 @@ class MessagesView(MasterView):
def query(self, session): def query(self, session):
return session.query(model.Message)\ return session.query(model.Message)\
.outerjoin(model.MessageRecipient)\ .outerjoin(model.MessageRecipient)\
.filter(model.MessageRecipient.recipient == self.request.user) .filter(model.MessageRecipient.recipient == self.request.user)
def configure_grid(self, g): def configure_grid(self, g):
@ -206,9 +207,9 @@ class MessagesView(MasterView):
message.subject = "Re: {}".format(old_message.subject) message.subject = "Re: {}".format(old_message.subject)
message.body = self.get_reply_body(old_message) message.body = self.get_reply_body(old_message)
if self.replying == 'all': if self.replying == 'all':
value = [(r.recipient_uuid, r.recipient.person.display_name) value = [(r.recipient.uuid, r.recipient.person.display_name)
for r in old_message.recipients for r in old_message.recipients
if r.recipient.active] if self.filter_reply_recipient(r.recipient)]
value = dict(value) value = dict(value)
value.pop(self.request.user.uuid, None) value.pop(self.request.user.uuid, None)
value = sorted(value.iteritems(), key=lambda r: r[1]) value = sorted(value.iteritems(), key=lambda r: r[1])
@ -216,8 +217,8 @@ class MessagesView(MasterView):
if old_message.sender is not self.request.user and old_message.sender.active: if old_message.sender is not self.request.user and old_message.sender.active:
value.insert(0, old_message.sender_uuid) value.insert(0, old_message.sender_uuid)
fs.recipients.set(value=value) fs.recipients.set(value=value)
else: elif self.filter_reply_recipient(old_message.sender):
fs.recipients.set(value=[old_message.sender_uuid]) fs.recipients.set(value=[old_message.sender.uuid])
fs.focus = fs.body fs.focus = fs.body
elif self.viewing: elif self.viewing:
@ -228,6 +229,9 @@ class MessagesView(MasterView):
fs.subject, fs.subject,
]) ])
def filter_reply_recipient(self, user):
return user.active
def get_reply_header(self, message): def get_reply_header(self, message):
sent = pytz.utc.localize(message.sent) sent = pytz.utc.localize(message.sent)
sent = localtime(self.rattail_config, sent) sent = localtime(self.rattail_config, sent)
@ -247,22 +251,34 @@ class MessagesView(MasterView):
return '\n'.join(lines) return '\n'.join(lines)
def get_recipient(self, message): def get_recipient(self, message):
for recip in message.recipients: """
if recip.recipient is self.request.user: Fetch the recipient from the given message, which corresponds to the
return recip current (request) user.
"""
for recipient in message.recipients:
if recipient.recipient is self.request.user:
return recipient
def template_kwargs_create(self, **kwargs): def template_kwargs_create(self, **kwargs):
kwargs['available_recipients'] = self.get_available_recipients()
kwargs['json'] = json
if self.replying: if self.replying:
message = self.get_instance() kwargs['original_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 return kwargs
def get_available_recipients(self):
"""
Return the full mapping of recipients which may be included in a
message sent by the current user.
"""
recips = {}
users = Session.query(model.User)\
.join(model.Person)\
.filter(model.User.active == True)
for user in users:
recips[user.uuid] = user.person.display_name
return recips
def template_kwargs_view(self, **kwargs): def template_kwargs_view(self, **kwargs):
message = kwargs['instance'] message = kwargs['instance']
return {'message': message, return {'message': message,
@ -395,29 +411,13 @@ class SentView(MessagesView):
def query(self, session): def query(self, session):
return session.query(model.Message)\ return session.query(model.Message)\
.filter(model.Message.sender == self.request.user) .filter(model.Message.sender == self.request.user)
def configure_grid(self, g): def configure_grid(self, g):
super(SentView, self).configure_grid(g) super(SentView, self).configure_grid(g)
g.filters['sender'].default_active = False g.filters['sender'].default_active = False
class RecipientsAutocomplete(AutocompleteView):
"""
Autocomplete AJAX view for the recipients field when sending a new message.
"""
def query(self, term):
return Session.query(model.User)\
.join(model.Person)\
.filter(model.User.active == True)\
.filter(model.Person.display_name.ilike('%{}%'.format(term)))\
.order_by(model.Person.display_name)
def display(self, user):
return user.person.display_name
def includeme(config): def includeme(config):
config.add_tailbone_permission('messages', 'messages.list', "List/Search Messages") config.add_tailbone_permission('messages', 'messages.list', "List/Search Messages")
@ -437,9 +437,4 @@ def includeme(config):
config.add_view(SentView, attr='index', route_name='messages.sent', config.add_view(SentView, attr='index', route_name='messages.sent',
permission='messages.list') permission='messages.list')
# recipients autocomplete
config.add_route('messages.recipients', '/messages/recipients')
config.add_view(RecipientsAutocomplete, route_name='messages.recipients',
renderer='json', permission='messages.create')
MessagesView.defaults(config) MessagesView.defaults(config)