# HG changeset patch # User Jeff Hammel # Date 1399824935 25200 # Node ID b0942f44413f14d7785075e3fade9b5495dba363 import from git://github.com/mozilla/toolbox.git diff -r 000000000000 -r b0942f44413f ABOUT.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ABOUT.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,103 @@ +toolbox +======= + +*an index of Mozilla software tools* + + +The Story of Toolbox +-------------------- + +Tools are only useful if you know where they exists and can find them. +Toolbox is an index of tools developed by and +for the Mozilla community. Toolbox is not a hosting service -- it is a +listing of tools which can live anywhere that are of use to Mozillians. + +It can also be used to track: + +* smart bookmarks +* code snippets +* webapps + +Each tool in the listing must provide the following attributes: + +* a *name* that uniquely identifies the tool +* a text *description* of the tool +* a canonical *URL* where you can find the tool + +Toolbox also tracks several optional classifiers for each tool in its +database. The classifiers are described below. + + +How to use Toolbox +------------------ + +The `index page <./>`_ of toolbox lists all tools with the most +recently updated first. All fields on a tool are clickable. Clicking on the +description lets you edit the description which will be saved after +you finish editing it. Hovering over the tool title or URL will display an +`edit button `_ which on clicking +will allow you to edit the appropriate data. +Clicking a URL, like `?author=harth <./?author=harth>`_ will give +you the tools that ``harth`` wrote. There is also full text search +using the ``?q=`` parameter (like `?q=firefox <./?q=firefox>`_ ) which +will search both the descriptions and all of the fields. + +You can also display results by a particular field by going to that +field name. For example, to display tools by author, go to +`/author `_ . You can add a new tool at +`/new `_ by providing its name, description, and URL. Upon +creation, you'll be redirected to the tool's index page where you can +add whatever classifiers you want. + + +Classifiers +----------- + +Outside of the required fields (name, description, and URL), a tool +has a number of classifier tags. These fields are: + +* usage: what the tool is for +* type: is the tool a particular kind of software such as an addon or a script? +* language: which programming language the tool is written in +* author: who wrote and/or maintains the software? + +You can freely add and remove classifiers for each project. +Autocomplete is enabled to help you find the classifier you want. + + +TODO +---- + +There is much more that we plan to add to Toolbox. The project source +code may be found at https://github.com/mozilla/toolbox . + +* add scrapers for hosted tools to automatically seed toolbox with data +* integrate author with community phonebook and bugzilla id +* the first time someone edits a description (etc.) from a pointed-to + file (e.g. a setup.py) then the project should be notified +* you should be able to edit a field, e.g. author. Changing one field + value should give the option to change all similar field values. +* a "Request a tool" link that functions like stack overflow; users + can request a tool. If it does not exist, it gets turned into a + bug. Users should also be able to point to a tool to answer the + question. Similarly, developers should be able to see a list of + requested tools and take ownership of them if desired +* Similarly, users should be able to note similarity of tools and + propose a consolidation strategy + +Oustanding issues are listed at +https://bugzilla.mozilla.org/buglist.cgi?resolution=---&component=Toolbox&product=Webtools +. Please file new bugs or feature requests at +https://bugzilla.mozilla.org/enter_bug.cgi?product=Webtools&component=Toolbox +or contact jhammel@mozilla.org or discuss in #ateam at irc.mozilla.org. + + +Other Resources +--------------- + +Mozilla tools are recorded on other sites too. + +* http://www.mozdev.org/ +* https://wiki.mozilla.org/User:Jprosevear/Tools +* http://infomonkey.cdleary.com/ +* http://userscripts.org/ diff -r 000000000000 -r b0942f44413f INSTALL.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/INSTALL.sh Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,14 @@ +#!/bin/bash + +# Installs toolbox in a virtualenv +VIRTUALENV=$(which virtualenv) +${VIRTUALENV} toolbox +cd toolbox +. bin/activate +mkdir src +git clone git://github.com/mozilla/toolbox.git +cd toolbox +python setup.py develop +# now run: +# paster serve paste.ini +# from the toolbox virtualenv to serve the sample app \ No newline at end of file diff -r 000000000000 -r b0942f44413f README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,343 @@ +The Story of Toolbox +==================== + +Toolbox is fundamentally a document-oriented approach to resource +indexing. A "tool" consists three mandatory string fields -- name, +description, and URL -- that are generic to the large class of problems +of web resources, as well as classifiers, such as author, usage, type, +etc. A tool may have an arbitrary number of classifier fields as +needed. Each classifier consists of a set of values with which a tool +is tagged. This gives toolbox the flexibility to fit a large number of +data models, such as PYPI, DOAP, and others. + + +Running Toolbox +--------------- + +You can download and run the toolbox software yourself: +http://github.com/k0s/toolbox + +To serve in baseline mode, install the software and run:: + + paster serve paste.ini + +This will serve the handlers and static content using the paste +(http://pythonpaste.org) webserver using ``README.txt`` as the +``/about`` page and serving the data in ``sample``. + +The dispatcher (``toolbox.dispatcher:Dispatcher``) is the central (WSGI) +webapp that designates per-request to a number of handlers (from +``handlers.py``). The dispatcher has a few options: + +* about: path to a restructured text file to serve at ``/about`` +* model_type: name of the backend to use (memory_cache, file_cache, or couch) +* template_dir: extra directory to look for templates + +These may be configured in the ``paste.ini`` file in the +``[app:toolbox]`` section by prepending with the namespace +``toolbox.``. It is advisable that you copy the example ``paste.ini`` +file for your own usage needs. Additional ``toolbox.``-namespaced +arguments will be passed to the model. For instance, to specify the +directory for the ``file_cache`` model, the provided ``paste.ini`` uses +``toolbox.directory = %(here)s/sample``. + + +Architecture +------------ + +Toolbox uses a fairly simple architecture with a single abstract data +model allowing an arbitrary number of implementations to be constructed:: + + Interfaces Implementations + + +----+ +-+-----+ + |HTTP| | |files| + +----+---\ +-----+ | +-----+ + |-|model|-+-+-----+ + +------+-/ +-----+ | |couch| + |script| | +-----+ + +------+ +-+------+ + | |memory| + | +------+ + +-+---+ + |...| + +---+ + +Toolbox was originally intended to use a directory of files, one per project, +as the backend. These were originally intended to be HTML files as the +above model may be clearly mapped as HTML:: + +

{{name}}

+

{{description}}

+ {{for field in fields}} +
    + {{for value in values[field]}} +
  • {{value}}
  • + {{endfor}} + {{endfor}} +
+ +This microformat approach allows not only easy editing of the HTML +documents, but the documents may be indepently served and displayed +without the toolbox server-side. + +The HTML microformat was never implemented (though, since the model +backend is pluggable, it easily could be). Instead, the original +implementation used JSON blobs stored in one file per tool. This +approach loses the displayable aspect, though since JSON is a defined +format with several good tools for exploring and manipulating the data +perhaps this disavantage is offset. + +A couch backend was also written. + + +------------+-----------+------------+ + |Displayable?|File-based?|Concurrency?| ++-----+------------+-----------+------------+ +|HTML |Yes |Yes |No | ++-----+------------+-----------+------------+ +|JSON |Not really |Yes |No | ++-----+------------+-----------+------------+ +|Couch|No |No |Yes? | ++-----+------------+-----------+------------+ + +The concurrency issue with file-based documennt backends may be +overcome by using locked files. Ideally, this is accomplished at the +filesystem level. If your filesystem does not promote this +functionality, it may be introduced programmatically. A rough cartoon +of a good implementation is as follows: + +1. A worker thread is spawned to write the data asynchronously. The +data is sent to the worker thread. + +2. The worker checks for the presence of a lockfile (herein further +detailed). If the lockfile exists and is owned by an active process, +the worker waits until said process is done with it. (For a more +robust implementation, the worker sends a request to write the file to +some controller.) + +3. The worker owns a lockfile based on its PID in some directory +parallel to the directory root under consideration (for example, +``/tmp/toolbox/lock/${PID}-${filename}.lck``). + +4. The worker writes to the file. + +5. The worker removes the lock + +The toolbox web service uses a dispatcher->handler framework. The +handlers are loosely pluggable (they are assigned in the dispatcher), +but could (and probably should) be made completely pluggable. That +said, the toolbox web system features an integration of templates, +static resources (javascript, css, images), and handlers, so true +pluggability is further away than just supporting pluggable handlers +in the dispatcher. + +Deployment, however, may be tailored as desired. Any of the given +templates may be overridden via passing a ``template_dir`` parameter +with a path to a directory that have templates of the appropriate +names as found in toolbox's ``templates`` directory. + +Likewise, the static files (css, js, etc.) are served using ``paste``'s +``StaticURLParser`` out of toolbox's ``static`` directory. (See +toolbox's ``factory.py``.) Notably this is *not* done using the WSGI +app itself. Doing it with middleware allows the deployment to be +customizable by writing your own factory. For example, instead of +using the ``paste`` webserver and the included ``paste.ini``, you +could use nginx or apache and ``mod_wsgi`` with a factory file +invoking ``Dispatcher`` with the desired arguments and serving the +static files with an arbitrary static file server. + +It is common sense, if rarely followed, that deployment should be +simple. If you want to get toolbox running on your desktop and/or for +testing, you should be able to do this easily (see the ``INSTALL.sh`` +for a simple installation using ``bash``; you'll probably want to +perform these steps by hand for any sort of real-world deployment). +If you want a highly customized deployment, then this will require +more expertise and manual setup. + +The template data and the JSON are closely tied together. This has the +distinct advantage of avoiding data translation steps and avoiding +code duplication. + +Toolbox uses several light-footprint libraries: + +* webob for Request/Response handling: http://pythonpaste.org/webob/ + +* tempita for (HTML) templates: http://pythonpaste.org/tempita/ + +* whoosh for search. This pure-python implementation of full-text + search is relatively fast (for python) and should scale decently to + the target scale of toolbox (1000s or 10000s of tools). While not as + fast as lucene, whoosh is easy to deploy and has a good API and + preserves toolbox as a deployable software product versus an + instance that requires the expert configuration, maintainence, and + tuning of several disparate software products that is both + non-automatable (cannot be installed with a script) and + time-consuming. http://packages.python.org/Whoosh/ + +* jQuery: jQuery is the best JavaScript library and everyone + should use it. http://jquery.com/ + +* jeditable for AJAXy editing: http://www.appelsiini.net/projects/jeditable + +* jquery-token for autocomplete: http://loopj.com/jquery-tokeninput/ + +* less for dynamic stylesheets: http://lesscss.org/ + + +User Interaction +---------------- + +A user will typically interact with Toolbox through the AJAX web +interface. The server side returns relatively simple (HTML) markup, +but structured in such a way that JavaScript may be utilized to +promote rich interaction. The simple HTML + complex JS manifests +several things: + +1. The document is a document. The tools HTML presented to the user (with +the current objectionable exception of the per-project Delete button) +is a document form of the data. It can be clearly and easily +translated to data (for e.g. import/export) or simply marked up using +(e.g.) JS to add functionality. By keeping concerns seperate +(presentation layer vs. interaction layer) a self-evident clarity is +maintained. + +2. Computation is shifted client-side. Often, an otherwise lightweight +webapp loses considerable performance rendering complex templates. By +keeping the templates light-weight and doing control presentation and +handling in JS, high performance is preserved. + + +What Toolbox Doesn't Do +----------------------- + +* versioning: toolbox exposes editing towards a canonical document. + It doesn't do versioning. A model instance may do whatever + versioning it desires, and since the models are pluggable, it would + be relatively painless to subclass e.g. the file-based model and + have a post-save hook which does an e.g. ``hg commit``. Customized + templates could be used to display this information. + +* authentication: the information presented by toolbox is freely + readable and editable. This is by intention, as by going to a "wiki" + model and presenting a easy to use, context-switching-free interface + curation is encouraged (ignoring the possibly imaginary problem of + wiki-spam). Access-level auth could be implemented using WSGI + middleware (e.g. repoze.who or bitsyauth) or through a front end + "webserver" integration layer such as Apache or nginx. Finer grained + control of the presentation layer could be realized by using custom + templates. + + +What Toolbox Would Like To Do +----------------------------- + +Ultimately, toolbox should be as federated as possible. The basic +architecture of toolbox as a web service + supporting scripts makes +this feasible and more self-contained than most proposed federated +services. The basic federated model has proved, in practice, +difficult to achieve through purely the (HTTP) client-server model, as +without complete federation and adherence to protocol offline cron +jobs should be utilized to pull external data sources. If a webservice +only desires to talk to others of its own type and are willing to keep +a queue of requests for when hosts are offline, entire HTTP federation +may be implemented with only a configuration-specified discovery +service to find the nodes. + + +Evolution +--------- + +Often, a piece software is presented as a state out of context (that +is minus the evolution which led it to be and led it to look further +out towards beyond the horizon). While this is an interesting special +effect for an art project, software being communication this +is only conducive to software in the darkest of black-box approaches. + +"Beers are like web frameworks: if they're not micro, you don't know +what you're talking about." - hipsterhacker + +For sites that fit the architecture of a given framework, it may be +advisable to make use of them. However, for most webapp/webservice +categories which have a finite scope and definitive intent, it is +often easier, more maintainable, and more legible to build a complete +HTTP->WSGI->app architecture than to try to hammer a framework into +fitting your problem or redefining the problem to fit the framework. +This approach was used for toolbox. + +The GenshiView template, http://k0s.org/hg/GenshiView, was invoked to +generate a basic dispatcher->handler system. The cruft was removed, +leaving only the basic structure and the TempitaHandler since tempita +is lightweight and it was envisioned that filesystem tempita templates +(MakeItSo!) would be used elsewhere in the project. The basic +handlers (projects views, field-sorted view, new, etc.) were written +and soon a usable interface was constructed. + +A ``sample`` directory was created to hold the JSON blobs. Because +this was done early on, two goals were achieved: + +1. the software could be dogfooded immediately using actual applicable +data. This helped expose a number of issues concerning the data format +right away. + +2. There was a place to put tools before the project reached a +deployable state (previously, a few had lived in a static state using +a rough sketch of the HTML microformat discussed above on +k0s.org). Since the main point of toolbox is to record Mozilla tools, +the wealth of references mentioned in passing could be put somewhere, +instead of passed by and forgotten. One wishes that they do not miss +the train while purchasing a ticket. + +The original intent, when the file-based JSON blob approach was to be +the deployed backend, was to have two repositories: one for the code +and one for the JSON blobs. When this approach was scrapped, the +file-based JSON blobs were relegated to the ``sample`` directory, with +the intent to be to import them into e.g. a couch database on actual +deployment (using an import script). The samples could then be used +for testing. + +The model has a single "setter" function, ``def update``, used for +both creating and updating projects. Due to this and due to the fact +the model was ABC/pluggable from the beginning, a converter ``export`` +function could be trivially written at the ABC-level:: + + def export(self, other): + """export the current model to another model instance""" + for project in self.get(): + other.update(project) + +This with an accompanying CLI utility was used to migrate from JSON +blob files in the ``sample`` directory to the couch instance. This +particular methodology as applied to an unexpected problem (the +unanticipated switch from JSON blobs to couch) is a good example of +the power of using a problem to drive the software forward (in this +case, creation of a universal export function and associated command +line utility). The alternative, a one-off manual data migration, would +have been just as time consuming, would not be repeatable, would not +have extended toolbox, and may have (like many one-offs do) infected +the code base with associated semi-permanant vestiges. In general, +problems should be used to drive innovation. This can only be done if +the software is kept in a reasonably good state. Otherwise +considerable (though probably worthwhile) refactoring should be done +prior to feature extension which will become cost-prohibitive in +time-critical situations where a one-off is (more) likely to be employed. + + +Use Cases +--------- + +The target use-case is software tools for Mozilla, or, more generally, +a software index. For this case, the default fields uses are given in +the paste.ini file: usage, author, type, language. More fields may be +added to the running instance in the future. + +However, the classifier classification can be used for a wide variety +of web-locatable resources. A few examples: + +* songs: artist, album, genre, instruments +* de.li.cio.us: type, media, author, site + + +Resources +--------- + +* http://readthedocs.org/ diff -r 000000000000 -r b0942f44413f TODO.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/TODO.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,112 @@ +TODO: "plain text" base format +------------------------------ + +Add an import/export model a la +http://k0s.org/portfolio/python/python-tools.txt; example: + +""" +* https://pypi.python.org/pypi/bidict + bidirectional (one-to-one) mapping data structure + {data structure} + +* https://pypi.python.org/pypi/etherpad_lite + Python interface for Etherpad-Lite's HTTP API + https://github.com/devjones/PyEtherpadLite + +* https://pypi.python.org/pypi/gitdb + git object database + {storage, versioning} +""" + + +TODO: Tool Sources +------------------ + +In addition to manually indexed tools, toolbox is intended to harvest +index data from distributed sources. Several scrapers should be +written and run on a scheduled basis (i.e. with a cron job or +preferably something that could actually be reliably depended on and +automatable via python). Useful project sources are: + +* setup.py for python projects +* addons.mozilla.org pages +* OpenWebApps: https://developer.mozilla.org/en/OpenWebApps/The_Manifest +* userscripts: e.g. https://www.squarefree.com/userscripts/tidybox.user.js + + +TODO: (Alternate) Links +----------------------- + +Currently, each tool has one canonical URL. +Since toolbox is an index, this has the distinct advantage of +associating a single URL with the project. It is assumed that the +linked-to resource should point to auxilliary resources as necessary. + +However, as an index is useful for correlating information -- +connecting the dots -- allowing a variety of links both allows the +browser to have information at their fingertips, but also to allow +mapping and intelligent manipulation of tools by their link types. +Several types of links may be recorded: + +* repository +* how to report bugs +* wiki +* pypi + + +TODO: Directory Structure +------------------------- + +Each function should live in its own module:: + + . + +-README.txt + +-ABOUT.txt + +-INSTALL.sh + +-setup.py + +-paste.ini + | + toolbox + | + +-web.py + +-factory.py + +-json.py + | + handler + || + |... + | + model + || + |... + | + static + || + |... + | + templates + + +URLs +---- + +A more RESTful proposed URL schema: + +/{{project}} +* PUT: replace the project +* GET: return the project +* POST: update the project +* DELETE: remove the project + +/{{project}}/{{field}} +* PUT: replace all field values +* POST: for lists, add field values +* GET: return field value(s) + +/{{project}}/{{field}}/{{value}} +* DELETE: remove value from a list field + +/{{field}} +* POST: rename a field value: /{{field}}?jahml=jhammel +* should also take a description + diff -r 000000000000 -r b0942f44413f paste.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/paste.ini Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,31 @@ +#!/usr/bin/env paster + +# sample config file for the paste webserver + +[DEFAULT] +debug = true +email_to = jhammel@mozilla.com +smtp_server = localhost +error_email_from = paste@localhost + +[exe] +command = serve + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 8080 + +[composite:main] +use = egg:Paste#urlmap +/ = toolbox + +set debug = false + +[app:toolbox] +paste.app_factory = toolbox.factory:paste_factory +toolbox.about = %(here)s/ABOUT.txt +toolbox.directory = %(here)s/sample +toolbox.fields = usage, author, type, language +toolbox.model_type = toolbox.model:FileCache +toolbox.reload = false \ No newline at end of file diff -r 000000000000 -r b0942f44413f plugin-model.gv.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugin-model.gv.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,14 @@ +#!/usr/bin/fdp -Tpng + +graph architecture { + rankdir=LR; + dispatcher -- templates; + dispatcher -- about; + dispatcher -- model; + model -- model_backends [label="plugin"]; + dispatcher -- handlers [label="plugin"]; + templates [shape=folder]; + about [shape=note]; + model_backends [shape=record label="memory_cache|file_cache|couch" rankdir=TB]; + handlers [shape=record label="handlers|...|...|"]; +} diff -r 000000000000 -r b0942f44413f relocator.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/relocator.ini Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,32 @@ +#!/usr/bin/env paster + +# sample config file for toolbox mounted at /toolbox + +[DEFAULT] +debug = true +email_to = jhammel@mozilla.com +smtp_server = localhost +error_email_from = paste@localhost + +[exe] +command = serve + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 8080 + +[composite:main] +use = egg:Paste#urlmap +/ = toolbox + +set debug = false + +[app:toolbox] +paste.app_factory = toolbox.factory:relocator_factory +toolbox.about = %(here)s/ABOUT.txt +toolbox.directory = %(here)s/sample +toolbox.fields = usage, author, type, language +toolbox.model_type = toolbox.model:FileCache +toolbox.reload = false +baseurl = http://127.0.0.1/toolbox diff -r 000000000000 -r b0942f44413f sample/Bugzilla_Helper.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/Bugzilla_Helper.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Quickly reply to your bugmail on bugzilla.mozilla.org without leaving Thunderbird", "url": "https://addons.mozilla.org/en-US/thunderbird/addon/bugzilla-helper/", "modified": 1298415219.4090421, "usage": ["bugzilla", "thunderbird"], "type": ["addon"], "name": "Bugzilla Helper"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/CrossWeave.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/CrossWeave.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "CrossWeave populates Firefox profiles based on data in a file in a YAML-like format. Right now it populates temporary profiles which are deleted after CrossWeave completes, but it would be easy to allow it to allow it to populate data in an existing profile.", "author": ["jgriffin"], "url": "https://wiki.mozilla.org/Auto-tools/Projects/CrossWeave", "modified": 1298943796.7334161, "usage": ["profiles"], "name": "CrossWeave"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/GetLatestTinderbox.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/GetLatestTinderbox.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "GetLatestTinderbox", "language": ["python"], "author": ["jhammel"], "url": "http://hg.mozilla.org/automation/getlatest-tinderbox/", "modified": 1298581466.0599027, "description": "gets the latest builds and logs from staging.mozilla.org"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/JetCrawl.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/JetCrawl.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "make history and bookmarks data more realistic for new profiles with this jetpack", "author": ["ddahl"], "url": "http://daviddahl.blogspot.com/2010/01/jetcrawl.html", "modified": 1298917305.2201991, "usage": ["profiles"], "type": ["jetpack"], "name": "JetCrawl"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/Mozuuid.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/Mozuuid.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "Mozuuid", "language": ["perl"], "author": ["smaug"], "url": "http://mozilla.pettay.fi/cgi-bin/mozuuid.pl", "modified": 1302953422.0, "usage": ["uuid"], "description": "Generates a new UUID, both in .idl and .h format"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/NightlyTesterTools.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/NightlyTesterTools.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Nightly Tester Tools is an addon for aiding testers of nightly builds of Mozilla apps like Firefox and Thunderbird.", "author": ["mossop", "harth"], "url": "https://addons.mozilla.org/en-US/firefox/addon/6543/", "modified": 1301358703.815768, "usage": ["nightlies"], "type": ["addon"], "name": "Nightly Tester Tools"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/ProfileManager.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/ProfileManager.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Manage profiles for Firefox and other XULRunner applications", "author": ["jgriffin", "jhammel"], "url": "http://hg.mozilla.org/automation/profilemanager/", "modified": 1298418052.5133755, "usage": ["profiles"], "name": "ProfileManager"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/Self-serve_in_bulk.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/Self-serve_in_bulk.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "Self-serve in bulk", "language": ["python"], "author": ["jdm"], "url": "http://www.joshmatthews.net/blog/2011/03/self-serve-now-in-bulk/", "modified": 1304783745.91575, "description": "Allows you to cancel all builds for a push"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/TBPL_Commit_Message_Copier.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/TBPL_Commit_Message_Copier.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "This addon lets you copy pre-formatted commit messages to your clipboard right from TBPL.", "author": ["KWierso"], "url": "https://addons.mozilla.org/en-US/firefox/addon/269586/", "modified": 1298421199.7653754, "usage": ["tbpl"], "type": ["addon"], "name": "TBPL Commit Message Copier"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/TidyBox.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/TidyBox.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "TidyBox", "author": ["jesse"], "url": "https://www.squarefree.com/userscripts/tidybox.user.js", "modified": 1302279703.049041, "type": ["greasemonkey script"], "usage": ["tinderbox"], "description": "Shrinks Tinderbox, letting you hover instead of scrolling to see information. If you want to click a link in a popup, click to freeze the popup in place."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/TryChooser_Syntax_Builder.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/TryChooser_Syntax_Builder.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "TryChooser Syntax Builder", "language": ["JavaScript"], "author": ["lsblakk"], "url": "http://people.mozilla.org/~lsblakk/trychooser/trychooser.html", "modified": 1304793532.876296, "type": ["tryserver"], "description": "Allows you to generate the \"try:\" line you need"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/Updating_uuids_for_lots_of_related_interfaces.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/Updating_uuids_for_lots_of_related_interfaces.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "Updating uuids for lots of related interfaces", "language": ["perl"], "author": ["sfink"], "url": "http://people.mozilla.org/~sfink/uploads/update-uuids", "modified": 1302953542.0, "usage": ["uuid"], "type": ["script"], "description": "This script updates IDL files with new UUIDs for the given interfaces as well as any interfaces affected by any of those interfaces (recursively)."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/archer-mozilla.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/archer-mozilla.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "This tree holds Python code to customize Archer, the GDB branch extensible via Python, for debugging Mozilla. ", "language": ["python"], "url": "http://hg.mozilla.org/users/jblandy_mozilla.com/archer-mozilla/", "modified": 1301956344.8905699, "usage": ["gdb"], "name": "archer-mozilla"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/bugzilla-tweaks.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/bugzilla-tweaks.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "bugzilla-tweaks", "author": ["harth", "ehsan"], "url": "https://bitbucket.org/ehsan/bugzilla-tweaks", "modified": 1385649107.143126, "usage": ["bugzilla"], "type": ["addon"], "description": "The Bugzilla Tweaks extensions, aiming to make bugzilla.mozilla.org usage easier."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/bzconsole.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/bzconsole.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "bzconsole", "language": ["python"], "author": ["jhammel"], "url": "http://k0s.org/mozilla/hg/bzconsole", "modified": 1298414293.0170419, "usage": ["bugzilla"], "description": "console API to bugzilla"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/bzexport.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/bzexport.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "bzexport", "language": ["python"], "author": ["ted"], "url": "http://hg.mozilla.org/users/tmielczarek_mozilla.com/bzexport/", "modified": 1298666218.418088, "usage": ["bugzilla", "hg", "mq"], "type": ["mercurial-extension"], "description": "bzexport: a Mercurial extension for attaching patches from a Mercurial repository to bugzilla from the command line."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/bztools.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/bztools.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "bztools", "language": ["python"], "author": ["LegNeato"], "url": "https://github.com/LegNeato/bztools", "modified": 1298423387.0853753, "usage": ["bugzilla"], "description": "Models and scripts to access the Bugzilla REST API"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/find-roots.pl.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/find-roots.pl.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Perl script to analyze cycle collector edge log", "language": ["perl"], "author": ["peterv"], "url": "https://bug466157.bugzilla.mozilla.org/attachment.cgi?id=468818", "modified": 1302553294.18314, "type": ["script"], "name": "find-roots.pl"} diff -r 000000000000 -r b0942f44413f sample/github-tools.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/github-tools.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "github-tools", "author": ["bhearsum"], "url": "https://github.com/bhearsum/github-tools", "modified": 1302714285.0, "usage": ["github"], "description": "Repository for scripts related to github\n"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/hg_bisect_aid.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/hg_bisect_aid.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "hg_bisect_aid", "author": ["standard8"], "url": "http://hg.mozilla.org/users/bugzilla_standard8.plus.com/useful-tools/file/tip/hg_bisect_aid", "modified": 1298581662.1799026, "usage": ["hg"], "description": "aid to bisect with hg"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/hgactivity.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/hgactivity.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "hgactivity", "author": ["dmose"], "url": "https://bitbucket.org/dmose/hgactivity", "modified": 1298581934.1279025, "usage": ["hg"], "description": "(Temporary, I hope!) fork of http://labs.freehackers.org/projects/hgactivity... with the ability to generate a monthly summary of checkins per-committer in CSV format, suitable for spreadsheet import."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/hgtool.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/hgtool.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "releng tool to do safe operations with hg. revision/branch on commandline will override those in props-file", "language": ["python"], "url": "http://hg.mozilla.org/build/tools/file/tip/buildfarm/utils/hgtool.py", "modified": 1298582024.0199025, "usage": ["hg"], "name": "hgtool"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/lithium.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/lithium.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "lithium", "language": ["python"], "author": ["jesse"], "url": "http://www.squarefree.com/lithium/", "modified": 1302295843.993751, "description": "Lithium is an automated testcase reduction tool developed by Jesse Ruderman."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/logparser.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/logparser.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "\nparses test logs into JSON and scrapes logs from stage.mozilla.org\n", "language": ["python"], "author": ["jgriffin", "jhammel"], "url": "http://hg.mozilla.org/automation/logparser/", "modified": 1298417685.9583635, "name": "logparser"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/memory-profiler.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/memory-profiler.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Memory Profiler Firefox extension: https://wiki.mozilla.org/Labs/Memory_Profiler\n", "author": ["atul"], "url": "https://github.com/toolness/memory-profiler", "modified": 1302551634.8831401, "type": ["addon"], "name": "memory-profiler"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/mozInstall.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/mozInstall.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "mozInstall", "language": ["python"], "author": ["ctalbert"], "url": "https://github.com/harthur/mozregression/blob/master/mozregression/mozInstall.py", "modified": 1299024970.4532111, "usage": ["dmg"], "description": "installs Firefox"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/mozmill-automation.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/mozmill-automation.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "This repository contains automation scripts for a couple of different test-runs. For use with Mozmill", "language": ["python"], "author": ["whimboo"], "url": "http://hg.mozilla.org/qa/mozmill-automation", "modified": 1299025362.369211, "usage": ["dmg"], "type": ["harness"], "name": "mozmill-automation"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/mozprocess.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/mozprocess.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "killable process and process management utilities", "language": ["python"], "author": ["ctalbert"], "url": "https://github.com/mozautomation/mozmill/tree/master/mozprocess", "modified": 1298516361.223263, "name": "mozprocess"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/mozprofile.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/mozprofile.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "mozprofile", "language": ["python"], "author": ["ctalbert"], "url": "https://github.com/mozautomation/mozmill/tree/master/mozprofile", "modified": 1298516189.463263, "usage": ["profiles", "addons"], "description": "mozprofile manages profiles for automation and test harnesses"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/mozregression.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/mozregression.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "mozregression", "language": ["python"], "author": ["harth"], "url": "http://harthur.github.com/mozregression/", "modified": 1299025056.385211, "usage": ["regression", "nightlies"], "description": "interactive regression range finder for Firefox nightlies"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/mozrunner.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/mozrunner.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "mozrunner runs XUL-based applications such as Firefox and Thunderbird", "language": ["python"], "author": ["ctalbert"], "url": "https://github.com/mozautomation/mozmill/tree/master/mozrunner", "modified": 1298516027.8272631, "name": "mozrunner"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/pybugzilla.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/pybugzilla.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Python library and command-line tools for interacting with Bugzilla via Gervase Markham's REST API", "language": ["python"], "author": ["atul"], "url": "https://github.com/toolness/pybugzilla", "modified": 1301358703.815768, "usage": ["bugzilla"], "name": "pybugzilla"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/python-profilemanager.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/python-profilemanager.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "A python version of profile manager; see also http://hg.mozilla.org/automation/profilemanager/", "language": ["python"], "author": ["jhammel"], "url": "http://k0s.org/mozilla/hg/ProfileManager/", "modified": 1298420504.6333754, "usage": ["profiles"], "name": "python-profilemanager"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/pyxpt.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/pyxpt.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "A module for working with XPCOM Type Libraries.", "language": ["python"], "author": ["ted"], "url": "http://hg.mozilla.org/users/tmielczarek_mozilla.com/pyxpt", "modified": 1301102002.2795377, "name": "pyxpt"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/qimportbz.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/qimportbz.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "qimportbz", "language": ["python"], "author": ["robarnold"], "url": "http://hg.mozilla.org/users/robarnold_cmu.edu/qimportbz", "modified": 1298666299.5580881, "usage": ["bugzilla", "hg", "mq"], "type": ["mercurial-extension"], "description": "hg qimportbz gives you a list of the patches with a descripting and some flags and you select which number you want. handy for pushing other people's patches"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/reverse-edges.pl.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/reverse-edges.pl.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "reverse-edges.pl", "language": ["perl"], "author": ["peterv"], "url": "https://bug466157.bugzilla.mozilla.org/attachment.cgi?id=468816", "modified": 1302553208.4951401, "description": "Perl script to process cycle collector edge log"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/screenshot-tool.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/screenshot-tool.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "screenshot program", "author": ["ted"], "url": "http://hg.mozilla.org/users/tmielczarek_mozilla.com/screenshot-tools", "modified": 1298420830.4173753, "usage": ["screenshots"], "name": "screenshot-tool"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/self-serve-tools.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/self-serve-tools.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "self-serve-tools", "language": ["python"], "author": ["Josh Matthews"], "url": "http://hg.mozilla.org/users/josh_joshmatthews.net/self-serve-tools/", "modified": 1302545970.29514, "description": "Python API to access the self-serve REST API."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/sendchanges.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/sendchanges.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "sendchanges", "language": ["python"], "author": ["jhammel"], "url": "http://k0s.org/mozilla/hg/sendchanges", "modified": 1298398495.6554279, "dependencies": ["GetLatestTinderbox"], "usage": ["buildbot"], "description": "send changes to a Mozilla buildmaster for testing"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/tinderstatus.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/tinderstatus.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"name": "tinderstatus", "author": ["Alexander J. Vincent"], "url": "http://tinderstatus.mozdev.org/", "modified": 1300321728.3247271, "usage": ["tinderbox"], "type": ["addon"], "description": "Tinderstatus is a small Firefox/SeaMonkey extension that displays the current status of the SeaMonkey, Firefox, Thunderbird, XULRunner, and Camino tinderboxen and whether the tree is open or closed in an icon on the browser's status bar, enabling you to keep close watch on the tree if you have recently checked in code, are sheriffing for the day, or are just interested in keeping tabs on how the code is doing."} \ No newline at end of file diff -r 000000000000 -r b0942f44413f sample/unpack-diskimage.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sample/unpack-diskimage.json Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +{"description": "Unpacks and mounts a .dmg file", "author": ["catlee", "kairo"], "url": "http://mxr.mozilla.org/mozilla-central/source/build/package/mac_osx/unpack-diskimage", "modified": 1298421274.8493755, "usage": ["dmg"], "name": "unpack-diskimage"} \ No newline at end of file diff -r 000000000000 -r b0942f44413f scripts/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/README.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +Scripts associated with toolbox but not part of the essential web service diff -r 000000000000 -r b0942f44413f scripts/greasemonkey_scraper.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/greasemonkey_scraper.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,22 @@ +""" +// ==UserScript== +// @name TidyBox +// @namespace http://squarefree.com/userscripts +// @description Shrinks Tinderbox, letting you hover instead of scrolling to see information. If you want to click a link in a popup, click to freeze the popup in place. +// @include http://tinderbox.mozilla.org/* +// @author Jesse Ruderman - http://www.squarefree.com/ +// ==/UserScript== + +// Version history: + +// 2008-02-23 Initial release +// 2008-05-19 Updated for http://tinderbox.mozilla.org/showbuilds.cgi?tree=Mozilla2 +// 2008-06-27 Detect "OS X" in addition to "Mac OS X". Detect "leak test". +// 2008-11-23 Make it work on static pages and on the Camino page. Add MIN_COLUMNS. +// 2009-02-04 Inline the log links and "add comment" button (contributed by Jonas Sicking) +// Also, add C links (contributed by Boris Zbarsky), and add links to the top-right. +// 2009-02-14 Fix a bug where the 'pushlog + tinderbox' link was missing on calm trees. +// 2009-08-08 Add 'M', 'E', 's', 'X' box types. +// 2009-08-18 Hide animated flames (patch from cjones). +// 2010-02-16 Update for split unit tests. Show opt vs debug. Fix animated-flame hiding. +""" diff -r 000000000000 -r b0942f44413f scripts/html2json.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/html2json.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +""" +script to convert HTML microformat files to JSON: + +
+

${PROJECT}

+

${DESCRIPTION}

+ + +
  • ${AUTHOR}
+
  • ${USAGE}
+
+""" + +### imports + +import os + +try: + from lxml import etree +except ImportError: + raise ImportError("""You need lxml to run this script. Try running + `easy_install lxml` + It will work if you're lucky""") + +try: + import json +except ImportError: + import simplejson as json + +### parse command line + +from optparse import OptionParser + +usage = '%prog file' +parser = OptionParser(usage=usage, description=__doc__) +parser.add_option('--pprint', dest='pprint', + action='store_true', default=False, + help="pretty-print the json") + +options, args = parser.parse_args() + +if not len(args) == 1: + parser.print_help() + parser.exit() +filename = args[0] +assert os.path.exists(filename), "%s not found" % filename + +### parse teh file +document = etree.parse(filename) +elements = document.findall(".//div[@class='project']") +if not elements: + root = document.getroot() + if root.tag == 'div' and 'project' in root.attrib.get('class', '').split(): + elements = [root] +if not elements: + parser.error('No
found') + +# print teh projects +for element in elements: + project = {} + header = element.find('.//h1') + link = header.find('a') + if link is not None: + project['name'] = link.text + project['url'] = link.attrib['href'] + else: + project['name'] = header.text + project['name'] = ' '.join(project['name'].strip().split()) + description = element.find("p[@class='description']") + if description is not None: + project['description'] = description.text or '' + project['description'] = ' '.join(project['description'].strip().split()) + for field in ('author', 'usage', 'language', 'type'): + e = element.find("ul[@class='%s']" % field) + if e is not None: + values = e.findall('li') + for value in values: + project.setdefault(field, []).append(value.text) + indent = options.pprint and 2 or None + print json.dumps(project, indent=indent) diff -r 000000000000 -r b0942f44413f scripts/setup_scraper.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/setup_scraper.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,28 @@ +""" +scrapes metadata from a python setup.py file +""" + +import imp +import sys +import urllib2 + +# dictionary of +data = {} + +def mock_setup(**kwargs): + """ + mock the distutils/setuptools setup function + """ + import pdb; pdb.set_trace() + +def setuppy2tool(url): + """ + reads a file path or URL (TODO) + returns a dict in the appropriate format + """ + + + +if __name__ == '__main__': + for arg in sys.argv[1:]: + print setuppy2tool(arg) diff -r 000000000000 -r b0942f44413f setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,47 @@ +from setuptools import setup, find_packages + +try: + description = file('README.txt').read() +except IOError: + description = '' + +version = "0.3" + +# dependencies +dependencies = [ + 'WebOb', + 'tempita', + 'paste', + 'pastescript', # technically optional, but here for ease of install + 'whoosh >= 2.5', + 'couchdb', + 'docutils', + 'pyloader', + 'theslasher', + 'pyes == 0.15', + ] + +setup(name='toolbox', + version=version, + description="a place to list Mozilla software tools", + long_description=description, + classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers + author='Jeff Hammel', + author_email='jhammel@mozilla.com', + url='https://github.com/mozilla/toolbox', + license="MPL", + packages=['toolbox'], + include_package_data=True, + zip_safe=False, + install_requires=dependencies, + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + toolbox-convert-model = toolbox.model:convert + toolbox-serve = toolbox.factory:main + + [paste.app_factory] + toolbox = toolbox.factory:paste_factory + """, + ) + diff -r 000000000000 -r b0942f44413f test/test.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test.ini Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,28 @@ +#!/usr/bin/env paster + +# config for the testing webserver + +[DEFAULT] +debug = true +email_to = jhammel@example.com +smtp_server = localhost +error_email_from = paste@localhost + +[exe] +command = serve + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 9090 + +[composite:main] +use = egg:Paste#urlmap +/ = toolbox + +set debug = false + +[app:toolbox] +paste.app_factory = toolbox.factory:paste_factory +toolbox.directory = %(here)s/test_json +toolbox.fields = usage author type language dependencies \ No newline at end of file diff -r 000000000000 -r b0942f44413f test/test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,145 @@ +#!/usr/bin/env python + +""" +doctest runner for toolbox +""" + +import doctest +import json +import os +import shutil +import sys +from cgi import escape +from optparse import OptionParser +from paste.fixture import TestApp +from time import time +from toolbox.dispatcher import Dispatcher + +# global +directory = os.path.dirname(os.path.abspath(__file__)) + + +class ToolboxTestApp(TestApp): + """WSGI app wrapper for testing JSON responses""" + + def __init__(self, **kw): + dispatcher_args = dict(model_type='memory_cache', fields=('usage', 'author', 'type', 'language', 'dependencies')) + dispatcher_args.update(kw) + app = Dispatcher(**dispatcher_args) + TestApp.__init__(self, app) + + def get(self, url='/', **kwargs): + kwargs.setdefault('params', {})['format'] = 'json' + response = TestApp.get(self, url, **kwargs) + return json.loads(response.body) + + def new(self, **kwargs): + kwargs['form-render-date'] = str(time()) + return self.post('/new', params=kwargs) + + def cleanup(self): + pass + + +class FileCacheTestApp(ToolboxTestApp): + """test the MemoryCache file-backed backend""" + + def __init__(self): + self.json_dir = os.path.join(directory, 'test_json') + shutil.rmtree(self.json_dir, ignore_errors=True) + os.makedirs(self.json_dir) + ToolboxTestApp.__init__(self, model_type='file_cache', directory=self.json_dir) + + def cleanup(self): + shutil.rmtree(self.json_dir, ignore_errors=True) + + +class CouchTestApp(ToolboxTestApp): + """test the MemoryCache file-backed backend""" + + def __init__(self): + ToolboxTestApp.__init__(self, model_type='couch', dbname='test_json') + for project in self.app.model.projects(): + self.app.model.delete(project) + + def cleanup(self): + for project in self.app.model.projects(): + self.app.model.delete(project) + + +app_classes = {'memory_cache': ToolboxTestApp, + 'file_cache': FileCacheTestApp, + 'couch': CouchTestApp} + + +def run_tests(app_cls, + raise_on_error=False, + cleanup=True, + report_first=False, + output=sys.stdout): + + results = {} + + # gather tests + tests = [ test for test in os.listdir(directory) + if test.endswith('.txt') ] + output.write("Tests:\n%s\n" % '\n'.join(tests)) + + for test in tests: + + # create an app + app = app_cls() + + # doctest arguments + extraglobs = {'here': directory, 'app': app, 'urlescape': escape} + doctest_args = dict(extraglobs=extraglobs, raise_on_error=raise_on_error) + if report_first: + doctest_args['optionflags'] = doctest.REPORT_ONLY_FIRST_FAILURE + + # run the test + try: + results[test] = doctest.testfile(test, **doctest_args) + except doctest.DocTestFailure, failure: + raise + except doctest.UnexpectedException, failure: + raise failure.exc_info[0], failure.exc_info[1], failure.exc_info[2] + finally: + if cleanup: + app.cleanup() + + return results + + +def main(args=sys.argv[1:]): + + # parse command line args + parser = OptionParser() + parser.add_option('--no-cleanup', dest='cleanup', + default=True, action='store_false', + help="cleanup following the tests") + parser.add_option('--raise', dest='raise_on_error', + default=False, action='store_true', + help="raise on first error") + parser.add_option('--report-first', dest='report_first', + default=False, action='store_true', + help="report the first error only (all tests will still run)") + parser.add_option('--model', dest='model', default='file_cache', + help="model to use") + options, args = parser.parse_args(args) + + # get arguments to run_tests + kw = dict([(i, getattr(options, i)) for i in ('raise_on_error', 'cleanup', 'report_first')]) + if options.model is not None: + try: + kw['app_cls'] = app_classes[options.model] + except KeyError: + parser.error("Model '%s' unknown (choose from: %s)" % (options.model, app_classes.keys())) + + # run the tests + results = run_tests(**kw) + if sum([i.failed for i in results.values()]): + sys.exit(1) # error + + +if __name__ == '__main__': + main() diff -r 000000000000 -r b0942f44413f test/test_json.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test_json.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,274 @@ +Test toolbox JSON handling +========================== + +Ensure we have no projects:: + + >>> app.get('/') + [] + +Also no authors:: + + >>> app.get('/author') + {} + +Make a project:: + + >>> project = {'name': 'foo', 'description': 'foo description', 'url': 'http://example.com'} + >>> response = app.new(**project) + >>> response.status + 303 + >>> newproject = app.get('/')[0] + >>> modified = newproject.pop('modified') + >>> project == newproject + True + +Get the project by name:: + + >>> foo = app.get('/foo') + >>> modified == foo.pop('modified') + True + >>> foo == project + True + +Add some fields to it:: + + >>> fields = {'author': 'jhammel'} + >>> url = '/' + project['name'] + >>> response = app.post(url, params=fields) + >>> app.get('/')[0]['author'] + [u'jhammel'] + >>> app.get('/author') + {u'jhammel': [u'foo']} + >>> fields = {'author': 'turing'} + >>> response = app.post(url, params=fields) + >>> sorted(app.get(url)['author']) + [u'jhammel', u'turing'] + >>> sorted(app.get('/')[0]['author']) + [u'jhammel', u'turing'] + +Now let's search for the project!:: + + >>> authors = app.get('/author') + >>> len(authors) + 2 + >>> authors['jhammel'] + [u'foo'] + >>> authors['turing'] + [u'foo'] + +Let's search for it a different way:: + + >>> project = app.get('/')[0] + >>> projects = app.get('/', params={'author': 'jhammel'}) + >>> newproject = projects[0] + >>> newproject == project + True + +Just to show that the search is doing something:: + + >>> app.get('/', params={'author': 'sauron'}) + [] + +Now lets add another project:: + + >>> project2 = {'name': 'bar', 'description': 'a bar downtown', 'url': 'http://www.example.com'} + >>> response = app.new(**project2) + >>> projects = app.get('/') + >>> len(projects) + 2 + >>> projects[0]['name'] + u'bar' + >>> projects[1]['name'] + u'foo' + >>> jhammels_projects = app.get('/', params={'author': 'jhammel'}) + >>> len(jhammels_projects) + 1 + >>> jhammels_projects[0]['name'] + u'foo' + +Test search:: + + >>> projects = app.get('/', params={'q': 'jhammel'}) + >>> len(projects) + 1 + >>> projects[0]['name'] + u'foo' + >>> projects = app.get('/', params={'q': 'downtown'}) + >>> len(projects) + 1 + >>> projects[0]['name'] + u'bar' + +Add some metadata. Make sure we see it:: + + >>> url = '/bar' + >>> response = app.post(url, {'author': 'turing'}) + >>> len(app.get()) + 2 + >>> len(app.get('/', params={'author': 'jhammel'})) + 1 + >>> len(app.get('/', params={'q': 'jhammel'})) + 1 + >>> len(app.get('/', params={'author': 'turing'})) + 2 + >>> len(app.get('/', params={'q': 'turing'})) + 2 + >>> projects = app.get('/', params={'author': 'turing', 'q': 'downtown'}) + >>> len(projects) + 1 + >>> projects[0]['name'] + u'bar' + +Add a third project, just for variety:: + + >>> response = app.new(name='fleem', description='fleem in a building downtown', url='http://example.net') + >>> projects = app.get('/') + >>> len(projects) + 3 + >>> sorted([i['name'] for i in app.get('/', params=dict(q='downtown'))]) + [u'bar', u'fleem'] + >>> [i['name'] for i in app.get('/', params=dict(q='building'))] + [u'fleem'] + +Delete some metadata:: + + >>> response = app.post('/bar', params={'action': 'delete', 'author': 'turing'}) + >>> projects = app.get('/', params={'author': 'turing'}) + >>> len(projects) + 1 + >>> projects[0]['name'] + u'foo' + >>> projects = app.get('/', params={'q': 'turing'}) + >>> len(projects) + 1 + >>> projects[0]['name'] + u'foo' + +Delete a project:: + + >>> response = app.post('/delete', params={'project': 'foo'}) + >>> len(app.get('/')) + 2 + >>> len(app.get('/', params={'author': 'jhammel'})) + 0 + >>> results = app.get('/', params={'q': 'jhammel'}) + >>> len(results) + 0 + +You're back to two basic projects without much metadata. Let's give them some!:: + + >>> projects = app.get('/') + >>> [sorted(project.keys()) for project in projects] + [[u'description', u'modified', u'name', u'url'], [u'description', u'modified', u'name', u'url']] + >>> bar_modified_last = projects[0]['modified'] + >>> fleem_modified_earlier = projects[1]['modified'] + >>> bar_modified_last > fleem_modified_earlier + True + >>> [project['name'] for project in projects] + [u'bar', u'fleem'] + >>> description = 'You could be swining on a star' + >>> response = app.post('/bar', params=dict(description=description)) + >>> projects = app.get('/', params={'q': 'star'}) + >>> len(projects) + 1 + >>> projects[0]['description'] == description + True + >>> response = app.post('/bar', params={'type': 'song', 'usage': 'music', 'author': 'Sinatra'}) + >>> songs = app.get('/', params={'type': 'song'}) + >>> len(songs) + 1 + >>> songs[0]['name'] == 'bar' + True + >>> songs = app.get('/', params={'q': 'song'}) + >>> len(songs) + 1 + >>> songs[0]['name'] == 'bar' + True + >>> response = app.post('/fleem', params={'type': 'song', 'description': 'Cotton Eye Joe', 'author': 'Rednex'}) + >>> songs = app.get('/', params={'type': 'song'}) + >>> len(songs) + 2 + >>> songs = app.get('/', params={'q': 'song'}) + >>> len(songs) + 2 + >>> songs = app.get('/', params={'type': 'song', 'q': 'star'}) + >>> len(songs) + 1 + >>> songs[0]['name'] + u'bar' + >>> songs = app.get('/', params={'type': 'song', 'author': 'Sinatra'}) + >>> len(songs) + 1 + >>> songs[0]['name'] + u'bar' + +Now try renaming a tool:: + >>> [i['name'] for i in app.get('/')] + [u'fleem', u'bar'] + >>> response = app.post('/bar', params={'name': 'star'}) + >>> songs = app.get('/') + >>> len(songs) + 2 + >>> projects = app.get('/', params={'q': 'star'}) + >>> len(projects) + 1 + >>> star = projects[0] + >>> star['name'] + u'star' + >>> star['type'] + [u'song'] + +You should not be allowed to rename a tool if another tool has the +same name:: + + >>> sorted([i['name'] for i in app.get('/')]) + [u'fleem', u'star'] + >>> response = app.post('/star', params={'name': 'fleem'}, status=403) # Forbidden + >>> sorted([i['name'] for i in app.get('/')]) + [u'fleem', u'star'] + +You should not be allowed to have multiple identical item in the same +field:: + + >>> app.get('/star')['author'] + [u'Sinatra'] + >>> response = app.post('/star', params={'action': 'replace', 'author': 'Sinatra,Sinatra'}) + >>> app.get('/star')['author'] + [u'Sinatra'] + +You can rename an entire set of fields:: + + >>> [project['type'] for project in app.get('/')] + [[u'song'], [u'song']] + >>> response = app.post('/type', params={'song': 'number one hit'}) + >>> [project['type'] for project in app.get('/')] + [[u'number one hit'], [u'number one hit']] + +Fields in the request should be comma-separated and stripped of whitespace:: + + >>> project = {'name': 'A New Project', 'description': 'new description', 'url': 'http://example.com'} + >>> project_url = '/' + urlescape(project['name']) + >>> response = app.new(**project) + >>> fields = {'type': 'song, project, something special'} + >>> response = app.post(project_url, params=fields) + >>> sorted(app.get(project_url)['type']) + [u'project', u'something special', u'song'] + +You won't be able to have multiple identical field values or empty values:: + + >>> response = app.post(project_url, params=dict(author=' john, , , fielding, the third,,')) + >>> sorted(app.get(project_url)['author']) + [u'fielding', u'john', u'the third'] + +You should not be able to rename a project:: + + >>> sorted([project['name'] for project in app.get('/')]) + [u'A New Project', u'fleem', u'star'] + >>> response = app.post('/star', params=dict(name=''), status=403) + >>> response.status + 403 + >>> response = app.post('/fleem', params=dict(name=' '), status=403) + >>> response.status + 403 + >>> sorted([project['name'] for project in app.get('/')]) + [u'A New Project', u'fleem', u'star'] diff -r 000000000000 -r b0942f44413f test/test_search.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test_search.txt Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,26 @@ +Test toolbox search +=================== + +Ensure toolbox search does what we want it to. First, create some +projects:: + + >>> app.get('/') + [] + >>> project = {'name': 'bzconsole', 'description': 'interact with bugzilla from the command line', 'url': 'http://k0s.org/mozilla/hg/bzconsole'} + >>> response = app.new(**project) + >>> project = {'name': 'my crazy addon', 'description': 'a crazy addon i made', 'url': 'http://a.m.o'} + >>> response = app.new(**project) + >>> [i['name'] for i in app.get('/')] + [u'my crazy addon', u'bzconsole'] + +Define a search interface for our convenience:: + + >>> def search(query): + ... return [i['name'] for i in app.get('/', params=dict(q=query))] + >>> search('addon') + [u'my crazy addon'] + +You should be able to search for a name:: + + >>> search('bzconsole') + [u'bzconsole'] diff -r 000000000000 -r b0942f44413f toolbox/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/__init__.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,1 @@ +# diff -r 000000000000 -r b0942f44413f toolbox/dispatcher.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/dispatcher.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,124 @@ +""" +request dispatcher WSGI app: +data persisting across requests should go here +""" + +import os + +from handlers import CreateProjectView +from handlers import DeleteProjectHandler +from handlers import FieldView +from handlers import ProjectView +from handlers import QueryView +from handlers import TagsView +from handlers import AboutView +from handlers import NotFound + +from model import models +from util import strsplit +from webob import Request, Response, exc + +here = os.path.dirname(os.path.abspath(__file__)) + +class Dispatcher(object): + """toolbox WSGI app which dispatchers to associated handlers""" + + # class defaults + defaults = { 'about': None, # file path to ReST about page + 'model_type': 'memory_cache', # type of model to use + 'handlers': None, + 'reload': True, # reload templates + 'reserved': None, # reserved URL namespaces + 'template_dir': None, # directory for template overrides + 'baseurl': '', # base URL for redirects + + # branding variables + 'site_name': 'toolbox', # name of the site + 'item_name': 'tool', # name of a single item + 'item_plural': None, # item_name's plural, or None for item_name + 's' + } + + def __init__(self, **kw): + """ + **kw arguments used to override defaults + additional **kw are passed to the model + """ + + # set instance parameters from kw and defaults + for key in self.defaults: + setattr(self, key, kw.pop(key, self.defaults[key])) + if self.item_plural is None: + self.item_plural = self.item_name + 's' + + # should templates be reloaded? + if isinstance(self.reload, basestring): + self.reload = self.reload.lower() == 'true' + + # model: backend storage and associated methods + if 'fields' in kw and isinstance(kw['fields'], basestring): + # split fields if given as a string + kw['fields'] = strsplit(kw['fields']) + if hasattr(self.model_type, '__call__'): + model = self.model_type + elif self.model_type in models: + model = models[self.model_type] + else: + try: + import pyloader + model = pyloader.load(self.model_type) + except: + raise AssertionError("model_type '%s' not found in %s" % (self.model_type, models.keys())) + self.model = model(**kw) + + # add an about view if file specified + if self.about: + about = file(self.about).read() + import docutils.core + about = docutils.core.publish_parts(about, writer_name='html')['body'] + self.about = about + + + # request handlers in order they will be tried + if self.handlers is None: + self.handlers = [ TagsView, + CreateProjectView, + FieldView, + QueryView, + DeleteProjectHandler, + ProjectView] + if self.about: + self.handlers.append(AboutView) + + # extend reserved URLS from handlers + if self.reserved is None: + self.reserved = set(['css', 'js', 'img']) + for handler in self.handlers: + if handler.handler_path: + self.reserved.add(handler.handler_path[0]) + + def __call__(self, environ, start_response): + + # get a request object + request = Request(environ) + + # get the path + path = request.path_info.strip('/').split('/') + if path == ['']: + path = [] + request.environ['path'] = path + + # load any new data + self.model.load() + + # match the request to a handler + for h in self.handlers: + handler = h.match(self, request) + if handler is not None: + break + else: + # our 404 handler + handler = NotFound(self, request) + + # get response + res = handler() + return res(environ, start_response) diff -r 000000000000 -r b0942f44413f toolbox/factory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/factory.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +""" +WSGI -> HTTP server factories for toolbox +""" + +import os +import sys + +from dispatcher import Dispatcher +from paste.urlparser import StaticURLParser +from pkg_resources import resource_filename +from theslasher import TheSlasher + +class PassthroughFileserver(object): + """serve files if they exist""" + + def __init__(self, app, directory): + self.app = app + self.directory = directory + self.fileserver = StaticURLParser(self.directory) + + def __call__(self, environ, start_response): + path = environ['PATH_INFO'].strip('/') + if path and os.path.exists(os.path.join(self.directory, path)) and '..' not in path: + return self.fileserver(environ, start_response) + return self.app(environ, start_response) + + +def paste_factory(global_conf=None, **app_conf): + """create a webob view and wrap it in middleware""" + + keystr = 'toolbox.' + static_directory = app_conf.pop('static', + resource_filename(__name__, 'static')) + args = dict([(key.split(keystr, 1)[-1], value) + for key, value in app_conf.items() + if key.startswith(keystr) ]) + app = TheSlasher(Dispatcher(**args)) # kill slashes + return PassthroughFileserver(app, static_directory) + +def wsgiref_factory(host='0.0.0.0', port=8080): + """wsgiref factory; for testing only""" + + from wsgiref import simple_server + app = Dispatcher() + app = PassthroughFileserver(app, resource_filename(__name__, 'static')) + server = simple_server.make_server(host=host, port=int(port), app=app) + fqdn = '127.0.0.1' if host =='0.0.0.0' else host + print "Serving toolbox at http://%s:%d/" % (fqdn, port) + server.serve_forever() + + +# WSGI factories available +factories = {'paste': paste_factory, + 'wsgiref': wsgiref_factory} + +def main(args=sys.argv[1:]): + """CLI entry point""" + + # parse command line + usage = '%prog [options]' + import argparse + parser = argparse.ArgumentParser(usage=usage, description=__doc__.strip()) + parser.add_argument('--factory', default='wsgiref', + choices=factories.keys(), + help="factory to use") + parser.add_argument('--port', type=int, default=8080, + help="port to serve on") + args = parser.parse_args() + + # serve toolbox + factory = factories[args.__dict__.pop('factory')] + factory_args = args.__dict__ + print "Serving using factory: %s" % getattr(factory, '__name__', str(factory)) + print "Factory arguments: %s" % factory_args + factory(**factory_args) + +if __name__ == '__main__': + main() diff -r 000000000000 -r b0942f44413f toolbox/handlers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/handlers.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,616 @@ +""" +request handlers: +these are instantiated for every request, then called +""" + +import cgi +import os +from datetime import datetime +from pkg_resources import resource_filename +from urllib import quote as _quote +from urlparse import urlparse +from util import strsplit +from util import JSONEncoder +from webob import Response, exc +from tempita import HTMLTemplate +from time import time + +# this is necessary because WSGI stupidly follows the CGI convention wrt encoding slashes +# http://comments.gmane.org/gmane.comp.web.pylons.general/5922 +encoded_slash = '%25%32%66' + +def quote(s, safe='/'): + if isinstance(s, unicode): + s = s.encode('utf-8', 'ignore') # hope we're using utf-8! + return _quote(s, safe).replace('/', encoded_slash) + +try: + import json +except ImportError: + import simplejson as json + +class HandlerMatchException(Exception): + """the handler doesn't match the request""" + +class Handler(object): + """general purpose request handler (view)""" + + methods = set(['GET']) # methods to listen to + handler_path = [] # path elements to match + + @classmethod + def match(cls, app, request): + + # check the method + if request.method not in cls.methods: + return None + + # check the path + if request.environ['path'] != cls.handler_path: + return None + + # check the constructor + try: + return cls(app, request) + except HandlerMatchException: + return None + + def __init__(self, app, request): + self.app = app + self.request = request + self.check_json() # is this a JSON request? + + def __call__(self): + return getattr(self, self.request.method.title())() + + def link(self, path=None): + """ + link relative to the site root + """ + path_info = self.request.path_info + segments = path_info.split('/') + if segments[0]: + segments.insert(0, '') + + if len(segments) <3: + if not path or path == '/': + return './' + return path + + nlayers = len(segments[2:]) + string = '../' * nlayers + + if not path or path == '/': + return string + return string + path + + def redirect(self, location, query=None, anchor=None): + return exc.HTTPSeeOther(location=self.app.baseurl + '/' + location + + (query and self.query_string(query) or '') + + (anchor and ('#' + anchor) or '')) + + def query_string(self, query): + """ + generate a query string; query is a list of 2-tuples + """ + return '?' + '&'.join(['%s=%s' % (i,j) + for i, j in query]) + + # methods for JSON + + def check_json(self): + """check to see if the request is for JSON""" + self.json = self.request.GET.pop('format', '') == 'json' + + def post_data(self): + """python dict from POST request""" + if self.json: + return json.loads(self.request.body) + else: + retval = self.request.POST.mixed() + for key in retval: + value = retval[key] + if isinstance(value, basestring): + retval[key] = value.strip() + else: + # TODO[?]: just throw away all empty values here + retval[key] = [i.strip() for i in value] + return retval + + def get_json(self): + """JSON to serialize if requested for GET""" + raise NotImplementedError # abstract base class + + +class TempitaHandler(Handler): + """handler for tempita templates""" + + template_dirs = [ resource_filename(__name__, 'templates') ] + + template_cache = {} + + css = ['css/html5boilerplate.css'] + + less = ['css/style.less'] + + js = ['js/jquery-1.6.min.js', + 'js/less-1.0.41.min.js', + 'js/jquery.timeago.js', + 'js/main.js'] + + def __init__(self, app, request): + Handler.__init__(self, app, request) + + # add application template_dir if specified + if app.template_dir: + self.template_dirs = self.template_dirs[:] + [app.template_dir] + + self.data = { 'request': request, + 'css': self.css, + 'item_name': self.app.item_name, + 'item_plural': self.app.item_plural, + 'less': self.less, + 'js': self.js, + 'site_name': app.site_name, + 'title': self.__class__.__name__, + 'hasAbout': bool(app.about), + 'urlescape': quote, + 'link': self.link} + + def find_template(self, name): + """find a template of a given name""" + # the application caches a dict of the templates if app.reload is False + if name in self.template_cache: + return self.template_cache[name] + + for d in self.template_dirs: + path = os.path.join(d, name) + if os.path.exists(path): + template = HTMLTemplate.from_filename(path) + if not self.app.reload: + self.template_cache[name] = template + return template + + def render(self, template, **data): + template = self.find_template(template) + if template is None: + raise Exception("I can't find your template") + return template.substitute(**data) + + def Get(self): + # needs to have self.template set + if self.json: + return Response(content_type='application/json', + body=json.dumps(self.get_json(), cls=JSONEncoder)) + self.data['content'] = self.render(self.template, **self.data) + return Response(content_type='text/html', + body=self.render('main.html', **self.data)) + +class ProjectsView(TempitaHandler): + """abstract base class for views of projects""" + + js = TempitaHandler.js[:] + js.extend(['js/jquery.tokeninput.js', + 'js/jquery.jeditable.js', + 'js/jquery.autolink.js', + 'js/project.js']) + + less = TempitaHandler.less[:] + less.extend(['css/project.less']) + + css = TempitaHandler.css[:] + css.extend(['css/token-input.css', + 'css/token-input-facebook.css']) + + def __init__(self, app, request): + """project views specific init""" + TempitaHandler.__init__(self, app, request) + self.data['fields'] = self.app.model.fields() + self.data['error'] = None + if not self.json: + self.data['format_date'] = self.format_date + + def get_json(self): + """JSON to serialize if requested""" + return self.data['projects'] + + def sort(self, field): + reverse = False + if field.startswith('-'): + field = field[1:] + reverse = True + if field == 'name': + self.data['projects'].sort(key=lambda value: value[field].lower(), reverse=reverse) + else: + self.data['projects'].sort(key=lambda value: value[field], reverse=reverse) + + def format_date(self, timestamp): + """return a string representation of a timestamp""" + format_string = '%Y-%m-%dT%H:%M:%SZ' + return datetime.utcfromtimestamp(timestamp).strftime(format_string) + + +class QueryView(ProjectsView): + """general index view to query projects""" + + template = 'index.html' + methods = set(['GET']) + + def __init__(self, app, request): + ProjectsView.__init__(self, app, request) + + # pop non-query parameters; + # sort is popped first so that it does go in the query + sort_type = self.request.GET.pop('sort', None) + query = self.request.GET.mixed() + self.data['query'] = query + search = query.pop('q', None) + self.data['search'] = search + + # query for tools + self.data['projects']= self.app.model.get(search, **query) + + # order the results + self.data['sort_types'] = [('name', 'name'), ('-modified', 'last updated')] + if search: + self.data['sort_types'].insert(0, ('search', 'search rank')) + if sort_type is None: + if search: + sort_type = 'search' + else: + # default + sort_type = '-modified' + self.data['sort_type'] = sort_type + if sort_type != 'search': + # preserve search order results + self.sort(sort_type) + + self.data['fields'] = self.app.model.fields() + self.data['title'] = self.app.site_name + + +class ProjectView(ProjectsView): + """view of a particular project""" + + template = 'index.html' + methods=set(['GET', 'POST']) + + @classmethod + def match(cls, app, request): + + # check the method + if request.method not in cls.methods: + return None + + # the path should match a project + if not len(request.environ['path']) == 1: + return None + + # get the project if it exists + projectname = request.environ['path'][0].replace('%2f', '/') # double de-escape slashes, see top of file + try: + # if its utf-8, we should try to keep it utf-8 + projectname = projectname.decode('utf-8') + except UnicodeDecodeError: + pass + project = app.model.project(projectname) + if not project: + return None + + # check the constructor + try: + return cls(app, request, project) + except HandlerMatchException: + return None + + def __init__(self, app, request, project): + ProjectsView.__init__(self, app, request) + self.data['fields'] = self.app.model.fields() + self.data['projects'] = [project] + self.data['title'] = project['name'] + + def get_json(self): + return self.data['projects'][0] + + def Post(self): + + # data + post_data = self.post_data() + project = self.data['projects'][0] + + # insist that you have a name + if 'name' in post_data and not post_data['name'].strip(): + self.data['title'] = 'Rename error' + self.data['error'] = 'Cannot give a project an empty name' + self.data['content'] = self.render(self.template, **self.data) + return Response(content_type='text/html', + status=403, + body=self.render('main.html', **self.data)) + + # don't allow overiding other projects with your fancy rename + if 'name' in post_data and post_data['name'] != project['name']: + if self.app.model.project(post_data['name']): + self.data['title'] = '%s -> %s: Rename error' % (project['name'], post_data['name']) + self.data['error'] = 'Cannot rename over existing project: %s' % (post_data['name'], post_data['name'] ) + self.data['content'] = self.render(self.template, **self.data) + return Response(content_type='text/html', + status=403, + body=self.render('main.html', **self.data)) + + # XXX for compatability with jeditable: + id = post_data.pop('id', None) + + action = post_data.pop('action', None) + old_name = project['name'] + if action == 'delete': + for field in self.app.model.fields(): + if field in post_data and field in project: + values = post_data.pop(field) + if isinstance(values, basestring): + values = [values] + for value in values: + project[field].remove(value) + if not project[field]: + project.pop(field) + else: + for field in self.app.model.required: + if field in post_data: + project[field] = post_data[field] + for field in self.app.model.fields(): + if field in post_data: + value = post_data[field] + if isinstance(value, basestring): + value = strsplit(value) + if action == 'replace': + # replace the field from the POST request + project[field] = value + else: + # append the items....the default action + project.setdefault(field, []).extend(value) + + # rename handling + if 'name' in post_data and post_data['name'] != old_name: + self.app.model.delete(old_name) + self.app.model.update(project) + return self.redirect(quote(project['name'])) + + self.app.model.update(project) + + # XXX for compatability with jeditable: + if id is not None: + return Response(content_type='text/plain', + body=cgi.escape(project['description'])) + + # XXX should redirect instead + return self.Get() + + +class FieldView(ProjectsView): + """view of projects sorted by a field""" + + template = 'fields.html' + methods=set(['GET', 'POST']) + js = TempitaHandler.js[:] + ['js/field.js'] + + @classmethod + def match(cls, app, request): + + # check the method + if request.method not in cls.methods: + return None + + # the path should match a project + if len(request.environ['path']) != 1: + return None + + # ensure the field exists + field = request.environ['path'][0] + if field not in app.model.fields(): + return None + + # check the constructor + try: + return cls(app, request, field) + except HandlerMatchException: + return None + + def __init__(self, app, request, field): + ProjectsView.__init__(self, app, request) + projects = self.app.model.field_query(field) + if projects is None: + projects = {} + self.data['field'] = field + self.data['values'] = projects + self.data['title'] = app.item_plural + ' by %s' % field + if self.request.method == 'GET': + # get project descriptions for tooltips + descriptions = {} + project_set = set() + for values in projects.values(): + project_set.update(values) + self.data['projects'] = dict([(name, self.app.model.project(name)) + for name in project_set]) + + def Post(self): + field = self.data['field'] + for key in self.request.POST.iterkeys(): + value = self.request.POST[key] + self.app.model.rename_field_value(field, key, value) + + return self.redirect(field, anchor=value) + + def get_json(self): + return self.data['values'] + + +class CreateProjectView(TempitaHandler): + """view to create a new project""" + + template = 'new.html' + methods = set(['GET', 'POST']) + handler_path = ['new'] + js = TempitaHandler.js[:] + js.extend(['js/jquery.tokeninput.js', + 'js/queryString.js', + 'js/new.js']) + + less = TempitaHandler.less[:] + less.extend(['css/new.less']) + + css = TempitaHandler.css[:] + css.extend(['css/token-input.css', + 'css/token-input-facebook.css']) + + def __init__(self, app, request): + TempitaHandler.__init__(self, app, request) + self.data['title'] = 'Add a ' + app.item_name + self.data['fields'] = self.app.model.fields() + + def check_name(self, name): + """ + checks a project name for validity + returns None on success or an error message if invalid + """ + reserved = self.app.reserved.copy() + if name in reserved or name in self.app.model.fields(): # check application-level reserved URLS + return 'reserved' + if self.app.model.project(name): # check projects for conflict + return 'conflict' + + def Post(self): + + # get some data + required = self.app.model.required + post_data = self.post_data() + + # ensure the form isn't over 24 hours old + day = 24*3600 + form_date = post_data.pop('form-render-date', -day) + try: + form_date = float(form_date) + except ValueError: + form_date = -day + if abs(form_date - time()) > day: + # if more than a day old, don't honor the request + return Response(content_type='text/plain', + status=400, + body="Your form is over a day old or you don't have Javascript enabled") + + # build up a project dict + project = dict([(i, post_data.get(i, '').strip()) + for i in required]) + + # check for errors + errors = {} + missing = set([i for i in required if not project[i]]) + if missing: # missing required fields + errors['missing'] = missing + # TODO check for duplicate project name + # and other url namespace collisions + name_conflict = self.check_name(project['name']) + if name_conflict: + errors[name_conflict] = [project['name']] + if errors: + error_list = [] + for key in errors: + # flatten the error dict into a list + error_list.extend([(key, i) for i in errors[key]]) + return self.redirect(self.request.path_info.strip('/'), error_list) + + # add fields to the project + for field in self.app.model.fields(): + value = post_data.get(field, '').strip() + values = strsplit(value) + if not value: + continue + project[field] = values or value + + self.app.model.update(project) + return self.redirect(quote(project['name'])) + + +class DeleteProjectHandler(Handler): + + methods = set(['POST']) + handler_path = ['delete'] + + def Post(self): + post_data = self.post_data() + project = post_data.get('project') + if project: + try: + self.app.model.delete(project) + except: + pass # XXX better than internal server error + + # redirect to query view + return self.redirect('') + + +class TagsView(TempitaHandler): + """view most popular tags""" + methods = set(['GET']) + handler_path = ['tags'] + template = 'tags.html' + + def __init__(self, app, request): + TempitaHandler.__init__(self, app, request) + self.data['fields'] = self.app.model.fields() + fields = self.request.GET.getall('field') or self.data['fields'] + query = self.request.GET.get('q', '') + self.data['title'] = 'Tags' + field_tags = dict((i, {}) for i in fields) + omit = self.request.GET.getall('omit') + ommitted = dict([(field, set()) for field in fields]) + for name in omit: + project = self.app.model.project(name) + if not project: + continue + for field in fields: + ommitted[field].update(project.get(field, [])) + + for project in self.app.model.get(): + if project in omit: + continue + # TODO: cache this for speed somehow + # possibly at the model level + for field in fields: + for value in project.get(field, []): + if value in ommitted[field] or query not in value: + continue + count = field_tags[field].get(value, 0) + 1 + field_tags[field][value] = count + tags = [] + for field in field_tags: + for value, count in field_tags[field].items(): + tags.append({'field': field, 'value': value, 'count': count, 'id': value, 'name': value}) + tags.sort(key=lambda x: x['count'], reverse=True) + + self.data['tags'] = tags + + def get_json(self): + return self.data['tags'] + + +class AboutView(TempitaHandler): + """the obligatory about page""" + methods = set(['GET']) + handler_path = ['about'] + template = 'about.html' + less = TempitaHandler.less[:] + ['css/about.less'] + def __init__(self, app, request): + TempitaHandler.__init__(self, app, request) + self.data['fields'] = self.app.model.fields() + self.data['title'] = 'about:' + self.app.site_name + self.data['about'] = self.app.about + +class NotFound(TempitaHandler): + def __init__(self, app, request): + TempitaHandler.__init__(self, app, request) + self.data['fields'] = self.app.model.fields() + + def __call__(self): + self.data['content'] = '

Not Found

' + return Response(content_type='text/html', + status=404, + body=self.render('main.html', **self.data)) diff -r 000000000000 -r b0942f44413f toolbox/model.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/model.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,456 @@ +""" +models for toolbox +""" + +import couchdb +import os +import pyes +import sys +from copy import deepcopy +from search import WhooshSearch +from time import time +from util import str2filename + +try: + import json +except ImportError: + import simplejson as json + +# TODO: types of fields: +# - string: a single string: {'type': 'string', 'name': 'name', 'required': True} +# - field: a list of strings: {'type': 'field', 'name', 'usage'} +# - dict: a subclassifier: {'type': '???', 'name': 'url', 'required': True} +# - computed values, such as modified + +class ProjectsModel(object): + """ + abstract base class for toolbox tools + """ + + def __init__(self, fields=None, required=('name', 'description', 'url'), + whoosh_index=None): + """ + - fields : list of fields to use, or None to calculate dynamically + - required : required data (strings) + - whoosh_index : directory to keep whoosh index in + """ + self.required = set(required) + + # reserved fields + self.reserved = self.required.copy() + self.reserved.update(['modified']) # last modified, a computed value + self.search = WhooshSearch(whoosh_index=whoosh_index) + + # classifier fields + self._fields = fields + self.field_set = set(fields or ()) + + def update_search(self, project): + """update the search index""" + assert self.required.issubset(project.keys()) # XXX should go elsewhere + fields = dict([(field, project[field]) + for field in self.fields() + if field in project]) + + # keys must be strings, not unicode, on some systems + f = dict([(str(i), j) for i, j in fields.items()]) + + self.search.update(name=project['name'], description=project['description'], **f) + + def fields(self): + """what fields does the model support?""" + if self._fields is not None: + return self._fields + return list(self.field_set) + + def projects(self): + """list of all projects""" + return [i['name'] for i in self.get()] + + def export(self, other): + """export the current model to another model instance""" + for project in self.get(): + other.update(project) + + def rename_field_value(self, field, from_value, to_value): + projects = self.get(None, **{field: from_value}) + for project in projects: + project[field].remove(from_value) + project[field].append(to_value) + self.update(project) + + ### implementor methods + + def update(self, project): + """update a project""" + raise NotImplementedError + + def get(self, search=None, **query): + """ + get a list of projects matching a query + the query should be key, value pairs to match; + if the value is single, it should be a string; + if the value is multiple, it should be a set which will be + ANDed together + """ + raise NotImplementedError + + def project(self, name): + """get a project of a particular name, or None if there is none""" + raise NotImplementedError + + def field_query(self, field): + """get projects according to a particular field, or None""" + raise NotImplementedError + + def delete(self, project): + raise NotImplementedError + + +class MemoryCache(ProjectsModel): + """ + sample implementation keeping everything in memory + """ + + def __init__(self, fields=None, whoosh_index=None): + + ProjectsModel.__init__(self, fields=fields, whoosh_index=whoosh_index) + + # indices + self._projects = {} + self.index = {} + + self.load() + + def update(self, project, load=False): + + if project['name'] in self._projects and project == self._projects[project['name']]: + return # nothing to do + if not load: + project['modified'] = time() + if self._fields is None: + fields = [i for i in project if i not in self.reserved] + self.field_set.update(fields) + else: + fields = self._fields + for field in fields: + for key, _set in self.index.get(field, {}).items(): + _set.discard(project['name']) + if not _set: + self.index[field].pop(key) + if field not in project: + continue + project[field] = list(set([i.strip() for i in project[field] if i.strip()])) + index = self.index.setdefault(field, {}) + values = project[field] + if isinstance(values, basestring): + values = [values] + for value in values: + index.setdefault(value, set()).update([project['name']]) + self._projects[project['name']] = deepcopy(project) + self.update_search(project) + if not load: + self.save(project) + + def get(self, search=None, **query): + """ + - search: text search + - query: fields to match + """ + order = None + if search: + results = self.search(search) + order = dict([(j,i) for i,j in enumerate(results)]) + else: + results = self._projects.keys() + results = set(results) + for key, values in query.items(): + if isinstance(values, basestring): + values = [values] + for value in values: + results.intersection_update(self.index.get(key, {}).get(value, set())) + if order: + # preserve search order + results = sorted(list(results), key=lambda x: order[x]) + return [deepcopy(self._projects[project]) for project in results] + + + def project(self, name): + if name in self._projects: + return deepcopy(self._projects[name]) + + def field_query(self, field): + if field in self.index: + return deepcopy(self.index.get(field)) + + def delete(self, project): + """ + delete a project + - project : name of the project + """ + if project not in self._projects: + return + del self._projects[project] + for field, classifiers in self.index.items(): + for key, values in classifiers.items(): + classifiers[key].discard(project) + if not classifiers[key]: + del classifiers[key] + self.search.delete(project) + + def load(self): + """for subclasses; in memory, load nothing""" + + def save(self, project): + """for subclasses; in memory, save nothing""" + + +class FileCache(MemoryCache): + """save in JSON blob directory""" + + def __init__(self, directory, fields=None, whoosh_index=None): + """ + - directory: directory of .json tool files + """ + # JSON blob directory + if not os.path.exists(directory): + os.makedirs(directory) + assert os.path.isdir(directory) + self.directory = directory + + self.files = {} + MemoryCache.__init__(self, fields=fields, whoosh_index=whoosh_index) + + def delete(self, project): + MemoryCache.delete(self, project) + os.remove(os.path.join(self.directory, self.files.pop(project))) + + def load(self): + """load JSON from the directory""" + for i in os.listdir(self.directory): + if not i.endswith('.json'): + continue + filename = os.path.join(self.directory, i) + try: + project = json.loads(file(filename).read()) + except: + print 'File: ' + i + raise + self.files[project['name']] = i + self.update(project, load='modified' in project) + + def save(self, project): + + filename = self.files.get(project['name']) + if not filename: + filename = str2filename(project['name']) + '.json' + filename = filename.encode('ascii', 'ignore') + filename = os.path.join(self.directory, filename) + try: + f = file(filename, 'w') + except Exception, e: + print filename, repr(filename) + raise + f.write(json.dumps(project)) + f.close() + + +class ElasticSearchCache(MemoryCache): + """ + store json in ElasticSearch + """ + + def __init__(self, + server="localhost:9200", + es_index="toolbox", + doc_type="projects", + fields=None, + whoosh_index=None): + self.es_index = es_index + self.doc_type = doc_type + + try: + self.es = pyes.ES([server]) + self.es.create_index(self.es_index) + except pyes.urllib3.connectionpool.MaxRetryError: + raise Exception("Could not connect to ES instance") + except pyes.exceptions.ElasticSearchException: + # this just means the index already exists + pass + MemoryCache.__init__(self, fields=fields, whoosh_index=whoosh_index) + + def es_query(self, query): + """make an ElasticSearch query and return the results""" + search = pyes.Search(query) + results = self.es.search(query=search, + indexes=[self.es_index], + doc_types=[self.doc_type], + size=0) + + # the first query is just used to determine the size of the set + if not 'hits' in results and not 'total' in results['hits']: + raise Exception("bad ES response %s" % json.dumps(results)) + total = results['hits']['total'] + + # repeat the query to retrieve the entire set + results = self.es.search(query=search, + indexes=[self.es_index], + doc_types=[self.doc_type], + size=total) + + if not 'hits' in results and not 'hits' in results['hits']: + raise Exception("bad ES response %s" % json.dumps(results)) + + return results + + def load(self): + """load all json documents from ES:toolbox/projects""" + query = pyes.MatchAllQuery() + results = self.es_query(query) + + for hit in results['hits']['hits']: + self.update(hit['_source'], True) + + def save(self, project): + query = pyes.FieldQuery() + query.add('name', project['name']) + results = self.es_query(query) + + # If there is an existing records in ES with the same + # project name, update that record. Otherwise create a new record. + id = None + if results['hits']['hits']: + id = results['hits']['hits'][0]['_id'] + + self.es.index(project, self.es_index, self.doc_type, id) + + def delete(self, project): + MemoryCache.delete(self, project) + query = pyes.FieldQuery() + query.add('name', project['name']) + results = self.es_query(query) + if results['hits']['hits']: + id = results['hits']['hits'][0]['_id'] + self.es.delete(self.es_index, self.doc_type, id) + + +class CouchCache(MemoryCache): + """ + store json files in couchdb + """ + + def __init__(self, + server="http://127.0.0.1:5984", + dbname="toolbox", + fields=None, + whoosh_index=None): + + # TODO: check if server is running + couchserver = couchdb.Server(server) + try: + self.db = couchserver[dbname] + except couchdb.ResourceNotFound: # XXX should not be a blanket except! + self.db = couchserver.create(dbname) + except: + raise Exception("Could not connect to couch instance. Make sure that you have couch running at %s and that you have database create priveleges if '%s' does not exist" % (server, dbname)) + MemoryCache.__init__(self, fields=fields, whoosh_index=whoosh_index) + + def load(self): + """load JSON objects from CouchDB docs""" + for id in self.db: + doc = self.db[id] + try: + project = doc['project'] + except KeyError: + continue # it's prob a design doc + self.update(project, load=True) + + def save(self, project): + name = project['name'] + try: + updated = self.db[name] + except: + updated = {} + updated['project'] = project + self.db[name] = updated + + def delete(self, project): + MemoryCache.delete(self, project) + del self.db[project] + +# directory of available models +models = {'memory_cache': MemoryCache, + 'file_cache': FileCache, + 'couch': CouchCache, + 'es': ElasticSearchCache} + +def convert(args=sys.argv[1:]): + """CLI front-end for model conversion""" + from optparse import OptionParser + usage = '%prog [global-options] from_model [options] to_model [options]' + description = "export data from one model to another" + parser = OptionParser(usage=usage, description=description) + parser.disable_interspersed_args() + parser.add_option('-l', '--list-models', dest='list_models', + action='store_true', default=False, + help="list available models") + parser.add_option('-a', '--list-args', dest='list_args', + metavar='MODEL', + help="list arguments for a model") + + options, args = parser.parse_args(args) + + # process global options + if options.list_models: + for name in sorted(models.keys()): + print name # could conceivably print docstring + parser.exit() + if options.list_args: + if not options.list_args in models: + parser.error("Model '%s' not found. (Choose from: %s)" % (options.list_args, models.keys())) + ctor = models[options.list_args].__init__ + import inspect + argspec = inspect.getargspec(ctor) + defaults = [[i, None] for i in argspec.args[1:]] # ignore self + for index, value in enumerate(reversed(argspec.defaults), 1): + defaults[-index][-1] = value + defaults = [[i,j] for i, j in defaults if i != 'fields'] + print '%s arguments:' % options.list_args + for arg, value in defaults: + print ' -%s %s' % (arg, value or '') + parser.exit() + + # parse models and their ctor args + sects = [] + _models = [] + for arg in args: + if arg.startswith('-'): + sects[-1].append(arg) + else: + _models.append(arg) + sects.append([]) + + # check models + if len(_models) != 2: + parser.error("Please provide two models. (You gave: %s)" % _models) + if not set(_models).issubset(models): + parser.error("Please use these models: %s (You gave: %s)" % (models, _models)) + + sects = [ [i.lstrip('-') for i in sect ] for sect in sects ] + + # require an equals sign + # XXX hacky but much easier to parse + if [ True for sect in sects + if [i for i in sect if '=' not in i] ]: + parser.error("All arguments must be `key=value`") + sects = [dict([i.split('=', 1) for i in sect]) for sect in sects] + + # instantiate models + from_model = models[_models[0]](**sects[0]) + to_model = models[_models[1]](**sects[1]) + + # convert the data + from_model.export(to_model) + +if __name__ == '__main__': + convert() diff -r 000000000000 -r b0942f44413f toolbox/search.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/search.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,107 @@ +import os +import shutil +import tempfile + +from time import sleep +from whoosh import fields +from whoosh import index +from whoosh.query import And +from whoosh.query import Or +from whoosh.query import Term +from whoosh.qparser import QueryParser +from whoosh.index import LockError + +class WhooshSearch(object): + """full-text search""" + + def __init__(self, whoosh_index=None): + """ + - whoosh_index : whoosh index directory + """ + self.schema = fields.Schema(name=fields.ID(unique=True, stored=True), + description=fields.TEXT) + self.keywords = set([]) + self.tempdir = False + if whoosh_index is None: + whoosh_index = tempfile.mkdtemp() + self.tempdir = True + if not os.path.exists(whoosh_index): + os.makedirs(whoosh_index) + self.index = whoosh_index + self.ix = index.create_in(self.index, self.schema) + + def update(self, name, description, **kw): + """update a document""" + + # forgivingly get the writer + timeout = 3. # seconds + ctr = 0. + incr = 0.2 + while ctr < timeout: + try: + writer = self.ix.writer() + break + except LockError: + ctr += incr + sleep(incr) + else: + raise + + # add keywords + for key in kw: + if key not in self.keywords: + writer.add_field(key, fields.KEYWORD) + self.keywords.add(key) + if not isinstance(kw[key], basestring): + kw[key] = ' '.join(kw[key]) + kw[key] = unicode(kw[key]) + + # convert to unicode for whoosh + # really whoosh should do this for us + # and really python should be unicode-based :( + name = unicode(name) + description = unicode(description) + + writer.update_document(name=name, description=description, **kw) + writer.commit() + + def delete(self, name): + """delete a document of a given name""" + writer = self.ix.writer() + name = unicode(name) + writer.delete_by_term('name', name) + writer.commit() + + def __call__(self, query): + """search""" + query = unicode(query) + query_parser = QueryParser("description", schema=self.ix.schema) + myquery = query_parser.parse(query) + +# Old code: too strict +# extendedquery = Or([myquery] + +# [Term(field, query) for field in self.keywords]) + + + # New code: too permissive +# extendedquery = [myquery] + excluded = set(['AND', 'OR', 'NOT']) + terms = [i for i in query.split() if i not in excluded] +# for field in self.keywords: +# extendedquery.extend([Term(field, term) for term in terms]) +# extendedquery = Or(extendedquery) + + # Code should look something like + #Or([myquery] + [Or( + # extendedquery = [myquery] + extendedquery = And([Or([myquery] + [Term('description', term), Term('name', term)] + + [Term(field, term) for field in self.keywords]) for term in terms]) + + # perform the search + searcher = self.ix.searcher() + return [i['name'] for i in searcher.search(extendedquery, limit=None)] + + def __del__(self): + if self.tempdir: + # delete the temporary directory, if present + shutil.rmtree(self.index) diff -r 000000000000 -r b0942f44413f toolbox/static/css/about.less --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/about.less Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,14 @@ +p { +margin: 1.0em; +} + +ul { +list-style-type: disc; +margin-bottom: 1.0em; +margin-top: 1.0em; +} + +em { +font-style: italic; +} + diff -r 000000000000 -r b0942f44413f toolbox/static/css/html5boilerplate.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/html5boilerplate.css Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,136 @@ +/* HTML5 ✰ Boilerplate */ + +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +blockquote, q { quotes: none; } +blockquote:before, blockquote:after, +q:before, q:after { content: ""; content: none; } +ins { background-color: #ff9; color: #000; text-decoration: none; } +mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; } +del { text-decoration: line-through; } +abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; } +table { border-collapse: collapse; border-spacing: 0; } +hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } +input, select { vertical-align: middle; } + +body { font:13px/1.231 sans-serif; *font-size:small; } +select, input, textarea, button { font:99% sans-serif; } +pre, code, kbd, samp { font-family: monospace, sans-serif; } + +html { overflow-y: scroll; } +a:hover, a:active { outline: none; } +ul, ol { margin-left: 2em; } +ol { list-style-type: decimal; } +nav ul, nav li { margin: 0; list-style:none; list-style-image: none; } +small { font-size: 85%; } +strong, th { font-weight: bold; } +td { vertical-align: top; } +sub, sup { font-size: 75%; line-height: 0; position: relative; } +sup { top: -0.5em; } +sub { bottom: -0.25em; } + +pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; } +textarea { overflow: auto; } +.ie6 legend, .ie7 legend { margin-left: -7px; } +input[type="radio"] { vertical-align: text-bottom; } +input[type="checkbox"] { vertical-align: bottom; } +.ie7 input[type="checkbox"] { vertical-align: baseline; } +.ie6 input { vertical-align: text-bottom; } +label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; } +button, input, select, textarea { margin: 0; } +input:valid, textarea:valid { } +input:invalid, textarea:invalid { border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; } +.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; } + + +::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; } +::selection { background:#FF5E99; color:#fff; text-shadow: none; } +a:link { -webkit-tap-highlight-color: #FF5E99; } +button { width: auto; overflow: visible; } +.ie7 img { -ms-interpolation-mode: bicubic; } + +body, select, input, textarea { color: #444; } +h1, h2, h3, h4, h5, h6 { font-weight: bold; } +a, a:active, a:visited { color: #607890; } +a:hover { color: #036; } + + +/** + * Primary styles + * + * Author: + */ + + + + + + + + + + + + + + +.ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; } +.hidden { display: none; visibility: hidden; } +.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } +.invisible { visibility: hidden; } +.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } +.clearfix:after { clear: both; } +.clearfix { zoom: 1; } + + +@media all and (orientation:portrait) { + +} + +@media all and (orientation:landscape) { + +} + +@media screen and (max-device-width: 480px) { + + /* html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */ +} + + +@media print { + * { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important; + -ms-filter: none !important; } + a, a:visited { color: #444 !important; text-decoration: underline; } + a[href]:after { content: " (" attr(href) ")"; } + abbr[title]:after { content: " (" attr(title) ")"; } + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } + pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } + thead { display: table-header-group; } + tr, img { page-break-inside: avoid; } + @page { margin: 0.5cm; } + p, h2, h3 { orphans: 3; widows: 3; } + h2, h3{ page-break-after: avoid; } +} + diff -r 000000000000 -r b0942f44413f toolbox/static/css/new.less --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/new.less Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,55 @@ +#new-container { + margin-top: 20px; + width: 600px; +} + +#new { + margin: 20px auto; +} + +table { + text-transform: lowercase; +} + +td { + padding: .5em .5em; + + &.field-name { + text-align: right; + } + + input { + padding: .2em; + + &:not(.submit) { + width: 250px; + } + + &[name="name"] { + width: 140px; + } + + &[name="description"] { + width: 320px; + } + } + + .token-input-list-facebook { + width: 240px; + } +} + +input[type="submit"] { + float: right; +} + +.button { + background-color: #F7F7F7; + border-radius: 8px 8px 8px 8px; + box-shadow: 0 1px 3px #999999; + color: #222222; + cursor: pointer; + display: inline-block; + margin: 0.3em; + padding: 0.4em 0.7em; +} \ No newline at end of file diff -r 000000000000 -r b0942f44413f toolbox/static/css/project.less --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/project.less Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,150 @@ +@greyed: #999; +@greyed-out: #aaa; + +.query { + font-size: 65%; + font-weight: normal; +} + +.query-value { + font-weight: bold; +} + +.query-search { + &:before, &:after { + content: '"'; + } +} + +.project { + .description { + margin-top: .2em; + } +} + +.project-title { + font-size: 1.4em; +} + +.date { + font-style: italic; + float: right; + + &:before { + content: "updated "; + } +} + +.fields { + margin-top: 10px; + line-height: 18px; +} + +.field-name { + display: inline; + float: left; + width: 14%; + font-weight: normal; + a { + color: inherit; + } +} + +.field-none { + color: @greyed-out; +} + +.field-value-container { + width: 86%; + + &:hover { + background-color: #D9FFD1; + } +} + +.field-value-item:not(:last-child) { + &:after { + content: ", "; + color: @greyed-out; + } +} + +.field-edit { + width: 86%; + display: inline-block; + + input { + display: inline-block; + } +} + +.field-value { + margin: 0; + display: inline; + + li { + display: inline; + } +} + +.edit-value { + display: inline-block; + width: 60%; + + &:hover { + cursor: pointer; + } +} + +.edit-message { + display: inline-block; + width: 100%; + + &:hover { + &:before { + content: "edit field"; + margin-left: 24px; + color: @greyed-out; + } + } +} + +.comma { + color: @greyed; +} + +.delete { + float: right; + cursor: pointer; +} + +.UEB { + margin-left: 0.5em; +} + +#sort-legend { + color: black; + font-size: small; + margin-right: 0.5em; +} + +#sort-order { + float: right; + font-size: 100%; + margin-top: 16px; + + ul { + float: left; + display: inline; + + li { + float: left; + + &:not(:last-child) &:not(:first-child) { + &:after { + content: "|"; + } + } + } + } +} \ No newline at end of file diff -r 000000000000 -r b0942f44413f toolbox/static/css/style.less --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/style.less Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,85 @@ +html { + font-family: 'Helvetica Neue', Helvetica, Arial,sans-serif; + background: -moz-linear-gradient(center top, #BFD6F3, #CDE0F1) no-repeat; + background: -webkit-gradient(linear,0% 0,0% 100%,from(#BFD6F3),to(#CDE0F1)); + min-height: 100%; + min-width: 100%; +} + +a { + text-decoration: none; + + &:hover { + color: #625; + } +} + +ul { + list-style: none; +} + +nav { + margin: 0.3em 0.5em; + font-size: 1.2em; + overflow: auto; + + li { + display: inline; + color: #625; + float: left; + margin: 0 0.3em; + } + + a, a:active, a:visited { + color: #3E4D5E; + } + + a:hover { + color: black; + } +} + +header { + overflow: auto; +} + +#title { + font-size: 2em; + display: inline-block; + margin-left: 3px; +} + +#container { + width: 780px; + margin: 2em auto; +} + +#search { + text-align: right; +} + +#search-submit { + background: url(../img/search.png); + width: 24px; + height: 24px; + border: none; +} + +#search-text { + width: 300px; + border-radius: 5px; +} + +#content > div { + background-color: white; + border: thin solid black; + border-radius: 1em; + margin-top: 0.5em; + box-shadow: 0em 0.2em 0.3em 0.0em black; + padding: 1em; + overflow: auto; +} + +.error { + color: red; +} diff -r 000000000000 -r b0942f44413f toolbox/static/css/token-input-facebook.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/token-input-facebook.css Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,122 @@ +/* Example tokeninput style #2: Facebook style */ +ul.token-input-list-facebook { + overflow: hidden; + height: auto !important; + height: 1%; + width: 400px; + border: 1px solid #8496ba; + cursor: text; + font-size: 12px; + font-family: Verdana; + min-height: 1px; + z-index: 999; + margin: 0; + padding: 0; + background-color: #fff; + list-style-type: none; + clear: left; +} + +ul.token-input-list-facebook li input { + border: 0; + width: 100px; + padding: 3px 8px; + background-color: white; + margin: 2px 0; + -webkit-appearance: caret; +} + +li.token-input-token-facebook { + overflow: hidden; + height: auto !important; + height: 15px; + margin: 3px; + padding: 1px 3px; + background-color: #eff2f7; + color: #000; + cursor: default; + border: 1px solid #ccd5e4; + font-size: 11px; + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + float: left; + white-space: nowrap; +} + +li.token-input-token-facebook p { + display: inline; + padding: 0; + margin: 0; +} + +li.token-input-token-facebook span { + color: #a6b3cf; + margin-left: 5px; + font-weight: bold; + cursor: pointer; +} + +li.token-input-selected-token-facebook { + background-color: #5670a6; + border: 1px solid #3b5998; + color: #fff; +} + +li.token-input-input-token-facebook { + float: left; + margin: 0; + padding: 0; + list-style-type: none; +} + +div.token-input-dropdown-facebook { + position: absolute; + width: 400px; + background-color: #fff; + overflow: hidden; + border-left: 1px solid #ccc; + border-right: 1px solid #ccc; + border-bottom: 1px solid #ccc; + cursor: default; + font-size: 11px; + font-family: Verdana; + z-index: 1; +} + +div.token-input-dropdown-facebook p { + margin: 0; + padding: 5px; + font-weight: bold; + color: #777; +} + +div.token-input-dropdown-facebook ul { + margin: 0; + padding: 0; +} + +div.token-input-dropdown-facebook ul li { + background-color: #fff; + padding: 3px; + margin: 0; + list-style-type: none; +} + +div.token-input-dropdown-facebook ul li.token-input-dropdown-item-facebook { + background-color: #fff; +} + +div.token-input-dropdown-facebook ul li.token-input-dropdown-item2-facebook { + background-color: #fff; +} + +div.token-input-dropdown-facebook ul li em { + font-weight: bold; + font-style: normal; +} + +div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { + background-color: #3b5998; + color: #fff; +} \ No newline at end of file diff -r 000000000000 -r b0942f44413f toolbox/static/css/token-input.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/css/token-input.css Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,113 @@ +/* Example tokeninput style #1: Token vertical list*/ +ul.token-input-list { + overflow: hidden; + height: auto !important; + height: 1%; + width: 400px; + border: 1px solid #999; + cursor: text; + font-size: 12px; + font-family: Verdana; + z-index: 999; + margin: 0; + padding: 0; + background-color: #fff; + list-style-type: none; + clear: left; +} + +ul.token-input-list li { + list-style-type: none; +} + +ul.token-input-list li input { + border: 0; + width: 350px; + padding: 3px 8px; + background-color: white; + -webkit-appearance: caret; +} + +li.token-input-token { + overflow: hidden; + height: auto !important; + height: 1%; + margin: 3px; + padding: 3px 5px; + background-color: #d0efa0; + color: #000; + font-weight: bold; + cursor: default; + display: block; +} + +li.token-input-token p { + float: left; + padding: 0; + margin: 0; +} + +li.token-input-token span { + float: right; + color: #777; + cursor: pointer; +} + +li.token-input-selected-token { + background-color: #08844e; + color: #fff; +} + +li.token-input-selected-token span { + color: #bbb; +} + +div.token-input-dropdown { + position: absolute; + width: 400px; + background-color: #fff; + overflow: hidden; + border-left: 1px solid #ccc; + border-right: 1px solid #ccc; + border-bottom: 1px solid #ccc; + cursor: default; + font-size: 12px; + font-family: Verdana; + z-index: 1; +} + +div.token-input-dropdown p { + margin: 0; + padding: 5px; + font-weight: bold; + color: #777; +} + +div.token-input-dropdown ul { + margin: 0; + padding: 0; +} + +div.token-input-dropdown ul li { + background-color: #fff; + padding: 3px; + list-style-type: none; +} + +div.token-input-dropdown ul li.token-input-dropdown-item { + background-color: #fafafa; +} + +div.token-input-dropdown ul li.token-input-dropdown-item2 { + background-color: #fff; +} + +div.token-input-dropdown ul li em { + font-weight: bold; + font-style: normal; +} + +div.token-input-dropdown ul li.token-input-selected-dropdown-item { + background-color: #d0efa0; +} + diff -r 000000000000 -r b0942f44413f toolbox/static/img/UEB16.png Binary file toolbox/static/img/UEB16.png has changed diff -r 000000000000 -r b0942f44413f toolbox/static/img/favicon.ico Binary file toolbox/static/img/favicon.ico has changed diff -r 000000000000 -r b0942f44413f toolbox/static/img/indicator.gif Binary file toolbox/static/img/indicator.gif has changed diff -r 000000000000 -r b0942f44413f toolbox/static/img/search.png Binary file toolbox/static/img/search.png has changed diff -r 000000000000 -r b0942f44413f toolbox/static/js/field.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/js/field.js Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,57 @@ +$(document).ready(function(){ + + var fieldname = $('#field-name').text(); + + // enable editing of field values + $('div.field').each(function() { + var fielddiv = $(this); + var field = $(this).attr('id'); + $(this).children('h2').each(function() { + var header = $(this); + var value = $(this).children('a').text(); + var UEB = $(''); + $(UEB).attr('title', 'rename ' + fieldname + ': ' + field); + $(UEB).css('visibility', 'hidden'); + var editField = function() { + var input = $(''); + $(input).val(field); + var submitHandler = function () { + var newvalue = $(this).val(); + if (newvalue != value) { + var hiddeninput = $(''); + $(hiddeninput).attr('name', value); + $(hiddeninput).val(newvalue); + var form = $('
'); + form.append(hiddeninput); + $(this).after(form); + $(form).submit(); + $(this).replaceWith(''); + return; + } + $(this).blur(function() {}); + $(this).replaceWith(header); + $(header).hover(function(eventObject) { $(this).children('img.UEB').css('visibility', 'visible'); }, + function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + + $(header).find('img.UEB').each(function() { + $(this).css('visibility', 'hidden'); + $(this).click(editField); + }); + } + $(header).replaceWith(input); + $(input).blur(submitHandler); + $(input).keypress(function(event) { + if (event.which == 13) { + $(this).blur(); + } + }); + $(input).focus(); + } + $(UEB).click(editField); + $(this).append(UEB); + $(this).hover(function(eventObject) { $(this).children('img.UEB').css('visibility', 'visible'); }, + function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + + }); + }); +}); diff -r 000000000000 -r b0942f44413f toolbox/static/js/jquery-1.6.min.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/js/jquery-1.6.min.js Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,16 @@ +/*! + * jQuery JavaScript Library v1.6 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon May 2 13:50:00 2011 -0400 + */ +(function(a,b){function cw(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function ct(a){if(!ch[a]){var b=f("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d===""){ci||(ci=c.createElement("iframe"),ci.frameBorder=ci.width=ci.height=0),c.body.appendChild(ci);if(!cj||!ci.createElement)cj=(ci.contentWindow||ci.contentDocument).document,cj.write("");b=cj.createElement(a),cj.body.appendChild(b),d=f.css(b,"display"),c.body.removeChild(ci)}ch[a]=d}return ch[a]}function cs(a,b){var c={};f.each(cn.concat.apply([],cn.slice(0,b)),function(){c[this]=a});return c}function cr(){co=b}function cq(){setTimeout(cr,0);return co=f.now()}function cg(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cf(){try{return new a.XMLHttpRequest}catch(b){}}function b_(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){name="data-"+c.replace(j,"$1-$2").toLowerCase(),d=a.getAttribute(name);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(e){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function H(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(H,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=d.userAgent,x,y,z,A=Object.prototype.toString,B=Object.prototype.hasOwnProperty,C=Array.prototype.push,D=Array.prototype.slice,E=String.prototype.trim,F=Array.prototype.indexOf,G={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?g=[null,a,null]:g=i.exec(a);if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6",length:0,size:function(){return this.length},toArray:function(){return D.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?C.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(D.apply(this,arguments),"slice",D.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:C,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;y.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!y){y=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",z,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",z),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&H()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):G[A.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!B.call(a,"constructor")&&!B.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||B.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
a",b=a.getElementsByTagName("*"),d=a.getElementsByTagName("a")[0];if(!b||!b.length||!d)return{};e=c.createElement("select"),f=e.appendChild(c.createElement("option")),g=a.getElementsByTagName("input")[0],i={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.55$/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:g.value==="on",optSelected:f.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},g.checked=!0,i.noCloneChecked=g.cloneNode(!0).checked,e.disabled=!0,i.optDisabled=!f.disabled;try{delete a.test}catch(r){i.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function click(){i.noCloneEvent=!1,a.detachEvent("onclick",click)}),a.cloneNode(!0).fireEvent("onclick")),g=c.createElement("input"),g.value="t",g.setAttribute("type","radio"),i.radioValue=g.value==="t",g.setAttribute("checked","checked"),a.appendChild(g),j=c.createDocumentFragment(),j.appendChild(a.firstChild),i.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",k=c.createElement("body"),l={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"};for(p in l)k.style[p]=l[p];k.appendChild(a),c.documentElement.appendChild(k),i.appendChecked=g.checked,i.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,i.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",i.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",m=a.getElementsByTagName("td"),q=m[0].offsetHeight===0,m[0].style.display="",m[1].style.display="none",i.reliableHiddenOffsets=q&&m[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(h=c.createElement("div"),h.style.width="0",h.style.marginRight="0",a.appendChild(h),i.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(h,null).marginRight,10)||0)===0),k.innerHTML="",c.documentElement.removeChild(k);if(a.attachEvent)for(p in{submit:1,change:1,focusin:1})o="on"+p,q=o in a,q||(a.setAttribute(o,"return;"),q=typeof a[o]=="function"),i[p+"Bubbles"]=q;return i}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[c]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;return(e.value||"").replace(p,"")}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||"set"in c&&c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b=a.selectedIndex,c=[],d=a.options,e=a.type==="select-one";if(b<0)return null;for(var g=e?b:0,h=e?b+1:d.length;g=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex",readonly:"readOnly"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);var h,i,j=g!==1||!f.isXMLDoc(a);c=j&&f.attrFix[c]||c,i=f.attrHooks[c]||(v&&(f.nodeName(a,"form")||u.test(c))?v:b);if(d!==b){if(d===null||d===!1&&!t.test(c)){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;d===!0&&!t.test(c)&&(d=c),a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j)return i.get(a,c);h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.getAttribute("value");a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}},propFix:{},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);c=i&&f.propFix[c]||c,h=f.propHooks[c];return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),f.support.getSetAttribute||(f.attrFix=f.extend(f.attrFix,{"for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder"}),v=f.attrHooks.name=f.attrHooks.value=f.valHooks.button={get:function(a,c){var d;if(c==="value"&&!f.nodeName(a,"button"))return a.getAttribute(c);d=a.getAttributeNode(c);return d&&d.specified?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var w=Object.prototype.hasOwnProperty,x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function J(a){var c=a.target,d,e;if(!!y.test(c.nodeName)&&!c.readOnly){d=f._data(c,"_change_data"),e=I(c),(a.type!=="focusout"||c.type!=="radio")&&f._data(c,"_change_data",e);if(d===b||e===d)return;if(d!=null||e)a.type="change",a.liveFired=b,f.event.trigger(a,arguments[1],c)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){return a.nodeName.toLowerCase()==="input"&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};be.optgroup=be.option,be.tbody=be.tfoot=be.colgroup=be.caption=be.thead,be.th=be.td,f.support.htmlSerialize||(be._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!be[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bh(a,d),e=bi(a),g=bi(d);for(h=0;e[h];++h)bh(e[h],g[h])}if(b){bg(a,d);if(c){e=bi(a),g=bi(d);for(h=0;e[h];++h)bg(e[h],g[h])}}return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[];for(var i=0,j;(j=a[i])!=null;i++){typeof j=="number"&&(j+="");if(!j)continue;if(typeof j=="string")if(!ba.test(j))j=b.createTextNode(j);else{j=j.replace(Z,"<$1>");var k=($.exec(j)||["",""])[1].toLowerCase(),l=be[k]||be._default,m=l[0],n=b.createElement("div");n.innerHTML=l[1]+j+l[2];while(m--)n=n.lastChild;if(!f.support.tbody){var o=_.test(j),p=k==="table"&&!o?n.firstChild&&n.firstChild.childNodes:l[1]===""&&!o?n.childNodes:[];for(var q=p.length-1;q>=0;--q)f.nodeName(p[q],"tbody")&&!p[q].childNodes.length&&p[q].parentNode.removeChild(p[q])}!f.support.leadingWhitespace&&Y.test(j)&&n.insertBefore(b.createTextNode(Y.exec(j)[0]),n.firstChild),j=n.childNodes}var r;if(!f.support.appendChecked)if(j[0]&&typeof (r=j.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bn.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bm.test(g)?g.replace(bm,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV;try{bU=e.href}catch(bW){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bX(bS),ajaxTransport:bX(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?b$(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b_(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bY(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bY(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bZ(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var ca=f.now(),cb=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+ca++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cb.test(b.url)||e&&cb.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cb,l),b.url===j&&(e&&(k=k.replace(cb,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cc=a.ActiveXObject?function(){for(var a in ce)ce[a](0,1)}:!1,cd=0,ce;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cf()||cg()}:cf,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cc&&delete ce[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cd,cc&&(ce||(ce={},f(a).unload(cc)),ce[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ch={},ci,cj,ck=/^(?:toggle|show|hide)$/,cl=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cm,cn=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],co,cp=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cs("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a=f.timers,b=a.length;while(b--)a[b]()||a.splice(b,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cm),cm=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cu=/^t(?:able|d|h)$/i,cv=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cw(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!cu.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="
";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cv.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cv.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cw(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cw(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){return this[0]?parseFloat(f.css(this[0],d,"padding")):null},f.fn["outer"+c]=function(a){return this[0]?parseFloat(f.css(this[0],d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff -r 000000000000 -r b0942f44413f toolbox/static/js/jquery.autolink.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/js/jquery.autolink.js Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,20 @@ +jQuery.fn.highlight = function (text, o) { + return this.each( function(){ + var replace = o || '$1'; + $(this).html( $(this).html().replace( new RegExp('('+text+'(?![\\w\\s?&.\\/;#~%"=-]*>))', "ig"), replace) ); + }); +} + +jQuery.fn.autolink = function () { + return this.each( function(){ + var re = /((http|https|ftp):\/\/[\w?=&.\/-;#~%-]+(?![\w\s?&.\/;#~%"=-]*>)[^.])/g; + $(this).html( $(this).html().replace(re, '$1 ') ); + }); +} + +jQuery.fn.mailto = function () { + return this.each( function() { + var re = /(([a-z0-9*._+]){1,}\@(([a-z0-9]+[-]?){1,}[a-z0-9]+\.){1,}([a-z]{2,4}|museum)(?![\w\s?&.\/;#~%"=-]*>))/g + $(this).html( $(this).html().replace( re, '$1' ) ); + }); +} \ No newline at end of file diff -r 000000000000 -r b0942f44413f toolbox/static/js/jquery.jeditable.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/js/jquery.jeditable.js Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,549 @@ +/* + * Jeditable - jQuery in place edit plugin + * + * Copyright (c) 2006-2009 Mika Tuupola, Dylan Verheul + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Project home: + * http://www.appelsiini.net/projects/jeditable + * + * Based on editable by Dylan Verheul : + * http://www.dyve.net/jquery/?editable + * + */ + +/** + * Version 1.7.2-dev + * + * ** means there is basic unit tests for this parameter. + * + * @name Jeditable + * @type jQuery + * @param String target (POST) URL or function to send edited content to ** + * @param Hash options additional options + * @param String options[method] method to use to send edited content (POST or PUT) ** + * @param Function options[callback] Function to run after submitting edited content ** + * @param String options[name] POST parameter name of edited content + * @param String options[id] POST parameter name of edited div id + * @param Hash options[submitdata] Extra parameters to send when submitting edited content. + * @param String options[type] text, textarea or select (or any 3rd party input type) ** + * @param Integer options[rows] number of rows if using textarea ** + * @param Integer options[cols] number of columns if using textarea ** + * @param Mixed options[height] 'auto', 'none' or height in pixels ** + * @param Mixed options[width] 'auto', 'none' or width in pixels ** + * @param String options[loadurl] URL to fetch input content before editing ** + * @param String options[loadtype] Request type for load url. Should be GET or POST. + * @param String options[loadtext] Text to display while loading external content. + * @param Mixed options[loaddata] Extra parameters to pass when fetching content before editing. + * @param Mixed options[data] Or content given as paramameter. String or function.** + * @param String options[indicator] indicator html to show when saving + * @param String options[tooltip] optional tooltip text via title attribute ** + * @param String options[event] jQuery event such as 'click' of 'dblclick' ** + * @param String options[submit] submit button value, empty means no button ** + * @param String options[cancel] cancel button value, empty means no button ** + * @param String options[cssclass] CSS class to apply to input form. 'inherit' to copy from parent. ** + * @param String options[style] Style to apply to input form 'inherit' to copy from parent. ** + * @param String options[select] true or false, when true text is highlighted ?? + * @param String options[placeholder] Placeholder text or html to insert when element is empty. ** + * @param String options[onblur] 'cancel', 'submit', 'ignore' or function ?? + * + * @param Function options[onsubmit] function(settings, original) { ... } called before submit + * @param Function options[onreset] function(settings, original) { ... } called before reset + * @param Function options[onerror] function(settings, original, xhr) { ... } called on error + * + * @param Hash options[ajaxoptions] jQuery Ajax options. See docs.jquery.com. + * + */ + +(function($) { + + $.fn.editable = function(target, options) { + + if ('disable' == target) { + $(this).data('disabled.editable', true); + return; + } + if ('enable' == target) { + $(this).data('disabled.editable', false); + return; + } + if ('destroy' == target) { + $(this) + .unbind($(this).data('event.editable')) + .removeData('disabled.editable') + .removeData('event.editable'); + return; + } + + var settings = $.extend({}, $.fn.editable.defaults, {target:target}, options); + + /* setup some functions */ + var plugin = $.editable.types[settings.type].plugin || function() { }; + var submit = $.editable.types[settings.type].submit || function() { }; + var buttons = $.editable.types[settings.type].buttons + || $.editable.types['defaults'].buttons; + var content = $.editable.types[settings.type].content + || $.editable.types['defaults'].content; + var element = $.editable.types[settings.type].element + || $.editable.types['defaults'].element; + var reset = $.editable.types[settings.type].reset + || $.editable.types['defaults'].reset; + var callback = settings.callback || function() { }; + var beforeedit = settings.beforeedit || function() { }; + var onedit = settings.onedit || function() { }; + var onsubmit = settings.onsubmit || function() { }; + var onreset = settings.onreset || function() { }; + var onerror = settings.onerror || reset; + + /* Show tooltip. */ + if (settings.tooltip) { + $(this).attr('title', settings.tooltip); + } + + settings.autowidth = 'auto' == settings.width; + settings.autoheight = 'auto' == settings.height; + + return this.each(function() { + + /* Save this to self because this changes when scope changes. */ + var self = this; + + /* Inlined block elements lose their width and height after first edit. */ + /* Save them for later use as workaround. */ + var savedwidth = $(self).width(); + var savedheight = $(self).height(); + + /* Save so it can be later used by $.editable('destroy') */ + $(this).data('event.editable', settings.event); + + /* If element is empty add something clickable (if requested) */ + if (!$.trim($(this).html())) { + $(this).html(settings.placeholder); + } + + $(this).bind(settings.event, function(e) { + + /* Abort if element is disabled. */ + if (true === $(this).data('disabled.editable')) { + return; + } + + /* Prevent throwing an exeption if edit field is clicked again. */ + if (self.editing) { + return; + } + + /* Abort if onedit hook returns false. */ + if (false === beforeedit.apply(this, [settings, self, e])) { + return; + } + + /* Prevent default action and bubbling. */ + e.preventDefault(); + e.stopPropagation(); + + /* Remove tooltip. */ + if (settings.tooltip) { + $(self).removeAttr('title'); + } + + /* Figure out how wide and tall we are, saved width and height. */ + /* Workaround for http://dev.jquery.com/ticket/2190 */ + if (0 == $(self).width()) { + settings.width = savedwidth; + settings.height = savedheight; + } else { + if (settings.width != 'none') { + settings.width = + settings.autowidth ? $(self).width() : settings.width; + } + if (settings.height != 'none') { + settings.height = + settings.autoheight ? $(self).height() : settings.height; + } + } + + /* Remove placeholder text, replace is here because of IE. */ + if ($(this).html().toLowerCase().replace(/(;|"|\/)/g, '') == + settings.placeholder.toLowerCase().replace(/(;|"|\/)/g, '')) { + $(this).html(''); + } + + self.editing = true; + self.revert = $(self).text().replace(/\s+/g, " "); + $(self).html(''); + + /* Create the form object. */ + var form = $('
'); + + /* Apply css or style or both. */ + if (settings.cssclass) { + if ('inherit' == settings.cssclass) { + form.attr('class', $(self).attr('class')); + } else { + form.attr('class', settings.cssclass); + } + } + + if (settings.style) { + if ('inherit' == settings.style) { + form.attr('style', $(self).attr('style')); + /* IE needs the second line or display wont be inherited. */ + form.css('display', $(self).css('display')); + } else { + form.attr('style', settings.style); + } + } + + /* Add main input element to form and store it in input. */ + var input = element.apply(form, [settings, self]); + + /* Set input content via POST, GET, given data or existing value. */ + var input_content; + + if (settings.loadurl) { + var t = setTimeout(function() { + input.disabled = true; + content.apply(form, [settings.loadtext, settings, self]); + }, 100); + + var loaddata = {}; + loaddata[settings.id] = self.id; + if ($.isFunction(settings.loaddata)) { + $.extend(loaddata, settings.loaddata.apply(self, [self.revert, settings])); + } else { + $.extend(loaddata, settings.loaddata); + } + $.ajax({ + type : settings.loadtype, + url : settings.loadurl, + data : loaddata, + async : false, + success: function(result) { + window.clearTimeout(t); + input_content = result; + input.disabled = false; + } + }); + } else if (settings.data) { + input_content = settings.data; + if ($.isFunction(settings.data)) { + input_content = settings.data.apply(self, [self.revert, settings]); + } + } else { + input_content = self.revert; + } + content.apply(form, [input_content, settings, self]); + + input.attr('name', settings.name); + + /* Add buttons to the form. */ + buttons.apply(form, [settings, self]); + + /* Add created form to self. */ + $(self).append(form); + + /* Attach 3rd party plugin if requested. */ + plugin.apply(form, [settings, self]); + + onedit.apply(this, [settings, self, e]) + + /* Focus to first visible form element. */ + $(':input:visible:enabled:first', form).focus(); + + /* Highlight input contents when requested. */ + if (settings.select) { + input.select(); + } + + /* discard changes if pressing esc */ + input.keydown(function(e) { + if (e.keyCode == 27) { + e.preventDefault(); + reset.apply(form, [settings, self]); + } + }); + + /* Discard, submit or nothing with changes when clicking outside. */ + /* Do nothing is usable when navigating with tab. */ + var t; + if ('cancel' == settings.onblur) { + input.blur(function(e) { + /* Prevent canceling if submit was clicked. */ + t = setTimeout(function() { + reset.apply(form, [settings, self]); + }, 500); + }); + } else if ('submit' == settings.onblur) { + input.blur(function(e) { + /* Prevent double submit if submit was clicked. */ + t = setTimeout(function() { + form.submit(); + }, 200); + }); + } else if ($.isFunction(settings.onblur)) { + input.blur(function(e) { + settings.onblur.apply(self, [input.val(), settings]); + }); + } else { + input.blur(function(e) { + /* TODO: maybe something here */ + }); + } + + form.submit(function(e) { + + if (t) { + clearTimeout(t); + } + + /* Do no submit. */ + e.preventDefault(); + + /* Call before submit hook. */ + /* If it returns false abort submitting. */ + if (false !== onsubmit.apply(form, [settings, self])) { + /* Custom inputs call before submit hook. */ + /* If it returns false abort submitting. */ + if (false !== submit.apply(form, [settings, self])) { + + /* Check if given target is function */ + if ($.isFunction(settings.target)) { + var str = settings.target.apply(self, [input.val(), settings]); + $(self).html(str); + self.editing = false; + callback.apply(self, [self.innerHTML, settings]); + /* TODO: this is not dry */ + if (!$.trim($(self).html())) { + $(self).html(settings.placeholder); + } + } else { + /* Add edited content and id of edited element to POST. */ + var submitdata = {}; + submitdata[settings.name] = input.val(); + submitdata[settings.id] = self.id; + /* Add extra data to be POST:ed. */ + if ($.isFunction(settings.submitdata)) { + $.extend(submitdata, settings.submitdata.apply(self, [self.revert, settings])); + } else { + $.extend(submitdata, settings.submitdata); + } + + /* Quick and dirty PUT support. */ + if ('PUT' == settings.method) { + submitdata['_method'] = 'put'; + } + + /* Show the saving indicator. */ + $(self).html(settings.indicator); + + /* Defaults for ajaxoptions. */ + var ajaxoptions = { + type : 'POST', + data : submitdata, + dataType: 'html', + url : settings.target, + success : function(result, status) { + if (ajaxoptions.dataType == 'html') { + $(self).html(result); + } + self.editing = false; + callback.apply(self, [result, settings]); + if (!$.trim($(self).html())) { + $(self).html(settings.placeholder); + } + }, + error : function(xhr, status, error) { + onerror.apply(form, [settings, self, xhr]); + } + }; + + /* Override with what is given in settings.ajaxoptions. */ + $.extend(ajaxoptions, settings.ajaxoptions); + $.ajax(ajaxoptions); + + } + } + } + + /* Show tooltip again. */ + $(self).attr('title', settings.tooltip); + + return false; + }); + }); + + /* Privileged methods */ + this.reset = function(form) { + /* Prevent calling reset twice when blurring. */ + if (this.editing) { + /* Before reset hook, if it returns false abort reseting. */ + if (false !== onreset.apply(form, [settings, self])) { + $(self).html(self.revert); + self.editing = false; + if (!$.trim($(self).html())) { + $(self).html(settings.placeholder); + } + /* Show tooltip again. */ + if (settings.tooltip) { + $(self).attr('title', settings.tooltip); + } + } + } + }; + }); + + }; + + + $.editable = { + types: { + defaults: { + element : function(settings, original) { + var input = $(''); + $(this).append(input); + return(input); + }, + content : function(string, settings, original) { + $(':input:first', this).val(string); + }, + reset : function(settings, original) { + original.reset(this); + }, + buttons : function(settings, original) { + var form = this; + if (settings.submit) { + /* If given html string use that. */ + if (settings.submit.match(/>$/)) { + var submit = $(settings.submit).click(function() { + if (submit.attr("type") != "submit") { + form.submit(); + } + }); + /* Otherwise use button with given string as text. */ + } else { + var submit = $('
'); + $(form).submit(function() { + var input = $(this).find('input[name="name"]') + var name = $(input).val() + name = name.trim(); + if (name.length == 0) { + $(this).append('
A project must have a name'); + return false; + } + return true; + }) + $(form).css('display', 'block'); + $(header).replaceWith(form); + $(form).find('button.cancel').click(function(){ + $(header).find('img.UEB').css('visibility', 'hidden'); + $(header).hover(nameHover, + function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + $(form).replaceWith(header); + }); + $(form).find('input[type=text]').keypress(function(event) { + if (event.which == 13) { + $(form).submit(); + } + }); + $(form).find('input[type=text]').focus(); + }); + } + var header = $(this).find('h1'); + $(header).append(UEB); + $(header).find('img.UEB').each(function() {$(this).css('visibility', 'hidden'); }); + $(header).hover(nameHover, + function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + $(this).find('a.home').each(function() { + var home = this; + $(this).wrap(''); + var wrapper = $(this).parent(); + var img = $(UEB); + $(wrapper).append(img); + img.css('visibility', 'hidden'); + function urlHover(eventObject) { + var img = $(this).find('img.UEB'); + $(img).css('visibility', 'visible'); + $(img).click(function() { + var link = $(home).attr('href'); + var size = link.length; + var input = $(''); + $(wrapper).replaceWith(input); + + function urlEditBlur() { + var newlink = $(this).val(); + var that = this; + newlink = newlink.trim(); + if (newlink != link) { + var throbber = $(''); + $(this).after(throbber); + $(this).hide(); + $.post(url, {"url": newlink}, function(data) { + $(throbber).remove(); + var a = $(wrapper).children('a'); + a.attr('href', newlink); + a.html(newlink); + $(wrapper).children('img.UEB').css('visibility', 'hidden'); + wrapper.hover(urlHover, function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + $(that).replaceWith(wrapper); + }); + } + else { + $(wrapper).children('img.UEB').css('visibility', 'hidden'); + wrapper.hover(urlHover, function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + $(this).replaceWith(wrapper); + } + } + $(input).blur(urlEditBlur); + $(input).keypress(function(event) { + if (event.which == 13) { + $(this).blur(); + } + }); + $(input).focus(); + }); + } + wrapper.hover(urlHover, function(eventObject) { $(this).children('img.UEB').css('visibility', 'hidden'); }); + }); + + // autocomplete + $(this).find(".edit-message").click(function() { + var container = $(this).parents('.field-value-container'); + var edit = $(this).parents('.field').children('.field-edit'); + var valueList = container.children('.field-values'); + + container.hide(); + edit.show(); + + var items = valueList.children('.field-value-item'); + var values = []; + for(var i = 0; i < items.length; i++) { + values.push($(items.get(i)).text().trim()); + } + + var tokenData = values.map(function(value) { + return {id: value, name: escape(value)}; + }); + + var input = edit.children('input'); + var field = $(this).parents(".field").attr('class').split(' ')[1]; + + input.tokenInput("tags?format=json&field=" + field + "&omit=" + project, { + theme: 'facebook', + prePopulate: tokenData, + autoFocus: true, + submitOnBlur: true, + canBlur: function(elem) { + return !container.find($(elem)).length; + }, + hintText: false, + onSubmit: function(values) { + edit.hide(); + container.show(); + valueList.empty(); + + if(!values.length) { + container.children(".field-value").remove(); + container.prepend($('
none
')); + } + else { + values.forEach(function(value) { + var li = $("
  • ") + .addClass("field-value-item"); + var a = $("") + .attr("href", "?" + field + "=" + value) + .attr("title", "tools with " + field + " " + value) + .text(value) + .appendTo(li); + + if(valueList.length == 0) { + container.children(".field-value").remove(); + valueList = $("
      ") + .prependTo(container); + } + valueList.append(li); + }); + } + + + var data = { + action: 'replace' + }; + data[field] = values.join(","); + + $.post(url, data, function() { + }); + } + }); + }); + }); + }); diff -r 000000000000 -r b0942f44413f toolbox/static/js/queryString.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/static/js/queryString.js Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,33 @@ +function parseQueryString() { + var args = {}; + var searchString = document.location.search.slice(1); + if (!searchString.length) { + // no query string to speak of + return null; + } + if (searchString[searchString.length -1] == '&') { + // remove trailing '&' + searchString = searchString.substr(0, searchString.length-1); + } + + // split by '&' + var params = document.location.search.slice(1).split("&"); + + for (p in params) { + var l = params[p].split("=").map(function(x) { + try { + return decodeURIComponent(x); + } catch(e) { + return x; + } + }); + if (l.length != 2) { + continue; + } + if (!args[l[0]]) { + args[l[0]] = []; + } + args[l[0]].push(l[1]); + } + return args; +} \ No newline at end of file diff -r 000000000000 -r b0942f44413f toolbox/templates/about.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/templates/about.html Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,5 @@ +

      {{title}}

      + +
      +{{about | html}} +
      diff -r 000000000000 -r b0942f44413f toolbox/templates/fields.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/templates/fields.html Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,12 @@ +

      {{item_plural.title()}} by {{field}}

      + +{{for value in sorted(values.keys(), key=lambda x: x.lower())}} +
      +

      {{value}}

      +
        + {{for project in sorted(values[value])}} +
      • {{project}}
      • + {{endfor}} +
      +
      +{{endfor}} diff -r 000000000000 -r b0942f44413f toolbox/templates/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/templates/index.html Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,92 @@ + +{{if len(projects) > 1}} +
      +

      {{len(projects)}} {{item_plural}} + +{{if search}} + matching {{search}} +{{endif}} +{{for loop, key_value in looper(query.items())}} + {{if loop.first}} with {{endif}} + {{py:key, value = key_value}} + + {{key}} + {{isinstance(value, basestring) and value or ', '.join(value)}} + + {{if not loop.last}} + and + {{endif}} +{{endfor}} + +

      + +{{endif}} +
      + +{{if not len(projects)}} +

      No {{item_plural}} found

      +{{endif}} +{{if error}} +

      {{error | html}}

      +{{endif}} + +{{for project in projects}} +
      + + + {{format_date(project['modified'])}} + + + +

      + {{project['name']}} +

      + +

      {{project.get('description', '')}}

      + + {{project['url']}} + + +
        + {{for field in fields}} +
      • +

        {{field}}:

        + + {{if (not field in project) or not project[field]}} +
        none
        + {{else}} +
          + {{for entry in sorted(project[field], key=lambda x: x.lower())}} +
        • + {{entry}} +
        • + {{endfor}} +
        + {{endif}} + +   + +
        + + + +
      • + {{endfor}} +
      + +
      +{{endfor}} + diff -r 000000000000 -r b0942f44413f toolbox/templates/main.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/templates/main.html Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,49 @@ + + + +{{title}} + + +{{for sheet in css}} + +{{endfor}} + +{{for sheet in less}} + +{{endfor}} + +{{for script in js}} + +{{endfor}} + + + + + + +
      + + +
      + {{content | html}} +
      +
      + + + diff -r 000000000000 -r b0942f44413f toolbox/templates/new.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/templates/new.html Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,4 @@ +
      +

      Add a {{item_name}}

      + +
      diff -r 000000000000 -r b0942f44413f toolbox/templates/tags.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/templates/tags.html Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,13 @@ +

      {{title}}

      + +
      + +
      diff -r 000000000000 -r b0942f44413f toolbox/util.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolbox/util.py Sun May 11 09:15:35 2014 -0700 @@ -0,0 +1,59 @@ +""" +utilities for toolbox +""" + +try: + import json +except ImportError: + import simplejson as json + +def strsplit(string): + """sensibly split a comma-separated string""" + string = string.strip() + if not string: + return [] + return [i.strip() for i in string.split(',')] + +def strreplace(string, translation): + """replace substrings from a translation matrix""" + for key, value in translation.items(): + string = string.replace(key, value) + return string + +def str2filename(string): + """converts a string to an acceptable filename""" + matrix = {' ': '_', + '>': '', + '<': '', + "'": '', + '"': '', + '&': '+', + '\\': '', + '\x00': '', + '/': ''} + return strreplace(string, matrix) + + +class JSONEncoder(json.JSONEncoder): + """provide additional serialization for JSON""" + + def default(self, obj): + if hasattr(obj, 'isoformat'): + return obj.isoformat() + if isinstance(obj, set): + return list(obj) + + return json.JSONEncoder.default(self, obj) + +if __name__ == '__main__': + # test the encoder + testjson = {} + + # test date encoding + from datetime import datetime + testjson['date'] = datetime.now() + + # test set encoding + testjson['set'] = set([1,2,3,2]) + + print json.dumps(testjson, cls=JSONEncoder)