Anonymous avatar Anonymous committed d2f0b7b Merge

Comments (0)

Files changed (7)

 
 tip (development version)
 -------------------------
+* webhelpers.html.grid:
+  - New module to create an HTML table from a list of records.
+* webhelpers.html.tags:
+  - New helpers ``Doctype`` (class) and ``xml_declaration``.
 * webhelpers.html.tools:
-  - New helper ``js_obfuscate`` implements the old rails helper of the same name.
+  - New helper ``js_obfuscate`` implements the old rails helpera.
+* webhelpers.util:
+  - New helper ``update_params`` to update query parameters in a URL.
 
 1.0b2 (2009-12-21)
 ------------------

unfinished/grid_test.py

-import grid
-import grid_pylons
-from webhelpers.html.builder import HTML, literal
+from webhelpers.html import HTML, literal
+import webhelpers.html.grid as grid
+import webhelpers.pylonslib.grid_pylons as grid_pylons
+
+def options_td(col_num, i, item):
+    return HTML.td("baz", class_="c%s" % (col_num))
 
 test_data = [
-             {'group_name':'foo','options':'lalala'},
-             {'group_name':'foo2','options':'lalala2'},
-             {'group_name':'foo3','options':'lalala3'},
-             {'group_name':'foo4','options':'lalala4'},
-             ]
+     {"group_name": "foo", "options": "lalala"},
+     {"group_name": "foo2", "options": "lalala2"},
+     {"group_name": "foo3", "options": "lalala3"},
+     {"group_name": "foo4", "options": "lalala4"},
+     ]
 
-test_grid = grid.Grid(test_data, columns=['_numbered','group_name','options'])
+test_grid = grid.Grid(test_data, columns=["_numbered","group_name","options"])
 test_grid.exclude_ordering = test_grid.columns
 test_grid.format = {
-'options':lambda col_num, i,item: HTML.tag('td', 'baz', class_='c%s' % (col_num))
-}
+    "options": options_td,
+    }
 
-
-print '<table>'
+print "<table>"
 print test_grid
-print '/<table>'
+print "</table>"

webhelpers/html/builder.py

 
 __all__ = ["HTML", "escape", "literal", "url_escape", "lit_sub"]
 
+# Not included in __all__ because for specialized purposes only: 
+# "format_attrs".
+
 class UnfinishedTag(object):
     
     """Represents an unfinished or empty tag."""
         args = kw.pop("c")
     closed = kw.pop("_closed", True)
     nl = kw.pop("_nl", False)
-    htmlArgs = [' %s="%s"' % (_attr_decode(attr), escape(value))
-                for attr, value in sorted(kw.iteritems())
-                if value is not None]
+    attrs_str = format_attrs(**kw)
     if not args and tag in empty_tags and closed:
         substr = '<%s%s />'
-        html = literal(substr % (tag, "".join(htmlArgs)))
+        html = literal(substr % (tag, attrs_str))
     else:
-        chunks = ["<%s%s>" % (tag, "".join(htmlArgs))]
+        chunks = ["<%s%s>" % (tag, attrs_str)]
         chunks.extend(escape(x) for x in args)
         if closed:
             chunks.append("</%s>" % tag)
         html += "\n"
     return literal(html)
 
