/lamson/html.py

https://github.com/feyin/lamson · Python · 180 lines · 100 code · 5 blank · 75 comment · 15 complexity · 92db2e91527739c6d6e6c378aad975dc MD5 · raw file

  1. """
  2. This implements an HTML Mail generator that uses templates and CleverCSS
  3. to produce an HTML message with inline CSS attributes so that it will
  4. display correctly. As long as you can keep most of the HTML and CSS simple you
  5. should have a high success rate at rendering this.
  6. How it works is you create an HtmlMail class and configure it with a CleverCSS
  7. stylesheet (also a template). This acts as your template for the appearance and
  8. the outer shell of your HTML.
  9. When you go to send, you use a markdown content template to generate the
  10. guts of your HTML. You hand this, variables, and email headers to
  11. HtmlMail.respond and it spits back a fully formed lamson.mail.MailResponse
  12. ready to send.
  13. The engine basically parses the CSS, renders your content template,
  14. render your outer template, and then applies the CSS directly to your HTML
  15. so your CSS attributes are inline and display in the HTML display.
  16. Each element is a template loaded by your loader: the CleverCSS template, out HTML
  17. template, and your own content.
  18. Finally, use this as a generator by making one and having crank out all the emails
  19. you need. Don't make one HtmlMail for each message.
  20. """
  21. from BeautifulSoup import BeautifulSoup
  22. import clevercss
  23. from lamson import mail, view
  24. from markdown2 import markdown
  25. class HtmlMail(object):
  26. """
  27. Acts as a lamson.mail.MailResponse generator that produces a properly
  28. formatted HTML mail message, including inline CSS applied to all HTML tags.
  29. """
  30. def __init__(self, css_template, html_template, variables={}, wiki=markdown):
  31. """
  32. You pass in a CleverCSS template (it'll be run through the template engine
  33. before CleverCSS), the html_template, and any variables that the CSS template
  34. needs.
  35. The CSS template is processed once, the html_template is processed each time
  36. you call render or respond.
  37. If you don't like markdown, then you can set the wiki variable to any callable
  38. that processes your templates.
  39. """
  40. self.template = html_template
  41. self.load_css(css_template, variables)
  42. self.wiki = wiki
  43. def load_css(self, css_template, variables):
  44. """
  45. If you want to change the CSS, simply call this with the new CSS and variables.
  46. It will change internal state so that later calls to render or respond use
  47. the new CSS.
  48. """
  49. self.css = view.render(variables, css_template)
  50. self.engine = clevercss.Engine(self.css)
  51. self.stylesheet = []
  52. for selector, style in self.engine.evaluate():
  53. attr = "; ".join("%s: %s" % (k,v) for k,v in style)
  54. selectors = selector[0].split()
  55. # root, path, attr
  56. self.stylesheet.append((selectors[0], selectors[1:], attr))
  57. def reduce_tags(self, name, tags):
  58. """
  59. Used mostly internally to find all the tags that fit the given
  60. CSS selector. It's fairly primitive, working only on tag names,
  61. classes, and ids. You shouldn't get too fancy with the CSS you create.
  62. """
  63. results = []
  64. for tag in tags:
  65. if name.startswith("#"):
  66. children = tag.findAll(attrs={"class": name[1:]})
  67. elif name.startswith("."):
  68. children = tag.findAll(attrs={"id": name[1:]})
  69. else:
  70. children = tag.findAll(name)
  71. if children:
  72. results += children
  73. return results
  74. def apply_styles(self, html):
  75. """
  76. Used mostly internally but helpful for testing, this takes the given HTML
  77. and applies the configured CSS you've set. It returns a BeautifulSoup
  78. object with all the style attributes set and nothing else changed.
  79. """
  80. doc = BeautifulSoup(html)
  81. roots = {} # the roots rarely change, even though the paths do
  82. for root, path, attr in self.stylesheet:
  83. tags = roots.get(root, None)
  84. if not tags:
  85. tags = self.reduce_tags(root, [doc])
  86. roots[root] = tags
  87. for sel in path:
  88. tags = self.reduce_tags(sel, tags)
  89. for node in tags:
  90. try:
  91. node['style'] += "; " + attr
  92. except KeyError:
  93. node['style'] = attr
  94. return doc
  95. def render(self, variables, content_template, pretty=False):
  96. """
  97. Works like lamson.view.render, but uses apply_styles to modify
  98. the HTML with the configured CSS before returning it to you.
  99. If you set the pretty=True then it will prettyprint the results,
  100. which is a waste of bandwidth, but helps when debugging.
  101. Remember that content_template is run through the template system,
  102. and then processed with self.wiki (defaults to markdown). This
  103. let's you do template processing and write the HTML contents like
  104. you would an email.
  105. You could also attach the content_template as a text version of the
  106. message for people without HTML. Simply set the .Body attribute
  107. of the returned lamson.mail.MailResponse object.
  108. """
  109. content = self.wiki(view.render(variables, content_template))
  110. lvars = variables.copy()
  111. lvars['content'] = content
  112. html = view.render(lvars, self.template)
  113. styled = self.apply_styles(html)
  114. if pretty:
  115. return styled.prettify()
  116. else:
  117. return str(styled)
  118. def respond(self, variables, content, **kwd):
  119. """
  120. Works like lamson.view.respond letting you craft a
  121. lamson.mail.MailResponse immediately from the results of
  122. a lamson.html.HtmlMail.render call. Simply pass in the
  123. From, To, and Subject parameters you would normally pass
  124. in for MailResponse, and it'll craft the HTML mail for
  125. you and return it ready to deliver.
  126. A slight convenience in this function is that if the
  127. Body kw parameter equals the content parameter, then
  128. it's assumed you want the raw markdown content to be
  129. sent as the text version, and it will produce a nice
  130. dual HTML/text email.
  131. """
  132. assert content, "You must give a contents template."
  133. if kwd.get('Body', None) == content:
  134. kwd['Body'] = view.render(variables, content)
  135. for key in kwd:
  136. kwd[key] = kwd[key] % variables
  137. msg = mail.MailResponse(**kwd)
  138. msg.Html = self.render(variables, content)
  139. return msg