Add initial support for email bounce management.
This commit is contained in:
		
							parent
							
								
									cfd5e5ae50
								
							
						
					
					
						commit
						f523146a4b
					
				
					 5 changed files with 329 additions and 0 deletions
				
			
		
							
								
								
									
										63
									
								
								tailbone/forms/renderers/bouncer.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								tailbone/forms/renderers/bouncer.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| # -*- 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/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| Batch Field Renderers | ||||
| """ | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import os | ||||
| import stat | ||||
| import random | ||||
| 
 | ||||
| from formalchemy.ext import fsblob | ||||
| 
 | ||||
| 
 | ||||
| class BounceMessageFieldRenderer(fsblob.FileFieldRenderer): | ||||
|     """ | ||||
|     Custom file field renderer for email bounce messages.  In readonly mode, | ||||
|     shows the filename and size. | ||||
|     """ | ||||
| 
 | ||||
|     @classmethod | ||||
|     def new(cls, request, handler): | ||||
|         name = 'Configured%s_%s' % (cls.__name__, unicode(random.random())[2:]) | ||||
|         return type(str(name), (cls,), dict(request=request, handler=handler)) | ||||
| 
 | ||||
|     @property | ||||
|     def storage_path(self): | ||||
|         return self.handler.root_msgdir | ||||
| 
 | ||||
|     def get_size(self): | ||||
|         size = super(BounceMessageFieldRenderer, self).get_size() | ||||
|         if size: | ||||
|             return size | ||||
|         bounce = self.field.parent.model | ||||
|         path = os.path.join(self.handler.msgpath(bounce)) | ||||
|         if os.path.isfile(path): | ||||
|             return os.stat(path)[stat.ST_SIZE] | ||||
|         return 0 | ||||
| 
 | ||||
|     def get_url(self, filename): | ||||
|         bounce = self.field.parent.model | ||||
|         return self.request.route_url('emailbounce.download', uuid=bounce.uuid) | ||||
							
								
								
									
										14
									
								
								tailbone/templates/emailbounces/crud.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tailbone/templates/emailbounces/crud.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| ## -*- coding: utf-8 -*- | ||||
| <%inherit file="/crud.mako" /> | ||||
| 
 | ||||
| <%def name="context_menu_items()"> | ||||
|   <% bounce = form.fieldset.model %> | ||||
|   <li>${h.link_to("Back to Email Bounces", url('emailbounces'))}</li> | ||||
|   % if not bounce.processed and request.has_perm('emailbounces.process'): | ||||
|       <li>${h.link_to("Mark this Email Bounce as Processed", url('emailbounce.process', uuid=bounce.uuid))}</li> | ||||
|   % elif bounce.processed and request.has_perm('emailbounces.unprocess'): | ||||
|       <li>${h.link_to("Mark this Email Bounce as UN-processed", url('emailbounce.unprocess', uuid=bounce.uuid))}</li> | ||||
|   % endif | ||||
| </%def> | ||||
| 
 | ||||
| ${parent.body()} | ||||
							
								
								
									
										6
									
								
								tailbone/templates/emailbounces/index.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tailbone/templates/emailbounces/index.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| ## -*- coding: utf-8 -*- | ||||
| <%inherit file="/grid.mako" /> | ||||
| 
 | ||||
| <%def name="title()">Email Bounces</%def> | ||||
| 
 | ||||
| ${parent.body()} | ||||
|  | @ -55,6 +55,7 @@ def includeme(config): | |||
| 
 | ||||
|     config.include('tailbone.views.auth') | ||||
|     config.include('tailbone.views.batches') | ||||
|     config.include('tailbone.views.bouncer') | ||||
|     config.include('tailbone.views.brands') | ||||
|     config.include('tailbone.views.categories') | ||||
|     config.include('tailbone.views.customergroups') | ||||
|  |  | |||
							
								
								
									
										245
									
								
								tailbone/views/bouncer.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								tailbone/views/bouncer.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,245 @@ | |||
| # -*- 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/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| Views for Email Bounces | ||||
| """ | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import os | ||||
| import datetime | ||||
| 
 | ||||
| from rattail.db import model | ||||
| from rattail.bouncer import get_handler | ||||
| 
 | ||||
| import formalchemy | ||||
| from pyramid.response import FileResponse | ||||
| from pyramid.httpexceptions import HTTPFound, HTTPNotFound | ||||
| from webhelpers.html import literal | ||||
| 
 | ||||
| from tailbone.db import Session | ||||
| from tailbone.views import SearchableAlchemyGridView, CrudView | ||||
| from tailbone.forms import renderers | ||||
| from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer | ||||
| from tailbone.grids.search import BooleanSearchFilter | ||||
| 
 | ||||
| 
 | ||||
| class EmailBouncesGrid(SearchableAlchemyGridView): | ||||
|     """ | ||||
|     Main grid view for email bounces. | ||||
|     """ | ||||
|     mapped_class = model.EmailBounce | ||||
|     config_prefix = 'emailbounces' | ||||
| 
 | ||||
|     def join_map(self): | ||||
|         return { | ||||
|             'processed_by': lambda q: q.outerjoin(model.User), | ||||
|         } | ||||
| 
 | ||||
|     def filter_map(self): | ||||
| 
 | ||||
|         def processed_is(q, v): | ||||
|             if v == 'True': | ||||
|                 return q.filter(model.EmailBounce.processed != None) | ||||
|             else: | ||||
|                 return q.filter(model.EmailBounce.processed == None) | ||||
| 
 | ||||
|         def processed_nt(q, v): | ||||
|             if v == 'True': | ||||
|                 return q.filter(model.EmailBounce.processed == None) | ||||
|             else: | ||||
|                 return q.filter(model.EmailBounce.processed != None) | ||||
| 
 | ||||
|         return self.make_filter_map( | ||||
|             ilike=['config_key', 'bounce_recipient_address', 'intended_recipient_address'], | ||||
|             processed={'is': processed_is, 'nt': processed_nt}, | ||||
|             processed_by=self.filter_ilike(model.User.username)) | ||||
| 
 | ||||
|     def filter_config(self): | ||||
|         return self.make_filter_config( | ||||
|             include_filter_config_key=True, | ||||
|             filter_type_config_key='lk', | ||||
|             filter_label_config_key="Source", | ||||
|             filter_factory_processed=BooleanSearchFilter, | ||||
|             filter_type_processed='is', | ||||
|             processed=False, | ||||
|             include_filter_processed=True, | ||||
|             filter_label_bounce_recipient_address="Bounced To", | ||||
|             filter_label_intended_recipient_address="Intended For") | ||||
| 
 | ||||
|     def sort_config(self): | ||||
|         return self.make_sort_config(sort='bounced', dir='desc') | ||||
| 
 | ||||
|     def sort_map(self): | ||||
|         return self.make_sort_map( | ||||
|             'config_key', 'bounced', 'bounce_recipient_address', 'intended_recipient_address', | ||||
|             processed_by=self.sorter(model.User.username)) | ||||
| 
 | ||||
|     def grid(self): | ||||
|         g = self.make_grid() | ||||
|         g.bounced.set(renderer=renderers.DateTimeFieldRenderer(self.rattail_config)) | ||||
|         g.configure( | ||||
|             include=[ | ||||
|                 g.config_key.label("Source"), | ||||
|                 g.bounced, | ||||
|                 g.bounce_recipient_address.label("Bounced To"), | ||||
|                 g.intended_recipient_address.label("Intended For"), | ||||
|                 g.processed_by, | ||||
|                 ], | ||||
|             readonly=True) | ||||
|         if self.request.has_perm('emailbounces.view'): | ||||
|             g.viewable = True | ||||
|             g.view_route_name = 'emailbounce' | ||||
|         if self.request.has_perm('emailbounces.delete'): | ||||
|             g.deletable = True | ||||
|             g.delete_route_name = 'emailbounce.delete' | ||||
|         return g | ||||
| 
 | ||||
| 
 | ||||
| class LinksFieldRenderer(formalchemy.FieldRenderer): | ||||
| 
 | ||||
|     def render_readonly(self, **kwargs): | ||||
|         value = self.raw_value | ||||
|         if not value: | ||||
|             return 'n/a' | ||||
|         html = literal('<ul>') | ||||
|         for link in value: | ||||
|             html += literal('<li>{0}:  <a href="{1}" target="_blank">{2}</a></li>'.format( | ||||
|                 link.type, link.url, link.title)) | ||||
|         html += literal('</ul>') | ||||
|         return html | ||||
| 
 | ||||
| 
 | ||||
| class EmailBounceCrud(CrudView): | ||||
|     """ | ||||
|     Main CRUD view for email bounces. | ||||
|     """ | ||||
|     mapped_class = model.EmailBounce | ||||
|     home_route = 'emailbounces' | ||||
|     pretty_name = "Email Bounce" | ||||
| 
 | ||||
|     def get_handler(self, bounce): | ||||
|         return get_handler(self.rattail_config, bounce.config_key) | ||||
| 
 | ||||
|     def fieldset(self, bounce): | ||||
|         assert isinstance(bounce, model.EmailBounce) | ||||
|         handler = self.get_handler(bounce) | ||||
|         fs = self.make_fieldset(bounce) | ||||
|         fs.bounced.set(renderer=renderers.DateTimeFieldRenderer(self.rattail_config)) | ||||
|         fs.processed.set(renderer=renderers.DateTimeFieldRenderer(self.rattail_config)) | ||||
|         fs.append(formalchemy.Field('message', | ||||
|                                     value=handler.msgpath(bounce), | ||||
|                                     renderer=BounceMessageFieldRenderer.new(self.request, handler))) | ||||
|         fs.append(formalchemy.Field('links', | ||||
|                                     value=list(handler.make_links(Session(), bounce.intended_recipient_address)), | ||||
|                                     renderer=LinksFieldRenderer)) | ||||
|         fs.configure( | ||||
|             include=[ | ||||
|                 fs.config_key.label("Source"), | ||||
|                 fs.message, | ||||
|                 fs.bounced, | ||||
|                 fs.intended_recipient_address.label("Intended For"), | ||||
|                 fs.bounce_recipient_address.label("Bounced To"), | ||||
|                 fs.links, | ||||
|                 fs.processed, | ||||
|                 fs.processed_by, | ||||
|             ], | ||||
|             readonly=True) | ||||
|         return fs | ||||
| 
 | ||||
|     def template_kwargs(self, form): | ||||
|         kwargs = super(EmailBounceCrud, self).template_kwargs(form) | ||||
|         bounce = form.fieldset.model | ||||
|         kwargs['handler'] = self.get_handler(bounce) | ||||
|         return kwargs | ||||
| 
 | ||||
|     def process(self): | ||||
|         """ | ||||
|         View for marking a bounce as processed. | ||||
|         """ | ||||
|         bounce = self.get_model_from_request() | ||||
|         if not bounce: | ||||
|             return HTTPNotFound() | ||||
|         bounce.processed = datetime.datetime.utcnow() | ||||
|         bounce.processed_by = self.request.user | ||||
|         self.request.session.flash("Email bounce has been marked processed.") | ||||
|         return HTTPFound(location=self.request.route_url('emailbounces')) | ||||
| 
 | ||||
|     def unprocess(self): | ||||
|         """ | ||||
|         View for marking a bounce as *unprocessed*. | ||||
|         """ | ||||
|         bounce = self.get_model_from_request() | ||||
|         if not bounce: | ||||
|             return HTTPNotFound() | ||||
|         bounce.processed = None | ||||
|         bounce.processed_by = None | ||||
|         self.request.session.flash("Email bounce has been marked UN-processed.") | ||||
|         return HTTPFound(location=self.request.route_url('emailbounces')) | ||||
| 
 | ||||
|     def download(self): | ||||
|         """ | ||||
|         View for downloading the message file associated with a bounce. | ||||
|         """ | ||||
|         bounce = self.get_model_from_request() | ||||
|         if not bounce: | ||||
|             return HTTPNotFound() | ||||
|         handler = self.get_handler(bounce) | ||||
|         path = handler.msgpath(bounce) | ||||
|         response = FileResponse(path, request=self.request) | ||||
|         response.headers[b'Content-Length'] = str(os.path.getsize(path)) | ||||
|         response.headers[b'Content-Disposition'] = b'attachment; filename="bounce.eml"' | ||||
|         return response | ||||
| 
 | ||||
| 
 | ||||
| def add_routes(config): | ||||
|     config.add_route('emailbounces', '/emailbounces/') | ||||
|     config.add_route('emailbounce', '/emailbounces/{uuid}') | ||||
|     config.add_route('emailbounce.process', '/emailbounces/{uuid}/process') | ||||
|     config.add_route('emailbounce.unprocess', '/emailbounces/{uuid}/unprocess') | ||||
|     config.add_route('emailbounce.delete', '/emailbounces/{uuid}/delete') | ||||
|     config.add_route('emailbounce.download', '/emailbounces/{uuid}/download') | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     add_routes(config) | ||||
| 
 | ||||
|     config.add_view(EmailBouncesGrid, route_name='emailbounces', | ||||
|                     renderer='/emailbounces/index.mako', | ||||
|                     permission='emailbounces.list') | ||||
| 
 | ||||
|     config.add_view(EmailBounceCrud, attr='read', route_name='emailbounce', | ||||
|                     renderer='/emailbounces/crud.mako', | ||||
|                     permission='emailbounces.view') | ||||
| 
 | ||||
|     config.add_view(EmailBounceCrud, attr='process', route_name='emailbounce.process', | ||||
|                     permission='emailbounces.process') | ||||
| 
 | ||||
|     config.add_view(EmailBounceCrud, attr='unprocess', route_name='emailbounce.unprocess', | ||||
|                     permission='emailbounces.unprocess') | ||||
| 
 | ||||
|     config.add_view(EmailBounceCrud, attr='download', route_name='emailbounce.download', | ||||
|                     permission='emailbounces.download') | ||||
| 
 | ||||
|     config.add_view(EmailBounceCrud, attr='delete', route_name='emailbounce.delete', | ||||
|                     permission='emailbounces.delete') | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar