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
|
@ -24,7 +24,10 @@
|
||||||
Forms
|
Forms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .simpleform import *
|
from formencode import Schema
|
||||||
|
|
||||||
|
from .core import Form, Field, FieldSet
|
||||||
|
from .simpleform import SimpleForm, FormRenderer
|
||||||
from .alchemy import AlchemyForm
|
from .alchemy import AlchemyForm
|
||||||
from .fields import *
|
from .fields import *
|
||||||
from .renderers import *
|
from .renderers import *
|
||||||
|
|
|
@ -64,6 +64,9 @@ class AlchemyForm(Object):
|
||||||
template = '/forms/form.mako'
|
template = '/forms/form.mako'
|
||||||
return render(template, kwargs)
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
def render_fields(self):
|
||||||
|
return self.fieldset.render()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.fieldset.sync()
|
self.fieldset.sync()
|
||||||
Session.flush()
|
Session.flush()
|
||||||
|
|
110
tailbone/forms/core.py
Normal file
110
tailbone/forms/core.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Forms Core
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from edbob.util import prettify
|
||||||
|
|
||||||
|
from rattail.util import OrderedDict
|
||||||
|
|
||||||
|
from pyramid.renderers import render
|
||||||
|
from formalchemy.helpers import content_tag
|
||||||
|
|
||||||
|
|
||||||
|
class Form(object):
|
||||||
|
"""
|
||||||
|
Base class for all forms.
|
||||||
|
"""
|
||||||
|
create_label = "Create"
|
||||||
|
update_label = "Save"
|
||||||
|
|
||||||
|
def __init__(self, request, readonly=False, action_url=None):
|
||||||
|
self.request = request
|
||||||
|
self.readonly = readonly
|
||||||
|
self.action_url = action_url
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kwargs.setdefault('form', self)
|
||||||
|
if self.readonly:
|
||||||
|
template = '/forms/form_readonly.mako'
|
||||||
|
else:
|
||||||
|
template = '/forms/form.mako'
|
||||||
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
def render_fields(self, **kwargs):
|
||||||
|
kwargs.setdefault('fieldset', self.fieldset)
|
||||||
|
if self.readonly:
|
||||||
|
template = '/forms/fieldset_readonly.mako'
|
||||||
|
else:
|
||||||
|
template = '/forms/fieldset.mako'
|
||||||
|
return render(template, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Field(object):
|
||||||
|
"""
|
||||||
|
Manually create instances of this class to populate a simple form.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, value=None, label=None, requires_label=True):
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
self._label = label or prettify(self.name)
|
||||||
|
self.requires_label = requires_label
|
||||||
|
|
||||||
|
def is_required(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def label(self):
|
||||||
|
return self._label
|
||||||
|
|
||||||
|
def label_tag(self, **html_options):
|
||||||
|
"""
|
||||||
|
Logic stolen from FormAlchemy so all fields can render their own label.
|
||||||
|
Original docstring follows.
|
||||||
|
|
||||||
|
return the <label /> tag for the field.
|
||||||
|
"""
|
||||||
|
html_options.update(for_=self.name)
|
||||||
|
if 'class_' in html_options:
|
||||||
|
html_options['class_'] += self.is_required() and ' field_req' or ' field_opt'
|
||||||
|
else:
|
||||||
|
html_options['class_'] = self.is_required() and 'field_req' or 'field_opt'
|
||||||
|
return content_tag('label', self.label(), **html_options)
|
||||||
|
|
||||||
|
def render_readonly(self):
|
||||||
|
if self.value is None:
|
||||||
|
return ''
|
||||||
|
return unicode(self.value)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldSet(object):
|
||||||
|
"""
|
||||||
|
Generic fieldset for use with manually-created simple forms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.fields = OrderedDict()
|
||||||
|
self.render_fields = self.fields
|
|
@ -1,9 +1,8 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2012 Lance Edgar
|
# Copyright © 2010-2015 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -21,11 +20,13 @@
|
||||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
``pyramid_simpleform`` Forms
|
Simple Forms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import pyramid_simpleform
|
||||||
from pyramid_simpleform import renderers
|
from pyramid_simpleform import renderers
|
||||||
|
|
||||||
from webhelpers.html import tags
|
from webhelpers.html import tags
|
||||||
|
@ -33,8 +34,24 @@ from webhelpers.html import HTML
|
||||||
|
|
||||||
from edbob.util import prettify
|
from edbob.util import prettify
|
||||||
|
|
||||||
|
from tailbone.forms import Form
|
||||||
|
|
||||||
__all__ = ['FormRenderer']
|
|
||||||
|
class SimpleForm(Form):
|
||||||
|
"""
|
||||||
|
Customized simple form.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, schema, obj=None, **kwargs):
|
||||||
|
super(SimpleForm, self).__init__(request, **kwargs)
|
||||||
|
self._form = pyramid_simpleform.Form(request, schema=schema, obj=obj)
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._form, attr)
|
||||||
|
|
||||||
|
def render(self, **kwargs):
|
||||||
|
kwargs['form'] = FormRenderer(self)
|
||||||
|
return super(SimpleForm, self).render(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class FormRenderer(renderers.FormRenderer):
|
class FormRenderer(renderers.FormRenderer):
|
||||||
|
@ -42,6 +59,9 @@ class FormRenderer(renderers.FormRenderer):
|
||||||
Customized form renderer. Provides some extra methods for convenience.
|
Customized form renderer. Provides some extra methods for convenience.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.form, attr)
|
||||||
|
|
||||||
def field_div(self, name, field, label=None):
|
def field_div(self, name, field, label=None):
|
||||||
errors = self.errors_for(name)
|
errors = self.errors_for(name)
|
||||||
if errors:
|
if errors:
|
||||||
|
|
|
@ -34,6 +34,8 @@ from sqlalchemy import orm
|
||||||
import formalchemy
|
import formalchemy
|
||||||
from webhelpers import paginate
|
from webhelpers import paginate
|
||||||
|
|
||||||
|
from edbob.util import prettify
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.newgrids import Grid, GridColumn, filters
|
from tailbone.newgrids import Grid, GridColumn, filters
|
||||||
|
|
||||||
|
@ -61,8 +63,10 @@ class AlchemyGrid(Grid):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(AlchemyGrid, self).__init__(*args, **kwargs)
|
super(AlchemyGrid, self).__init__(*args, **kwargs)
|
||||||
self._fa_grid = formalchemy.Grid(self.model_class, instances=self.data,
|
fa_grid = formalchemy.Grid(self.model_class, instances=self.data,
|
||||||
session=Session(), request=self.request)
|
session=Session(), request=self.request)
|
||||||
|
fa_grid.prettify = prettify
|
||||||
|
self._fa_grid = fa_grid
|
||||||
|
|
||||||
def __delattr__(self, attr):
|
def __delattr__(self, attr):
|
||||||
delattr(self._fa_grid, attr)
|
delattr(self._fa_grid, attr)
|
||||||
|
@ -150,10 +154,8 @@ class AlchemyGrid(Grid):
|
||||||
Returns an iterator for all currently-visible columns.
|
Returns an iterator for all currently-visible columns.
|
||||||
"""
|
"""
|
||||||
for field in self._fa_grid.render_fields.itervalues():
|
for field in self._fa_grid.render_fields.itervalues():
|
||||||
column = GridColumn()
|
column = GridColumn(field.key, field.label())
|
||||||
column.field = field
|
column.field = field
|
||||||
column.key = field.key
|
|
||||||
column.label = field.label()
|
|
||||||
yield column
|
yield column
|
||||||
|
|
||||||
def iter_rows(self):
|
def iter_rows(self):
|
||||||
|
|
|
@ -26,6 +26,8 @@ Core Grid Classes
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from edbob.util import prettify
|
||||||
|
|
||||||
from rattail.db.api import get_setting, save_setting
|
from rattail.db.api import get_setting, save_setting
|
||||||
|
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
|
@ -136,6 +138,17 @@ class Grid(object):
|
||||||
sorters.update(updates)
|
sorters.update(updates)
|
||||||
return sorters
|
return sorters
|
||||||
|
|
||||||
|
def make_sorter(self, key, foldcase=False):
|
||||||
|
"""
|
||||||
|
Returns a function suitable for a sort map callable, with typical logic
|
||||||
|
built in for sorting a data set comprised of dicts, on the given key.
|
||||||
|
"""
|
||||||
|
if foldcase:
|
||||||
|
keyfunc = lambda v: v[key].lower()
|
||||||
|
else:
|
||||||
|
keyfunc = lambda v: v[key]
|
||||||
|
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc')
|
||||||
|
|
||||||
def load_settings(self, store=True):
|
def load_settings(self, store=True):
|
||||||
"""
|
"""
|
||||||
Load current/effective settings for the grid, from the request query
|
Load current/effective settings for the grid, from the request query
|
||||||
|
@ -150,9 +163,11 @@ class Grid(object):
|
||||||
settings = {
|
settings = {
|
||||||
'sortkey': self.default_sortkey,
|
'sortkey': self.default_sortkey,
|
||||||
'sortdir': self.default_sortdir,
|
'sortdir': self.default_sortdir,
|
||||||
'pagesize': self.default_pagesize,
|
|
||||||
'page': self.default_page,
|
|
||||||
}
|
}
|
||||||
|
if self.pageable:
|
||||||
|
settings['pagesize'] = self.default_pagesize
|
||||||
|
settings['page'] = self.default_page
|
||||||
|
if self.filterable:
|
||||||
for filtr in self.iter_filters():
|
for filtr in self.iter_filters():
|
||||||
settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active
|
settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active
|
||||||
settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb
|
settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb
|
||||||
|
@ -169,7 +184,7 @@ class Grid(object):
|
||||||
|
|
||||||
# If request has filter settings, grab those, then grab sort/pager
|
# If request has filter settings, grab those, then grab sort/pager
|
||||||
# settings from request or session.
|
# settings from request or session.
|
||||||
elif self.request_has_settings('filter'):
|
elif self.filterable and self.request_has_settings('filter'):
|
||||||
self.update_filter_settings(settings, 'request')
|
self.update_filter_settings(settings, 'request')
|
||||||
if self.request_has_settings('sort'):
|
if self.request_has_settings('sort'):
|
||||||
self.update_sort_settings(settings, 'request')
|
self.update_sort_settings(settings, 'request')
|
||||||
|
@ -591,7 +606,7 @@ class Grid(object):
|
||||||
return tags.link_to(action.label, url)
|
return tags.link_to(action.label, url)
|
||||||
|
|
||||||
def iter_rows(self):
|
def iter_rows(self):
|
||||||
return []
|
return self.make_visible_data()
|
||||||
|
|
||||||
def get_row_attrs(self, row, i):
|
def get_row_attrs(self, row, i):
|
||||||
"""
|
"""
|
||||||
|
@ -642,7 +657,7 @@ class Grid(object):
|
||||||
return self.cell_attrs
|
return self.cell_attrs
|
||||||
|
|
||||||
def render_cell(self, row, column):
|
def render_cell(self, row, column):
|
||||||
return ''
|
return unicode(row[column.key])
|
||||||
|
|
||||||
def get_pagesize_options(self):
|
def get_pagesize_options(self):
|
||||||
# TODO: Make configurable or something...
|
# TODO: Make configurable or something...
|
||||||
|
@ -661,8 +676,10 @@ class GridColumn(object):
|
||||||
|
|
||||||
Human-facing label for the column, i.e. displayed in the header.
|
Human-facing label for the column, i.e. displayed in the header.
|
||||||
"""
|
"""
|
||||||
key = None
|
|
||||||
label = None
|
def __init__(self, key, label=None):
|
||||||
|
self.key = key
|
||||||
|
self.label = label or prettify(key)
|
||||||
|
|
||||||
|
|
||||||
class GridAction(object):
|
class GridAction(object):
|
||||||
|
@ -673,7 +690,7 @@ class GridAction(object):
|
||||||
|
|
||||||
def __init__(self, key, label=None, url='#', icon=None):
|
def __init__(self, key, label=None, url='#', icon=None):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.label = label or key.capitalize()
|
self.label = label or prettify(key)
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,10 @@ div.field-wrapper div.field {
|
||||||
float: left;
|
float: left;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
|
|
||||||
|
/* NOTE: This was added specifically for email settings (description),
|
||||||
|
who knows what it breaks...hopefully nothing. */
|
||||||
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper div.field input[type=text],
|
div.field-wrapper div.field input[type=text],
|
||||||
|
|
30
tailbone/templates/email/profiles/view.mako
Normal file
30
tailbone/templates/email/profiles/view.mako
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
<script type="text/javascript">
|
||||||
|
% if not email.get_template('html'):
|
||||||
|
$(function() {
|
||||||
|
$('#preview-html').button('disable');
|
||||||
|
$('#preview-html').attr('title', "There is no HTML template on file for this email.");
|
||||||
|
});
|
||||||
|
% endif
|
||||||
|
% if not email.get_template('txt'):
|
||||||
|
$(function() {
|
||||||
|
$('#preview-txt').button('disable');
|
||||||
|
$('#preview-txt').attr('title', "There is no TXT template on file for this email.");
|
||||||
|
});
|
||||||
|
% endif
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
${parent.body()}
|
||||||
|
|
||||||
|
<form action="${url('email.preview')}" name="send-email-preview" method="post">
|
||||||
|
<a id="preview-html" class="button" href="${url('email.preview')}?key=${instance['key']}&type=html" target="_blank">Preview HTML</a>
|
||||||
|
<a id="preview-txt" class="button" href="${url('email.preview')}?key=${instance['key']}&type=txt" target="_blank">Preview TXT</a>
|
||||||
|
or
|
||||||
|
<input type="text" name="recipient" value="${request.user.email_address}" />
|
||||||
|
<input type="submit" name="send_${instance['key']}" value="Send Preview Email" />
|
||||||
|
</form>
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="form">
|
<div class="form">
|
||||||
${h.form(form.action_url, id=form_id or None, method='post', enctype='multipart/form-data')}
|
${h.form(form.action_url, id=form_id or None, method='post', enctype='multipart/form-data')}
|
||||||
|
|
||||||
${form.fieldset.render()|n}
|
${form.render_fields()|n}
|
||||||
|
|
||||||
% if buttons:
|
% if buttons:
|
||||||
${buttons|n}
|
${buttons|n}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<div class="form">
|
<div class="form">
|
||||||
${form.fieldset.render()|n}
|
${form.render_fields()|n}
|
||||||
% if buttons:
|
% if buttons:
|
||||||
${buttons|n}
|
${buttons|n}
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<%inherit file="/base.mako" />
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
<%def name="title()">${model_title}: ${unicode(instance)}</%def>
|
<%def name="title()">${model_title}: ${instance_title}</%def>
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
<%def name="context_menu_items()">
|
||||||
<li>${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}</li>
|
<li>${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}</li>
|
||||||
|
|
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.renderers import get_renderer, render_to_response
|
||||||
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
|
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
|
||||||
|
|
||||||
|
from tailbone import forms
|
||||||
from tailbone.views import View
|
from tailbone.views import View
|
||||||
from tailbone.newgrids import filters, AlchemyGrid, GridAction
|
from tailbone.newgrids import filters, AlchemyGrid, GridAction
|
||||||
from tailbone.forms import AlchemyForm
|
|
||||||
|
|
||||||
|
|
||||||
class MasterView(View):
|
class MasterView(View):
|
||||||
"""
|
"""
|
||||||
Base "master" view class. All model master views should derive from this.
|
Base "master" view class. All model master views should derive from this.
|
||||||
"""
|
"""
|
||||||
|
filterable = True
|
||||||
|
pageable = True
|
||||||
|
|
||||||
creatable = True
|
creatable = True
|
||||||
viewable = True
|
viewable = True
|
||||||
editable = True
|
editable = True
|
||||||
|
@ -115,7 +118,12 @@ class MasterView(View):
|
||||||
instance = self.get_instance()
|
instance = self.get_instance()
|
||||||
form = self.make_form(instance)
|
form = self.make_form(instance)
|
||||||
return self.render_to_response('view', {
|
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):
|
def edit(self):
|
||||||
"""
|
"""
|
||||||
|
@ -156,13 +164,13 @@ class MasterView(View):
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_model_class(cls):
|
def get_model_class(cls, error=True):
|
||||||
"""
|
"""
|
||||||
Returns the data model class for which the master view exists.
|
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))
|
raise NotImplementedError("You must define the `model_class` for: {0}".format(cls))
|
||||||
return cls.model_class
|
return getattr(cls, 'model_class', None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_normalized_model_name(cls):
|
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
|
otherwise it will be a simple lower-cased version of the associated
|
||||||
model class name.
|
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
|
@classmethod
|
||||||
def get_model_key(cls):
|
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 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
|
@classmethod
|
||||||
def get_model_title_plural(cls):
|
def get_model_title_plural(cls):
|
||||||
|
@ -313,11 +325,11 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'width': 'full',
|
'width': 'full',
|
||||||
'filterable': True,
|
'filterable': self.filterable,
|
||||||
'sortable': True,
|
'sortable': True,
|
||||||
'default_sortkey': getattr(self, 'default_sortkey', None),
|
'default_sortkey': getattr(self, 'default_sortkey', None),
|
||||||
'sortdir': getattr(self, 'sortdir', 'asc'),
|
'sortdir': getattr(self, 'sortdir', 'asc'),
|
||||||
'pageable': True,
|
'pageable': self.pageable,
|
||||||
'main_actions': self.get_main_actions(),
|
'main_actions': self.get_main_actions(),
|
||||||
'more_actions': self.get_more_actions(),
|
'more_actions': self.get_more_actions(),
|
||||||
'checkbox': self.checkbox,
|
'checkbox': self.checkbox,
|
||||||
|
@ -383,7 +395,11 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
Hopefully generic kwarg generator for basic action routes.
|
Hopefully generic kwarg generator for basic action routes.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
mapper = orm.object_mapper(row)
|
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]
|
keys = [k.key for k in mapper.primary_key]
|
||||||
values = [getattr(row, k) for k in keys]
|
values = [getattr(row, k) for k in keys]
|
||||||
return dict(zip(keys, values))
|
return dict(zip(keys, values))
|
||||||
|
@ -394,10 +410,9 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
factory = self.get_grid_factory()
|
factory = self.get_grid_factory()
|
||||||
key = self.get_grid_key()
|
key = self.get_grid_key()
|
||||||
data = self.make_query()
|
data = self.get_data()
|
||||||
kwargs = self.make_grid_kwargs()
|
kwargs = self.make_grid_kwargs()
|
||||||
grid = factory(key, self.request, data=data, model_class=self.model_class, **kwargs)
|
grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
|
||||||
grid._fa_grid.prettify = prettify
|
|
||||||
self.configure_grid(grid)
|
self.configure_grid(grid)
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
return grid
|
return grid
|
||||||
|
@ -413,12 +428,17 @@ class MasterView(View):
|
||||||
This requirement is a result of using FormAlchemy under the hood, and
|
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()`.
|
it is in fact a call to :meth:`formalchemy:formalchemy.tables.Grid.configure()`.
|
||||||
"""
|
"""
|
||||||
|
if hasattr(grid, 'configure'):
|
||||||
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
|
Generate the base data set for the grid. This typically will be a
|
||||||
override this method; override :meth:`query()` instead.
|
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:
|
if session is None:
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
|
@ -484,7 +504,7 @@ class MasterView(View):
|
||||||
kwargs.setdefault('cancel_url', self.get_index_url())
|
kwargs.setdefault('cancel_url', self.get_index_url())
|
||||||
else:
|
else:
|
||||||
kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
|
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
|
form.readonly = self.viewing
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue