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
def remove_field(self, key):
if key in self.fields:
self.fields.remove(key)
def insert_before(self, field, newfield):
self.fields.insert_before(field, newfield)
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:
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):
if not self.model_class:
@ -538,7 +549,7 @@ class Form(object):
label = HTML.tag('label', self.get_label(field_name), for_=field_name)
field = self.render_field_value(field_name) or ''
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):
record = self.model_instance

View file

@ -15,17 +15,17 @@
// validate message before sending
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.");
$('.recipients input').data('ui-tagit').tagInput.focus();
$('.recipients_ input').data('ui-tagit').tagInput.focus();
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.");
form.find('input[name="Message--subject"]').focus();
form.find('input[name="subject"]').focus();
return false;
}
@ -34,7 +34,7 @@
$(function() {
var recipients = $('.recipients input');
var recipients = $('.recipients_ input');
recipients.tagit({

View file

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

View file

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

View file

@ -26,79 +26,21 @@ Message Views
from __future__ import unicode_literals, absolute_import
import json
import pytz
import six
from rattail import enum
from rattail.db import model
from rattail.time import localtime
import formalchemy
from formalchemy.helpers import text_field
import colander
from deform import widget as dfwidget
from pyramid import httpexceptions
from webhelpers2.html import tags, HTML
from tailbone import forms
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
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):
"""
Base class for message views.
@ -109,7 +51,21 @@ class MessagesView(MasterView):
checkboxes = True
replying = False
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):
if self.listing:
@ -117,7 +73,7 @@ class MessagesView(MasterView):
if self.viewing:
message = self.get_instance()
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"
elif not recipient:
return "Sent Messages"
@ -178,7 +134,7 @@ class MessagesView(MasterView):
def render_sent(self, message, column_name):
return raw_datetime(self.rattail_config, message.sent)
def render_sender(self, message, column_name):
def render_sender(self, message, field):
sender = message.sender
if sender is self.request.user:
return 'you'
@ -197,50 +153,63 @@ class MessagesView(MasterView):
return "{}, ...".format(', '.join(recips[:4]))
return ""
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 render_recipients_full(self, message, field):
recipients = message.recipients
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) 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...
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:
f.remove('sender', 'sent')
# Must create a new 'sender' field so that we can feed it the
# current user as default value, but prevent attaching user to the
# (new) underlying message instance...ugh
fs.append(formalchemy.Field('sender', value=self.request.user,
renderer=forms.renderers.UserFieldRenderer,
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
f.insert_after('recipients', 'recipients_')
f.remove('recipients')
f.set_node('recipients_', colander.SchemaNode(colander.Set()))
f.set_widget('recipients_', RecipientsWidget())
f.set_label('recipients_', "To")
if self.replying:
old_message = self.get_instance()
message.subject = "Re: {}".format(old_message.subject)
message.body = self.get_reply_body(old_message)
f.set_default('subject', "Re: {}".format(old_message.subject))
f.set_default('body', self.get_reply_body(old_message))
# Determine an initial set of recipients, based on reply
# method. This value will be set to a 'pseudo' field to avoid
# touching the new model instance and causing a crap commit.
# Determine an initial set of recipients, based on reply method.
# If replying to all, massage the list a little so that the
# current user is not listed, and the sender is listed first.
@ -250,38 +219,37 @@ class MessagesView(MasterView):
if self.filter_reply_recipient(r.recipient)]
value = dict(value)
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]
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)
f.set_default('recipients_', ','.join(value))
# Just a normal reply, to sender only.
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.
fs.focus = fs.body
# TODO?
# # Set focus to message body instead of recipients, when replying.
# fs.focus = fs.body
elif self.viewing:
f.remove('body')
# Viewing an existing message is a heck of a lot easier...
fs.configure(include=[
fs.sender.with_renderer(SenderFieldRenderer).label("From"),
fs.recipients.with_renderer(RecipientsFieldRenderer).label("To"),
fs.sent,
fs.subject,
])
def objectify(self, form, data):
message = super(MessagesView, self).objectify(form, data)
def before_create(self, form):
"""
This is where we must assign the current user as sender for new
messages, for now. I'm still not quite happy with this...
"""
super(MessagesView, self).before_create(form)
message = form.fieldset.model
if self.creating:
if self.request.user:
message.sender = self.request.user
for uuid in data['recipients_']:
user = self.Session.query(model.User).get(uuid)
if user:
message.add_recipient(user, status=self.enum.MESSAGE_STATUS_INBOX)
return message
def flash_after_create(self, obj):
self.request.session.flash("Message has been sent: {}".format(
self.get_instance_title(obj)))
@ -290,8 +258,7 @@ class MessagesView(MasterView):
return user.active
def get_reply_header(self, message):
sent = pytz.utc.localize(message.sent)
sent = localtime(self.rattail_config, sent)
sent = localtime(self.rattail_config, message.sent, from_utc=True)
sent = sent.strftime(self.reply_header_sent_format)
return "On {}, {} wrote:".format(sent, message.sender.person.display_name)
@ -318,7 +285,6 @@ class MessagesView(MasterView):
def template_kwargs_create(self, **kwargs):
kwargs['available_recipients'] = self.get_available_recipients()
kwargs['json'] = json
if self.replying:
kwargs['original_message'] = self.get_instance()
return kwargs
@ -371,7 +337,7 @@ class MessagesView(MasterView):
return self.redirect(self.request.get_referrer(
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:
recipient.status = new_status
return self.redirect(self.request.route_url('messages.{}'.format(
@ -385,7 +351,7 @@ class MessagesView(MasterView):
if self.request.method == 'POST':
uuids = self.request.POST.get('uuids', '').split(',')
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:
recip = self.Session.query(model.MessageRecipient)\
.filter(model.MessageRecipient.message_uuid == uuid)\
@ -436,7 +402,7 @@ class InboxView(MessagesView):
def query(self, 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):
@ -452,7 +418,7 @@ class ArchiveView(MessagesView):
def query(self, 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):
@ -481,6 +447,20 @@ class SentView(MessagesView):
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):
config.add_tailbone_permission('messages', 'messages.list', "List/Search Messages")