Commits

Charles McLaughlin committed b881416

initial commit

  • Participants

Comments (0)

Files changed (46)

+Copyright (c) 2008 Eric Holscher <eric@ericholscher.com> 
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+include LICENSE
+recursive-include kong/templates *.html
+recursive-include kong/templates *.txt
+This project is an interesting combination that work together to solve two problems. The main problem is deployment testing and custom monitoring. It allows you to define a twill test that allows you to test all of the sites you deploy against. These tests could also be used for a monitoring purpose, and run every so often by a system like Nagios.
+
+The documentation is available at django-kong.rtfd.org 
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoKong.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoKong.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."

docs/source/_static/Admin.png

Added
New image

docs/source/_static/Index.png

Added
New image

docs/source/_static/Site Result.png

Added
New image

docs/source/_static/Test.png

Added
New image

docs/source/_static/Type.png

Added
New image

docs/source/conf.py

+# -*- coding: utf-8 -*-
+#
+# Django Kong documentation build configuration file, created by
+# sphinx-quickstart on Wed Nov 18 09:17:59 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.intersphinx']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Django Kong'
+copyright = u'2009, Eric Holscher'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.9'
+# The full version, including alpha/beta/rc tags.
+release = '0.9'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'DjangoKongdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'DjangoKong.tex', u'Django Kong Documentation',
+   u'Eric Holscher', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}

docs/source/index.rst

+Welcome to Django Kong's documentation!
+=======================================
+
+A simple example
+-----------------
+
+You can see a `basic version <http://golem.ericholscher.com/kong/>`_ running for my personal site. It is super barebones, but it should give you an idea of what exactly is possible.
+
+Get the code
+-------------
+
+The `source <http://github.com/ericholscher/django-kong>`_ is available on Github. I would like to thank `Nathan Borror <http://nathanborror.com>`_ for the design parts that are pretty :)
+
+
+Contents
+--------
+
+.. toctree::
+   :maxdepth: 2
+   :glob:
+
+   meta
+   tutorial
+
+What's with the name?
+----------------------
+
+Originally Kong was called paradigm, because it was going to change the way we thought about deployment. After much convincing from coworkers that this was too enterprisey, during the Djangocon 09 sprints, I was given the name Donkey Kong. I thought it would be a fun play on words to name a project django kong, because it sounds like Djangocon, and it plays off of Donkey Kong. Then I just needed to find a way to associate Kong with what the project actually does, because it's a Deployment Testing Tool for King/Donkey Kong sized sites :)

docs/source/meta.rst

+Meta Documentation
+==================
+
+What is the point
+-----------------
+
+Kong came about to solve a problem. At the Lawrence Journal-World, we have over 20 sites that we maintain that run a couple different versions of software that we make. Every time we wanted to push code live, we had no good way of making sure that we didn't break shit, other than hand testing sites or spidering them. Kong is a middle ground in between those 2 approaches, allowing you to specify certain behaviors that you want to test across all of your sites. By using Twill as the language, it lets us do interesting things like fill out forms and follow links, providing you with interesting ways of testing that your sites are functioning correctly after a deployment.
+
+Basic Architecture
+------------------
+
+Kong has two main top-level ideas. `Site`s and `Types`. A `Type` is a collection of `Site`s. When you define a test, you either define it for a Type of site -- which will then apply to every Site in that Type. You can also assign a test to a specific Site, for something that is custom functionality for that site. The idea is to define tests mostly on Type's, which will then automatically be run against all sites added to that Type. Then for special cased functionality, you can run that on only a collection of 1 or 2 sites.

docs/source/tutorial.rst

+Tutorial
+========
+
+
+Getting Started
+---------------
+
+At work we have to manage a ton of Django based sites. Just for our World Company sites, we have over 50 different settings files, and this doesn't take into account the sites that we host for other clients. At this size it becomes basically impossible to test each site in a browser when you push things to production. To solve this problem I have written a very basic server description tool. This allows you to describe sites (settings file, python path, url, etc.) and servers.
+
+.. image:: _static/Admin.png
+
+On top of this base, I have written a way to run tests against these sites. You can categorize the sites by the type of site they are (We have Marketplace, ported Ellington, and old Ellington sites). This allows you to run tests against different types of sites. You may also have custom applications that run on only one or two certain domains. You can specify specific sites for tests to be run against as well.
+
+.. image:: _static/Type.png
+
+The tests are written in `Twill <http://twill.idyll.org/commands.html>`_, which is a simple Python DSL for testing. Twill was chosen because it is really simple, and does functional testing well. The twill tests are actually rendered as Django templates, so you get the site that you are testing against in the context. A simple example that tests the front page of a site is as follows::
+
+    go {{ site.url }}
+    code 200
+    find "Latest News"
+
+This simply loads the Site's front page, checks that the status code was 200, and checks that the string Latest News is on that page. The arguments to find are actually a regex, allowing for lots of power in checking for content.
+
+The interface for this in the Admin is pretty simple.
+
+.. image:: _static/Test.png
+
+You can see that this Test will run against any of the Sites that we have defined in the "Sites in Kansas" Type.
+
+This then gives you the ability to view all of the results for your tests in a web interface. Below is an example of the live view that I see when looking at our servers. We have only just started using Kong, but the tests it provides are really useful to make sure that functionality works after a deployment.
+
+.. image:: _static/Index.png
+
+You can also see the history of a test on a site. Currently it shows the last 15 results, but paginating this page will be easy. It allows you to see if your test has been running well over time. Another nice thing is that it measures the Duration of the test, so that you can see if it is going slow or fast.
+
+.. image:: _static/Site%20Result.png
+
+As you can see, the data display is really basic. It will be improved, but currently its basically the "simplest thing that could possibly work".
+
+
+Using it yourself
+-----------------
+
+When we deploy code changes, I generally run the Kong tests against our sites, making sure that things work. When we launch something new, I will write a kong test to exercise it across all sites. The tests usually take a minute to write, and save lots of time and heart ache, knowing all the sites work.
+
+At the moment the tests can be kicked off by a django management command. The `check_sites` command will allow you to run all of the tests for a given Type or Test. Allowing you to run all of the Ellington tests across all sites, or just run one test across all sites.
+
+..
+     django-admin.py check_sites --type ellington
+     django-admin.py check_sites --test test-front-page
+
+We currently have this wired up to a cron job that runs every 10 minutes. If you set the `KONG_MAIL_MANAGERS` settings to True, it will send an email to the site managers on a test failure. At some point in the future, I will be integrating Kong into Nagios, so that Nagios will handle the running and alerting of errors. That is eventually the way that it will be run.
+
+There are a lot of ways that this can be improved, however in it's current state it works for me. I figured releasing it will allow anyone who needs something like this to be able to use it. There is no documentation or tests, which will be fixed soon! The web display can also be improved a ton, and that is a high priority as well.
+

example_project/__init__.py

Empty file added.

example_project/manage.py

+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

example_project/settings.py

+import os
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+PROJECT_DIR = os.path.dirname(__file__)
+
+ADMINS = (
+    ('Test', 'test@example.com'),
+)
+
+MANAGERS = ADMINS
+DATABASE_ENGINE = 'sqlite3'           # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = os.path.join(PROJECT_DIR, 'kong_db')             # Or path to database file if using sqlite3.
+
+TIME_ZONE = 'America/Chicago'
+LANGUAGE_CODE = 'en-us'
+SITE_ID = 1
+USE_I18N = True
+MEDIA_ROOT = os.path.join(PROJECT_DIR, 'media')
+MEDIA_URL = '/media/'
+ADMIN_MEDIA_PREFIX = '/media/'
+SECRET_KEY = 'BZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ'
+
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+#     'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    #'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+
+ROOT_URLCONF = 'kong.urls'
+
+TEMPLATE_DIRS = (
+    os.path.join(PROJECT_DIR, 'templates')
+)
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.admin',
+    'django.contrib.admindocs',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.humanize',
+    'kong',
+)
+
+
+RUN_ONLINE_TESTS = True

example_project/urls.py

+from django.conf.urls.defaults import *
+from django.conf import settings
+
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+    (r'^admin/', include(admin.site.urls)),
+    (r'', include('kong.urls')),
+    (r'kong/', include('kong.urls')),
+
+)
+
+if settings.DEBUG:
+    urlpatterns += patterns('',
+        (r'^%s/(?P<path>.*)$' % settings.MEDIA_URL.strip('/'), 'django.views.static.serve',
+        {'document_root': settings.MEDIA_ROOT})
+    )
+

kong/__init__.py

Empty file added.
+from django.contrib import admin
+from kong.models import Test, TestResult, Site, Type
+
+class TestResultAdmin(admin.ModelAdmin):
+    search_fields = ('content', 'site__slug')
+    list_filter = ('succeeded',)
+    list_display = ('test', 'site', 'run_date', 'succeeded')
+
+class SiteInline(admin.TabularInline):
+    fields = ('slug', 'is_live', 'servername')
+    list_filter = ('is_live',)
+    model = Site
+
+class TestAdmin(admin.ModelAdmin):
+    search_fields = ('site', 'test')
+    prepopulated_fields = {"slug": ("name",)}
+    save_as = True
+
+class SiteAdmin(admin.ModelAdmin):
+    search_fields = ('servername',)
+    list_display = ('servername', 'slug', 'type')
+    prepopulated_fields = {"slug": ("name",)}
+
+class TypeAdmin(admin.ModelAdmin):
+    search_fields = ('slug', 'name')
+    prepopulated_fields = {"slug": ("name",)}
+    inlines = [
+        SiteInline,
+    ]
+
+admin.site.register(Site, SiteAdmin)
+admin.site.register(Type, TypeAdmin)
+admin.site.register(Test, TestAdmin)
+admin.site.register(TestResult, TestResultAdmin)

kong/fixtures/test_data.json

