diff --git a/tailbone/forms2/core.py b/tailbone/forms2/core.py index 2832d0aa..120db31d 100644 --- a/tailbone/forms2/core.py +++ b/tailbone/forms2/core.py @@ -335,7 +335,7 @@ class Form(object): self.readonly_fields = set(readonly_fields or []) self.model_instance = model_instance self.model_class = model_class - if self.model_instance and not self.model_class: + if self.model_instance and not self.model_class and not isinstance(self.model_instance, dict): self.model_class = type(self.model_instance) if self.model_class and self.fields is None: self.set_fields(self.make_fields()) @@ -647,7 +647,10 @@ class Form(object): # get initial form values from model instance kwargs = {} if self.model_instance: - kwargs['appstruct'] = schema.dictify(self.model_instance) + if self.model_class: + kwargs['appstruct'] = schema.dictify(self.model_instance) + else: + kwargs['appstruct'] = self.model_instance # create form form = deform.Form(schema, **kwargs) diff --git a/tailbone/views/email.py b/tailbone/views/email.py index 35490d5b..e3240b9a 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar +# Copyright © 2010-2018 Lance Edgar # # This file is part of Rattail. # @@ -26,27 +26,18 @@ Email Views from __future__ import unicode_literals, absolute_import +import six + from rattail import mail from rattail.db import api from rattail.config import parse_list -import formalchemy -from formalchemy.helpers import text_area -from pyramid.httpexceptions import HTTPFound +import colander +from deform import widget as dfwidget from webhelpers2.html import HTML -from tailbone import forms from tailbone.db import Session -from tailbone.views import View, MasterView2 as MasterView - - -class EmailListFieldRenderer(formalchemy.TextAreaFieldRenderer): - - def render(self, **kwargs): - if isinstance(kwargs.get('size'), tuple): - kwargs['size'] = 'x'.join([str(i) for i in kwargs['size']]) - value = '\n'.join(parse_list(self.value)) - return text_area(self.name, content=value, **kwargs) +from tailbone.views import View, MasterView3 as MasterView class ProfilesView(MasterView): @@ -61,6 +52,7 @@ class ProfilesView(MasterView): pageable = False creatable = False deletable = False + grid_columns = [ 'key', 'prefix', @@ -69,6 +61,20 @@ class ProfilesView(MasterView): 'enabled', ] + form_fields = [ + 'key', + 'fallback_key', + 'description', + 'prefix', + 'subject', + 'sender', + 'replyto', + 'to', + 'cc', + 'bcc', + 'enabled', + ] + def get_data(self, session=None): data = [] for email in mail.iter_emails(self.rattail_config): @@ -113,13 +119,13 @@ class ProfilesView(MasterView): 'key': email.key, 'fallback_key': email.fallback_key, 'description': email.__doc__, - 'prefix': email.get_prefix(data, magic=False), - 'subject': email.get_subject(data, render=False), - 'sender': email.get_sender(), - 'replyto': email.get_replyto(), - 'to': get_recips('to'), - 'cc': get_recips('cc'), - 'bcc': get_recips('bcc'), + 'prefix': email.get_prefix(data, magic=False) or '', + 'subject': email.get_subject(data, render=False) or '', + 'sender': email.get_sender() or '', + 'replyto': email.get_replyto() or '', + 'to': get_recips('to') or '', + 'cc': get_recips('cc') or '', + 'bcc': get_recips('bcc') or '', 'enabled': email.get_enabled(), } @@ -140,45 +146,58 @@ class ProfilesView(MasterView): return profile['key'] != 'user_feedback' return True - def make_form(self, email, **kwargs): - """ - Make a simple form for use with CRUD views. - """ - fs = forms.GenericFieldSet(email) - fs.append(formalchemy.Field('key', value=email['key'], readonly=True)) - fs.append(formalchemy.Field('fallback_key', value=email['fallback_key'], readonly=True)) - fs.append(formalchemy.Field('description', value=email['description'], readonly=True)) - fs.append(formalchemy.Field('prefix', value=email['prefix'], label="Subject Prefix")) - fs.append(formalchemy.Field('subject', value=email['subject'], label="Subject Text")) - fs.append(formalchemy.Field('sender', value=email['sender'], label="From")) - fs.append(formalchemy.Field('replyto', value=email['replyto'], label="Reply-To")) - fs.append(formalchemy.Field('to', value=email['to'], renderer=EmailListFieldRenderer, size='60x6')) - fs.append(formalchemy.Field('cc', value=email['cc'], renderer=EmailListFieldRenderer, size='60x2')) - fs.append(formalchemy.Field('bcc', value=email['bcc'], renderer=EmailListFieldRenderer, size='60x2')) - fs.append(formalchemy.Field('enabled', type=formalchemy.types.Boolean, value=email['enabled'])) + def configure_form(self, f): + super(ProfilesView, self).configure_form(f) - form = forms.AlchemyForm(self.request, fs, - creating=self.creating, - editing=self.editing, - action_url=self.request.current_route_url(_query=None), - cancel_url=self.get_action_url('view', email)) - form.readonly = self.viewing - return form + # key + f.set_readonly('key') - def save_form(self, form): - fs = form.fieldset - fs.sync() - key = fs.key._value + # fallback_key + f.set_readonly('fallback_key') - session = Session() - api.save_setting(session, 'rattail.mail.{}.prefix'.format(key), fs.prefix._value) - api.save_setting(session, 'rattail.mail.{}.subject'.format(key), fs.subject._value) - api.save_setting(session, 'rattail.mail.{}.from'.format(key), fs.sender._value) - api.save_setting(session, 'rattail.mail.{}.replyto'.format(key), fs.replyto._value) - api.save_setting(session, 'rattail.mail.{}.to'.format(key), (fs.to._value or '').replace('\n', ', ')) - api.save_setting(session, 'rattail.mail.{}.cc'.format(key), (fs.cc._value or '').replace('\n', ', ')) - api.save_setting(session, 'rattail.mail.{}.bcc'.format(key), (fs.bcc._value or '').replace('\n', ', ')) - api.save_setting(session, 'rattail.mail.{}.enabled'.format(key), unicode(fs.enabled._value).lower()) + # description + f.set_readonly('description') + + # prefix + f.set_label('prefix', "Subject Prefix") + + # subject + f.set_label('subject', "Subject Text") + + # sender + f.set_label('sender', "From") + + # replyto + f.set_label('replyto', "Reply-To") + + # to + f.set_widget('to', dfwidget.TextAreaWidget(cols=60, rows=6)) + + # cc + f.set_widget('cc', dfwidget.TextAreaWidget(cols=60, rows=2)) + + # bcc + f.set_widget('bcc', dfwidget.TextAreaWidget(cols=60, rows=2)) + + # enabled + f.set_type('enabled', 'boolean') + + def make_form_schema(self): + return EmailProfileSchema() + + def save_edit_form(self, form): + key = self.request.matchdict['key'] + data = self.form_deserialized + session = self.Session() + api.save_setting(session, 'rattail.mail.{}.prefix'.format(key), data['prefix']) + api.save_setting(session, 'rattail.mail.{}.subject'.format(key), data['subject']) + api.save_setting(session, 'rattail.mail.{}.from'.format(key), data['sender']) + api.save_setting(session, 'rattail.mail.{}.replyto'.format(key), data['replyto']) + api.save_setting(session, 'rattail.mail.{}.to'.format(key), (data['to'] or '').replace('\n', ', ')) + api.save_setting(session, 'rattail.mail.{}.cc'.format(key), (data['cc'] or '').replace('\n', ', ')) + api.save_setting(session, 'rattail.mail.{}.bcc'.format(key), (data['bcc'] or '').replace('\n', ', ')) + api.save_setting(session, 'rattail.mail.{}.enabled'.format(key), six.text_type(data['enabled']).lower()) + return data def template_kwargs_view(self, **kwargs): key = self.request.matchdict['key'] @@ -186,6 +205,25 @@ class ProfilesView(MasterView): return kwargs +class EmailProfileSchema(colander.MappingSchema): + + prefix = colander.SchemaNode(colander.String()) + + subject = colander.SchemaNode(colander.String()) + + sender = colander.SchemaNode(colander.String()) + + replyto = colander.SchemaNode(colander.String(), missing='') + + to = colander.SchemaNode(colander.String()) + + cc = colander.SchemaNode(colander.String(), missing='') + + bcc = colander.SchemaNode(colander.String(), missing='') + + enabled = colander.SchemaNode(colander.Boolean()) + + class EmailPreview(View): """ Lists available email templates, and can show previews of each. @@ -196,8 +234,8 @@ class EmailPreview(View): # 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'))) + url = self.request.get_referrer(default=self.request.route_url('emailprofiles')) + return self.redirect(url) # Maybe render a preview? key = self.request.GET.get('key') diff --git a/tailbone/views/master3.py b/tailbone/views/master3.py index 4d72798f..08bbabb7 100644 --- a/tailbone/views/master3.py +++ b/tailbone/views/master3.py @@ -61,6 +61,9 @@ class MasterView3(MasterView2): if schema is None: schema = self.make_form_schema() + # TODO: SQLAlchemy class instance is assumed *unless* we get a dict + # (seems like we should be smarter about this somehow) + # if not self.creating and not isinstance(instance, dict): if not self.creating: kwargs['model_instance'] = instance kwargs = self.make_form_kwargs(**kwargs) @@ -103,14 +106,15 @@ class MasterView3(MasterView2): Configure the primary form. By default this just sets any primary key fields to be readonly (if we have a :attr:`model_class`). """ - - if self.editing and self.model_class: - mapper = orm.class_mapper(self.model_class) - for key in mapper.primary_key: - for field in form.fields: - if field == key.name: - form.set_readonly(field) - break + if self.editing: + model_class = self.get_model_class(error=False) + if model_class: + mapper = orm.class_mapper(model_class) + for key in mapper.primary_key: + for field in form.fields: + if field == key.name: + form.set_readonly(field) + break form.remove_field('uuid')