Add readonly support for email profile settings.
More to come... Also this required some form tweaking/overhaul(s).
This commit is contained in:
parent
ba6bf87ded
commit
ef40af814a
13 changed files with 453 additions and 48 deletions
196
tailbone/views/email.py
Normal file
196
tailbone/views/email.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2015 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Email Views
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from rattail import mail
|
||||
|
||||
from tailbone import forms
|
||||
from tailbone.views import MasterView, View
|
||||
from tailbone.newgrids import Grid, GridColumn
|
||||
|
||||
|
||||
class ProfilesView(MasterView):
|
||||
"""
|
||||
Master view for email admin (settings/preview).
|
||||
"""
|
||||
normalized_model_name = 'emailprofile'
|
||||
model_title = "Email Profile"
|
||||
model_key = 'key'
|
||||
url_prefix = '/email/profiles'
|
||||
|
||||
grid_factory = Grid
|
||||
filterable = False
|
||||
pageable = False
|
||||
|
||||
creatable = False
|
||||
editable = False
|
||||
deletable = False
|
||||
|
||||
def get_data(self, session=None):
|
||||
data = []
|
||||
for email in mail.iter_emails(self.rattail_config):
|
||||
key = email.key or email.__name__
|
||||
email = email(self.rattail_config, key)
|
||||
data.append(self.normalize(email))
|
||||
return data
|
||||
|
||||
def normalize(self, email):
|
||||
return {
|
||||
'key': email.key,
|
||||
'description': email.__doc__,
|
||||
'prefix': email.get_prefix(),
|
||||
'subject': email.get_subject(),
|
||||
'sender': email.get_sender(),
|
||||
'replyto': email.get_replyto(),
|
||||
'to': email.get_recips('to'),
|
||||
'cc': email.get_recips('cc'),
|
||||
'bcc': email.get_recips('bcc'),
|
||||
}
|
||||
|
||||
def configure_grid(self, g):
|
||||
g.columns = [
|
||||
GridColumn('key'),
|
||||
GridColumn('prefix'),
|
||||
GridColumn('subject'),
|
||||
]
|
||||
|
||||
g.sorters['key'] = g.make_sorter('key', foldcase=True)
|
||||
g.sorters['prefix'] = g.make_sorter('prefix', foldcase=True)
|
||||
g.sorters['subject'] = g.make_sorter('subject', foldcase=True)
|
||||
g.default_sortkey = 'subject'
|
||||
|
||||
# g.main_actions = []
|
||||
g.more_actions = []
|
||||
|
||||
def get_instance(self):
|
||||
key = self.request.matchdict['key']
|
||||
return self.normalize(mail.get_email(self.rattail_config, key))
|
||||
|
||||
def get_instance_title(self, email):
|
||||
return email['key']
|
||||
|
||||
def make_form(self, email, **kwargs):
|
||||
"""
|
||||
Make a simple form for use with CRUD views.
|
||||
"""
|
||||
# TODO: This needs all kinds of help still...
|
||||
|
||||
class EmailSchema(forms.Schema):
|
||||
pass
|
||||
|
||||
form = forms.SimpleForm(self.request, schema=EmailSchema(), obj=email)
|
||||
form.creating = self.creating
|
||||
form.editing = self.editing
|
||||
form.readonly = self.viewing
|
||||
|
||||
if self.creating:
|
||||
form.cancel_url = self.get_index_url()
|
||||
else:
|
||||
form.cancel_url = self.get_action_url('view', email)
|
||||
|
||||
form.fieldset = forms.FieldSet()
|
||||
form.fieldset.fields['key'] = forms.Field('key', value=email['key'])
|
||||
form.fieldset.fields['prefix'] = forms.Field('prefix', value=email['prefix'], label="Subject Prefix")
|
||||
form.fieldset.fields['subject'] = forms.Field('subject', value=email['subject'], label="Subject Text")
|
||||
form.fieldset.fields['description'] = forms.Field('description', value=email['description'])
|
||||
form.fieldset.fields['sender'] = forms.Field('sender', value=email['sender'], label="From")
|
||||
form.fieldset.fields['replyto'] = forms.Field('replyto', value=email['replyto'], label="Reply-To")
|
||||
form.fieldset.fields['to'] = forms.Field('to', value=', '.join(email['to']) if email['to'] else None)
|
||||
form.fieldset.fields['cc'] = forms.Field('cc', value=', '.join(email['cc']) if email['cc'] else None)
|
||||
form.fieldset.fields['bcc'] = forms.Field('bcc', value=', '.join(email['bcc']) if email['bcc'] else None)
|
||||
|
||||
return form
|
||||
|
||||
def template_kwargs_view(self, **kwargs):
|
||||
key = self.request.matchdict['key']
|
||||
kwargs['email'] = mail.get_email(self.rattail_config, key)
|
||||
return kwargs
|
||||
|
||||
|
||||
class EmailPreview(View):
|
||||
"""
|
||||
Lists available email templates, and can show previews of each.
|
||||
"""
|
||||
|
||||
def __call__(self):
|
||||
|
||||
# Forms submitted via POST are only used for sending emails.
|
||||
if self.request.method == 'POST':
|
||||
self.email_template()
|
||||
return HTTPFound(location=self.request.get_referrer(
|
||||
default=self.request.route_url('emailprofiles')))
|
||||
|
||||
# Maybe render a preview?
|
||||
key = self.request.GET.get('key')
|
||||
if key:
|
||||
type_ = self.request.GET.get('type', 'html')
|
||||
return self.preview_template(key, type_)
|
||||
|
||||
assert False, "should not be here"
|
||||
|
||||
def email_template(self):
|
||||
recipient = self.request.POST.get('recipient')
|
||||
if recipient:
|
||||
keys = filter(lambda k: k.startswith('send_'), self.request.POST.iterkeys())
|
||||
key = keys[0][5:] if keys else None
|
||||
if key:
|
||||
email = mail.get_email(self.rattail_config, key)
|
||||
data = email.sample_data(self.request)
|
||||
msg = email.make_message(data)
|
||||
|
||||
subject = msg['Subject']
|
||||
del msg['Subject']
|
||||
msg['Subject'] = "[preview] {0}".format(subject)
|
||||
|
||||
del msg['To']
|
||||
del msg['Cc']
|
||||
del msg['Bcc']
|
||||
msg['To'] = recipient
|
||||
|
||||
mail.deliver_message(self.rattail_config, msg)
|
||||
|
||||
self.request.session.flash("Preview for '{0}' was emailed to {1}".format(key, recipient))
|
||||
|
||||
def preview_template(self, key, type_):
|
||||
email = mail.get_email(self.rattail_config, key)
|
||||
template = email.get_template(type_)
|
||||
data = email.sample_data(self.request)
|
||||
self.request.response.text = template.render(**data)
|
||||
if type_ == 'txt':
|
||||
self.request.response.content_type = b'text/plain'
|
||||
return self.request.response
|
||||
|
||||
|
||||
def includeme(config):
|
||||
ProfilesView.defaults(config)
|
||||
|
||||
config.add_route('email.preview', '/email/preview/')
|
||||
config.add_view(EmailPreview, route_name='email.preview',
|
||||
renderer='/email/preview.mako',
|
||||
permission='admin')
|
|
@ -35,15 +35,18 @@ import formalchemy
|
|||
from pyramid.renderers import get_renderer, render_to_response
|
||||
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
|
||||
|
||||
from tailbone import forms
|
||||
from tailbone.views import View
|
||||
from tailbone.newgrids import filters, AlchemyGrid, GridAction
|
||||
from tailbone.forms import AlchemyForm
|
||||
|
||||
|
||||
class MasterView(View):
|
||||
"""
|
||||
Base "master" view class. All model master views should derive from this.
|
||||
"""
|
||||
filterable = True
|
||||
pageable = True
|
||||
|
||||
creatable = True
|
||||
viewable = True
|
||||
editable = True
|
||||
|
@ -115,7 +118,12 @@ class MasterView(View):
|
|||
instance = self.get_instance()
|
||||
form = self.make_form(instance)
|
||||
return self.render_to_response('view', {
|
||||
'instance': instance, 'form': form})
|
||||
'instance': instance,
|
||||
'form': form,
|
||||
'instance_title': self.get_instance_title(instance)})
|
||||
|
||||
def get_instance_title(self, instance):
|
||||
return unicode(instance)
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
|
@ -156,13 +164,13 @@ class MasterView(View):
|
|||
##############################
|
||||
|
||||
@classmethod
|
||||
def get_model_class(cls):
|
||||
def get_model_class(cls, error=True):
|
||||
"""
|
||||
Returns the data model class for which the master view exists.
|
||||
"""
|
||||
if not hasattr(cls, 'model_class'):
|
||||
if not hasattr(cls, 'model_class') and error:
|
||||
raise NotImplementedError("You must define the `model_class` for: {0}".format(cls))
|
||||
return cls.model_class
|
||||
return getattr(cls, 'model_class', None)
|
||||
|
||||
@classmethod
|
||||
def get_normalized_model_name(cls):
|
||||
|
@ -172,7 +180,9 @@ class MasterView(View):
|
|||
otherwise it will be a simple lower-cased version of the associated
|
||||
model class name.
|
||||
"""
|
||||
return getattr(cls, 'normalized_model_name', cls.get_model_class().__name__.lower())
|
||||
if hasattr(cls, 'normalized_model_name'):
|
||||
return cls.normalized_model_name
|
||||
return cls.get_model_class().__name__.lower()
|
||||
|
||||
@classmethod
|
||||
def get_model_key(cls):
|
||||
|
@ -189,7 +199,9 @@ class MasterView(View):
|
|||
"""
|
||||
Return a "humanized" version of the model name, for display in templates.
|
||||
"""
|
||||
return getattr(cls, 'model_title', cls.model_class.__name__)
|
||||
if hasattr(cls, 'model_title'):
|
||||
return cls.model_title
|
||||
return cls.model_class.__name__
|
||||
|
||||
@classmethod
|
||||
def get_model_title_plural(cls):
|
||||
|
@ -313,11 +325,11 @@ class MasterView(View):
|
|||
"""
|
||||
return {
|
||||
'width': 'full',
|
||||
'filterable': True,
|
||||
'filterable': self.filterable,
|
||||
'sortable': True,
|
||||
'default_sortkey': getattr(self, 'default_sortkey', None),
|
||||
'sortdir': getattr(self, 'sortdir', 'asc'),
|
||||
'pageable': True,
|
||||
'pageable': self.pageable,
|
||||
'main_actions': self.get_main_actions(),
|
||||
'more_actions': self.get_more_actions(),
|
||||
'checkbox': self.checkbox,
|
||||
|
@ -383,10 +395,14 @@ class MasterView(View):
|
|||
"""
|
||||
Hopefully generic kwarg generator for basic action routes.
|
||||
"""
|
||||
mapper = orm.object_mapper(row)
|
||||
keys = [k.key for k in mapper.primary_key]
|
||||
values = [getattr(row, k) for k in keys]
|
||||
return dict(zip(keys, values))
|
||||
try:
|
||||
mapper = orm.object_mapper(row)
|
||||
except orm.exc.UnmappedInstanceError:
|
||||
return {self.model_key: row[self.model_key]}
|
||||
else:
|
||||
keys = [k.key for k in mapper.primary_key]
|
||||
values = [getattr(row, k) for k in keys]
|
||||
return dict(zip(keys, values))
|
||||
|
||||
def make_grid(self):
|
||||
"""
|
||||
|
@ -394,10 +410,9 @@ class MasterView(View):
|
|||
"""
|
||||
factory = self.get_grid_factory()
|
||||
key = self.get_grid_key()
|
||||
data = self.make_query()
|
||||
data = self.get_data()
|
||||
kwargs = self.make_grid_kwargs()
|
||||
grid = factory(key, self.request, data=data, model_class=self.model_class, **kwargs)
|
||||
grid._fa_grid.prettify = prettify
|
||||
grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
|
||||
self.configure_grid(grid)
|
||||
grid.load_settings()
|
||||
return grid
|
||||
|
@ -413,12 +428,17 @@ class MasterView(View):
|
|||
This requirement is a result of using FormAlchemy under the hood, and
|
||||
it is in fact a call to :meth:`formalchemy:formalchemy.tables.Grid.configure()`.
|
||||
"""
|
||||
grid.configure()
|
||||
if hasattr(grid, 'configure'):
|
||||
grid.configure()
|
||||
|
||||
def make_query(self, session=None):
|
||||
def get_data(self, session=None):
|
||||
"""
|
||||
Make the base query to be used for the grid. Subclasses should not
|
||||
override this method; override :meth:`query()` instead.
|
||||
Generate the base data set for the grid. This typically will be a
|
||||
SQLAlchemy query against the view's model class, but subclasses may
|
||||
override this to support arbitrary data sets.
|
||||
|
||||
Note that if your view is typical and uses a SA model, you should not
|
||||
override this methid, but override :meth:`query()` instead.
|
||||
"""
|
||||
if session is None:
|
||||
session = self.Session()
|
||||
|
@ -484,7 +504,7 @@ class MasterView(View):
|
|||
kwargs.setdefault('cancel_url', self.get_index_url())
|
||||
else:
|
||||
kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
|
||||
form = AlchemyForm(self.request, fieldset, **kwargs)
|
||||
form = forms.AlchemyForm(self.request, fieldset, **kwargs)
|
||||
form.readonly = self.viewing
|
||||
return form
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue