| Index: appengine/monorail/framework/template_helpers.py
|
| diff --git a/appengine/monorail/framework/template_helpers.py b/appengine/monorail/framework/template_helpers.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..5127699b671dae80d977662ee0b0ff83188b5643
|
| --- /dev/null
|
| +++ b/appengine/monorail/framework/template_helpers.py
|
| @@ -0,0 +1,309 @@
|
| +# Copyright 2016 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is govered 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."""
|
| +
|
| +import cgi
|
| +import cStringIO
|
| +import httplib
|
| +import logging
|
| +import time
|
| +import types
|
| +
|
| +from third_party import ezt
|
| +
|
| +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, unicode):
|
| + 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.dev_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).iteritems()))
|
| + return '%s({%s})' % (self.__class__.__name__, fields)
|
| +
|
| +
|
| +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
|
|
|