Refactor messages view to use master3

This commit is contained in:
Lance Edgar 2017-12-04 17:52:25 -06:00
parent 84ebf5d929
commit c3fb86e391
5 changed files with 133 additions and 141 deletions

View file

@ -331,13 +331,24 @@ class Form(object):
return fields return fields
def remove_field(self, key): def insert_before(self, field, newfield):
if key in self.fields: self.fields.insert_before(field, newfield)
self.fields.remove(key)
def remove_fields(self, *args): def insert_after(self, field, newfield):
self.fields.insert_after(field, newfield)
def remove(self, *args):
for arg in args: for arg in args:
self.remove_field(arg) if arg in self.fields:
self.fields.remove(arg)
# TODO: deprecare / remove this
def remove_field(self, key):
self.remove(key)
# TODO: deprecare / remove this
def remove_fields(self, *args):
self.remove(*args)
def make_schema(self): def make_schema(self):
if not self.model_class: if not self.model_class:
@ -538,7 +549,7 @@ class Form(object):
label = HTML.tag('label', self.get_label(field_name), for_=field_name) label = HTML.tag('label', self.get_label(field_name), for_=field_name)
field = self.render_field_value(field_name) or '' field = self.render_field_value(field_name) or ''
field_div = HTML.tag('div', class_='field', c=field) field_div = HTML.tag('div', class_='field', c=field)
return HTML.tag('div', class_='field-wrapper {}'.format(field), c=label + field_div) return HTML.tag('div', class_='field-wrapper {}'.format(field_name), c=label + field_div)
def render_field_value(self, field_name): def render_field_value(self, field_name):
record = self.model_instance record = self.model_instance

View file

@ -15,17 +15,17 @@
// validate message before sending // validate message before sending
function validate_message_form() { function validate_message_form() {
var form = $('#new-message'); var form = $('#deform');
if (! form.find('input[name="Message--recipients"]').val()) { if (! form.find('input[name="recipients_"]').val()) {
alert("You must specify some recipient(s) for the message."); alert("You must specify some recipient(s) for the message.");
$('.recipients input').data('ui-tagit').tagInput.focus(); $('.recipients_ input').data('ui-tagit').tagInput.focus();
return false; return false;
} }
if (! form.find('input[name="Message--subject"]').val()) { if (! form.find('input[name="subject"]').val()) {
alert("You must provide a subject for the message."); alert("You must provide a subject for the message.");
form.find('input[name="Message--subject"]').focus(); form.find('input[name="subject"]').focus();
return false; return false;
} }
@ -34,7 +34,7 @@
$(function() { $(function() {
var recipients = $('.recipients input'); var recipients = $('.recipients_ input');
recipients.tagit({ recipients.tagit({

View file

@ -26,7 +26,7 @@
<%def name="extra_styles()"> <%def name="extra_styles()">
${parent.extra_styles()} ${parent.extra_styles()}
<style type="text/css"> <style type="text/css">
.field-wrapper.recipients .everyone { .recipients .everyone {
cursor: pointer; cursor: pointer;
display: none; display: none;
} }

View file

@ -158,8 +158,9 @@ class MasterView3(MasterView2):
def save_create_form(self, form): def save_create_form(self, form):
self.before_create(form) self.before_create(form)
obj = self.objectify(form, self.form_deserialized) with self.Session().no_autoflush:
self.before_create_flush(obj, form) obj = self.objectify(form, self.form_deserialized)
self.before_create_flush(obj, form)
self.Session.add(obj) self.Session.add(obj)
self.Session.flush() self.Session.flush()
return obj return obj

View file

@ -26,79 +26,21 @@ Message Views
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import json
import pytz
import six import six
from rattail import enum
from rattail.db import model from rattail.db import model
from rattail.time import localtime from rattail.time import localtime
import formalchemy import colander
from formalchemy.helpers import text_field from deform import widget as dfwidget
from pyramid import httpexceptions from pyramid import httpexceptions
from webhelpers2.html import tags, HTML from webhelpers2.html import tags, HTML
from tailbone import forms
from tailbone.db import Session from tailbone.db import Session
from tailbone.views import MasterView2 as MasterView from tailbone.views import MasterView3 as MasterView
from tailbone.util import raw_datetime from tailbone.util import raw_datetime
class SenderFieldRenderer(forms.renderers.UserFieldRenderer):
def render_readonly(self, **kwargs):
sender = self.raw_value
if sender is self.request.user:
return 'you'
return super(SenderFieldRenderer, self).render_readonly(**kwargs)
class RecipientsField(formalchemy.Field):
"""
Custom field for recipients, used when sending new messages.
"""
is_collection = True
def sync(self):
if not self.is_readonly():
message = self.parent.model
for uuid in self._deserialize():
user = Session.query(model.User).get(uuid)
if user:
message.add_recipient(user, status=enum.MESSAGE_STATUS_INBOX)
class RecipientsFieldRenderer(formalchemy.FieldRenderer):
def render(self, **kwargs):
uuids = self.value
value = ','.join(uuids) if uuids else ''
return text_field(self.name, value=value, **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
if not recipients:
return ''
recips = [r for r in recipients if r.recipient is not self.request.user]
recips = sorted([r.recipient.display_name for r in recips])
if len(recips) < len(recipients):
recips.insert(0, 'you')
max_display = 5
if len(recips) > max_display:
basic = HTML.literal("{}, ".format(', '.join(recips[:max_display-1])))
more = tags.link_to("({} more)".format(len(recips[max_display-1:])), '#', class_='more')
everyone = HTML.tag('span', class_='everyone', c=', '.join(recips[max_display-1:]))
return basic + more + everyone
return ', '.join(recips)
class MessagesView(MasterView): class MessagesView(MasterView):
""" """
Base class for message views. Base class for message views.
@ -109,7 +51,21 @@ class MessagesView(MasterView):
checkboxes = True checkboxes = True
replying = False replying = False
reply_header_sent_format = '%a %d %b %Y at %I:%M %p' reply_header_sent_format = '%a %d %b %Y at %I:%M %p'
grid_columns = ['subject', 'sender', 'recipients', 'sent']
grid_columns = [
'subject',
'sender',
'recipients',
'sent',
]
form_fields = [
'sender',
'recipients',
'sent',
'subject',
'body',
]
def get_index_title(self): def get_index_title(self):
if self.listing: if self.listing:
@ -117,7 +73,7 @@ class MessagesView(MasterView):
if self.viewing: if self.viewing:
message = self.get_instance() message = self.get_instance()
recipient = self.get_recipient(message) recipient = self.get_recipient(message)
if recipient and recipient.status == enum.MESSAGE_STATUS_ARCHIVE: if recipient and recipient.status == self.enum.MESSAGE_STATUS_ARCHIVE:
return "Message Archive" return "Message Archive"
elif not recipient: elif not recipient:
return "Sent Messages" return "Sent Messages"
@ -178,7 +134,7 @@ class MessagesView(MasterView):
def render_sent(self, message, column_name): def render_sent(self, message, column_name):
return raw_datetime(self.rattail_config, message.sent) return raw_datetime(self.rattail_config, message.sent)
def render_sender(self, message, column_name): def render_sender(self, message, field):
sender = message.sender sender = message.sender
if sender is self.request.user: if sender is self.request.user:
return 'you' return 'you'
@ -197,50 +153,63 @@ class MessagesView(MasterView):
return "{}, ...".format(', '.join(recips[:4])) return "{}, ...".format(', '.join(recips[:4]))
return "" return ""
def make_form(self, instance, **kwargs): def render_recipients_full(self, message, field):
form = super(MessagesView, self).make_form(instance, **kwargs) recipients = message.recipients
if self.creating: if not recipients:
form.id = 'new-message' return ""
form.cancel_url = self.request.get_referrer(default=self.request.route_url('messages.inbox')) recips = [r for r in recipients
form.create_label = "Send Message" if r.recipient is not self.request.user]
return form recips = sorted([r.recipient.display_name for r in recips])
if len(recips) < len(recipients) and (
message.sender is not self.request.user or not recips):
recips.insert(0, 'you')
max_display = 5
if len(recips) > max_display:
basic = HTML.literal("{}, ".format(', '.join(recips[:max_display-1])))
more = tags.link_to("({} more)".format(len(recips[max_display-1:])), '#', class_='more')
everyone = HTML.tag('span', class_='everyone', c=', '.join(recips[max_display-1:]))
return basic + more + everyone
return ', '.join(recips)
# TODO!!
# def make_form(self, instance, **kwargs):
# form = super(MessagesView, self).make_form(instance, **kwargs)
# if self.creating:
# form.id = 'new-message'
# form.cancel_url = self.request.get_referrer(default=self.request.route_url('messages.inbox'))
# form.create_label = "Send Message"
# return form
def configure_form(self, f):
super(MessagesView, self).configure_form(f)
def configure_fieldset(self, fs):
# TODO: A fair amount of this still seems hacky... # TODO: A fair amount of this still seems hacky...
f.set_renderer('sender', self.render_sender)
f.set_label('sender', "From")
f.set_type('sent', 'datetime')
f.set_renderer('recipients', self.render_recipients_full)
f.set_label('recipients', "To")
f.set_widget('body', dfwidget.TextAreaWidget(cols=50, rows=15))
if self.creating: if self.creating:
f.remove('sender', 'sent')
# Must create a new 'sender' field so that we can feed it the f.insert_after('recipients', 'recipients_')
# current user as default value, but prevent attaching user to the f.remove('recipients')
# (new) underlying message instance...ugh f.set_node('recipients_', colander.SchemaNode(colander.Set()))
fs.append(formalchemy.Field('sender', value=self.request.user, f.set_widget('recipients_', RecipientsWidget())
renderer=forms.renderers.UserFieldRenderer, f.set_label('recipients_', "To")
label="From", readonly=True))
# Sort of the same thing for recipients, although most of that logic is below.
fs.append(RecipientsField('recipients', label="To", renderer=RecipientsFieldRenderer))
fs.configure(include=[
fs.sender,
fs.recipients,
fs.subject,
fs.body.textarea(size='50x15'),
])
# We'll assign some properties directly on the new message;
# apparently that's safe and won't cause it to be committed.
# Notably, we can't assign the sender yet. Also the actual
# recipients assignment is handled by that field's sync().
message = fs.model
if self.replying: if self.replying:
old_message = self.get_instance() old_message = self.get_instance()
message.subject = "Re: {}".format(old_message.subject) f.set_default('subject', "Re: {}".format(old_message.subject))
message.body = self.get_reply_body(old_message) f.set_default('body', self.get_reply_body(old_message))
# Determine an initial set of recipients, based on reply # Determine an initial set of recipients, based on reply method.
# method. This value will be set to a 'pseudo' field to avoid
# touching the new model instance and causing a crap commit.
# If replying to all, massage the list a little so that the # If replying to all, massage the list a little so that the
# current user is not listed, and the sender is listed first. # current user is not listed, and the sender is listed first.
@ -250,37 +219,36 @@ class MessagesView(MasterView):
if self.filter_reply_recipient(r.recipient)] 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.items(), key=lambda r: r[1])
value = [r[0] for r in value] value = [r[0] for r in value]
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) f.set_default('recipients_', ','.join(value))
# Just a normal reply, to sender only. # Just a normal reply, to sender only.
elif self.filter_reply_recipient(old_message.sender): elif self.filter_reply_recipient(old_message.sender):
fs.recipients.set(value=[old_message.sender.uuid]) f.set_default('recipients_', old_message.sender.uuid)
# Set focus to message body instead of recipients, when replying. # TODO?
fs.focus = fs.body # # Set focus to message body instead of recipients, when replying.
# fs.focus = fs.body
elif self.viewing: elif self.viewing:
f.remove('body')
# Viewing an existing message is a heck of a lot easier... def objectify(self, form, data):
fs.configure(include=[ message = super(MessagesView, self).objectify(form, data)
fs.sender.with_renderer(SenderFieldRenderer).label("From"),
fs.recipients.with_renderer(RecipientsFieldRenderer).label("To"),
fs.sent,
fs.subject,
])
def before_create(self, form): if self.creating:
""" if self.request.user:
This is where we must assign the current user as sender for new message.sender = self.request.user
messages, for now. I'm still not quite happy with this...
""" for uuid in data['recipients_']:
super(MessagesView, self).before_create(form) user = self.Session.query(model.User).get(uuid)
message = form.fieldset.model if user:
message.sender = self.request.user message.add_recipient(user, status=self.enum.MESSAGE_STATUS_INBOX)
return message
def flash_after_create(self, obj): def flash_after_create(self, obj):
self.request.session.flash("Message has been sent: {}".format( self.request.session.flash("Message has been sent: {}".format(
@ -290,8 +258,7 @@ class MessagesView(MasterView):
return user.active return user.active
def get_reply_header(self, message): def get_reply_header(self, message):
sent = pytz.utc.localize(message.sent) sent = localtime(self.rattail_config, message.sent, from_utc=True)
sent = localtime(self.rattail_config, sent)
sent = sent.strftime(self.reply_header_sent_format) sent = sent.strftime(self.reply_header_sent_format)
return "On {}, {} wrote:".format(sent, message.sender.person.display_name) return "On {}, {} wrote:".format(sent, message.sender.person.display_name)
@ -318,7 +285,6 @@ class MessagesView(MasterView):
def template_kwargs_create(self, **kwargs): def template_kwargs_create(self, **kwargs):
kwargs['available_recipients'] = self.get_available_recipients() kwargs['available_recipients'] = self.get_available_recipients()
kwargs['json'] = json
if self.replying: if self.replying:
kwargs['original_message'] = self.get_instance() kwargs['original_message'] = self.get_instance()
return kwargs return kwargs
@ -371,7 +337,7 @@ class MessagesView(MasterView):
return self.redirect(self.request.get_referrer( return self.redirect(self.request.get_referrer(
default=self.request.route_url('messages_inbox'))) default=self.request.route_url('messages_inbox')))
new_status = enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else enum.MESSAGE_STATUS_ARCHIVE new_status = self.enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else self.enum.MESSAGE_STATUS_ARCHIVE
if recipient.status != new_status: if recipient.status != new_status:
recipient.status = new_status recipient.status = new_status
return self.redirect(self.request.route_url('messages.{}'.format( return self.redirect(self.request.route_url('messages.{}'.format(
@ -385,7 +351,7 @@ class MessagesView(MasterView):
if self.request.method == 'POST': if self.request.method == 'POST':
uuids = self.request.POST.get('uuids', '').split(',') uuids = self.request.POST.get('uuids', '').split(',')
if uuids: if uuids:
new_status = enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else enum.MESSAGE_STATUS_ARCHIVE new_status = self.enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else self.enum.MESSAGE_STATUS_ARCHIVE
for uuid in uuids: for uuid in uuids:
recip = self.Session.query(model.MessageRecipient)\ recip = self.Session.query(model.MessageRecipient)\
.filter(model.MessageRecipient.message_uuid == uuid)\ .filter(model.MessageRecipient.message_uuid == uuid)\
@ -436,7 +402,7 @@ class InboxView(MessagesView):
def query(self, session): def query(self, session):
q = super(InboxView, self).query(session) q = super(InboxView, self).query(session)
return q.filter(model.MessageRecipient.status == enum.MESSAGE_STATUS_INBOX) return q.filter(model.MessageRecipient.status == self.enum.MESSAGE_STATUS_INBOX)
class ArchiveView(MessagesView): class ArchiveView(MessagesView):
@ -452,7 +418,7 @@ class ArchiveView(MessagesView):
def query(self, session): def query(self, session):
q = super(ArchiveView, self).query(session) q = super(ArchiveView, self).query(session)
return q.filter(model.MessageRecipient.status == enum.MESSAGE_STATUS_ARCHIVE) return q.filter(model.MessageRecipient.status == self.enum.MESSAGE_STATUS_ARCHIVE)
class SentView(MessagesView): class SentView(MessagesView):
@ -481,6 +447,20 @@ class SentView(MessagesView):
default_active=True, default_verb='contains') default_active=True, default_verb='contains')
class RecipientsWidget(dfwidget.TextInputWidget):
def deserialize(self, field, pstruct):
if pstruct is colander.null:
return []
elif not isinstance(pstruct, six.string_types):
raise colander.Invalid(field.schema, "Pstruct is not a string")
if self.strip:
pstruct = pstruct.strip()
if not pstruct:
return []
return pstruct.split(',')
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")