Commit f35b32eb by Gabriel Falcão

huge refactoring in documentation

parent 65daa975
......@@ -12,6 +12,8 @@ PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
export PYTHONPATH:= ${PYTHONPATH}:${PWD}/../:${PWD}/_ext
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
......
from sphinx.writers import html as sphinx_htmlwriter
class LettuceHTMLTranslator(sphinx_htmlwriter.SmartyPantsHTMLTranslator):
"""
Lettuce-customized HTML transformations of documentation. Based on
djangodocs.DjangoHTMLTranslator
"""
def visit_section(self, node):
node['ids'] = map(lambda x: "lettuce-%s" % x, node['ids'])
sphinx_htmlwriter.SmartyPantsHTMLTranslator.visit_section(self, node)
"""
Sphinx plugins for Django documentation.
"""
import docutils.nodes
import docutils.transforms
import sphinx
import sphinx.addnodes
try:
from sphinx import builders
except ImportError:
import sphinx.builder as builders
import sphinx.directives
import sphinx.environment
try:
import sphinx.writers.html as sphinx_htmlwriter
except ImportError:
import sphinx.htmlwriter as sphinx_htmlwriter
import sphinx.roles
from docutils import nodes
def setup(app):
app.add_crossref_type(
directivename = "setting",
rolename = "setting",
indextemplate = "pair: %s; setting",
)
app.add_crossref_type(
directivename = "templatetag",
rolename = "ttag",
indextemplate = "pair: %s; template tag"
)
app.add_crossref_type(
directivename = "templatefilter",
rolename = "tfilter",
indextemplate = "pair: %s; template filter"
)
app.add_crossref_type(
directivename = "fieldlookup",
rolename = "lookup",
indextemplate = "pair: %s; field lookup type",
)
app.add_description_unit(
directivename = "django-admin",
rolename = "djadmin",
indextemplate = "pair: %s; django-admin command",
parse_node = parse_django_admin_node,
)
app.add_description_unit(
directivename = "django-admin-option",
rolename = "djadminopt",
indextemplate = "pair: %s; django-admin command-line option",
parse_node = parse_django_adminopt_node,
)
app.add_config_value('django_next_version', '0.0', True)
app.add_directive('versionadded', parse_version_directive, 1, (1, 1, 1))
app.add_directive('versionchanged', parse_version_directive, 1, (1, 1, 1))
app.add_transform(SuppressBlockquotes)
# Monkeypatch PickleHTMLBuilder so that it doesn't die in Sphinx 0.4.2
if sphinx.__version__ == '0.4.2':
monkeypatch_pickle_builder()
def parse_version_directive(name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
env = state.document.settings.env
is_nextversion = env.config.django_next_version == arguments[0]
ret = []
node = sphinx.addnodes.versionmodified()
ret.append(node)
if not is_nextversion:
if len(arguments) == 1:
linktext = 'Please, see the release notes <releases-%s>' % (arguments[0])
xrefs = sphinx.roles.xfileref_role('ref', linktext, linktext, lineno, state)
node.extend(xrefs[0])
node['version'] = arguments[0]
else:
node['version'] = "Development version"
node['type'] = name
if len(arguments) == 2:
inodes, messages = state.inline_text(arguments[1], lineno+1)
node.extend(inodes)
if content:
state.nested_parse(content, content_offset, node)
ret = ret + messages
env.note_versionchange(node['type'], node['version'], node, lineno)
return ret
class SuppressBlockquotes(docutils.transforms.Transform):
"""
Remove the default blockquotes that encase indented list, tables, etc.
"""
default_priority = 300
suppress_blockquote_child_nodes = (
docutils.nodes.bullet_list,
docutils.nodes.enumerated_list,
docutils.nodes.definition_list,
docutils.nodes.literal_block,
docutils.nodes.doctest_block,
docutils.nodes.line_block,
docutils.nodes.table
)
def apply(self):
for node in self.document.traverse(docutils.nodes.block_quote):
if len(node.children) == 1 and isinstance(node.children[0], self.suppress_blockquote_child_nodes):
node.replace_self(node.children[0])
class DjangoHTMLTranslator(sphinx_htmlwriter.SmartyPantsHTMLTranslator):
"""
Django-specific reST to HTML tweaks.
"""
# Don't use border=1, which docutils does by default.
def visit_table(self, node):
self.body.append(self.starttag(node, 'table', CLASS='docutils'))
# <big>? Really?
def visit_desc_parameterlist(self, node):
self.body.append('(')
self.first_param = 1
def depart_desc_parameterlist(self, node):
self.body.append(')')
pass
#
# Don't apply smartypants to literal blocks
#
def visit_literal_block(self, node):
self.no_smarty += 1
sphinx_htmlwriter.SmartyPantsHTMLTranslator.visit_literal_block(self, node)
def depart_literal_block(self, node):
sphinx_htmlwriter.SmartyPantsHTMLTranslator.depart_literal_block(self, node)
self.no_smarty -= 1
#
# Turn the "new in version" stuff (versoinadded/versionchanged) into a
# better callout -- the Sphinx default is just a little span,
# which is a bit less obvious that I'd like.
#
# FIXME: these messages are all hardcoded in English. We need to chanage
# that to accomodate other language docs, but I can't work out how to make
# that work and I think it'll require Sphinx 0.5 anyway.
#
version_text = {
'deprecated': 'Deprecated in Django %s',
'versionchanged': 'Changed in Django %s',
'versionadded': 'New in Django %s',
}
def visit_versionmodified(self, node):
self.body.append(
self.starttag(node, 'div', CLASS=node['type'])
)
title = "%s%s" % (
self.version_text[node['type']] % node['version'],
len(node) and ":" or "."
)
self.body.append('<span class="title">%s</span> ' % title)
def depart_versionmodified(self, node):
self.body.append("</div>\n")
# Give each section a unique ID -- nice for custom CSS hooks
# This is different on docutils 0.5 vs. 0.4...
if hasattr(sphinx_htmlwriter.SmartyPantsHTMLTranslator, 'start_tag_with_title') and sphinx.__version__ == '0.4.2':
def start_tag_with_title(self, node, tagname, **atts):
node = {
'classes': node.get('classes', []),
'ids': ['s-%s' % i for i in node.get('ids', [])]
}
return self.starttag(node, tagname, **atts)
else:
def visit_section(self, node):
old_ids = node.get('ids', [])
node['ids'] = ['s-' + i for i in old_ids]
if sphinx.__version__ != '0.4.2':
node['ids'].extend(old_ids)
sphinx_htmlwriter.SmartyPantsHTMLTranslator.visit_section(self, node)
node['ids'] = old_ids
def parse_django_admin_node(env, sig, signode):
command = sig.split(' ')[0]
env._django_curr_admin_command = command
title = "django-admin.py %s" % sig
signode += sphinx.addnodes.desc_name(title, title)
return sig
def parse_django_adminopt_node(env, sig, signode):
"""A copy of sphinx.directives.CmdoptionDesc.parse_signature()"""
from sphinx import addnodes
from sphinx.directives.desc import option_desc_re
count = 0
firstname = ''
for m in option_desc_re.finditer(sig):
optname, args = m.groups()
if count:
signode += addnodes.desc_addname(', ', ', ')
signode += addnodes.desc_name(optname, optname)
signode += addnodes.desc_addname(args, args)
if not count:
firstname = optname
count += 1
if not firstname:
raise ValueError
return firstname
def monkeypatch_pickle_builder():
import shutil
from os import path
try:
import cPickle as pickle
except ImportError:
import pickle
from sphinx.util.console import bold
def handle_finish(self):
# dump the global context
outfilename = path.join(self.outdir, 'globalcontext.pickle')
f = open(outfilename, 'wb')
try:
pickle.dump(self.globalcontext, f, 2)
finally:
f.close()
self.info(bold('dumping search index...'))
self.indexer.prune(self.env.all_docs)
f = open(path.join(self.outdir, 'searchindex.pickle'), 'wb')
try:
self.indexer.dump(f, 'pickle')
finally:
f.close()
# copy the environment file from the doctree dir to the output dir
# as needed by the web app
shutil.copyfile(path.join(self.doctreedir, builders.ENV_PICKLE_FILENAME),
path.join(self.outdir, builders.ENV_PICKLE_FILENAME))
# touch 'last build' file, used by the web application to determine
# when to reload its environment and clear the cache
open(path.join(self.outdir, builders.LAST_BUILD_FILENAME), 'w').close()
builders.PickleHTMLBuilder.handle_finish = handle_finish
/**
* Sphinx stylesheet -- basic theme
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-size: 1em;
}
img {
border: 0;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable dl, table.indextable dd {
margin-top: 0;
margin-bottom: 0;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
/* -- general body styles --------------------------------------------------- */
a.headerlink {
visibility: hidden;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.field-list ul {
padding-left: 1em;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px 7px 0 7px;
background-color: #ffe;
width: 40%;
float: right;
}
p.sidebar-title {
font-weight: bold;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px 7px 0 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
div.admonition dl {
margin-bottom: 0;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
border: 0;
border-collapse: collapse;
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 0;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
table.field-list td, table.field-list th {
border: 0 !important;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
}
/* -- other body styles ----------------------------------------------------- */
dl {
margin-bottom: 15px;
}
dd p {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dt:target, .highlight {
background-color: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.refcount {
color: #060;
}
.optional {
font-size: 1.3em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
}
td.linenos pre {
padding: 5px 0px;
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
margin-left: 0.5em;
}
table.highlighttable td {
padding: 0 0.5em 0 0.5em;
}
tt.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
}
tt.descclassname {
background-color: transparent;
}
tt.xref, a tt {
background-color: transparent;
font-weight: bold;
}
h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
background-color: transparent;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}
@import url(reset-fonts-grids.css);
@import url(djangodocs.css);
@import url(homepage.css);
\ No newline at end of file
#index p.rubric { font-size:150%; font-weight:normal; margin-bottom:.2em; color:#252; }
#index div.section dt { font-weight: normal; }
#index #s-nutshell, #index #s-getting-involved {
background:none repeat scroll 0 0 #F0F7F0;
margin: 2em 0 2em 2em;
border-radius:6px 6px;
-webkit-border-radius:6px;
-moz-border-radius:6px 6px;
box-shadow:0 1px 2px rgba(0, 0, 0, 0.6);
-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.6);
-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.6);
overflow:visible;
float:right;
clear:both;
margin:2em 0 2em 2em;
padding:1em;
width: 400px;
}
#index #s-nutshell, #index #s-getting-involved a {color: #386848}
#index #s-nutshell, #index #s-getting-involved h1 {color: #333}
#index #s-nutshell, #index #s-getting-involved h2 { margin: 0; }
#index #s-nutshell, #index #s-getting-involved strong { font-size: 20px;}
#index #s-getting-involved {
padding: 2em 0 2em 6em;
background:#FFFFD0;
width: 356px;
}
#index #s-getting-involved h1 {
font-weight: bold;
margin: 0;
}
#index #s-getting-involved a {
font-weight: bold;
font-size: 14px;
color: #666630;
}
/*** setup ***/
@import url(reset-fonts-grids.css);
html, body {background: url(./background.jpg)}
body { font:14px Helvetica, arial, sans-serif; color:black}
body {font:14px Helvetica, arial, sans-serif; color:black}
#custom-doc { width:76.54em;*width:74.69em;min-width:995px; max-width:100em; margin:auto; text-align:left; padding-top:16px; margin-top:0}
#hd { padding: 4px 0 12px 0}
#bd { background:#C5D88A}
......@@ -171,4 +172,45 @@ div#contents ul ul li { margin-top: 0.3em}
img {
margin: 10px 0 10px 0;
}
\ No newline at end of file
}#index p.rubric { font-size:150%; font-weight:normal; margin-bottom:.2em; color:#252; }
#index div.section dt { font-weight: normal; }
#index #lettuce-nutshell, #index #lettuce-getting-involved {
background:none repeat scroll 0 0 #F0F7F0;
margin: 2em 0 2em 2em;
border-radius:6px 6px;
-webkit-border-radius:6px;
-moz-border-radius:6px 6px;
box-shadow:0 1px 2px rgba(0, 0, 0, 0.6);
-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.6);
-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.6);
overflow:visible;
float:right;
clear:both;
margin:2em 0 2em 2em;
padding:1em;
width: 400px;
}
#index #lettuce-nutshell, #index #lettuce-getting-involved a {color: #386848}
#index #lettuce-nutshell, #index #lettuce-getting-involved h1 {color: #333}
#index #lettuce-nutshell, #index #lettuce-getting-involved h2 { margin: 0; }
#index #lettuce-nutshell, #index #lettuce-getting-involved strong { font-size: 20px;}
#index #lettuce-getting-involved {
padding: 2em 0 2em 6em;
background:#FFFFD0;
width: 356px;
}
#index #lettuce-getting-involved h1 {
font-weight: bold;
margin: 0;
}
#index #lettuce-getting-involved a {
font-weight: bold;
font-size: 14px;
color: #666630;
}
{% extends "!layout.html" %}
{%- macro secondnav() %}
{%- if prev %}
&laquo; <a href="{{ prev.link|e }}" title="{{ prev.title|e }}">previous</a>
......
# -*- coding: utf-8 -*-
# <Lettuce - Behaviour Driven Development for python>
# Copyright (C) <2010> Gabriel Falcão <gabriel@nacaolivre.org>
#
# Django documentation build configuration file, created by
# sphinx-quickstart on Thu Mar 27 09:06:53 2008.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This file is execfile()d with the current directory set to its containing dir.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# The contents of this file are pickled, so don't put values in the namespace
# that aren't pickleable (module imports are okay, they're removed automatically).
#
# All configuration values have a default value; values that are commented out
# serve to show the default value.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from lettuce import version, release
import sys
from os.path import dirname, abspath, join
# If your extensions are in another directory, add it here.
_localfile = lambda *x: join(abspath(dirname(__file__)), *x)
sys.path.append(_localfile("_ext"))
sys.path.append(_localfile("../"))
copyright = u'Gabriel Falcão <gabriel@nacaolivre.org>'
# General configuration
# ---------------------
import lettuce
# 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.autodoc',
'sphinx.ext.doctest',
......@@ -30,95 +26,24 @@ extensions = [
'sphinx.ext.coverage'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'contents'
# General substitutions.
project = 'Lettuce'
copyright = u'Gabriel Falcão <gabriel@nacaolivre.org>'
# The default replacements for |version| and |release|, also used in various
# other places throughout the built documents.
#
# The short X.Y version.
version = lettuce.version
# The full version, including alpha/beta/rc tags.
release = "%s (%s release)" % (version, lettuce.release)
# 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.
release = "%s (%s release)" % (version, release)
today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# 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 = False
# 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.
add_module_names = True
show_authors = True
html_use_smartypants = True
html_copy_source = True
pygments_style = 'bw'
# Sphinx will recurse into subversion configuration folders and try to read
# any document file within. These should be ignored.
# Note: exclude_dirnames is new in Sphinx 0.5
exclude_dirnames = ['.git']
# Options for HTML output
# -----------------------
# The style sheet to use for HTML and HTML Help pages. A file of that name
# must exist either in Sphinx' static/ path, or in one of the custom paths
# given in html_static_path.
html_style = 'default.css'
# 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_style = 'lettuce-docs.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
# HTML translator class for the builder
html_translator_class = "djangodocs.DjangoHTMLTranslator"
# Content template for the index page.
#html_index = ''
# 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_translator_class = "adjusts.LettuceHTMLTranslator"
html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If true, the reST sources are included in the HTML build as _sources/<name>.
html_copy_source = True
# Output file base name for HTML help builder.
htmlhelp_basename = 'Lettucedoc'
.. _tutorial-tables:
.. rubric:: All you need to know, from leaves to root
Handling data with tables
=========================
......@@ -12,7 +13,7 @@ the new state of those.
It means that as you write tests with lettuce, it can be very useful
to handle data within steps.
Step tables are here for you:
Step tables are here for you
.. highlight:: ruby
......@@ -28,22 +29,16 @@ Step tables are here for you:
| name | monthly due | billed |
| Anton | $ 500 | no |
| Jack | $ 400 | no |
| John | $ 540 | no |
| Garrick | $ 438.11 | no |
| Gabriel | $ 300 | no |
| Gloria | $ 442.65 | no |
| Gaye | $ 630.22 | no |
| Ken | $ 907.86 | no |
| Leonard | $ 742.84 | no |
When I bill names starting with "G"
Then I see those billed students:
| Garrick | $ 438.11 | yes |
| Gabriel | $ 300 | yes |
| Gloria | $ 442.65 | yes |
| Gaye | $ 630.22 | yes |
| Gabriel | $ 300 | no |
| Gloria | $ 442.65 | no |
And those that weren't:
| Anton | $ 500 | no |
| Jack | $ 400 | no |
| John | $ 540 | no |
| Ken | $ 907.86 | no |
| Leonard | $ 742.84 | no |
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment