Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/framework/template_helpers.py b/framework/template_helpers.py
new file mode 100644
index 0000000..5f383c3
--- /dev/null
+++ b/framework/template_helpers.py
@@ -0,0 +1,326 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Some utility classes for interacting with templates."""
+
+from __future__ import division
+from __future__ import print_function
+from __future__ import absolute_import
+
+import cgi
+import cStringIO
+import httplib
+import logging
+import time
+import types
+
+import ezt
+import six
+
+from protorpc import messages
+
+import settings
+from framework import framework_constants
+
+
+_DISPLAY_VALUE_TRAILING_CHARS = 8
+_DISPLAY_VALUE_TIP_CHARS = 120
+
+
+class PBProxy(object):
+ """Wraps a Protocol Buffer so it is easy to acceess from a template."""
+
+ def __init__(self, pb):
+ self.__pb = pb
+
+ def __getattr__(self, name):
+ """Make the getters template friendly.
+
+ Psudo-hack alert: When attributes end with _bool, they are converted in
+ to EZT style bools. I.e., if false return None, if true return True.
+
+ Args:
+ name: the name of the attribute to get.
+
+ Returns:
+ The value of that attribute (as an EZT bool if the name ends with _bool).
+ """
+ if name.endswith('_bool'):
+ bool_name = name
+ name = name[0:-5]
+ else:
+ bool_name = None
+
+ # Make it possible for a PBProxy-local attribute to override the protocol
+ # buffer field, or even to allow attributes to be added to the PBProxy that
+ # the protocol buffer does not even have.
+ if name in self.__dict__:
+ if callable(self.__dict__[name]):
+ val = self.__dict__[name]()
+ else:
+ val = self.__dict__[name]
+
+ if bool_name:
+ return ezt.boolean(val)
+ return val
+
+ if bool_name:
+ # return an ezt.boolean for the named field.
+ return ezt.boolean(getattr(self.__pb, name))
+
+ val = getattr(self.__pb, name)
+
+ if isinstance(val, messages.Enum):
+ return int(val) # TODO(jrobbins): use str() instead
+
+ if isinstance(val, messages.Message):
+ return PBProxy(val)
+
+ # Return a list of values whose Message entries
+ # have been wrapped in PBProxies.
+ if isinstance(val, (list, messages.FieldList)):
+ list_to_return = []
+ for v in val:
+ if isinstance(v, messages.Message):
+ list_to_return.append(PBProxy(v))
+ else:
+ list_to_return.append(v)
+ return list_to_return
+
+ return val
+
+ def DebugString(self):
+ """Return a string representation that is useful in debugging."""
+ return 'PBProxy(%s)' % self.__pb
+
+ def __eq__(self, other):
+ # Disable warning about accessing other.__pb.
+ # pylint: disable=protected-access
+ return isinstance(other, PBProxy) and self.__pb == other.__pb
+
+
+_templates = {}
+
+
+def GetTemplate(
+ template_path, compress_whitespace=True, eliminate_blank_lines=False,
+ base_format=ezt.FORMAT_HTML):
+ """Make a MonorailTemplate if needed, or reuse one if possible."""
+ key = template_path, compress_whitespace, base_format
+ if key in _templates:
+ return _templates[key]
+
+ template = MonorailTemplate(
+ template_path, compress_whitespace=compress_whitespace,
+ eliminate_blank_lines=eliminate_blank_lines, base_format=base_format)
+ _templates[key] = template
+ return template
+
+
+class cStringIOUnicodeWrapper(object):
+ """Wrapper on cStringIO.StringIO that encodes unicode as UTF-8 as it goes."""
+
+ def __init__(self):
+ self.buffer = cStringIO.StringIO()
+
+ def write(self, s):
+ if isinstance(s, six.text_type):
+ utf8_s = s.encode('utf-8')
+ else:
+ utf8_s = s
+ self.buffer.write(utf8_s)
+
+ def getvalue(self):
+ return self.buffer.getvalue()
+
+
+SNIFFABLE_PATTERNS = {
+ '%PDF-': '%NoNoNo-',
+}
+
+
+class MonorailTemplate(object):
+ """A template with additional functionality."""
+
+ def __init__(self, template_path, compress_whitespace=True,
+ eliminate_blank_lines=False, base_format=ezt.FORMAT_HTML):
+ self.template_path = template_path
+ self.template = None
+ self.compress_whitespace = compress_whitespace
+ self.base_format = base_format
+ self.eliminate_blank_lines = eliminate_blank_lines
+
+ def WriteResponse(self, response, data, content_type=None):
+ """Write the parsed and filled in template to http server."""
+ if content_type:
+ response.content_type = content_type
+
+ response.status = data.get('http_response_code', httplib.OK)
+ whole_page = self.GetResponse(data)
+ if data.get('prevent_sniffing'):
+ for sniff_pattern, sniff_replacement in SNIFFABLE_PATTERNS.items():
+ whole_page = whole_page.replace(sniff_pattern, sniff_replacement)
+ start = time.time()
+ response.write(whole_page)
+ logging.info('wrote response in %dms', int((time.time() - start) * 1000))
+
+ def GetResponse(self, data):
+ """Generate the text from the template and return it as a string."""
+ template = self.GetTemplate()
+ start = time.time()
+ buf = cStringIOUnicodeWrapper()
+ template.generate(buf, data)
+ whole_page = buf.getvalue()
+ logging.info('rendering took %dms', int((time.time() - start) * 1000))
+ logging.info('whole_page len is %r', len(whole_page))
+ if self.eliminate_blank_lines:
+ lines = whole_page.split('\n')
+ whole_page = '\n'.join(line for line in lines if line.strip())
+ logging.info('smaller whole_page len is %r', len(whole_page))
+ logging.info('smaller rendering took %dms',
+ int((time.time() - start) * 1000))
+ return whole_page
+
+ def GetTemplate(self):
+ """Parse the EZT template, or return an already parsed one."""
+ # We don't operate directly on self.template to avoid races.
+ template = self.template
+
+ if template is None or settings.local_mode:
+ start = time.time()
+ template = ezt.Template(
+ fname=self.template_path,
+ compress_whitespace=self.compress_whitespace,
+ base_format=self.base_format)
+ logging.info('parsed in %dms', int((time.time() - start) * 1000))
+ self.template = template
+
+ return template
+
+ def GetTemplatePath(self):
+ """Accessor for the template path specified in the constructor.
+
+ Returns:
+ The string path for the template file provided to the constructor.
+ """
+ return self.template_path
+
+
+class EZTError(object):
+ """This class is a helper class to pass errors to EZT.
+
+ This class is used to hold information that will be passed to EZT but might
+ be unset. All unset values return None (ie EZT False)
+ Example: page errors
+ """
+
+ def __getattr__(self, _name):
+ """This is the EZT retrieval function."""
+ return None
+
+ def AnyErrors(self):
+ return len(self.__dict__) != 0
+
+ def DebugString(self):
+ return 'EZTError(%s)' % self.__dict__
+
+ def SetError(self, name, value):
+ self.__setattr__(name, value)
+
+ def SetCustomFieldError(self, field_id, value):
+ # This access works because of the custom __getattr__.
+ # pylint: disable=access-member-before-definition
+ # pylint: disable=attribute-defined-outside-init
+ if self.custom_fields is None:
+ self.custom_fields = []
+ self.custom_fields.append(EZTItem(field_id=field_id, message=value))
+
+ any_errors = property(AnyErrors, None)
+
+def FitUnsafeText(text, length):
+ """Trim some unsafe (unescaped) text to a specific length.
+
+ Three periods are appended if trimming occurs. Note that we cannot use
+ the ellipsis character (&hellip) because this is unescaped text.
+
+ Args:
+ text: the string to fit (ASCII or unicode).
+ length: the length to trim to.
+
+ Returns:
+ An ASCII or unicode string fitted to the given length.
+ """
+ if not text:
+ return ""
+
+ if len(text) <= length:
+ return text
+
+ return text[:length] + '...'
+
+
+def BytesKbOrMb(num_bytes):
+ """Return a human-readable string representation of a number of bytes."""
+ if num_bytes < 1024:
+ return '%d bytes' % num_bytes # e.g., 128 bytes
+ if num_bytes < 99 * 1024:
+ return '%.1f KB' % (num_bytes / 1024.0) # e.g. 23.4 KB
+ if num_bytes < 1024 * 1024:
+ return '%d KB' % (num_bytes / 1024) # e.g., 219 KB
+ if num_bytes < 99 * 1024 * 1024:
+ return '%.1f MB' % (num_bytes / 1024.0 / 1024.0) # e.g., 21.9 MB
+ return '%d MB' % (num_bytes / 1024 / 1024) # e.g., 100 MB
+
+
+class EZTItem(object):
+ """A class that makes a collection of fields easily accessible in EZT."""
+
+ def __init__(self, **kwargs):
+ """Store all the given key-value pairs as fields of this object."""
+ vars(self).update(kwargs)
+
+ def __repr__(self):
+ fields = ', '.join('%r: %r' % (k, v) for k, v in
+ sorted(vars(self).items()))
+ return '%s({%s})' % (self.__class__.__name__, fields)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+
+def ExpandLabels(page_data):
+ """If page_data has a 'labels' list, expand it into 'label1', etc.
+
+ Args:
+ page_data: Template data which may include a 'labels' field.
+ """
+ label_list = page_data.get('labels', [])
+ if isinstance(label_list, types.StringTypes):
+ label_list = [label.strip() for label in page_data['labels'].split(',')]
+
+ for i in range(len(label_list)):
+ page_data['label%d' % i] = label_list[i]
+ for i in range(len(label_list), framework_constants.MAX_LABELS):
+ page_data['label%d' % i] = ''
+
+
+class TextRun(object):
+ """A fragment of user-entered text that needs to be safely displyed."""
+
+ def __init__(self, content, tag=None, href=None):
+ self.content = content
+ self.tag = tag
+ self.href = href
+ self.title = None
+ self.css_class = None
+
+ def FormatForHTMLEmail(self):
+ """Return a string that can be used in an HTML email body."""
+ if self.tag == 'a' and self.href:
+ return '<a href="%s">%s</a>' % (
+ cgi.escape(self.href, quote=True),
+ cgi.escape(self.content, quote=True))
+
+ return cgi.escape(self.content, quote=True)