Commits

Anonymous committed d68d723

Committing version 1.2.2. Please read CHANGES and README files for details

  • Participants
  • Tags 1.2.2

Comments (0)

Files changed (25)

+syntax: glob
+
+# Backup files left behind by the Emacs editor.
+*~
+# Lock files used by the Emacs editor.
+.\#*
+# Temporary files used by the vim editor.
+.*.swp
+# A hidden file created by the Mac OS X Finder.
+.DS_Store
+# Compiled Python scripts
+.*.pyc
+.*.pyo
+# Metadata for Python package 
+**/PKG-INFO
+**/*.egg-info
+# Temporary folders
+**/temp
+**/temp/*

File trac-dev/gviz/CHANGES

+
+What's new in version 1.2.2
+-----------------------------
+
+- The meaning of the different columns in the table returned by GViz 
+  data sources as well as the default labels can be documented using 
+  `gviz_col`. This function is compliant with annotations as defined 
+  by PEP 3107.
+
+- Data sources' documentation can be included in wiki pages using 
+  `GVizDataSource` macro.
+
+- Docstrings have changed a little.
+
+- Bug fixed: Until now, if no protocol handler was enabled then 
+  the project environment became broken, because of a ValueError 
+  exception being raised. This is annoying and therefore has been 
+  fixed. Now if a URL having `gviz` prefix is accessed and 
+  there's no protocol handler available, a ``Not Found`` (404) HTTP 
+  response is returned to the caller with message 
+  ``Unable to find any protocol handler``.
+
+What's new in version 1.2.1
+-----------------------------
+
+- The `gviz_api` module (version 1.0.0) is redistributed in this 
+  package. This has been made to fall back to this implementation 
+  if no other has been previously installed.
+
+- From this version on, the work is licensed under the Apache License. 
+  See COPYRIGHT and NOTICE for details.
+
+- Babel is no longer a dependency.
+
+- Full TracLinks expansion in `iGoogleGadget` macro parameters.
+
+- iGoogle gadgets logo included as image resource. Borders template 
+  updated.
+
+- Bug fixed: Until now, if an underlying RPC handler was disabled 
+  then environment initialization failed since an annoying ValueError 
+  was raised. That won't happen anymore. Now, once a method is 
+  requested on the corresponding GViz data source provider, a GViz 
+  error response is returned to the caller containing an explanatory 
+  message.
+
+What's new in version 1.2.0
+---------------------------
+
+- Added support to display iGoogle gadgets using WikiFormatting 
+  extensions (i.e. iGoogleGadget macro).
+
+- Added support to upload and host custom image-based gadget borders 
+  inside the project environment.
+
+- Bug fixed : All requests handled by IGVizDataProvider implementors 
+  not having `tq` and `tqx` parameters 
+  (e.g. http://myserver.com/project_name/gviz/ticket/status) now 
+  output the correct data. Previously when accessing these URLs the
+  HTTP response body was an HTML error page.
+
+What's new in version 1.1.0
+---------------------------
+
+- Support has been added to implement Google Visualization API data 
+  sources by reusing existing Trac XML RPC components.
+
+- Some (but not all) data sources for Trac ticket system are provided.
+  They are based on the following Trac XML-RPC providers (listed 
+  hereafter by namespace): ticket.milestone, ticket.severity, 
+  ticket.type, ticket.resolution, ticket.priority, ticket.component,
+  ticket.version, ticket.status.
+
+What's new in version 1.0.0
+---------------------------
+
+- An architecture is available so as to provide a project's data in
+  the format specified in Google Visualization API protocol 
+  specification (version 0.5) api.TracGVizSystem.
+
+- Multiple protocol handlers (e.g. for different versions, and 
+  protocol evolution) are allowed by implementing the interface
+  api.IGVizProtocolHandler. There is native support for version 0.5 of
+  Google Visualization API protocol through GViz_0_5.
+
+- It is possible to register new data sources by implementing the 
+  interface api.IGVizDataProvider
+
+- A pluggable architecture is at hand to retrieve table contents
+  in multiple formats by merely implementing api.IGVizTableEncoder
+  interface. The following formats are supported : JSON 
+  (class stdfmt.GVizJsonEncoder), HTML (class stdfmt.GVizHtmlEncoder),
+  CSV (class stdfmt.GVizCSVEncoder).
+
+- The exception conditions mentioned in Google Visualization API
+  protocol specification (version 0.5) have been identified.

File trac-dev/gviz/COPYRIGHT

+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

File trac-dev/gviz/NOTICE

+
+This package includes a verbatim copy of `gviz_api` module 
+(version 1.0.0). This has been done so as to ease the installation 
+process and provide the means to fall-back to this implementation if 
+`gviz_api` is not installed in the target deployment environment.
+Herinafter you will also find a verbatim copy of the contents of the 
+README file distributed with the aformentioned module.
+
+==============================
+Copyright (C) 2008 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Installing the library:
+  python ./setup.py install
+  (You might need root privileges to do this)
+
+Testing the library:
+  python ./setup.py test
+
+==============================
+
+By installing TracGViz plugin, you will have a completely functional 
+instance of `gviz_api` (version 1.0.0) module. The maintainers of 
+this package ARE NOT RESPONSIBLE of providing the latest version 
+of this module in newer versions, since this has been done as 
+a last recourse alternative. If a newer (or any other) version is 
+already installed in your system, it will be used instead. However 
+the authors and maintainers WILL NOT ensure that the current plugin 
+implementation will be compatible with successive versions of 
+`gviz_api` and ARE NOT responsible if future contributions included 
+in the aforementioned module turn out to be incompatible with the 
+current implementation.
+
+Google Visualization API, and iGoogle Gadgets  are technologies 
+offered by Google Inc. in the form of a Software as a Service (SaaS) 
+platform. If you are using TracGViz plugin, make sure you have 
+already read and agreed the related Terms of Use of these services 
+and/or End-User License Agreement (EULA).
+
+Youtube is a registered trademark owned by Google Inc.
+
+All other trademarks and labels are a property of their respective 
+owners. The authors and maintainers of TracGViz plugin 
+ARE NOT RESPONSIBLE if the usage given to this package violates any 
+of the aformentioned statements, or if any end-user violates 
+any other COPYRIGHT statements.

File trac-dev/gviz/README

+
+= Trac integration with Google Visualization API =
+
+This plugin has been developped in order to expose the data managed by [http://trac.edgewall.org/ Trac] to widgets implemented using [http://code.google.com/apis/visualization Google Visualization API]. The following data is provided so far in the form of [http://code.google.com/apis/visualization data tables]:
+
+  - Project components.
+  - Project milestones.
+  - Ticket priorities.
+  - Ticket resolution values.
+  - Ticket severity values.
+  - Ticket status values.
+  - Ticket types.
+  - Project versions.
+
+On the other hand, it will also allow [http://trac.edgewall.org/ Trac] users to use WikiFormatting in order to insert different widgets built with different [http://www.google.com/more Google APIs]. Until now this includes:
+
+  - [http://www.google.com/ig iGoogle] [http://code.google.com\apis\gadgets gadgets]. In this case, the different [http://code.google.com/apis/visualization/documentation/gadgetgallery.html Visualization Gadgets] can be helpful to vary the way data managed by [http://trac.edgewall.org/ Trac] itself gets renderred in wiki pages, or maybe in third-party sites. Besides, some other [http://code.google.com\apis\gadgets gadgets] containing multiple informations, or even mini-applications, may be pasted in wiki pages. This may yields in ease of use, code reuse and, finally, lovely and useful [http://trac.edgewall.org/ Trac] sites. Since [http://www.google.com/ig iGoogle] [http://code.google.com\apis\gadgets gadgets] are displayed inside '''iframes''', security risks '''should''' be minimal ... however, consult [http://code.google.com/apis/gadgets/docs/spec.html gadgets specification] for more details.
+
+== !ToDo ==
+
+Outstanding tasks are :
+
+[[TicketQuery(component=plugin_trac_gviz&priority=major, format=list, rows=id|summary)]]
+
+== Dependencies ==
+
+This plugin depends on the following components to be installed:
+
+  - [http://peak.telecommunity.com/DevCenter/setuptools  setuptools]
+
+  - [http://genshi.edgewall.org/ Genshi]
+  
+  - The [http://code.google.com/apis/visualization/documentation/dev/gviz_api_lib.html Data Source Python Library]
+    must be installed so that the outputs be consistent with
+    [http://code.google.com/apis/visualization/ Google Visualization API] [http://code.google.com/apis/visualization/documentation/dev/implementing_data_source.html data source protocol specification], and widgets can actually render the data managed by [http://trac.edgewall.org/ Trac] environments.
+
+== Installation ==
+
+This plugin has been tested with 
+[http://trac.edgewall.org/ Trac] [http://trac-hacks.org/wiki/0.11 0.11.2]. You can see it in action [http://opensvn.csie.org/traccgi/swlcu here]. 
+
+The first installation step is to [wiki:TracPlugins install this plugin] 
+either for a particular environment or otherwise make it available to
+all the environments:
+
+{{{
+$ easy_install /path/to/unpacked/TracGViz-x.y.z.zip
+}}}
+
+,, where ''x.y.z'' is the version of the plugin,,
+
+... or alternately ...
+
+{{{
+$ easy_install TracGViz
+}}}
+
+In case of having internet connection and access to [http://pypi.python.org/pypi PyPI] or a simlar repository, both these methods '''should''' automatically retrieve the [#Dependencies external dependencies] from there.
+
+== Configuration ==
+
+In order to enable TracGViz plugin, the only thing to do is to add the
+following lines to [wiki:TracIni trac.ini].
+
+{{{
+[components]
+tracgviz.* = enabled
+}}}
+
+== What about coders ==
+
+
+
+=== Design considerations ===
+
+Hereinafter you will find some rules considered to develop this plugin. They are based in [http://code.google.com/apis/visualization/documentation/implementing_data_source.htm#overview a few basic rules] suggested by [http://www.google.com Google] engineers.
+
+  - Trac accepts `HTTP GET` requests, however the site itself should be available to your clients.
+  - Since the underlying protocol supports versioning, the solution provides [xxx extension points] to ease the task of adding protocol handlers for specific versions. Once they are implemented, your '''should''' support requests using previous versions as well as the current version since a built-in mechanism selects the handler matching the protocol version specified in the request. We'd really like to support new versions as soon as they are released to avoid breaking any clients that upgrade to the newest version quickly. However, time is a '''very''' scarce resource these days. There are two possible ways to overcome this situations:
+    - '''Volunteer''' a little, [mailto://flioops.project@gmail.com?subject=TracGVizPlugin%20Patch submit patches], it will be great if many others contribute as well ... don't hesitate [mailto://flioops.project@gmail.com?subject=TracGVizPlugin%20add_member be part of the team].
+    - Donate to this project (link available hopefully soon), so as to be able to leave everything else behind and fulfill your particular request. '''Time''' is more than '''gold'''.
+  - We try to do everything so as to not fail if unknown properties are sent as part of the request. However, if you discover any bug, '''please''' [query:status=new|assigned|reopened|closed&component=plugin_trac_gviz find out related tickets]. If none is found then [/newticket?component=plugin_trac_gviz create a new ticket]. 
+  - Conversely, built-in protocol handlers parse only the properties that they expect. If new versions introduce new properties, then the system uses the handler for the latest available version, and tries to send a response back to the client. [code.google.com/apis/visualization/documentation/querylanguage.html Google Visualization API query language] [ticket:xxx is not supported yet], but hopefully soon.
+  - Some data sources '''may''' accept custom parameters for custom visualizations. However, they '''always''' return responses in the standard format.
+  - Since third-party sites might be willing to use the data managed by your [http://trac.edgewall.org/ Trac] instance, it is very important to [ticket:xxx document the data source requirements] carefully. Due to the infinite possibilities offered by the plugin and [http://trac.edgewall.org/ Trac] architectures, and also since plugin developpers might want to implement their own data sources, the only feasible way to manage all this complexity is to automate documentation tasks. This feature is not ready, but will include (... in a near future ...) the following: 
+    - Any custom parameters that you accept,
+    - Whether or not [code.google.com/apis/visualization/documentation/querylanguage.html Google Visualization API query language] is supported, and ...
+    - What kind of data is returned, and the structure of that data (what the rows and columns represent, and any labeling).
+  - Integration with the [TracPermissions permissions system] has not being finished ... [ticket:xxx yet]. Therefore no standard security precautions have been taken, and the site may accept requests from unknown clients. Take this into consideration if you store any sensitive data.
+  - All request and response strings '''should''' be UTF-8 encoded. Else, [/newticket?component=plugin_trac_gvi let us know].
+
+== Bug / feature requests ==
+
+Existing bugs and feature requests for [wiki:En/Devel/TracGViz TracGViz] are
+[query:status=new|assigned|reopened&component=plugin_trac_gviz here].
+If you have any issues, create a [/newticket?component=plugin_trac_gvi new ticket].
+
+

File trac-dev/gviz/TODO

+
+Outstanding tasks
+-----------------
+
+- Add IPermissionRequestor to TracGVizSystem and others
+
+- Implement standard data source providers
+
+- Cache the requests and optimize the call/response life cycle by 
+  using the `reqId` parameter.
+
+- Implement GViz queries.
+
+- Return the proper MIME type for JSON in the HTTP responses.
+
+- Remove unnecessary imports.
+
+- Implement various gadget border providers.
+
+- Allow people to upload and use their own image and CSS borders in
+  gadgets.
+
+- Ensure that pictures submitted for image-based gadget borders have 
+  the proper dimensions.
+
+- Implement local gadget borders with the help of Trac Resources.
+
+- Implement preview capabilities for gadgets and borders.
+
+- Implement navigation to Trac Gadgets area.
+
+- Implement context navigation contributors to Trac Gadgets area.
+
+- Add a page to view the images in a gadget border.
+
+- Add a page to list available borders.
+
+- Add support for CSS-based and external (HTTP) gadget borders.
+
+- Separate GVizSystem and GVizModule ?
+
+- Provide default border images for gadgets.

File trac-dev/gviz/__init__.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""Trac Data Sources for Google Visualization API. Embed iGoogle gadgets using WikiFormatting.
+
+This package (plugin) provides components so that Trac be able to
+use widgets and related technologies provided by Google.
+
+- It allows to feed data managed by Trac to widgets based on Google 
+  Visualisation API. 
+- It allows embedding iGoogle Gadgets in wiki pages using WikiFormatting.
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+from trac.log import logger_factory
+
+from os.path import join
+
+__all__ = 'TracGVizSystem', 'GViz_0_5', 'GVizJsonEncoder'\
+            'GVizCSVEncoder', 'GVizHtmlEncoder'
+
+try:
+    from api import TracGVizSystem
+    from proto import GViz_0_5
+    from stdfmt import GVizJsonEncoder, GVizHtmlEncoder, GVizCSVEncoder
+    from ticket import *
+    from ig import *
+    from wiki import *
+    msg = 'Ok'
+except Exception, exc:
+    msg = "IG: Exception %s raised: '%s'" % (exc.__class__.__name__, str(exc))

File trac-dev/gviz/_gviz_api.py

+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Converts Python data into data for Google Visualization API clients.
+
+This library can be used to create a google.visualization.DataTable usable by
+visualizations built on the Google Visualization API. Output formats are raw
+JSON, JSON response, and JavaScript.
+
+See http://code.google.com/apis/visualization/ for documentation on the
+Google Visualization API.
+"""
+
+__author__ = "Amit Weinstein, Misha Seltzer"
+
+import datetime
+
+
+class DataTableException(Exception):
+  """The general exception object thrown by DataTable."""
+  pass
+
+
+class DataTable(object):
+
+  """Wraps the data to convert to a Google Visualization API DataTable.
+
+  Create this object, populate it with data, then call one of the ToJS...
+  methods to return a string representation of the data in the format described.
+
+  You can clear all data from the object to reuse it, but you cannot clear
+  individual cells, rows, or columns. You also cannot modify the table schema
+  specified in the class constructor.
+
+  You can add new data one or more rows at a time. All data added to an
+  instantiated DataTable must conform to the schema passed in to __init__().
+
+  You can reorder the columns in the output table, and also specify row sorting
+  order by column. The default column order is according to the original
+  table_description parameter. Default row sort order is ascending, by column
+  1 values. For a dictionary, we sort the keys for order.
+
+  The data and the table_description are closely tied, as described here:
+
+  The table schema is defined in the class constructor's table_description
+  parameter. The user defines each column using a tuple of
+  (id[, type, [label]]). The default value for type is string, and label
+  is the same as ID if not specified.
+
+  table_description is a dictionary or list, containing one or more column
+  descriptor tuples, nested dictionaries, and lists. Each dictionary key, list
+  element, or dictionary element must eventually be defined as
+  a column description tuple. Here's an example of a dictionary where the key
+  is a tuple, and the value is a list of two tuples:
+    {('a', 'number'): [('b', 'number'), ('c', 'string')]}
+
+  This flexibility in data entry enables you to build and manipulate your data
+  in a Python structure that makes sense for your program.
+
+  Add data to the table using the same nested design as the table's
+  table_description, replacing column descriptor tuples with cell data, and
+  each row is an element in the top level collection. This will be a bit
+  clearer after you look at the following examples showing the
+  table_description, matching data, and the resulting table:
+
+  Columns as list of tuples [col1, col2, col3]
+    table_description: [('a', 'number'), ('b', 'string')]
+    AppendData( [[1, 'z'], [2, 'w'], [4, 'o'], [5, 'k']] )
+    Table:
+    a  b   <--- these are column ids/labels
+    1  z
+    2  w
+    4  o
+    5  k
+
+  Dictionary of columns, where key is a column, and value is a list of
+  columns  {col1: [col2, col3]}
+    table_description: {('a', 'number'): [('b', 'number'), ('c', 'string')]}
+    AppendData( data: {1: [2, 'z'], 3: [4, 'w']}
+    Table:
+    a  b  c
+    1  2  z
+    3  4  w
+
+  Dictionary where key is a column, and the value is itself a dictionary of
+  columns {col1: {col2, col3}}
+    table_description: {('a', 'number'): {'b': 'number', 'c': 'string'}}
+    AppendData( data: {1: {'b': 2, 'c': 'z'}, 3: {'b': 4, 'c': 'w'}}
+    Table:
+    a  b  c
+    1  2  z
+    3  4  w
+  """
+
+  def __init__(self, table_description, data=None):
+    """Initialize the data table from a table schema and (optionally) data.
+
+    See the class documentation for more information on table schema and data
+    values.
+
+    Args:
+      table_description: A table schema, following one of the formats described
+                         in TableDescriptionParser(). Schemas describe the
+                         column names, data types, and labels. See
+                         TableDescriptionParser() for acceptable formats.
+      data: Optional. If given, fills the table with the given data. The data
+            structure must be consistent with schema in table_description. See
+            the class documentation for more information on acceptable data. You
+            can add data later by calling AppendData().
+
+    Raises:
+      DataTableException: Raised if the data and the description did not match,
+                          or did not use the supported formats.
+    """
+    self.__columns = self.TableDescriptionParser(table_description)
+    self.__data = []
+    if data:
+      self.LoadData(data)
+
+  @staticmethod
+  def SingleValueToJS(value, value_type):
+    """Translates a single value and type into a JS value.
+
+    Internal helper method.
+
+    Args:
+      value: The value which should be converted
+      value_type: One of "string", "number", "boolean", "date", "datetime" or
+                  "timeofday".
+
+    Returns:
+      The proper JS format (as string) of the given value according to the
+      given value_type. For None, we simply return "null".
+      If a tuple is given, it should be of the form (value, formatted value)
+      where the formatted value is a string. In such a case, we return
+      the tuple of (JS value as string, formatted value).
+      The real type of the given value is not strictly checked. For example,
+      any type can be used for string - as we simply take its str( ) and for
+      boolean value we just check "if value".
+      Examples:
+        SingleValueToJS(False, "boolean") returns "false"
+        SingleValueToJS((5, "5$"), "number") returns ("5", "'5$'")
+
+    Raises:
+      DataTableException: The value and type did not match in a not-recoverable
+                          way, for example given value 'abc' for type 'number'.
+    """
+    if isinstance(value, tuple):
+      # In case of a tuple, we run the same function on the value itself and
+      # add the formatted value.
+      if len(value) != 2:
+        raise DataTableException("Wrong format for value and formatting - %s." %
+                                 str(value))
+      if not isinstance(value[1], str):
+        raise DataTableException("Formatted value is not string, given %s." %
+                                 type(value[1]))
+      js_value = DataTable.SingleValueToJS(value[0], value_type)
+      if js_value == "null":
+        raise DataTableException("An empty cell can not have formatting.")
+      # Here we use python built-in escaping mechanism for string using repr.
+      return (js_value, repr(str(value[1])))
+
+    # The standard case - no formatting.
+    t_value = type(value)
+    if value is None:
+      return "null"
+    if value_type == "boolean":
+      if value:
+        return "true"
+      return "false"
+
+    elif value_type == "number":
+      if isinstance(value, (int, long, float)):
+        return str(value)
+      raise DataTableException("Wrong type %s when expected number" % t_value)
+
+    elif value_type == "string":
+      if isinstance(value, tuple):
+        raise DataTableException("Tuple is not allowed as string value.")
+      return repr(str(value))
+
+    elif value_type == "date":
+      if not isinstance(value, (datetime.date, datetime.datetime)):
+        raise DataTableException("Wrong type %s when expected date" % t_value)
+        # We need to shift the month by 1 to match JS Date format
+      return "new Date(%d,%d,%d)" % (value.year, value.month - 1, value.day)
+
+    elif value_type == "timeofday":
+      if not isinstance(value, (datetime.time, datetime.datetime)):
+        raise DataTableException("Wrong type %s when expected time" % t_value)
+      return "[%d,%d,%d]" % (value.hour, value.minute, value.second)
+
+    elif value_type == "datetime":
+      if not isinstance(value, datetime.datetime):
+        raise DataTableException("Wrong type %s when expected datetime" %
+                                 t_value)
+      return "new Date(%d,%d,%d,%d,%d,%d)" % (value.year,
+                                              value.month - 1,  # To match JS
+                                              value.day,
+                                              value.hour,
+                                              value.minute,
+                                              value.second)
+    # If we got here, it means the given value_type was not one of the
+    # supported types.
+    raise DataTableException("Unsupported type %s" % value_type)
+
+  @staticmethod
+  def ColumnTypeParser(description):
+    """Parses a single column description. Internal helper method.
+
+    Args:
+      description: a column description in the possible formats:
+       'id'
+       ('id',)
+       ('id', 'type')
+       ('id', 'type', 'label')
+    Returns:
+      Dictionary with the following keys: id, label and type where:
+        - If label not given, it equals the id.
+        - If type not given, string is used by default.
+
+    Raises:
+      DataTableException: The column description did not match the RE.
+    """
+    if not description:
+      raise DataTableException("Description error: empty description given")
+
+    if not isinstance(description, (str, tuple)):
+      raise DataTableException("Description error: expected either string or "
+                               "tuple, got %s." % type(description))
+
+    if isinstance(description, str):
+      description = (description,)
+
+    # According to the tuple's length, we fill the keys
+    # We verify everything is of type string
+    for elem in description:
+      if not isinstance(elem, str):
+        raise DataTableException(("Description error: expected tuple of "
+                                  "strings, current element of type %s." %
+                                  type(elem)))
+    desc_dict = {"id": description[0],
+                 "label": description[0],
+                 "type": "string"}
+    if len(description) > 1:
+      desc_dict["type"] = description[1].lower()
+      if len(description) > 2:
+        desc_dict["label"] = description[2]
+        if len(description) > 3:
+          raise DataTableException("Description error: tuple of length > 3")
+    return desc_dict
+
+  @staticmethod
+  def TableDescriptionParser(table_description, depth=0):
+    """Parses the table_description object for internal use.
+
+    Parses the user-submitted table description into an internal format used
+    by the Python DataTable class. Returns the flat list of parsed columns.
+
+    Args:
+      table_description: A description of the table which should comply
+                         with one of the formats described below.
+      depth: Optional. The depth of the first level in the current description.
+             Used by recursive calls to this function.
+
+    Returns:
+      List of columns, where each column represented by a dictionary with the
+      keys: id, label, type, depth, container which means the following:
+      - id: the id of he column
+      - name: The name of the column
+      - type: The datatype of the elements in this column. Allowed types are
+              described in ColumnTypeParser().
+      - depth: The depth of this column in the table description
+      - container: 'dict', 'iter' or 'scalar' for parsing the format easily.
+      The returned description is flattened regardless of how it was given.
+
+    Raises:
+      DataTableException: Error in a column description or in the description
+                          structure.
+
+    Examples:
+      A column description can be of the following forms:
+       'id'
+       ('id',)
+       ('id', 'type')
+       ('id', 'type', 'label')
+       or as a dictionary:
+       'id': 'type'
+       'id': ('type',)
+       'id': ('type', 'label')
+      If the type is not specified, we treat it as string.
+      If no specific label is given, the label is simply the id.
+
+      input: [('a', 'date'), ('b', 'timeofday')]
+      output: [{'id': 'a', 'label': 'a', 'type': 'date',
+                'depth': 0, 'container': 'iter'},
+               {'id': 'b', 'label': 'b', 'type': 'timeofday',
+               'depth': 0, 'container': 'iter'}]
+
+      input: {'a': [('b', 'number'), ('c', 'string', 'column c')]}
+      output: [{'id': 'a', 'label': 'a', 'type': 'string',
+                'depth': 0, 'container': 'dict'},
+               {'id': 'b', 'label': 'b', 'type': 'number',
+                'depth': 1, 'container': 'iter'},
+               {'id': 'c', 'label': 'column c', 'type': 'string',
+                'depth': 1, 'container': 'iter'}]
+
+      input:  {('a', 'number', 'column a'): { 'b': 'number', 'c': 'string'}}
+      output: [{'id': 'a', 'label': 'column a', 'type': 'number',
+                'depth': 0, 'container': 'dict'},
+               {'id': 'b', 'label': 'b', 'type': 'number',
+                'depth': 1, 'container': 'dict'},
+               {'id': 'c', 'label': 'c', 'type': 'string',
+                'depth': 1, 'container': 'dict'}]
+
+      input: { ('w', 'string', 'word'): ('c', 'number', 'count') }
+      output: [{'id': 'w', 'label': 'word', 'type': 'string',
+                'depth': 0, 'container': 'dict'},
+               {'id': 'c', 'label': 'count', 'type': 'number',
+                'depth': 1, 'container': 'scalar'}]
+    """
+    # For the recursion step, we check for a scalar object (string or tuple)
+    if isinstance(table_description, (str, tuple)):
+      parsed_col = DataTable.ColumnTypeParser(table_description)
+      parsed_col["depth"] = depth
+      parsed_col["container"] = "scalar"
+      return [parsed_col]
+
+    # Since it is not scalar, table_description must be iterable.
+    if not hasattr(table_description, "__iter__"):
+      raise DataTableException("Expected an iterable object, got %s" %
+                               type(table_description))
+    if not isinstance(table_description, dict):
+      # We expects a non-dictionary iterable item.
+      columns = []
+      for desc in table_description:
+        parsed_col = DataTable.ColumnTypeParser(desc)
+        parsed_col["depth"] = depth
+        parsed_col["container"] = "iter"
+        columns.append(parsed_col)
+      if not columns:
+        raise DataTableException("Description iterable objects should not"
+                                 " be empty.")
+      return columns
+    # The other case is a dictionary
+    if not table_description:
+      raise DataTableException("Empty dictionaries are not allowed inside"
+                               " description")
+
+    # The number of keys in the dictionary separates between the two cases of
+    # more levels below or this is the most inner dictionary.
+    if len(table_description) != 1:
+      # This is the most inner dictionary. Parsing types.
+      columns = []
+      # We sort the items, equivalent to sort the keys since they are unique
+      for key, value in sorted(table_description.items()):
+        # We parse the column type as (key, type) or (key, type, label) using
+        # ColumnTypeParser.
+        if isinstance(value, tuple):
+          parsed_col = DataTable.ColumnTypeParser((key,) + value)
+        else:
+          parsed_col = DataTable.ColumnTypeParser((key, value))
+        parsed_col["depth"] = depth
+        parsed_col["container"] = "dict"
+        columns.append(parsed_col)
+      return columns
+    # This is an outer dictionary, must have at most one key.
+    parsed_col = DataTable.ColumnTypeParser(table_description.keys()[0])
+    parsed_col["depth"] = depth
+    parsed_col["container"] = "dict"
+    return ([parsed_col] +
+            DataTable.TableDescriptionParser(table_description.values()[0],
+                                             depth=depth + 1))
+
+  @property
+  def columns(self):
+    """Returns the parsed table description."""
+    return self.__columns
+
+  def NumberOfRows(self):
+    """Returns the number of rows in the current data stored in the table."""
+    return len(self.__data)
+
+  def LoadData(self, data):
+    """Loads new data to the data table, clearing existing data."""
+    self.__data = []
+    self.AppendData(data)
+
+  def AppendData(self, data):
+    """Appends new data to the table.
+
+    Data is appended in rows. Data must comply with
+    the table schema passed in to __init__(). See SingleValueToJS() for a list
+    of acceptable data types. See the class documentation for more information
+    and examples of schema and data values.
+
+    Args:
+      data: The row to add to the table. The data must conform to the table
+            description format.
+
+    Raises:
+      DataTableException: The data structure does not match the description.
+    """
+    # If the maximal depth is 0, we simply iterate over the data table
+    # lines and insert them using InnerLoadData. Otherwise, we simply
+    # let the InnerLoadData handle all the levels.
+    if not self.__columns[-1]["depth"]:
+      for line in data:
+        self._InnerAppendData({}, line, 0)
+    else:
+      self._InnerAppendData({}, data, 0)
+
+  def _InnerAppendData(self, prev_col_values, data, col_index):
+    """Inner function to assist LoadData."""
+    # We first check that col_index has not exceeded the columns size
+    if col_index >= len(self.__columns):
+      raise DataTableException("The data does not match description, too deep")
+
+    # Dealing with the scalar case, the data is the last value.
+    if self.__columns[col_index]["container"] == "scalar":
+      prev_col_values[self.__columns[col_index]["id"]] = data
+      self.__data.append(prev_col_values)
+      return
+
+    if self.__columns[col_index]["container"] == "iter":
+      if not hasattr(data, "__iter__") or isinstance(data, dict):
+        raise DataTableException("Expected iterable object, got %s" %
+                                 type(data))
+      # We only need to insert the rest of the columns
+      # If there are less items than expected, we only add what there is.
+      for value in data:
+        if col_index >= len(self.__columns):
+          raise DataTableException("Too many elements given in data")
+        prev_col_values[self.__columns[col_index]["id"]] = value
+        col_index += 1
+      self.__data.append(prev_col_values)
+      return
+
+    # We know the current level is a dictionary, we verify the type.
+    if not isinstance(data, dict):
+      raise DataTableException("Expected dictionary at current level, got %s" %
+                               type(data))
+    # We check if this is the last level
+    if self.__columns[col_index]["depth"] == self.__columns[-1]["depth"]:
+      # We need to add the keys in the dictionary as they are
+      for col in self.__columns[col_index:]:
+        if col["id"] in data:
+          prev_col_values[col["id"]] = data[col["id"]]
+      self.__data.append(prev_col_values)
+      return
+
+    # We have a dictionary in an inner depth level.
+    if not data.keys():
+      # In case this is an empty dictionary, we add a record with the columns
+      # filled only until this point.
+      self.__data.append(prev_col_values)
+    else:
+      for key in sorted(data):
+        col_values = dict(prev_col_values)
+        col_values[self.__columns[col_index]["id"]] = key
+        self._InnerAppendData(col_values, data[key], col_index + 1)
+
+  def _PreparedData(self, sort_keys=()):
+    """Prepares the data for enumeration - sorting it by sort_keys.
+
+    Args:
+      sort_keys: list of keys to sort by. Receives a single key to sort by, or
+                 a list of keys for secondary sort. Each key can be a name of a
+                 column, or a tuple containing the name of the column and the
+                 sort direction as second parameter ("asc" or "desc").
+                 For example: ("col1", "desc")
+
+    Returns:
+      The data sorted by the keys given.
+
+    Raises:
+      DataTableException: Sort direction not in 'asc' or 'desc'
+    """
+    if not sort_keys:
+      return self.__data
+
+    proper_sort_keys = []
+    if isinstance(sort_keys, str) or (
+        isinstance(sort_keys, tuple) and len(sort_keys) == 2 and
+        sort_keys[1].lower() in ["asc", "desc"]):
+      sort_keys = (sort_keys,)
+    for key in sort_keys:
+      if isinstance(key, str):
+        proper_sort_keys.append((key, 1))
+      elif (isinstance(key, (list, tuple)) and len(key) == 2 and
+            key[1].lower() in ("asc", "desc")):
+        proper_sort_keys.append((key[0], key[1].lower() == "asc" and 1 or -1))
+      else:
+        raise DataTableException("Expected tuple with second value: "
+                                 "'asc' or 'desc'")
+
+    def SortCmpFunc(row1, row2):
+      """cmp function for sorted. Compares by keys and 'asc'/'desc' keywords."""
+      for key, asc_mult in proper_sort_keys:
+        cmp_result = asc_mult * cmp(row1.get(key), row2.get(key))
+        if cmp_result:
+          return cmp_result
+      return 0
+
+    return sorted(self.__data, cmp=SortCmpFunc)
+
+  def ToJSCode(self, name, columns_order=None, order_by=()):
+    """Writes the data table as a JS code string.
+
+    This method writes a string of JS code that can be run to
+    generate a DataTable with the specified data. Typically used for debugging
+    only.
+
+    Args:
+      name: The name of the table. The name would be used as the DataTable's
+            variable name in the created JS code.
+      columns_order: Optional. Specifies the order of columns in the
+                     output table. Specify a list of all column IDs in the order
+                     in which you want the table created.
+      order_by: Optional. Specifies the name of the column(s) to sort by, and
+                (optionally) which direction to sort in. Default sort direction
+                is asc. Following formats are accepted:
+                "string_col_name"  -- For a single key in default (asc) order.
+                ("string_col_name", "asc|desc") -- For a single key.
+                [("col_1","asc|desc"), ("col_2","asc|desc")] -- For more than
+                    one column, an array of tuples of (col_name, "asc|desc").
+
+    Returns:
+      A string of JS code that, when run, generates a DataTable with the given
+      name and the data stored in the DataTable object.
+      Example result:
+        "var tab1 = new google.visualization.DataTable();
+         tab1.addColumn('string', 'a', 'a');
+         tab1.addColumn('number', 'b', 'b');
+         tab1.addColumn('boolean', 'c', 'c');
+         tab1.addRows(10);
+         tab1.setCell(0, 0, 'a');
+         tab1.setCell(0, 1, 1);
+         tab1.setCell(0, 2, true);
+         ...
+         tab1.setCell(9, 0, 'c');
+         tab1.setCell(9, 1, 3, '3$');
+         tab1.setCell(9, 2, false);"
+
+    Raises:
+      DataTableException: The data does not match the type.
+    """
+    if not columns_order:
+      columns_order = [col["id"] for col in self.__columns]
+    col_dict = dict([(col["id"], col) for col in self.__columns])
+
+    # We first create the table with the given name
+    jscode = "var %s = new google.visualization.DataTable();\n" % name
+
+    # We add the columns to the table
+    for col in columns_order:
+      jscode += "%s.addColumn('%s', '%s', '%s');\n" % (name,
+                                                       col_dict[col]["type"],
+                                                       col_dict[col]["label"],
+                                                       col_dict[col]["id"])
+    jscode += "%s.addRows(%d);\n" % (name, len(self.__data))
+
+    # We now go over the data and add each row
+    for (i, row) in enumerate(self._PreparedData(order_by)):
+      # We add all the elements of this row by their order
+      for (j, col) in enumerate(columns_order):
+        if col not in row or row[col] is None:
+          continue
+        value = self.SingleValueToJS(row[col], col_dict[col]["type"])
+        if isinstance(value, tuple):
+          # We have a formatted value as well
+          jscode += ("%s.setCell(%d, %d, %s, %s);\n" %
+                     (name, i, j, value[0], value[1]))
+        else:
+          jscode += "%s.setCell(%d, %d, %s);\n" % (name, i, j, value)
+    return jscode
+
+  def ToJSon(self, columns_order=None, order_by=()):
+    """Writes a JSON strong that can be used in a JS DataTable constructor.
+
+    This method writes a JSON string that can be passed directly into a Google
+    Visualization API DataTable constructor. Use this output if you are
+    hosting the visualization HTML on your site, and want to code the data
+    table in Python. Pass this string into the
+    google.visualization.DataTable constructor, e.g,:
+      ... on my page that hosts my visualization ...
+      google.setOnLoadCallback(drawTable);
+      function drawTable() {
+        var data = new google.visualization.DataTable(_my_JSon_string, 0.5);
+        myTable.draw(data);
+      }
+
+    Args:
+      columns_order: Optional. Specifies the order of columns in the
+                     output table. Specify a list of all column IDs in the order
+                     in which you want the table created.
+      order_by: Optional. Specifies the name of the column(s) to sort by, and
+                (optionally) which direction to sort in. Default sort direction
+                is asc. Following formats are accepted:
+                "string_col_name"  -- For a single key in default (asc) order.
+                ("string_col_name", "asc|desc") -- For a single key.
+                [("col_1","asc|desc"), ("col_2","asc|desc")] -- For more than
+                    one column, an array of tuples of (col_name, "asc|desc").
+
+    Returns:
+      A JSon constructor string to generate a JS DataTable with the data
+      stored in the DataTable object.
+      Example result (the result is without the newlines):
+       {cols: [{id:'a',label:'a',type:'number'},
+               {id:'b',label:'b',type:'string'},
+              {id:'c',label:'c',type:'number'}],
+        rows: [{c:[{v:1},{v:'z'},{v:2}]}, c:{[{v:3,f:'3$'},{v:'w'},{v:null}]}]}
+
+    Raises:
+      DataTableException: The data does not match the type.
+    """
+    if not columns_order:
+      columns_order = [col["id"] for col in self.__columns]
+    col_dict = dict([(col["id"], col) for col in self.__columns])
+
+    # Creating the columns jsons
+    cols_jsons = ["{id:'%(id)s',label:'%(label)s',type:'%(type)s'}" %
+                  col_dict[col_id] for col_id in columns_order]
+
+    # Creating the rows jsons
+    rows_jsons = []
+    for row in self._PreparedData(order_by):
+      cells_jsons = []
+      for col in columns_order:
+        # We omit the {v:null} for a None value of the not last column
+        value = row.get(col, None)
+        if value is None and col != columns_order[-1]:
+          cells_jsons.append("")
+        else:
+          value = self.SingleValueToJS(value, col_dict[col]["type"])
+          if isinstance(value, tuple):
+            # We have a formatted value as well
+            cells_jsons.append("{v:%s,f:%s}" % value)
+          else:
+            cells_jsons.append("{v:%s}" % value)
+      rows_jsons.append("{c:[%s]}" % ",".join(cells_jsons))
+
+    # We now join the columns jsons and the rows jsons
+    json = "{cols: [%s],rows: [%s]}" % (",".join(cols_jsons),
+                                        ",".join(rows_jsons))
+    return json
+
+  def ToJSonResponse(self, columns_order=None, order_by=(), req_id=0):
+    """Writes a table as a JSON response that can be returned as-is to a client.
+
+    This method writes a JSON response to return to a client in response to a
+    Google Visualization API query. This string can be processed by the calling
+    page, and is used to deliver a data table to a visualization hosted on
+    a different page.
+
+    Args:
+      columns_order: Optional. Passed straight to self.ToJSon().
+      order_by: Optional. Passed straight to self.ToJSon().
+      req_id: Optional. The response id, as retrieved by the request.
+
+    Returns:
+      A JSON response string to be received by JS the visualization Query
+      object. This response would be translated into a DataTable on the
+      client side.
+      Example result (newlines added for readability):
+       google.visualization.Query.setResponse({
+          'version':'0.5', 'reqId':'0', 'status':'OK',
+          'table': {cols: [...], rows: [...]}});
+
+    Note: The URL returning this string can be used as a data source by Google
+          Visualization Gadgets or from JS code.
+    """
+    table = self.ToJSon(columns_order, order_by)
+    return ("google.visualization.Query.setResponse({'version':'0.5', "
+            "'reqId':'%s', 'status':'OK', 'table': %s});") % (req_id, table)

File trac-dev/gviz/api.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""Trac Data Source able to feed widgets implemented with Google Visualization API.
+
+Components allowing Trac to export a project (environment)'s data so
+that different widgets based on Google Visualization (GViz) API
+be able to use that information in order to render a web GUI item.
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+from trac.core import Interface, Component, ExtensionPoint, implements, TracError
+from trac.config import Option
+from trac.env import IEnvironmentSetupParticipant
+from trac.util import get_pkginfo
+from trac.web.api import IRequestHandler, RequestDone
+from pkg_resources import resource_string, resource_filename
+from trac.web.chrome import ITemplateProvider
+
+try:
+    import gviz_api
+except ImportError:
+    import _gviz_api as gviz_api
+# from babel.support import Translations
+from os import listdir, environ
+from os.path import isdir
+import hashlib
+import sys
+
+class GVizProviderNotFound(Exception):
+    """Exception raised to denote that there is no GViz provider able
+    to handle the request.
+    """
+
+class GVizNotSupported(Exception):
+    """Exception raised to denote that an unsopported feature has been
+    requested by the client.
+    """
+
+class GVizBadRequest(Exception):
+    """Exception raised to denote that an unsopported feature has been
+    requested by the client.
+    """
+
+class GVizUnknownProvider(Exception):
+    """Exception raised to denote that there is no provider able to
+    to handle a given request.
+    """
+
+def gviz_col(col_id, col_doc):
+    """This function can be used to document the meaning of the 
+    different columns in the table returned by GViz data sources as 
+    well as the default label and column order. This function is 
+    compliant with annotations as defined by PEP 3107.
+    """
+    def decorator(func):
+        try:
+            anns = func.func_annotations
+        except AttributeError:
+            func.func_annotations = {'return' : \
+                    {col_id: col_doc}}
+        else:
+            anns.setdefault('return', {})[col_id] = col_doc
+        return func
+    return decorator
+
+class IGVizDataProvider(Interface):
+    """All the components providing GViz data tables to clients
+    ought to implement this interface.
+    """
+    def gviz_namespace():
+        """ Provide the path to access this data source provider.
+        This is used to determine the URL clients will retrieve
+        the data from.
+        
+        Return a list of items, each one being a string denoting
+        either a path handle or a regular expression used to 
+        match the requested path. Each such item *must not* match a
+        string containing the slash (i.e. `/`) character.
+        
+        Note : regex in path handles are not supported yet.
+        """
+    
+    def get_data_schema():
+        """Provide the schema used to populate GViz data tables out 
+        of the Python object containing the table contents.
+        
+        Return an iterator of (permission, schema). See the
+        documentation for `gviz_api.DataTable` class for more details
+        about the `scheme` field.
+        """
+    
+    def get_data(tq, **tqx):
+        """ Return a Python object containing all the data to be
+        returned to the client so as to fill a data table object.
+        
+        @param tq the query requested by the client.
+	    @param tqx custom arguments specified by the client. Every 
+	                standard parameter defined by the Visualization
+	                protocol (see the values in
+	                `TracGVizSystem.reserved_args`) is removed since
+	                they have special meanings no matter what data
+	                is provided by a specific GViz data source.
+        """
+
+class IGVizProtocolHandler(Interface):
+    """All components implementing a specific version of the Google
+    Visualization Protocol must implement this interfaces.
+    """
+    def get_version():
+        """ Return a tuple like (epoch, major[, minor]) representing
+        the protocol version implemented by the component.
+        """
+    
+    def get_std_params():
+        """Return a mapping object representing the parameters having
+        special meanings according to the protocol specification.
+        The mapping keys should be the name of the parameter, whereas
+        the mapping values should be the parameter's default value.
+        """
+    
+    def output_response(_req, _table, _error=None,
+                        _warnings=None, **std_params):
+        """Send the response back to the client.
+        
+        @param req an object encapsulating the data submitted by
+                    the client in the HTTP request.
+        @param table the instance of `gviz_api.DataTable` containing
+                    the data requested by the client.
+	    @param error an exception object raised while processing the
+	                request or `None` otherwise.
+	    @param warnings a sequence of warning messages returned to 
+	                the client.
+	    @param std_params the values bound to the standard parameters
+	                in the request string.
+        """
+
+class TracGVizSystem(Component):
+    """A component responsible for dispatching requests addressed to 
+    the different GViz data sources available in the system. The 
+    requests processed by the appropriate provider return a response 
+    back to the client conforming to the formats supported by Google 
+    Visualization API. By default it should return data in 
+    `JSON Response Format`, but this decision is delegated to the 
+    underlying protocol implementation.
+    """
+    implements(IRequestHandler, ITemplateProvider) # TODO : Add IPermissionRequestor
+    providers = ExtensionPoint(IGVizDataProvider)
+    handlers = ExtensionPoint(IGVizProtocolHandler)
+    
+    def __init__(self):
+        self._cache = dict(['/gviz/' + '/'.join(p.gviz_namespace()), p] \
+                        for p in self.providers)
+        self.log.debug('IG: Providers cache %s', (self._cache,))
+        self._handlers = dict( \
+                ['.'.join(str(x) for x in h.get_version()), h] \
+                for h in self.handlers)
+        try:
+            self._latest = max(h.get_version() for h in self.handlers)
+        except ValueError:
+            self._latest = None
+        else:
+            self._latest = '.'.join(str(x) for x in self._latest)
+    
+    # IRequestHandler methods
+    def match_request(self, req):
+        """Return whether the requested path starts with `/gviz`
+        prefix or not.
+        """
+        url_path = req.path_info
+        if url_path.startswith('/gviz'):
+            try:
+                return url_path[5] == '/'
+            except IndexError:
+                return True
+    
+    def process_request(self, req):
+        """Perform one of the following actions :
+        
+        - If the request is addressed to a specific data source then
+          send back the corresponding data table's contents in one of
+          the formats supported by Google Visualization API (defaults
+          to JSON Response Format).
+        
+        - If the request is addressed directly to the `root path` (i.e. 
+          req.path_info == '/gviz') then render an informative page
+          displaying all the registered data sources' specification.
+        
+        - Else send back a `Not Found` error message.
+        """
+        url_path = req.path_info
+        if url_path in ('/gviz', '/gviz/'):
+            return 'gviz_index.html', dict(), None
+        else:
+            if self._latest is None:
+                req.send_response(404)
+                req.end_headers()
+                req.write('Unable to find any protocol handler')
+                raise RequestDone()
+            try:
+                provider = self._cache[url_path]
+            except KeyError:
+                exc = GVizUnknownProvider('Unknown provider "%s"' %
+                                            (url_path,))
+                handler = self._handlers[self._latest]
+                handler.output_response(req, None, exc, None)
+                raise RequestDone()
+            else:
+                try:
+                    params = req.args['tqx']
+                    self.log.debug("IG: Plain params : %s" % (params,))
+                except KeyError:
+                    params = std_params = dict()
+                    handler = self._handlers[self._latest]
+                else:
+                    try:
+                        params = dict(i.split(':', 1) \
+                                for i in str(params).split(';'))
+                    except:
+                        exc = GVizBadRequest('Syntax error in "%s"' %
+                                            (params,))
+                        handler = self._handlers[self._latest]
+                        handler.output_response(req, None, exc, \
+                                                None, **std_params)
+                        raise RequestDone()
+                    else:
+                        version = params.get('version', self._latest)
+                        try:
+                            handler = self._handlers[version]
+                            # TODO: Provide better matching for protocol handlers
+                        except KeyError, exc:
+                            handler = self._handlers[self._latest]
+                            exc = GVizNotSupported('Unsupported protocol version '
+                                    '"%s"' % (exc.message,))
+                            handler.output_response(req, None, exc, \
+                                                    None, **std_params)
+                            raise RequestDone()
+                        defaults = handler.get_std_params()
+                        std_params = dict([k, params.pop(k, v)] \
+                                for k,v in defaults.iteritems())
+                self.log.debug("IG: Request parameters : %s" % (params,))
+                table = None
+                try:
+                    data = provider.get_data( \
+                            req.args.get('tq', None), **params)
+                    table = gviz_api.DataTable(provider.get_data_schema(), data)
+                    handler.output_response(req, table, None, \
+                                            None, **std_params)
+                except RequestDone:
+                    raise
+                except Exception, exc:
+                    handler.output_response(req, table, 
+                            sys.exc_info()[1:], exc, **std_params)
+                    raise RequestDone()
+                
+                raise RequestDone()
+    
+    # ITemplateProvider methods
+    def get_htdocs_dirs(self):
+        yield ('gviz', resource_filename('tracgviz', 'htdocs'))
+
+    def get_templates_dirs(self):
+        return [resource_filename('tracgviz', 'templates')]
+
+class IGVizTableEncoder(Interface):
+    """Implementing this interface is mandatory for every component
+    in charge of converting a table contents into a character
+    stream suitable for trasmitting it over an HTTP connection.
+    """
+    def supported_versions():
+        """Return a sequence containing tuples, each one specifying
+        the protocol versions supported by this format encoder. The
+        tuples should be as follows :
+        
+        (Relationship, VersionNumber)
+        
+        where :
+        
+        Relationship := '<' | '>' | '<=' | '>=' | '==' | '!='
+        VersionNumber := (Number ',' Number [',' Number])
+        
+        e.g. [('==', (0, 1, 2)),('==', (0, 1, 5)), 
+              ('>=', (0, 2)), ('!=', (0, 4)), ('<', (1, 0))]
+        """
+    
+    def stream_contents(table, params={}):
+        """Convert the table contents to a format suitable for
+        trasmitting them over an HTTP connection.
+        
+        @param table the target instance of `gviz_api.DataTable`.
+        @std_params the values of the standard parameters.
+        """
+    
+    def get_format_id():
+        """Return the token identifying the format outputted by this 
+        formatter object.
+        """
+    
+    def get_content_type():
+        """Return the content-type associated with this format 
+        encoder.
+        """
+
+class GVizDataNotModifiedError(Exception):
+    """Exception raised when the hash generated from the data
+    returned by the GViz provider matches the hash sent by the 
+    client.
+    """
+    message = ''
+
+
+# TODO : Implement the different data source providers
+
+# TODO : Cache the requests and optimize the call/response life cycle
+#            by using the `reqId` parameter.
+

File trac-dev/gviz/htdocs/img.jpg

Added
New image

File trac-dev/gviz/ig/__init__.py

+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""This package contains many components and tools useful to 
+integrate features of iGoogle gadgets with Trac.
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+from api import *
+from db import *
+from web_ui import *
+from wiki import *

File trac-dev/gviz/ig/api.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""Core components to integrate Trac with Google Gadgets technology
+(i.e. iGoogle container). 
+
+This package states how to enhance and extend the different features
+available in Trac Gadgets subsystem. The goal for all this 
+extensibility is to ease the process of embedding gadgets in Trac 
+site by providing easy-to-use wiki formatting constructs.
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+from trac.core import Interface, Component, ExtensionPoint, implements
+from trac.config import Option
+from trac.util import get_pkginfo
+from trac.web.api import IRequestHandler, RequestDone
+from trac.wiki.api import IWikiSyntaxProvider
+from pkg_resources import resource_string, resource_filename
+
+from genshi.builder import tag
+
+from os import listdir, environ
+from os.path import isdir
+
+class IGadgetBorderImgStore(Interface):
+    """The interface implemented by the components responsible for
+    storing the pictures to use in gadgets borders, and keep track of
+    the border sets.
+    
+    Images are stored in a directory inside the environment's 
+    `htdocs/gadget/border` folder. This dir name matchs the name 
+    chosen for the border id. Therefore the URL specified in the 
+    gadget are available at URLs like :
+    
+    - http://<server>/<env>/chrome/site/gadget/border/<border_id>/
+    
+    ... or using any of the following TracLinks syntax:
+    
+    - htdocs:gadget/border/<border_id>/
+    
+    - gadget:border:<border_id>
+    
+    The latest is the most portable way (and therefore is 
+    recommended) since images might be stored in a different
+    repository.
+    
+    Note: The final slash MUST always be there, except in 
+            TracLinks starting with `gadget:border` prefix.
+    """
+    def store_images(id, tl, tr, bl, br, l, r, b, tt, tn):
+        """Store the images in an image-based gadget border.
+        
+        @param id the border identifier
+        @param tl the contents of the top-left corner image
+        @param tr the contents of the top-right corner image
+        @param bl the contents of the bottom-left corner image
+        @param br the contents of the bottom-right corner image
+        @param l the contents of the left side image
+        @param r the contents of the right side image
+        @param b  the contents of the bottom image
+        @param tt the contents of the top level image (with title)
+        @param tn  the contents of the top level image (no title)
+        
+        Note: All the previous values (except `id`) may also be file 
+                objects from where contents will be retrieved.
+        """
+    def load_image(id, img_id):
+        """Retrieve the contents of a picture in an image-based 
+        gadget border.
+        
+        @param id the border identifier
+        @param img_id one of `tl`, `tr`, `bl`, `br`, `l`, `r`, `b`, 
+                    `tt`, `tn` so as to identify the requested image
+        @return a file-like object used to retrieve the image contents
+                    or `None` if such file does not exists
+        """
+
+BORDER_IMAGES = ['tl', 'tr', 'bl', 'br', 'l', 'r', 'b', 'tt', 'tn']
+
+class IGadgetBorderProvider(Interface):
+    """Interface implemented by all those components offering borders
+    gadgets will be surronded with. This includes both image-based 
+    and CSS-based borders.
+    """
+    def get_namespace():
+        """Specify the namespace identifying this borders provider.
+        
+        @return a string or sequence of strings
+        """
+    def get_border_spec(border_id):
+        """Retrieve the string to set the `border` parameter in the 
+        URL needed to paste the gadget into the HTML of the target 
+        web page.
+        
+        @param border_id a string identifying the target border
+        @return a string value formatted as explained for gatget 
+                borders in iGoogle Gadgets specification.
+                See code.google.com/apis/gadgets/docs/legacy/publish.html#Borders
+                for more details.
+        """
+    def get_description():
+        """Describe this border provider.
+        
+        @return an informative string describing this border provider.
+        """
+    def enum_borders():
+        """List all the borders managed by this particular 
+        border provider.
+        
+        @return a sequence containing all the available border ids
+        """
+
+class IGadgetTracLinksResolver(Interface):
+    """Interface implemented by those classes being
+    TracLinks providers registered under the `gadget` namespace.
+    """
+    def get_link_resolvers(self):
+        """Return an iterable over (namespace, formatter) tuples.
+        Namespaces value must be tuples.
+        
+        Each formatter should be a function of the form
+        fmt(formatter, ns, target, label), and should
+        return some HTML fragment.
+        The `label` is already HTML escaped, whereas the `target` is not.
+        """
+
+class GadgetBordersManager(Component):
+    """Component responsible for managing gadget borders.
+    """
+    bprov = ExtensionPoint(IGadgetBorderProvider)
+    
+    # TODO : Implement
+    def __init__(self):
+        self.providers = dict([p.get_namespace(), p] for p in self.bprov)
+    
+

File trac-dev/gviz/ig/db.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""This module contains some Trac components so as to enhance Trac 
+users experience, and to ease some common tasks related to gadgets 
+and embedding them in wiki pages.
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+from trac.core import Interface, Component, ExtensionPoint, implements
+from trac.config import Option
+from trac.util import get_pkginfo
+from trac.web.api import IRequestHandler, RequestDone
+from pkg_resources import resource_string, resource_filename
+from trac.env import IEnvironmentSetupParticipant
+from trac.perm import IPermissionRequestor
+from trac.web.api import IRequestHandler
+from trac.web.chrome import ITemplateProvider, add_stylesheet
+
+from os.path import join, exists, isabs
+from os import makedirs, listdir
+import types
+from shutil import copyfileobj
+
+from api import IGadgetBorderImgStore, BORDER_IMAGES
+
+class DefaultBordersRepository(Component):
+    """A repository that stores gadget border images under the 
+    environment's `htdocs/gadgets/borders` folder.
+    
+    Images are stored in a directory inside the environment's 
+    `htdocs/gadget/border` folder. This dir name matchs the name 
+    chosen for the border id. Therefore the URL specified in the 
+    gadget are available at URLs like :
+    
+    - http://<server>/<env>/chrome/site/gadget/border/<border_id>/
+    
+    ... or using any of the following TracLinks syntax:
+    
+    - htdocs:gadget/border/<border_id>/ (... in case )
+    
+    - gadget:border:<border_id>
+    
+    Note: The final slash MUST always be there, except in 
+            TracLinks starting with `gadget:border` prefix.
+    """
+    implements(IEnvironmentSetupParticipant, IGadgetBorderImgStore)
+    cfg_path = Option('gadgets', 'border_img_path', \
+                        default=None, \
+                        doc="The path where border images are stored."
+                            " Defaults to the environment's "
+                            "`htdocs/gadget/border` folder.")
+    
+    def __init__(self):
+        self.log.info("IG: Configured path for gadget borders %s",\
+                        repr(self.cfg_path))
+        if not self.cfg_path:
+            self.path = join(self.env.get_htdocs_dir(), 'gadget', 'border')
+        elif not isabs(self.cfg_path):
+            self.path = join(self.env.path, self.cfg_path)
+        else:
+            self.path = self.cfg_path
+        self.log.info("IG: Image repository for gadget borders in %s",\
+                        self.path)
+    
+    # IEnvironmentSetupParticipant methods
+    def environment_created(self):
+        """Create `htdocs/gadget/border` folder.
+        """
+        self.log.info('IG: Creating gadget borders folder in %s', self.path)
+        makedirs(self.path)
+    
+    def environment_needs_upgrade(self, db):
+        """Create `htdocs/gadget/border` folder if not already there.
+        """
+        if not exists(self.path):
+            self.log.info('IG: Creating gadget borders folder in %s', \
+                            self.path)
+            makedirs(self.path)
+        return False
+    
+    def upgrade_environment(self, db): pass
+    
+    # IGadgetBordersStore methods
+    def _image_file_name(self, id, img_id):
+        return join(self.path, id, img_id + '.gif')
+    
+    def store_images(self, id, **img_data):
+        """Store gadget border images under the environment's 
+        `htdocs/gadgets/borders` folder.
+        """
+        path = join(self.path, id)
+        if not exists(path):
+            makedirs(path)
+        self.log.debug('IG: About to write image files for %s', id)
+        # self.log.debug('IG: Image data %s', img_data)
+        for img_id, contents in img_data.iteritems():
+            if img_id in BORDER_IMAGES and contents:
+                filename = self._image_file_name(id, img_id)
+                self.log.debug('IG: Writing image file %s', filename)
+                fo = file(filename, 'wb')
+                if isinstance(contents, types.StringTypes):
+                    fo.write(contents)
+                else:
+                    copyfileobj(contents, fo)
+                fo.close()
+        
+    def load_image(self, id, img_id):
+        """Retrieve the contents of a given picture in an image-based 
+        gadget border.
+        """
+        fnm = self._image_file_name(id, img_id)
+        if exists(fnm):
+            try:
+                return open(fnm, 'rb')
+            except:
+                self.log.exception('IG: Error reading image file %s', fnm)
+                return None
+        else:
+            return None
+    
+    def enum_img_borders(self):
+        """Return the name of the folders containing images.
+        """
+        return listdir(self.path)

File trac-dev/gviz/ig/htdocs/icon_home1.png

Added
New image

File trac-dev/gviz/ig/templates/ig_border_edit.html

+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+  <xi:include href="layout.html" />
+  <head>
+    <title>Upload images for gadget borders :</title>
+  </head>
+
+  <body>
+    <div id="content" class="wiki">
+
+      <h1>Create an image-based gadget border </h1>
+
+      <img src="../../../chrome/ig/icon_home1.png" style="float:left;margin-right:20px;"/>
+      <div id="description" xml:space="preserve">
+        Select the images you want to show around
+        <a class="ext-link" href="http://www.google.com/ig/">
+          <span class="icon">iGoogle</span>
+        </a>
+        <a class="ext-link" href="http://code.google.com/apis/gadgets/">
+          <span class="icon">Gadgets</span>
+        </a>
+        in your pages once this border is specified.
+      </div>
+
+      <div class="buttons" style="float:clear;">
+        <form class="mod" action="" method="post" enctype="multipart/form-data" >
+          <input type="hidden" name="action" value="new" />
+          <fieldset>
+            <legend>Border properties</legend>
+            <div class="field">
+              <label>Specify a name to identify this border:
+                <input type="text" id="id" name="id" size="40" value="${fields and fields['id']}"/> <br />
+              </label>
+            </div>
+            <hr />
+            <h3>Select images :</h3>
+            <div style="text-align: center">
+              <div py:for="i, img_spec in enumerate([{'fnm' : 'b', 'w':1, 'h':28, 'loc':_('bottom')},
+                    {'fnm' : 'tt', 'w':1, 'h':28, 'loc':_('top (with title)')},
+                    {'fnm' : 'tn', 'w':1, 'h':12, 'loc':_('top (no title)')},
+                    {'fnm' : 'l', 'w':8, 'h':1, 'loc':_('left side')},
+                    {'fnm' : 'r', 'w':8, 'h':1, 'loc':_('right side')},
+                    {'fnm' : 'bl', 'w':8, 'h':28, 'loc':_('bottom-left corner')},
+                    {'fnm' : 'br', 'w':8, 'h':28, 'loc':_('bottom-right corner')},
+                    {'fnm' : 'tl', 'w':8, 'h':28, 'loc':_('top-left corner')},
+                    {'fnm' : 'tr', 'w':8, 'h':28, 'loc':_('top-right corner')}])"
+                    class="field" style="float:left; margin-right: 10px;text-align: left;">
+                <label>
+                  Image at the ${img_spec['loc']}: <br />
+                  <input type="file" id="${img_spec['fnm']}" name="${img_spec['fnm']}" />
+                </label>
+              </div>
+            </div>
+          </fieldset>
+          <div class="buttons">
+            <input type="submit" value="Upload" accesskey="u" />
+          </div>
+        </form>
+      </div>
+    </div>
+  </body>
+</html>

File trac-dev/gviz/ig/web_ui.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""This module contains some Trac components so as to enhance Trac 
+users experience, and to ease some common tasks related to gadgets 
+and embedding them in wiki pages.
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+from trac.core import Interface, Component, ExtensionPoint, \
+        implements, TracError
+from trac.config import Option