+[
+    {
+        "pk": 1, 
+        "model": "kong.site", 
+        "fields": {
+            "is_live": true, 
+            "servername": "www2.ljworld.com", 
+            "type": null, 
+            "name": "Ljworld", 
+            "slug": "ljworld"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "kong.site", 
+        "fields": {
+            "is_live": true, 
+            "servername": "ericholscher.com", 
+            "type": 1, 
+            "name": "Eric Holscher", 
+            "slug": "eric-holscher"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "kong.type", 
+        "fields": {
+            "name": "Mine", 
+            "slug": "mine"
+        }
+    }, 
+    {
+        "pk": 1, 
+        "model": "kong.test", 
+        "fields": {
+            "body": "go {{ site.url }}/\r\ncode 200", 
+            "sites": [
+                1
+            ], 
+            "name": "Front Page", 
+            "types": [], 
+            "slug": "front-page"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "kong.test", 
+        "fields": {
+            "body": "go {{ site.url }}/blog/search/?q=awesome\r\ncode 200", 
+            "sites": [], 
+            "name": "Test Search", 
+            "types": [
+                1
+            ], 
+            "slug": "test-search"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "kong.testresult", 
+        "fields": {
+            "run_date": "2010-09-02 00:06:22", 
+            "succeeded": true, 
+            "site": 2, 
+            "content": "", 
+            "duration": 371933, 
+            "test": 2
+        }
+    },
+    {
+        "pk": 2, 
+        "model": "kong.testresult", 
+        "fields": {
+            "run_date": "2010-09-01 23:59:28", 
+            "succeeded": true, 
+            "site": 1, 
+            "content": "", 
+            "duration": 721178, 
+            "test": 1
+        }
+    }
+
+]

kong/management/__init__.py

Empty file added.

kong/management/commands/__init__.py

Empty file added.

kong/management/commands/check_sites.py

+from django.core.management.base import BaseCommand
+from kong.models import Test
+from kong.models import Site, Type
+from kong.utils import run_test, run_tests_for_type, run_tests_for_site, run_tests_for_box
+from optparse import OptionParser, make_option
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option("-t", "--test", dest="test"),
+        make_option("-s", "--site", dest="site"),
+        make_option("-T", "--type", dest="type"),
+        make_option("-l", "--list", dest="list", action="store_true", default=False),
+    )
+
+    def handle(self, *args, **options):
+        TEST = options.get('test')
+        SITE = options.get('site')
+        TYPE = options.get('type')
+        LIST = options.get('list')
+
+        passed =  True
+        if TEST:
+            print "Running test: %s" % TEST
+            test = Test.objects.get(slug=TEST)
+            passed = run_test(test)
+        elif TYPE:
+            print "Running tests for type : %s" % TYPE
+            type = Type.objects.get(slug=TYPE)
+            passed = run_tests_for_type(type)
+        elif SITE:
+            print "Running tests for site : %s" % SITE
+            site = Site.objects.get(slug=SITE)
+            passed = run_tests_for_site(site)
+        elif LIST:
+            print "All Sites:"
+            for site in Site.objects.all():
+                print site.slug
+            print "All Tests:"
+            for test in Test.objects.all():
+                print test.slug
+        else:
+            print "No action"
+            return 0
+
+        if passed:
+            return 0
+        else:
+            return 2
+from django.db import models
+from django.template import Template, Context
+from django.db.models import permalink
+from django.contrib.localflavor.us import models as USmodels
+import datetime
+import urlparse
+
+class Site(models.Model):
+    name = models.CharField(max_length=80, blank=True)
+    slug = models.SlugField()
+    type = models.ForeignKey('Type', related_name='sites', null=True, blank=True)
+    servername = models.CharField(max_length=100, default='example.com',
+                                  help_text='This is the address of your actual site')
+    is_live = models.BooleanField(default=True)
+
+    def __unicode__(self):
+        return "%s: %s" % (self.slug, self.servername)
+
+    @permalink
+    def get_absolute_url(self):
+        return ('kong_site_detail', [self.slug])
+
+    @property
+    def url(self):
+        curr_site = self.servername
+        if urlparse.urlsplit(curr_site).scheme == '':
+            curr_site = "http://%s" % curr_site
+        return curr_site
+
+    @property
+    def all_tests(self):
+        return Test.objects.filter(sites=self) | Test.objects.filter(types=self.type)
+
+    def latest_results(self):
+        """
+        This returns a list of the latest testresult for each test
+        defined for a site.
+        """
+        ret_val = []
+        for test in self.all_tests.all():
+            try:
+                latest_result = test.test_results.filter(site=self)[0]
+                ret_val.append(latest_result)
+            except IndexError:
+                #No result for test
+                pass
+        return ret_val
+
+class Type(models.Model):
+    name = models.CharField(max_length=40)
+    slug = models.SlugField(blank=True)
+
+    def __unicode__(self):
+        return self.name
+
+    def all_sites(self):
+        return self.sites.all()
+
+class Test(models.Model):
+    name = models.CharField(max_length=250)
+    slug = models.SlugField(blank=True)
+    sites = models.ManyToManyField(Site, blank=True, null=True, related_name='tests')
+    types = models.ManyToManyField(Type, blank=True, null=True, related_name='tests')
+    body = models.TextField()
+
+    def __unicode__(self):
+        return self.name
+
+    def render(self, site):
+        return Template(self.body).render(Context({'site': site, 'test': self})).encode()
+
+    @permalink
+    def get_absolute_url(self):
+        return ('kong_testresults_detail', [self.slug])
+
+    @property
+    def all_sites(self):
+        return self.sites.all() | Site.objects.filter(type__in=self.types.all())
+
+
+class TestResult(models.Model):
+    test = models.ForeignKey(Test, related_name='test_results')
+    site = models.ForeignKey(Site, related_name='test_results')
+    run_date = models.DateTimeField(default=datetime.datetime.now, db_index=True)
+    duration = models.IntegerField(null=True)
+    succeeded = models.BooleanField()
+    content = models.TextField()
+
+    class Meta:
+        ordering = ('-run_date',)
+
+    def __unicode__(self):
+        return "%s for %s" % (self.test, self.site)
+
+    @permalink
+    def get_absolute_url(self):
+        return ('kong_testresults_detail', [self.slug])

kong/plugins/__init__.py

Empty file added.

kong/plugins/kong_munin.py

+from kong.models import TestResult, Test
+import munin
+
+def slugify(string):
+    return string.replace('-','_')
+
+class KongDuration(munin.Plugin):
+    tests = Test.objects.all()
+
+    def fetch(self):
+        limit = len(self.tests) - 1
+        results = TestResult.objects.filter(site__pk=2)[:limit]
+        for result in results:
+            yield ('%s.value' % slugify(result.test.slug), result.duration)
+    
+    def config(self):
+        yield ('graph_title', 'LJWorld')
+        yield ('graph_args', '-l 0 --base 1000')
+        yield ('graph_vlabel', 'Duration')
+        yield ('graph_scale', 'no')
+        yield ('graph_category', 'kong')
+        yield ('graph_info', 'Shows the duration of Kong tests.')
+        for test in  self.tests:
+            yield ("%s.label" % slugify(test.slug), test.name)
+            yield ("%s.info" % slugify(test.slug), test.name)
+            yield ("%s.draw" % slugify(test.slug), "LINE1")
+ 
+if __name__ == '__main__':
+    munin.run(KongDuration)

kong/plugins/munin.py

+"""
+Originally from: http://github.com/jacobian/munin-plugins/blob/master/munin.py
+
+Microframework for writing Munin plugins with Python.
+
+Howto:
+
+    * Subclass ``Plugin``.
+    
+    * Define at least ``fetch`` and ``config``, and possibly others (see
+      below).
+    
+    * Add a main invocation to your script.
+
+A simple example::
+
+    import munin
+    
+    class Load(munin.Plugin):
+        
+        def fetch(self):
+            load1, load5, load15 = open("/proc/loadavg").split(' ')[:3]
+            return [
+                ("load1.value", load1),
+                ("load5.value", load5),
+                ("load15.value", load15)
+            ]
+            
+        def config(self):
+            return [
+                ("graph_title", "Load"),
+                ("graph_args", "-l 0 --base 1000"),
+                ("graph_vlabel", "Load"),
+                ("load1.label", "1 min"),
+                ("load5.label", "5 min"),
+                ("load15.label", "15 min")
+            ]
+
+    if __name__ == '__main__':
+        munin.run(Load)
+
+For more complex uses, read the code. It's short.
+"""
+
+import os
+import sys
+
+class Plugin(object):
+    
+    def __init__(self):
+        self.env = {}
+        for var, default in self.__get_dynamic_attr("env_vars", None, {}).items():
+            self.env[var] = os.environ.get(var, default)
+        
+    def __get_dynamic_attr(self, attname, arg, default=None):
+        """
+        Gets "something" from self, which could be an attribute or
+        a callable with either 0 or 1 arguments (besides self).
+        
+        Stolen from django.contrib.syntication.feeds.Feed.
+        """
+        try:
+            attr = getattr(self, attname)
+        except AttributeError:
+            return default
+        if callable(attr):
+            # Check func_code.co_argcount rather than try/excepting the
+            # function and catching the TypeError, because something inside
+            # the function may raise the TypeError. This technique is more
+            # accurate.
+            if hasattr(attr, 'func_code'):
+                argcount = attr.func_code.co_argcount
+            else:
+                argcount = attr.__call__.func_code.co_argcount
+            if argcount == 2: # one argument is 'self'
+                return attr(arg)
+            else:
+                return attr()
+        return attr
+    
+    def main(self, argv):
+        if "_" in argv[0]: 
+            _, arg = argv[0].rsplit("_", 1)
+        else:
+            arg = None
+        args = argv[1:]
+        
+        if "suggest" in args and hasattr(self, "suggest"):
+            for suggested in self.__get_dynamic_attr("suggest", arg):
+                print suggested
+            return 0
+                
+        if "autoconf" in args:
+            if self.__get_dynamic_attr("autoconf", arg, True):
+                print "yes"
+                return 0
+            else:
+                print "no"
+                return 1
+                
+        if "config" in args:
+            for field, value in self.__get_dynamic_attr("config", arg, []):
+                print "%s %s" % (field, value)
+            return 0
+        
+        for field, value in self.__get_dynamic_attr("fetch", arg, []):
+            print "%s %s" % (field, value)
+        return 0
+            
+def run(plugin):
+    if callable(plugin):
+        plugin = plugin()
+    sys.exit(plugin.main(sys.argv))

kong/templates/base.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+   "http://www.w3.org/TR/html4/strict.dtd">
+
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+  <title>{% block title %}{% endblock %} | Django Kong</title>
+  <script src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js" type="text/javascript" charset="utf-8"></script>
+  <script src="http://flot.googlecode.com/svn/trunk/jquery.flot.js" type="text/javascript" charset="utf-8"></script>
+  <script src="http://flot.googlecode.com/svn/trunk/jquery.flot.selection.js" type="text/javascript" charset="utf-8"></script>
+   <style type="text/css" media="screen">
+    body { padding: 5px; font-family: 'Helvetica-Neue', helvetica, arial, sans-serif; background: #eee; }
+    a { text-decoration: none; }
+
+    .site { display: inline-block; margin: 2px 2px 5px 2px; padding: 6px 2px; background: #fff; border: 1px solid #ddd; -webkit-border-radius: 6px; -moz-border-radius: 6px; -webkit-box-shadow: 1px 1px 3px #ddd; vertical-align: top; }
+    .site h3 { margin: 0; padding: 3px 0; font-size: 14px; line-height: 20px; color: #333; text-align: center; }
+    .site h3 a { margin: 0; padding: 3px 0; font-size: 14px; line-height: 20px; color: #333; text-align: center; }
+
+    .sites p { margin: 0; font-size: 16px; color: #333; }
+    .sites p a { display: block; margin: 0 5px; padding: 8px; border: 1px solid #ddd; border-bottom: none; background: #eee; color: #333; }
+    .sites p a small { display: block; margin-top: 3px; font-size: 12px; color: #999; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
+    .sites p a strong { display: block; margin-bottom: 3px; font-size: 15px; color: #999; }
+    .sites p a em { font-weight: bold; font-style: normal; }
+
+    .sites p.first a { -webkit-border-top-left-radius: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-topleft: 4px; -moz-border-radius-topright: 4px; }
+    .sites p.last a { border-bottom: 1px solid #c8de9d; -webkit-border-bottom-left-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomleft: 4px; -moz-border-radius-bottomright: 4px; }
+
+    .sites p a:hover { background: #175e99 !important; border-color: #175e99 !important; color: #fff !important; }
+    .sites p a:hover small,
+    .sites p a:hover strong { color: #fff !important; }
+
+    .sites p a.passed { background: #eeffcc; border-color: #c8de9d; }
+    .sites p a.passed small { color: #616b4c; }
+    .sites p a.passed strong { color: #98a978; }
+
+    .sites p a.succeeded { background: #cc4949; border-color: #ad3e3e; }
+    .sites p a.succeeded small,
+    .sites p a.succeeded strong { color: #fff; }
+    .executed { margin: 9px; padding: 5px; background-color: #ddd; }
+    .sites pre { margin: 5px; }
+    .test-content { margin: 9px; padding: 5px; background-color: #ddd; }
+    </style>
+  {% block extra_head %}
+  {% endblock %}
+</head>
+<body class="{% block body_class %}{% endblock %}" id="{% block body_id %}{% endblock %}">
+<a href="{% url kong_index %}">Home</a> | 
+<a href="{% url kong_dashboard %}">Dashboard</a> | 
+<a href="{% url kong_failed %}">Failed</a>
+{% block content %}
+{% endblock %}
+</body>
+</html>

kong/templates/base_kong.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+   "http://www.w3.org/TR/html4/strict.dtd">
+
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+  <title>{% block title %}{% endblock %} | Django Kong</title>
+  <script src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js" type="text/javascript" charset="utf-8"></script>
+  <script src="http://flot.googlecode.com/svn/trunk/jquery.flot.js" type="text/javascript" charset="utf-8"></script>
+  <script src="http://flot.googlecode.com/svn/trunk/jquery.flot.selection.js" type="text/javascript" charset="utf-8"></script>
+   <style type="text/css" media="screen">
+    body { padding: 5px; font-family: 'Helvetica-Neue', helvetica, arial, sans-serif; background: #eee; }
+    a { text-decoration: none; }
+
+    .site { display: inline-block; margin: 2px 2px 5px 2px; padding: 6px 2px; background: #fff; border: 1px solid #ddd; -webkit-border-radius: 6px; -moz-border-radius: 6px; -webkit-box-shadow: 1px 1px 3px #ddd; vertical-align: top; }
+    .site h3 { margin: 0; padding: 3px 0; font-size: 14px; line-height: 20px; color: #333; text-align: center; }
+
+    .sites p { margin: 0; font-size: 16px; color: #333; }
+    .sites p a { display: block; margin: 0 5px; padding: 8px; border: 1px solid #ddd; border-bottom: none; background: #eee; color: #333; }
+    .sites p a small { display: block; margin-top: 3px; font-size: 12px; color: #999; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
+    .sites p a strong { display: block; margin-bottom: 3px; font-size: 15px; color: #999; }
+    .sites p a em { font-weight: bold; font-style: normal; }
+
+    .sites p.first a { -webkit-border-top-left-radius: 4px; -webkit-border-top-right-radius: 4px; -moz-border-radius-topleft: 4px; -moz-border-radius-topright: 4px; }
+    .sites p.last a { border-bottom: 1px solid #c8de9d; -webkit-border-bottom-left-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomleft: 4px; -moz-border-radius-bottomright: 4px; }
+
+    .sites p a:hover { background: #175e99 !important; border-color: #175e99 !important; color: #fff !important; }
+    .sites p a:hover small,
+    .sites p a:hover strong { color: #fff !important; }
+
+    .sites p a.passed { background: #eeffcc; border-color: #c8de9d; }
+    .sites p a.passed small { color: #616b4c; }
+    .sites p a.passed strong { color: #98a978; }
+
+    .sites p a.succeeded { background: #cc4949; border-color: #ad3e3e; }
+    .sites p a.succeeded small,
+    .sites p a.succeeded strong { color: #fff; }
+    .executed { margin: 9px; padding: 5px; background-color: #ddd; }
+    .sites pre { margin: 5px; }
+    .test-content { margin: 9px; padding: 5px; background-color: #ddd; }
+    </style>
+  {% block extra_head %}
+  {% endblock %}
+</head>
+<body class="{% block body_class %}{% endblock %}" id="{% block body_id %}{% endblock %}">
+{% block content %}
+{% endblock %}
+</body>
+</html>

kong/templates/kong/dashboard.html

+{% extends "base.html" %}
+
+{% block title %} Latest Results {% endblock %}
+
+{% block content %}
+{% load kong_tags %}
+
+<div class="sites">
+  {% for site_slug, succeeded in results.items %}
+  <div class="site">
+  <h3> <a href="{% url kong_site_detail site_slug %}">{{ site_slug }}</a> </h3>
+    <p class="first">
+    <a class="{{ succeeded|yesno:"passed,succeeded" }}" href="{% url kong_site_detail site_slug %}">
+        <strong>{{ succeeded|yesno:"PASSED,FAILED" }}</strong>
+      </a>
+    </p>
+  </div>
+  {% endfor %}
+</div>
+
+{% endblock %}

kong/templates/kong/failed.html

+{% extends "base.html" %}
+
+{% block title %} Latest Results {% endblock %}
+
+{% block content %}
+{% load kong_tags %}
+
+<div class="sites">
+  <div class="site">
+    {% for result in results %}
+  <h3>
+{{ result.site.slug }} : {{ result.test }}
+ </h3>
+    <p>
+      <a class="{{ result.succeeded|yesno:"passed,succeeded" }}" href="{% url kong_testresult_for_site result.site.slug result.test.slug %}" title="{{ result.site.url }}">
+        <strong>{{ result.succeeded|yesno:"PASSED,FAILED" }}</strong>
+        <small><em>{{ result.test.name }}</em> | {{ result.run_date|timesince }} ago</small>
+        <small>Duration (ms): {{ result.duration|micro_to_milli }}</small>
+      </a>
+    </p>
+    {% endfor %}
+  </div>
+</div>
+
+{% endblock %}

kong/templates/kong/failed_email.txt

+Your test {{ test.name }} has failed.
+
+It was running against the site {{ kong_site.name }}.
+
+Check it out here: http://{{ real_site.domain }}{% url kong_test_detail test.slug test.pk %}
+
+The error generated was:
+{{ error }}
+
+The test run was:
+
+{{ test.body }}

kong/templates/kong/graph_test.html

+{% extends "base.html" %}
+
+{% block title %}
+Results for {{ test }}
+{% endblock %}
+
+{% block content %}
+{% load kong_tags %}
+
+<div class="sites">
+<h1> {{ test }} </h1>
+  {% for site in sites %}
+  <div class="site">
+      <h3> {{ site.slug }} </h3>
+      <div class="placeholder" id="placeholder-{{ site.slug }}" style="position:relative; width:640px; height:280px;"></div>
+  </div>
+  {% endfor %}
+</div>
+
+
+<script type="text/javascript">
+{% for site_test_slug, results in flot_list %}
+$(function () {
+    var data = [ {{ results }} ]
+
+    var options = {
+        label: '{{ result.test.name }}',
+        points: { show: true },
+        lines: { show: true },
+        xaxis: { mode: "time" },
+        yaxis: { min: 0, max: 1000 },
+        selection: { mode: "x" },
+        grid: { hoverable: true, clickable: true },
+    };
+
+    var plot = $.plot($("#placeholder-{{ site_test_slug }}"), data , options );
+
+    function showTooltip(x, y, contents, id) {
+        $('<div id="' + id + '">' + contents + '</div>').css( {
+            position: 'absolute',
+            display: 'none',
+            top: y + 5,
+            left: x + 5,
+            border: '1px solid #fdd',
+            padding: '2px',
+            'background-color': '#fee',
+            opacity: 0.80
+        }).appendTo("body").fadeIn(200);
+    }
+
+    var previousPoint = null;
+    $("#placeholder-{{ site_test_slug }}").bind("plothover", function (event, pos, item) {
+
+        if (item) {
+            if (previousPoint != item.datapoint) {
+                previousPoint = item.datapoint;
+
+                $("#tooltip").remove();
+                var x = item.datapoint[0].toFixed(2),
+                    y = item.datapoint[1].toFixed(2);
+	        var d = new Date();
+	        d.setTime(x);
+
+                showTooltip(item.pageX, item.pageY, d.toLocaleString() + " : " + y, 'tooltip');
+            }
+        }
+        else {
+            $("#tooltip").remove();
+            previousPoint = null;
+        }
+    });
+});
+{% endfor %}
+</script>
+{% endblock %}
+

kong/templates/kong/index.html

+{% extends "base.html" %}
+
+{% block title %} Latest Results {% endblock %}
+
+{% block content %}
+{% load kong_tags %}
+
+<div class="sites">
+  {% for site_slug, testresults in results %}
+  <div class="site">
+  <h3> <a href="{% url kong_site_detail site_slug %}">{{ site_slug }}</a> </h3>
+    {% for result in testresults %}
+    {{ result.name }}
+    <p class="{% if forloop.last %}last{% endif %}{% if forloop.first %} first{% endif %}">
+      <a class="{{ result.succeeded|yesno:"passed,succeeded" }}" href="{% url kong_testresult_for_site result.site.slug result.test.slug %}" title="{{ result.site.url }}">
+        <strong>{{ result.succeeded|yesno:"PASSED,FAILED" }}</strong>
+        <small><em>{{ result.test.name }}</em> | {{ result.run_date|date:"g:iA"|lower }}</small>
+        <small>Duration (ms): {{ result.duration|micro_to_milli }}</small>
+      </a>
+      <div class="placeholder" id="placeholder-{{ result.site.slug }}-{{ result.test.slug }}" style="position:relative; width:200px; height:40px;"></div>
+    </p>
+    {% endfor %}
+  </div>
+  {% endfor %}
+</div>
+
+<script type="text/javascript">
+{% for site_test_slug, results in flot_list.items %}
+    $(function () {
+            var data = [{{ results }}]
+            var options = {
+                xaxis: {
+                    ticks: [],
+                    mode: "time"
+                },
+                yaxis: {
+                    ticks: []
+                },
+                grid: {
+                    color: "#fff",
+                    hoverable: true,
+                    clickable: true
+                },
+            }
+
+            $.plot($("#placeholder-{{ site_test_slug }}"), data, options );
+    });
+{% endfor %}
+
+function showTooltip(x, y, contents, id) {
+    $('<div id="' + id + '">' + contents + '</div>').css( {
+        position: 'absolute',
+        display: 'none',
+        top: y + 5,
+        left: x + 5,
+        border: '1px solid #fdd',
+        padding: '2px',
+        'background-color': '#fee',
+        opacity: 0.80
+    }).appendTo("body").fadeIn(200);
+}
+
+var previousPoint = null;
+$(".placeholder").bind("plothover", function (event, pos, item) {
+
+    if (item) {
+        if (previousPoint != item.datapoint) {
+            previousPoint = item.datapoint;
+
+            $("#tooltip").remove();
+            var x = item.datapoint[0].toFixed(2),
+                y = item.datapoint[1].toFixed(2);
+
+            showTooltip(item.pageX, item.pageY, y, 'tooltip');
+        }
+    }
+    else {
+        $("#tooltip").remove();
+        previousPoint = null;
+    }
+});
+
+</script>
+
+{% endblock %}

kong/templates/kong/test_detail.html

+{% extends "base.html" %}
+
+{% block title %}
+Results for {{ result }}
+{% endblock %}
+
+{% block content %}
+{% load kong_tags %}
+<h1> {{ result.test }} </h1>
+{% if result.site.name %}
+<h2> {{ result.site.name }} </h2>
+{% else %}
+<h2> {{ result.site.slug }} </h2>
+{% endif %}
+
+<div class="sites">
+<div class="site">
+    <p>
+    <a class="{{ result.succeeded|yesno:"passed,succeeded" }}" title="{{ result.site.url }}">
+            <strong>{{ result.succeeded|yesno:"PASSED,FAILED" }}</strong>
+            <small>run {{ result.run_date|timesince }} ago </small>
+            <small>Duration (ms) {{ result.duration|micro_to_milli }}</small>
+        </a>
+    </p>
+{% if result.content %}
+<div class="test-content">
+<strong> Content </strong>
+<pre>
+{{ result.content }}
+</pre>
+</div>
+{% endif %}
+<div class="executed">
+<strong> Code Executed </strong>
+<pre>
+{{ result|render_twill|urlize }}
+</pre>
+</div>
+</div>
+
+{% endblock %}

kong/templates/kong/test_list_for_site.html

+{% extends "base.html" %}
+
+{% block title %}
+Results for {{ result }}
+{% endblock %}
+
+{% block content %}
+{% load kong_tags %}
+<h1> {{ result.test }} </h1>
+{% if result.site.name %}
+<h2> {{ result.site.name }} </h2>
+{% else %}
+<h2> {{ result.site.slug }} </h2>
+{% endif %}
+<h5> <a href="{% url kong_run_test_on_site result.site.slug result.test.slug %}">Run This Test</a> </h5>
+
+<div class="sites">
+<div class="site">
+    <h3> Latest Run </h3>
+    <p>
+    <a href="{% url kong_test_detail result.test.slug result.pk %}" class="{{ result.succeeded|yesno:"passed,succeeded" }}" title="{{ result.site.url }}">
+            <strong>{{ result.succeeded|yesno:"PASSED,FAILED" }}</strong>
+            <small>run at {{ result.run_date|date:"g:iA"|lower }}</small>
+            <small>Duration (ms) {{ result.duration|micro_to_milli }}</small>
+        </a>
+    </p>
+{% if result.content %}
+<div class="test-content">
+<strong> Content </strong>
+<pre>
+{{ result.content }}
+</pre>
+</div>
+{% endif %}
+<div class="executed">
+<strong> Code Executed </strong>
+<pre>
+{{ result|render_twill|urlize }}
+</pre>
+</div>
+<center><strong> Duration </strong></center>
+<div id="placeholder" style="position:relative; width:640px; height:280px;"> </div>
+<div id="overview" style="margin-left:50px;margin-top:20px;width:400px;height:50px"></div>
+
+</div>
+</div>
+
+
+<script type="text/javascript">
+$(function () {
+    var data = [ {{ flot_list }} ]
+
+    var options = {
+        label: '{{ result.test.name }}',
+        points: { show: true },
+        lines: { show: true },
+        xaxis: { mode: "time" },
+        selection: { mode: "x" },
+        grid: { hoverable: true, clickable: true },
+    };
+
+    var plot = $.plot($("#placeholder"), data , options );
+
+    var overview = $.plot($("#overview"), data, {
+        series: {
+            lines: { show: true, lineWidth: 1 },
+            shadowSize: 0
+        },
+        xaxis: { ticks: [], mode: "time" },
+        yaxis: { ticks: [], autoscaleMargin: 0.1 },
+        selection: { mode: "x" }
+    });
+    $("#placeholder").bind("plotselected", function (event, ranges) {
+        // do the zooming
+        plot = $.plot($("#placeholder"), data,
+                      $.extend(true, {}, options, {
+                          xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }
+                      }));
+
+        // don't fire event on the overview to prevent eternal loop
+        overview.setSelection(ranges, true);
+    });
+
+    $("#overview").bind("plotselected", function (event, ranges) {
+        plot.setSelection(ranges);
+    });
+
+    function showTooltip(x, y, contents, id) {
+        $('<div id="' + id + '">' + contents + '</div>').css( {
+            position: 'absolute',
+            display: 'none',
+            top: y + 5,
+            left: x + 5,
+            border: '1px solid #fdd',
+            padding: '2px',
+            'background-color': '#fee',
+            opacity: 0.80
+        }).appendTo("body").fadeIn(200);
+    }
+
+    var previousPoint = null;
+    $("#placeholder").bind("plothover", function (event, pos, item) {
+
+        if (item) {
+            if (previousPoint != item.datapoint) {
+                previousPoint = item.datapoint;
+
+                $("#tooltip").remove();
+                var x = item.datapoint[0].toFixed(2),
+                    y = item.datapoint[1].toFixed(2);
+
+                showTooltip(item.pageX, item.pageY, y, 'tooltip');
+            }
+        }
+        else {
+            $("#tooltip").remove();
+            previousPoint = null;
+        }
+    });
+
+
+    // This would allow you to click a spot and have the
+    // number stay, but it stays on zooming and looks odd.
+    /*
+    $("#placeholder").bind("plotclick", function (event, pos, item) {
+        if (item) {
+            plot.highlight(item.series, item.datapoint);
+            showTooltip(item.pageX, item.pageY, item.datapoint[1].toFixed(2), 'fancy');
+        }
+    });
+    */
+
+
+
+});
+</script>
+
+{% comment %}
+{% load charts %}
+{% chart %}
+  {% chart-size "600x400" %}
+  {% chart-type "line" %}
+  {% chart-title "Duration over time" 18 "cc0000" %}
+  {% chart-data duration_list %}
+  {% axis "left" %}
+    {% axis-range "0" "1" %}
+  {% endaxis %}
+
+  {% chart-range-marker "h" "000000" 1 .997 %}
+{% endchart %}
+{% endcomment %}
+
+{% endblock %}

kong/templatetags/__init__.py

Empty file added.

kong/templatetags/kong_tags.py

+from django import template
+
+register = template.Library()
+
+@register.filter
+def micro_to_milli(value):
+    return value/1000
+
+@register.filter
+def render_twill(result):
+    return result.test.render(result.site)

kong/tests/__init__.py

+from model_tests import *

kong/tests/model_tests.py

+from django.test import TestCase
+from django.conf import settings
+from django.core import mail
+
+from kong.models import Test, Site, TestResult
+from kong.utils import execute_test, _send_error
+
+class SanitizeTest(TestCase):
+
+    fixtures = ['test_data.json']
+
+    def setUp(self):
+
+        self.test = Test.objects.get(slug='front-page')
+        self.site = self.test.sites.all()[0]
+
+    def test_results(self):
+        self.assertEqual(str(self.site.latest_results()), '[<TestResult: Front Page for ljworld: www2.ljworld.com>]')
+
+    def test_sites(self):
+        self.assertEqual(str(self.test.all_sites), '[<Site: ljworld: www2.ljworld.com>]')
+
+    def test_sending_errors(self):
+        settings.KONG_MAIL_MANAGERS = True
+        _send_error(self.site, self.test, 'Awesome stuffs')
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertTrue('example.com' in mail.outbox[0].body)
+
+    def test_execution(self):
+        if getattr(settings, 'RUN_ONLINE_TESTS', False):
+            result = execute_test(self.site, self.test)
+            #If this fails because our sites are down, I'm sorry :D
+            self.assertTrue(result)
+        else:
+            print "WARNING: Skipping online tests. Set RUN_ONLINE_TESTS to True in your settings to run them"
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+     (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+     (r'^admin/', include(admin.site.urls)),
+     url(r'^$', 'kong.views.index', name='kong_index'),
+     url(r'index/$', 'kong.views.index', name='kong_index'),
+     url(r'failed/$', 'kong.views.failed', name='kong_failed'),
+     url(r'dashboard/$', 'kong.views.dashboard', name='kong_dashboard'),
+
+     url(r'^sites/(?P<site_slug>.*?)/(?P<test_slug>.*?)/run/$',
+         'kong.views.run_test_on_site',
+         name='kong_run_test_on_site'
+         ),
+     url(r'^sites/(?P<site_slug>.*?)/(?P<test_slug>.*?)/',
+         'kong.views.test_detail_for_site',
+         name='kong_testresult_for_site'
+         ),
+     url(r'^sites/(?P<site_slug>.*?)/',
+         'kong.views.site_detail',
+         name='kong_site_detail'
+         ),
+     url(r'^tests/(?P<test_slug>.*?)/(?P<num_total>\d+)/(?P<div_by>\d+)/',
+         'kong.views.graph_test',
+         name='kong_graph_test'
+         ),
+     url(r'^tests/(?P<test_slug>.*?)/(?P<pk>\d+)/',
+         'kong.views.test_detail',
+         name='kong_test_detail'
+         ),
+)
+import datetime
+import StringIO
+import sys
+
+from django.conf import settings
+from django.core.mail import mail_managers, mail_admins
+from django.template.loader import render_to_string
+from django.contrib.sites.models import Site
+
+from kong.models import Test, TestResult
+from twill.parse import execute_string
+from twill.errors import TwillAssertionError
+
+def _send_error(kong_site, test, content):
+    real_site = Site.objects.get_current()
+    message = render_to_string('kong/failed_email.txt', {'kong_site': kong_site,
+                                                         'test': test,
+                                                         'error': content,
+                                                         'real_site': real_site})
+    if getattr(settings, 'KONG_MAIL_MANAGERS', False):
+        mail_managers('Kong Test Failed: %s (%s)' % (test, kong_site), message)
+    if getattr(settings, 'KONG_MAIL_ADMINS', False):
+        mail_admins('Kong Test Failed: %s (%s)' % (test, kong_site), message)
+
+def execute_test(site, test):
+    import twill.commands as commands
+    twill_script = test.render(site)
+    content = ''
+    old_err = sys.stderr
+    new_err = StringIO.StringIO()
+    commands.ERR = new_err
+
+    now = datetime.datetime.now()
+    try:
+        execute_string(twill_script)
+        succeeded = True
+        content = new_err.getvalue().strip()
+    except Exception, e:
+        succeeded = False
+        content = new_err.getvalue().strip() + "\n\nException:\n\n" + str(e)
+        _send_error(site, test, content)
+
+    end = datetime.datetime.now()
+    duration = end - now
+    duration = duration.microseconds
+    commands.ERR = old_err
+    TestResult.objects.create(site=site,
+                              test=test,
+                              succeeded=succeeded,
+                              duration=duration,
+                              content=content)
+    return succeeded
+
+def run_tests_for_type(type):
+    all_passed = True
+    for site in type.all_sites():
+        for test in site.tests.all():
+            passed = execute_test(site, test)
+            all_passed = passed and all_passed
+
+        for test in type.tests.all():
+            try:
+                passed = execute_test(site, test)
+                all_passed = passed and all_passed
+            except Exception, e:
+                print e
+                print "Moving on"
+    return all_passed
+
+def run_test_for_type(type, test):
+    all_passed = True
+    for site in type.all_sites():
+        passed = execute_test(site, test)
+        all_passed = passed and all_passed
+    return all_passed
+
+def run_tests_for_site(site):
+    print "Running all tests for site: %s" % site
+    all_passed = True
+    for test in site.tests.all():
+        passed = execute_test(site, test)
+        all_passed = passed and all_passed
+    return all_passed
+
+def run_test_for_site(site, test):
+    return execute_test(site, test)
+
+def run_tests_for_box(box):
+    all_passed = True
+    for site in box.sites.all():
+        passed = run_tests_for_site(site)
+        all_passed = passed and all_passed
+
+    return all_passed
+
+def run_test(test):
+    print "Running all tests for %s" % test
+    sites = test.sites.all()
+    types = test.types.all()
+    all_passed = True
+
+    #Run test for the sites it points to
+    for site in sites:
+        passed = execute_test(site, test)
+        all_passed = passed and all_passed
+
+    #Run tests for the types of sites it points to
+    for type in types:
+        passed  = run_test_for_type(type, test)
+        all_passed = passed and all_passed
+
+    return all_passed
+import calendar
+from collections import defaultdict
+import itertools
+
+from django.shortcuts import render_to_response
+from django.template.context import RequestContext
+from django.views.generic import list_detail
+
+from kong.models import TestResult, Test
+from kong.utils import execute_test
+from kong.models import Site, Type
+
+def _render_to_result_list(request, sites, template_name='kong/index.html'):
+    ret_val = defaultdict(list)
+    flot_val = {}
+    for site in sites:
+        results = site.latest_results()
+        ret_val[site.slug].extend(results)
+        for result in results:
+            flot_val["%s-%s" % (result.site.slug, result.test.slug)] = flotify(result)
+    return render_to_response(template_name,
+                       {'results': ret_val.items(),
+                        'flot_list': flot_val},
+                       context_instance=RequestContext(request))
+
+def split_seq(iterable, size):
+    it = iter(iterable)
+    item = list(itertools.islice(it, size))
+    while item:
+        yield item
+        item = list(itertools.islice(it, size))
+
+
+def get_timestamp(time):
+    #Need this for flot timestamps..
+    return calendar.timegm(time.timetuple()) * 1000
+
+def flotify(result, num=50):
+    """
+    Return a list of (timestamp, duration) sets for test result.
+    """
+    results = list(TestResult.objects.filter(test=result.test, site=result.site)[:num])
+    results.reverse()
+    return [[get_timestamp(result.run_date), result.duration/1000] for result in results]
+
+def graphify(sites, test, num_total, div_by):
+    num_split = int(num_total)/int(div_by)
+    flot_val = defaultdict(list)
+    for site in sites:
+        tests = list(TestResult.objects.filter(test=test, site=site)[:num_total])
+        tests.reverse()
+        for result_list in split_seq(tests, num_split):
+            time = sum([result.duration/1000 for result in result_list])/len(result_list)
+            flot_val[site.slug].append([get_timestamp(result_list[0].run_date), time])
+    return flot_val
+
+
+def index(request):
+    sites = Site.objects.all()
+    return _render_to_result_list(request, sites)
+
+def site_detail(request, site_slug):
+    sites = Site.objects.filter(slug=site_slug)
+    return _render_to_result_list(request, sites)
+
+def test_detail(request, test_slug, pk):
+    result = TestResult.objects.get(pk=pk)
+    return render_to_response('kong/test_detail.html',
+                       {'result': result},
+                       context_instance=RequestContext(request))
+
+def test_detail_for_site(request, site_slug, test_slug):
+    test = Test.objects.get(slug=test_slug)
+    site = Site.objects.get(slug=site_slug)
+    result = TestResult.objects.filter(test=test, site=site)[0]
+    flot_list = flotify(result)
+    return render_to_response('kong/test_list_for_site.html',
+                       {'result': result,
+                        'flot_list': flot_list
+                        },
+                       context_instance=RequestContext(request))
+
+def run_test_on_site(request, site_slug, test_slug):
+    test = Test.objects.get(slug=test_slug)
+    site = Site.objects.get(slug=site_slug)
+    execute_test(site, test)
+    return test_detail_for_site(request, site_slug, test_slug)
+
+
+def graph_test(request, test_slug, num_total=5000, div_by=50):
+    test = Test.objects.get(slug=test_slug)
+    sites = test.all_sites.all()
+    flot_val = graphify(sites, test, num_total, div_by)
+    return render_to_response('kong/graph_test.html',
+                              {'sites': list(sites),
+                               'flot_list': flot_val.items(),
+                                'test': test,
+                              },
+                              context_instance=RequestContext(request))
+
+
+
+def dashboard(request):
+    ret_val = {}
+    for site in Site.objects.all():
+        results = site.latest_results()
+        succ = True
+        for result in results:
+            if not result.succeeded:
+                succ = False
+                fail = result
+        ret_val[site.slug] = succ
+    return render_to_response('kong/dashboard.html',
+                       {'results': ret_val},
+                       context_instance=RequestContext(request))
+
+def failed(request):
+    results = TestResult.objects.filter(succeeded=False)[:20]
+    return render_to_response('kong/failed.html',
+                       {'results': results},
+                       context_instance=RequestContext(request))
+from django.db import connection
+
+cursor = connection.cursor()
+cursor.execute('alter table kong_site add column "servername" varchar(100)')
+cursor.execute('alter table kong_site drop column client_id')
+cursor.execute('alter table kong_site drop column settings')
+cursor.execute('alter table kong_site drop column pythonpath')
+cursor.execute('select site_ptr_id, servername from kong_hostedsite')
+rows = cursor.fetchall()
+names = dict([(row[0], row[1]) for row in rows])
+
+from kong.models import Site
+for site in Site.objects.all():
+    site.servername = names[site.pk]
+    site.save()
+
+"""
+cursor.execute('delete table kong_alias')
+cursor.execute('delete table kong_client')
+cursor.execute('delete table kong_deploytarget')
+cursor.execute('delete table kong_deploytarget_servers')
+cursor.execute('delete table kong_hostedsite')
+cursor.execute('delete table kong_hostedsite_on_servers')
+cursor.execute('delete table kong_server')
+cursor.execute('delete table kong_test_sites')
+"""
+twill
+from distutils.core import setup
+
+setup(
+    name = "django-kong",
+    version = "0.9",
+    packages = [
+        "kong",
+        "kong.management",
+        "kong.management.commands",
+        "kong.templatetags",
+        "kong.tests",
+    ],
+    author = "Eric Holscher",
+    author_email = "eric@ericholscher.com",
+    description = "A server description and deployment testing tool for King Kong sized sites",
+    url = "http://github.com/ericholscher/django-kong/tree/master",
+    package_data = {
+        'kong': [
+            'templates/*.html',
+            'templates/kong/*.html',
+            'templates/kong/*.txt',
+        ],
+    },
+)