+def format_attrs(**attrs):
+    """Format HTML attributes into a string of ' key="value"' pairs which
+    can be inserted into an HTML tag.
+
+    The attributes are sorted alphabetically.  If any value is None, the entire
+    attribute is suppressed.
+
+    Usage:
+    >>> format_attrs(p=2, q=3)
+    literal(u' p="2" q="3"')
+    >>> format_attrs(p=2, q=None)
+    literal(u' p="2"')
+    >>> format_attrs(p=None)
+    literal(u'')
+    """
+    strings = [u' %s="%s"' % (_attr_decode(attr), escape(value))
+        for attr, value in sorted(attrs.iteritems())
+        if value is not None]
+    return literal("".join(strings))
 
 class literal(unicode):
     """Represents an HTML literal.

webhelpers/html/grid.py

+from webhelpers.html.builder import HTML, literal
+
+class Grid(object):
+    
+    def __init__(self, itemlist, columns, column_formats=None, start_number=1):
+        self.custom_record_format = None
+        self.labels = {}
+        self.exclude_ordering = ["_numbered"]
+        self.itemlist = itemlist
+        if "_numbered" in columns:
+            self.labels["_numbered"] = "no."
+        self.columns = columns
+        self.column_formats = column_formats or {}
+        self._start_number = start_number
+        
+
+    def make_headers(self):
+        header_columns = []
+            
+        for i, column in enumerate(self.columns):
+            # let"s generate header column contents
+            label_text = ""
+            if column in self.labels:
+                label_text = self.labels[column]
+            else:
+                label_text = column.replace("_", " ").title()
+            # handle non clickable columns
+            if column in self.exclude_ordering:
+                header = self.default_header_column_format(i + 1, column, 
+                    label_text)
+            # handle clickable columns
+            else:
+                header = self.generate_header_link(i + 1, column, label_text)
+                if header is None:
+                    header = self.default_header_column_format(i + 1, column, 
+                        label_text)                    
+            header_columns.append(header)               
+        return HTML(*header_columns)
+    
+    def make_columns(self, i, record):
+        columns = []        
+        for col_num, column in enumerate(self.columns):
+            if column in self.column_formats:
+                columns.append(self.column_formats[column](col_num, i, record))
+            else:
+                if column == "_numbered":
+                    r = self.numbered_column_format(col_num, 
+                        i + self._start_number, record)
+                else:
+                    r = self.default_column_format(col_num, i, record, column)
+                columns.append(r)
+        return HTML(*columns)
+    
+    def __html__(self):
+        """ renders the grid """
+        records = []
+        #first render headers record
+        headers = self.make_headers()
+        r = self.default_header_record_format(headers)
+        records.append(r)
+        for i, record in enumerate(self.itemlist):
+            if i % 2 == 0:
+                class_name = "even"
+            else:
+                class_name = "odd"
+            columns = self.make_columns(i, record)
+            if self.custom_record_format is None:
+                r = self.default_record_format(i, record, class_name, columns)
+            else:
+                r = self.custom_record_format(i, record, class_name, columns)
+            records.append(r)
+        return HTML(*records)
+    
+    def __str__(self):
+        return self.__html__()
+
+    def generate_header_link(self, column_number, column, label_text):
+        return None
+
+    #### Default HTML tag formats ####
+
+    def default_column_format(self, column_number, i, record, column_name):
+        class_name = "c%s" % (column_number)
+        return HTML.tag("td", record[column_name], class_=class_name)
+    
+    def numbered_column_format(self, column_number, i, record):
+        class_name = "c%s" % (column_number)
+        return HTML.tag("td", i, class_=class_name)
+    
+    def default_record_format(self, i, record, class_name, columns):
+        return HTML.tag("tr", columns, class_=class_name)
+
+    def default_record_format(self, i, record, class_name, columns):
+        return HTML.tag("tr", columns, class_=class_name)
+
+    def default_header_record_format(self, headers):
+        return HTML.tag("tr", headers, class_="header")
+
+    def default_header_ordered_column_format(self, column_number, order, 
+        column_name, header_label):
+        header_label = HTML(header_label, HTML.tag("span", class_="marker"))
+        if column_name == "_numbered":
+            column_name = "numbered"
+        class_name = "c%s ordering %s %s" % (column_number, order, column_name)
+        return HTML.tag("td", header_label, class_=class_name)
+
+    def default_header_column_format(self, column_number, column_name, 
+        header_label):
+        if column_name == "_numbered":
+            column_name = "numbered"
+        class_name = "c%s %s" % (column_number, column_name)
+        return HTML.tag("td", header_label, class_=class_name)
+    

webhelpers/html/tags.py

            "th_sortable",
            # Other non-form tags
            "ol", "ul", "image", "BR",
-           # Head tags
+           # Head tags and document type
            "stylesheet_link", "javascript_link", "auto_discovery_link",
+           "Doctype", "xml_declaration",
            # Utility functions
            "css_classes", "convert_boolean_attrs",
            ]
 
 #### Tags for the HTML head
 
+    #### Private methods
+    def _make_doctype(self, type, uri, dtd):
+        return literal('<!DOCTYPE %s PUBLIC "%s" "%s">') % (type, uri, dtd)
+
 def javascript_link(*urls, **attrs):
     """Return script include tags for the specified javascript URLs.
     
     attrs.setdefault("title", title)
     return HTML.link(rel="alternate", type=feed_type, href=url, **attrs)
 
+#### Document type and XML declaration
+
+class Doctype(object):
+    """Document type declarations for HTML and XHTML."""
+
+    def html5(self):
+        """Create a <!DOCTYPE> for HTML 5.
+        
+           Usage:
+           >>> Doctype().html5()
+           literal(u'<!doctype html>')
+        """
+        return literal("<!doctype html>")
+
+    def xhtml1(self, subtype="transitional", version="1.0"):
+        """Create a <!DOCTYPE> for XHTML 1.
+
+           Usage:
+           >>> Doctype().xhtml1()
+           literal(u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">')
+           >>> Doctype().xhtml1("strict")
+           literal(u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">')
+           >>> Doctype().xhtml1("frameset")
+           literal(u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">')
+        """
+        if subtype in ["transitional", "loose"]:
+            name = "Transitional"
+            dtd = "transitional"
+        else:
+            name = subtype.capitalize()
+            dtd = subtype.lower()
+        uri = "-//W3C//DTD XHTML %s %s//EN" % (version, name)
+        dtd = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-%s.dtd" % dtd
+        return self._make_doctype("html", uri, dtd)
+
+    def html4(self, subtype="transitional", version="4.01"):
+        """Create a <!DOCTYPE> for HTML 4.
+
+           Usage:
+           >>> Doctype().html4()
+           literal(u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">')
+           >>> Doctype().html4("strict")
+           literal(u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">')
+           >>> Doctype().html4("frameset")
+           literal(u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">')
+        """
+        if subtype in ["transitional", "loose"]:
+            name = "Transitional"
+            dtd = "loose"
+        else:
+            name = subtype.capitalize()
+            dtd = subtype.lower()
+        uri = "-//W3C//DTD HTML %s %s//EN" % (version, name)
+        dtd = "http://www.w3.org/TR/html4/%s.dtd" % dtd
+        return self._make_doctype("HTML", uri, dtd)
+
+    #### Private methods
+    def _make_doctype(self, type, uri, dtd):
+        return literal('<!DOCTYPE %s PUBLIC "%s" "%s">') % (type, uri, dtd)
+
+
+def xml_declaration(version="1.0", encoding="utf-8"):
+    """Create an XML declaration.
+
+       Usage:
+       >>> xml_declaration()
+       literal(u'<?xml version="1.0" encoding="utf-8" ?>')
+    """
+    return literal('<?xml version="%s" encoding="%s" ?>') % (version, encoding)
+
 
 
 ########## INTERNAL FUNCTIONS ##########

webhelpers/pylonslib/grid.py

+from webhelpers.html.builder import HTML, literal
+import webhelpers.html.grid as grid
+
+class NoRequestException(Exception):
+    pass
+
+class GridPylons(grid.Grid):
+    
+    def generate_header_link(self, column_number, column, label_text):
+        from pylons import request, url
+
+        # this will handle possible URL generation
+        if not hasattr(self, "request"):
+            raise NoRequestException(
+                "Could not find self.request for this grid")
+        request_copy = self.request.copy().GET
+        if "order_by" in request_copy:
+            self.order_column = request_copy.pop("order_by")
+        else:
+            self.order_column = None
+            
+        if (self.order_column and column == self.order_column[0:-4] and 
+            self.order_column[-3:] == "asc"):
+            new_ordering = column + "_dsc"
+        else:
+            new_ordering = column + "_asc"
+        url_href = url(order_by=new_ordering, **request_copy)        
+        label_text = HTML.tag("a", href=url_href, c=label_text)
+        # Is the current column the one we're ordering on?
+        if (self.order_column and column == self.order_column[0:-4] and 
+            self.order_column[-3:] == "asc"):
+            return self.default_header_ordered_column_format(i, "asc", column, 
+                label_text)
+        elif (self.order_column and column == self.order_column[0:-4] and 
+            self.order_column[-3:] == "dsc"):
+            return self.default_header_ordered_column_format(i, "dsc", column, 
+                label_text)
+        else:
+            return self.default_header_column_format(i, column, label_text)
+        

webhelpers/util.py

 import copy
 import sys
 import urllib
+import urlparse
 from UserDict import DictMixin
 from xml.sax.saxutils import XMLGenerator
 
+def update_params(url, **params):
+    """Update query parameters in a URL.
+
+    ``url`` is any URL.
+    ``params`` are query parameters to add or replace. If any value is None,
+    the corresponding parameter is deleted from the URL if present.
+
+    Return the new URL.
+
+    This function does not handle multiple parameters with the same name.
+    It will arbitrarily choose one value and discard the others.
+
+    *Debug mode:* if a pseudo-parameter '_debug' with a true value is passed,
+    return a tuple: ``[0]`` is the URL without query string or fragment,
+    ``[1]`` is the final query parameters as a dict, and ``[2]`` is the
+    fragment part of the original URL or the empty string.
+
+    Usage::
+    >>> update_params("foo", new1="NEW1")
+    'foo?new1=NEW1'
+    >>> update_params("foo?p=1", p="2")
+    'foo?p=2'
+    >>> update_params("foo?p=1", p=None)
+    'foo'
+    >>> update_params("http://example.com/foo?new1=OLD1#myfrag", new1="NEW1")
+    'http://example.com/foo?new1=NEW1#myfrag'
+    >>> update_params("http://example.com/foo?new1=OLD1#myfrag", new1="NEW1", _debug=True)
+    ('http://example.com/foo', {'new1': 'NEW1'}, 'myfrag')
+    """
+    debug = params.pop("_debug", False)
+    orig_url = url
+    url, fragment = urlparse.urldefrag(url)
+    if "?" in url:
+        url, qs = url.split("?", 1)
+        query = urlparse.parse_qs(qs)
+    else:
+        query = {}
+    for key, value in params.iteritems():
+        if value is not None:
+            query[key] = value
+        elif key in query:
+            del query[key]
+    if debug:
+        return url, query, fragment
+    qs = urllib.urlencode(query)
+    if qs:
+        qs = "?" + qs
+    if fragment:
+        fragment = "#" + fragment
+    return "%s%s%s" % (url, qs, fragment)
+
 def cgi_escape(s, quote=False):
     """Replace special characters '&', '<' and '>' by SGML entities.
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.