Rewrite to Django from scratch. Now it's much more user-friendly. v0.2
authorW. Trevor King <wking@drexel.edu>
Thu, 4 Aug 2011 19:39:18 +0000 (15:39 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 4 Aug 2011 19:40:02 +0000 (15:40 -0400)
Bump to version 0.2 (retroactively tagging the previous version as 0.1).

The flask icon is from DesignKode

From their website [1]:

DesignKode is releasing this set of 40 free high quality icons for
your web site and application GUI designs. All icons in this set are
32 x 32 pixel PNG image files. You may freely use these icons in your
commercial or personal projects without attribution.

[1] http://www.designkode.com/blog/free-developer-icons

30 files changed:
.gitignore
.htaccess [deleted file]
README
bin/cook.py [deleted file]
cookbook/__init__.py
cookbook/admin.py [new file with mode: 0644]
cookbook/cookbook.py [deleted file]
cookbook/models.py [new file with mode: 0644]
cookbook/mom.py [deleted file]
cookbook/server.py [deleted file]
cookbook/template/base.html [deleted file]
cookbook/template/edit-recipe.html [deleted file]
cookbook/template/recipe.html [deleted file]
cookbook/template/recipes.html [deleted file]
cookbook/tests.py [new file with mode: 0644]
cookbook/urls.py [new file with mode: 0644]
cookbook/views.py [new file with mode: 0644]
doc/abbreviations [deleted file]
example/__init__.py [new file with mode: 0644]
example/data/static/cookbook.ico [new file with mode: 0644]
example/data/static/style.css [new file with mode: 0644]
example/data/template/admin/edit_inline/tabular.html [new file with mode: 0644]
example/data/template/cookbook/base.html [new file with mode: 0644]
example/data/template/cookbook/recipe.html [new file with mode: 0644]
example/data/template/cookbook/recipes.html [new file with mode: 0644]
example/data/template/cookbook/tags.html [new file with mode: 0644]
example/manage.py [new file with mode: 0644]
example/settings.py [new file with mode: 0644]
example/urls.py [new file with mode: 0644]
setup.py [new file with mode: 0755]

index 58e2130cc5c3cc1a829710ab58f160bc384a6ef0..f5d4d3d16e0f991dfedf3b07551357828ad4f72d 100644 (file)
@@ -1,3 +1,5 @@
+MANIFEST
+sqlite3.db
+dist
+build
 *.pyc
-recipe
-mom*
diff --git a/.htaccess b/.htaccess
deleted file mode 100644 (file)
index 50c2d6b..0000000
--- a/.htaccess
+++ /dev/null
@@ -1 +0,0 @@
-guest:cookbook:8eb3d4841608f011af3dd4bdc4c8b340
diff --git a/README b/README
index 152440d6e32f9109d9b3e8ee759d59249eaa52ee..e7515633db52e1e3188bd1c51350221c06db95b9 100644 (file)
--- a/README
+++ b/README
@@ -1,13 +1,60 @@
-Cookbook provides a Python interface to your recipes so you can throw
-out all those index cards.
+Cookbook is a recipe manager written in Python_ using the Django_
+framework.
 
-The entry script:
-  ./bin/cook.py
-fires up the server, binding it to a local interface/port.  It also
-translates cookbook formats, e.g. from my mom's rough text file to the
-more robust directory of YAML files that Cookbook uses internally.
+Install
+=======
 
-The recipes themselves will generally be stored in per-recipe YAML
-files under 'recipe'.
+Download
+--------
 
-Send any bug reports to W. Trevor King <wking@drexel.edu>.
+Cookbook is published as a Git_ repository.  See `cookbook's web
+interface`__ for more information.
+
+__ cookbook_
+
+Dependencies
+------------
+
+Outside of Django_ and the Python_ standard libraries, the only
+required dependency is `django-taggit`_ (docs__).
+
+__ dt2-docs_
+
+Quick-start
+===========
+
+If you don't have a Django project and you just want to run cookbook as
+a stand-alone service, you can use the example project written up in
+`example`.  Set up the project (once)::
+
+  $ python example/manage.py syncdb
+
+See the `Django documentation`_ for more details.
+
+Run
+===
+
+Run the app on your local host (as many times as you like)::
+
+  $ python example/manage.py runserver
+
+You may need to add the current directory to `PYTHONPATH` so `python`
+can find the `cookbook` package.  If you're running `bash`, that will
+look like
+
+  $ PYTHONPATH=".:$PYTHONPATH" python example/manage.py runserver
+
+Hacking
+=======
+
+This project was largely build following the `Django tutorial`_.
+That's a good place to start if you're new to Django.
+
+.. _Python: http://www.python.org/
+.. _Django: https://www.djangoproject.com/
+.. _Git: http://git-scm.com/
+.. _cookbook: http://physics.drexel.edu/~wking/code/git/gitweb.cgi?p=cookbook.git
+.. _django-taggit: https://github.com/alex/django-taggit
+.. _dt2-docs: http://django-taggit.readthedocs.org/en/latest/
+.. _Django documentation: https://docs.djangoproject.com/
+.. _Django tutorial: https://docs.djangoproject.com/en/1.3/intro/tutorial01/
diff --git a/bin/cook.py b/bin/cook.py
deleted file mode 100755 (executable)
index a832e33..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/python
-
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of Cookbook.
-#
-# Cookbook 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.
-#
-# Cookbook 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.
-#
-# You should have received a copy of the GNU General Public License
-# along with Cookbook.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import with_statement
-
-import os
-import os.path
-import uuid
-
-import cookbook
-from cookbook import Cookbook
-from cookbook.mom import MomParser
-from cookbook.cookbook import test as test_cookbook
-
-try:
-    import jinja2
-    import cherrypy
-    from cookbook.server import Server
-    from cookbook.server import test as test_server
-except ImportError, e:
-    cherrypy = e
-    def test_server():
-        pass
-
-
-if __name__ == '__main__':
-    import optparse
-    import sys
-
-    p = optparse.OptionParser()
-    p.add_option('-t', '--test', dest='test', default=False,
-                 action='store_true', help='run internal tests and exit')
-    p.add_option('-m', '--mom', dest='mom', metavar='PATH',
-                 help="load Mom's cookbook")
-    p.add_option('-s', '--serve', dest='serve', default=False,
-                 action='store_true', help='serve cookbook')
-    p.add_option('-a', '--address', dest='address', default='127.0.0.1',
-                 metavar='ADDR',
-                 help='address that the server will bind to (%default)')
-    p.add_option('-p', '--port', dest='port', default='8080',
-                 metavar='PORT',
-                 help='port that the server will listen on (%default)')
-
-    options,arguments = p.parse_args()
-
-    if options.test == True:
-        test_cookbook()
-        test_server()
-        sys.exit(0)
-
-    if isinstance(cherrypy, ImportError):
-        raise cherrypy
-
-    sys.stderr.write('Loading cookbook\n')
-    if options.mom == None:
-        c = Cookbook()
-        c.load()
-    else:
-        p = MomParser()
-        c = p.parse(options.mom)
-
-    if options.serve == False:
-        sys.stderr.write('Saving cookbook\n')
-        c.save('new-recipe')
-    else:
-        # HACK! to ensure we *always* get utf-8 output
-        #reload(sys)
-        #sys.setdefaultencoding('utf-8')
-        sys.stderr.write('Serving cookbook\n')
-        module_dir = os.path.dirname(os.path.abspath(cookbook.__file__))
-        static_dir = os.path.join(module_dir, 'static')
-        if not os.path.exists(static_dir):
-            os.mkdir(static_dir)
-        template_dir = os.path.join(module_dir, 'template')
-        config = os.path.join(module_dir, 'config')
-        s = Server(c, template_dir)
-        if cherrypy.__version__.startswith('3.'):
-            cherrypy.config.update({ # http://www.cherrypy.org/wiki/ConfigAPI
-                    'server.socket_host': options.address,
-                    'server.socket_port': int(options.port),
-                    'tools.decode.on': True,
-                    'tools.encode.on': True,
-                    'tools.encode.encoding': 'utf8',
-                    'tools.staticdir.root': static_dir,
-                    })
-            if cherrypy.__version__.startswith('3.2'):
-                get_ha1 = cherrypy.lib.auth_digest.get_ha1_file_htdigest(
-                    '.htaccess')
-                digest_auth = {
-                    'tools.auth_digest.on': True,
-                    'tools.auth_digest.realm': 'cookbook',
-                    'tools.auth_digest.get_ha1': get_ha1,
-                    'tools.auth_digest.key': str(uuid.uuid4()),
-                    }
-            else:
-                passwds = {}
-                with open('.htaccess', 'r') as f:
-                    for line in f:
-                        user,realm,ha1 = line.strip().split(':')
-                        passwds[user] = ha1  # use the ha1 as the password
-                digest_auth = {
-                    'tools.digest_auth.on': True,
-                    'tools.digest_auth.realm': 'cookbook',
-                    'tools.digest_auth.users': passwds,
-                    }
-                print passwds
-                del(passwds)
-            app_config = {
-                '/static': {
-                    'tools.staticdir.on': True,
-                    'tools.staticdir.dir': '',
-                    },
-                '/edit': digest_auth,
-                '/add_tag': digest_auth,
-                '/remove_tag': digest_auth,
-                }
-            cherrypy.quickstart(root=s, config=app_config)
-        elif cherrypy.__version__.startswith('2.'):
-            cherrypy.root = s
-            cherrypy.config.update({
-                    'server.environment': 'production',
-                    'server.socket_host': options.address,
-                    'server.socket_port': int(options.port),
-                    'decoding_filter.on': True,
-                    'encoding_filter.on': True,
-                    'encodinf_filter.encoding': 'utf8',
-                    'static_filter.on': True,
-                    'static_filter.dir': static_dir,
-                    })
-            import pprint
-            pprint.pprint(cherrypy.config.configs)
-            cherrypy.server.start()
-        s.cleanup()
index 920f373620e424a0dac4d5bea8128c84a4d7e8a2..b650ceb084b2938790ee0f4366f5e989dfa3930d 100644 (file)
@@ -1,11 +1 @@
-"""Cookbook package, for manipulating and serving cookbooks.
-
-Submodules:
-* cookbook, basic cookbook classes
-* server, for serving a cookbook over HTTP
-* mom, for parsing my mom's cookbook format
-"""
-
-from .cookbook import Cookbook
-
-__all__ = {'Cookbook':Cookbook}
+__version__ = '0.2'
diff --git a/cookbook/admin.py b/cookbook/admin.py
new file mode 100644 (file)
index 0000000..cb6a1b1
--- /dev/null
@@ -0,0 +1,55 @@
+from django import forms
+from django.contrib import admin
+from django.db import models as django_models
+
+import models
+
+
+class IngredientInline (admin.TabularInline):
+    model = models.Ingredient
+    extra = 0
+
+
+class IngredientBlockAdmin (admin.ModelAdmin):
+    fieldsets = [
+        (None, {'fields': ['name']}),
+        ('Directions', {'fields': ['directions_markdown'],
+                        'classes': ['collapse']}),
+        ]
+    inlines = [IngredientInline]
+
+    list_display = ['name', 'recipe']
+    extra = 0
+
+
+class IngredientBlockInline (admin.TabularInline):
+    model = models.IngredientBlock
+    fieldsets = [
+        (None, {'fields': ['name']}),
+        ]
+    inlines = [IngredientInline]
+
+    list_display = ['name']
+    extra = 0
+    show_edit_link = True  # https://code.djangoproject.com/ticket/13163
+    # work around 13163
+    #template = 'admin/edit_inline/tabular-13163.html'
+
+class RecipeAdmin (admin.ModelAdmin):
+    fieldsets = [
+        (None, {'fields': ['name']}),
+        ('Metadata', {'fields': ['author', 'source', 'url', 'x_yield', 'tags'],
+                      'classes': ['collapse']}),
+        ('Directions', {'fields': ['directions_markdown']}),
+        ]
+    inlines = [IngredientBlockInline]
+
+    list_display = ['name']
+
+
+admin.site.register(models.Recipe, RecipeAdmin)
+admin.site.register(models.IngredientBlock, IngredientBlockAdmin)
+admin.site.register(models.Amount)
+admin.site.register(models.Unit)
+admin.site.register(models.UnitSystem)
+admin.site.register(models.UnitType)
diff --git a/cookbook/cookbook.py b/cookbook/cookbook.py
deleted file mode 100644 (file)
index 24a70fe..0000000
+++ /dev/null
@@ -1,578 +0,0 @@
-#!/usr/bin/python
-# -*- encoding: utf-8 -*-
-#
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of Cookbook.
-#
-# Cookbook 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.
-#
-# Cookbook 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.
-#
-# You should have received a copy of the GNU General Public License
-# along with Cookbook.  If not, see <http://www.gnu.org/licenses/>.
-
-"""Represent a cookbook and recipes with Python classes.
-"""
-
-from __future__ import with_statement
-
-import os
-import os.path
-import textwrap
-import types
-from urllib import quote_plus
-
-import yaml
-
-
-def string_for_yaml(unicode_):
-    """
-    >>> string_for_yaml(None)
-    >>> string_for_yaml('all ascii')
-    'all ascii'
-    >>> string_for_yaml(u'all ascii')
-    'all ascii'
-    >>> string_for_yaml(u'½ ascii')
-    u'\\xc2\\xbd ascii'
-    """
-    if unicode_ == None:
-        return unicode_
-    try:
-        string = unicode_.encode('ascii')
-        return string
-    except UnicodeEncodeError:
-        return unicode_
-
-def to_yaml_object(obj):
-    """
-    >>> to_yaml_object(None)
-    >>> to_yaml_object('all ascii')
-    'all ascii'
-    >>> to_yaml_object(u'all ascii')
-    'all ascii'
-    >>> to_yaml_object('all ascii')
-    'all ascii'
-    >>> to_yaml_object(u'½ ascii')
-    u'\\xc2\\xbd ascii'
-    >>> class x (object):
-    ...     def to_yaml(self):
-    ...         return 'to_yaml return value'
-    >>> to_yaml_object(x())
-    'to_yaml return value'
-    >>> to_yaml_object([u'all ascii', u'½ ascii'])
-    ['all ascii', u'\\xc2\\xbd ascii']
-    """
-    if obj == None:
-        return obj
-    if hasattr(obj, 'to_yaml'):
-        return obj.to_yaml()
-    if type(obj) in types.StringTypes:
-        return string_for_yaml(obj)
-    if hasattr(obj, '__len__'):
-        ret = []
-        for item in obj:
-            ret.append(to_yaml_object(item))
-        return ret
-    raise NotImplementedError(
-        'cannot convert %s to YAMLable dict:\n%s' % (type(obj), unicode(obj)))
-
-
-class Amount (object):
-    """
-    >>> import pprint
-    >>> a = Amount(value='1', units='T.')
-    >>> str(a)
-    '1 T.'
-    >>> pprint.pprint(a.to_yaml())
-    {'alternatives': [], 'range_': None, 'units': 'T.', 'value': '1'}
-    >>> x = Amount()
-    >>> x.from_yaml(a.to_yaml())
-    >>> str(x)
-    '1 T.'
-    >>> b = Amount(value='1')
-    >>> str(b)
-    '1'
-    >>> pprint.pprint(b.to_yaml())
-    {'alternatives': [], 'range_': None, 'units': None, 'value': '1'}
-    >>> x.from_yaml(b.to_yaml())
-    >>> str(x)
-    '1'
-    >>> c = Amount(value='15', units='mL', alternatives=[a])
-    >>> str(c)
-    '15 mL (1 T.)'
-    >>> pprint.pprint(c.to_yaml())
-    {'alternatives': [{'alternatives': [],
-                       'range_': None,
-                       'units': 'T.',
-                       'value': '1'}],
-     'range_': None,
-     'units': 'mL',
-     'value': '15'}
-    >>> x.from_yaml(c.to_yaml())
-    >>> str(x)
-    '15 mL (1 T.)'
-    >>> d = Amount(units='T.', range_=['1','2'])
-    >>> str(d)
-    '1-2 T.'
-    >>> pprint.pprint(d.to_yaml())
-    {'alternatives': [], 'range_': ['1', '2'], 'units': 'T.', 'value': None}
-    >>> x.from_yaml(d.to_yaml())
-    >>> str(x)
-    '1-2 T.'
-    """
-    def __init__(self, value=None, units=None, range_=None, alternatives=None):
-        self.value = value
-        self.units = units
-        self.range_ = range_
-        if alternatives == None:
-            alternatives = []
-        self.alternatives = alternatives
-
-    def __str__(self):
-        return str(self.__unicode__())
-
-    def __unicode__(self):
-        if self.range_ == None:
-            value = self.value
-        else:
-            value = '-'.join(self.range_)
-        ret = [value]
-        if self.units != None:
-            ret.append(self.units)
-        if len(self.alternatives) > 0:
-            ret.append('(%s)' % ', '.join(
-                    [unicode(a) for a in self.alternatives]))
-        return ' '.join([x for x in ret if x != None])
-
-    def to_yaml(self):
-        d = {}
-        for key in ['value', 'range_', 'units', 'alternatives']:
-            d[key] = to_yaml_object(getattr(self, key))
-        return d
-
-    def from_yaml(self, dict):
-        for key in ['value', 'range_', 'units']:
-            setattr(self, key, dict.get(key, None))
-        self.alternatives = []
-        for a in dict.get('alternatives', []):
-            amount = Amount()
-            amount.from_yaml(a)
-            self.alternatives.append(amount)
-
-
-class Ingredient (object):
-    """
-    >>> import pprint
-    >>> i = Ingredient('eye of newt', Amount('1'))
-    >>> str(i)
-    '1 eye of newt'
-    >>> pprint.pprint(i.to_yaml())
-    {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '1'},
-     'name': 'eye of newt',
-     'note': None}
-    >>> x = Ingredient()
-    >>> x.from_yaml(i.to_yaml())
-    >>> str(x)
-    '1 eye of newt'
-    >>> j = Ingredient('salamanders', Amount('2'), 'diced fine')
-    >>> str(j)
-    '2 salamanders, diced fine'
-    >>> pprint.pprint(j.to_yaml())
-    {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '2'},
-     'name': 'salamanders',
-     'note': 'diced fine'}
-    >>> x.from_yaml(j.to_yaml())
-    >>> str(x)
-    '2 salamanders, diced fine'
-    """
-    def __init__(self, name=None, amount=None, note=None):
-        self.name = name
-        self.amount = amount
-        self.note = note
-
-    def __str__(self):
-        return str(self.__unicode__())
-
-    def __unicode__(self):
-        if self.note == None:
-            return '%s %s' % (unicode(self.amount or ''), self.name)
-        return '%s %s, %s' % (unicode(self.amount or ''), self.name, self.note)
-
-    def to_yaml(self):
-        d = {}
-        for key in ['name', 'amount', 'note']:
-            d[key] = to_yaml_object(getattr(self, key))
-        return d
-
-    def from_yaml(self, dict):
-        for key in ['name', 'note']:
-            setattr(self, key, dict.get(key, None))
-        self.amount = Amount()
-        self.amount.from_yaml(dict.get('amount', {}))
-
-
-class IngredientBlock (list):
-    """
-    >>> import pprint
-    >>> ib = IngredientBlock(None, [
-    ...         Ingredient('eye of newt', Amount('1')),
-    ...         Ingredient('salamanders', Amount('2'), 'diced fine')])
-    >>> print str(ib)
-    Ingredients:
-      1 eye of newt
-      2 salamanders, diced fine
-    >>> pprint.pprint(ib.to_yaml())
-    {'ingredients': [{'amount': {'alternatives': [],
-                                 'range_': None,
-                                 'units': None,
-                                 'value': '1'},
-                      'name': 'eye of newt',
-                      'note': None},
-                     {'amount': {'alternatives': [],
-                                 'range_': None,
-                                 'units': None,
-                                 'value': '2'},
-                      'name': 'salamanders',
-                      'note': 'diced fine'}],
-     'name': None}
-    >>> x = IngredientBlock()
-    >>> x.from_yaml(ib.to_yaml())
-    >>> print str(x)
-    Ingredients:
-      1 eye of newt
-      2 salamanders, diced fine
-    >>> ib.name = 'Dressing'
-    >>> print str(ib)
-    Dressing:
-      1 eye of newt
-      2 salamanders, diced fine
-    >>> pprint.pprint(ib.to_yaml())
-    {'ingredients': [{'amount': {'alternatives': [],
-                                 'range_': None,
-                                 'units': None,
-                                 'value': '1'},
-                      'name': 'eye of newt',
-                      'note': None},
-                     {'amount': {'alternatives': [],
-                                 'range_': None,
-                                 'units': None,
-                                 'value': '2'},
-                      'name': 'salamanders',
-                      'note': 'diced fine'}],
-     'name': 'Dressing'}
-    >>> x = IngredientBlock()
-    >>> x.from_yaml(ib.to_yaml())
-    >>> print str(x)
-    Dressing:
-      1 eye of newt
-      2 salamanders, diced fine
-    """
-    def __init__(self, name=None, *args, **kwargs):
-        self.name = name
-        super(IngredientBlock, self).__init__(*args, **kwargs)
-
-    def heading(self):
-        if self.name == None:
-            return 'Ingredients'
-        return self.name
-
-    def __str__(self):
-        return str(self.__unicode__())
-
-    def __unicode__(self):
-        ret = [unicode(i) for i in self]
-        ret.insert(0, '%s:' % self.heading())
-        return '\n  '.join(ret)
-
-    def to_yaml(self):
-        d = {}
-        for key in ['name']:
-            d[key] = to_yaml_object(getattr(self, key))
-        d['ingredients'] = to_yaml_object(list(self))
-        return d
-
-    def from_yaml(self, dict):
-        self.name = dict.get('name', None)
-        while len(self) > 0:
-            self.pop()
-        for i in dict.get('ingredients', []):
-            ingredient = Ingredient()
-            ingredient.from_yaml(i)
-            self.append(ingredient)
-
-
-class Directions (list):
-    """
-    >>> import pprint
-    >>> d = Directions(['paragraph 1', 'paragraph 2', 'paragraph 3'])
-    >>> print str(d)
-    paragraph 1
-    <BLANKLINE>
-    paragraph 2
-    <BLANKLINE>
-    paragraph 3
-    >>> pprint.pprint(d.to_yaml())
-    'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
-    >>> x = Directions()
-    >>> x.from_yaml(d.to_yaml())
-    >>> print str(x)
-    paragraph 1
-    <BLANKLINE>
-    paragraph 2
-    <BLANKLINE>
-    paragraph 3
-    >>> pprint.pprint(x.to_yaml())
-    'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
-    """
-    def __str__(self):
-        return str(self.__unicode__())
-
-    def __unicode__(self):
-        return '\n\n'.join(self)
-
-    def wrapped_paragraphs(self, *args, **kwargs):
-        return ['\n'.join(
-                textwrap.wrap(
-                    paragraph,
-                    *args,
-                    **kwargs))
-                for paragraph in self]
-
-    def wrap(self, *args, **kwargs):
-        return '\n\n'.join(self.wrapped_paragraphs(*args, **kwargs))
-
-    def to_yaml(self):
-        return string_for_yaml('\n\n'.join([paragraph.rstrip('\n')
-                                            for paragraph in self]))
-
-    def from_yaml(self, string):
-        if string == None:
-            return
-        while len(self) > 0:
-            self.pop()
-        for paragraph in string.split('\n\n'):
-            self.append(paragraph)
-
-
-class Recipe (object):
-    """
-    >>> import pprint
-    >>> r = Recipe('Hot Newt Dressing',
-    ...     [IngredientBlock(None, [
-    ...         Ingredient('eye of newt', Amount('1')),
-    ...         Ingredient('salamanders', Amount('2'), 'diced fine')])],
-    ...     Directions(['Mix ingredients until salamander starts to smoke',
-    ...                 'Serve warm over rice']),
-    ...     yield_='enough for one apprentice',
-    ...     author='Merlin',
-    ...     source='CENSORED',
-    ...     url='http://merlin.uk/recipes/1234/',
-    ...     tags=['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'])
-    >>> print str(r)
-    Recipe: Hot Newt Dressing
-    Yield: enough for one apprentice
-    From: Merlin
-    Source: CENSORED
-    URL: http://merlin.uk/recipes/1234/
-    Ingredients:
-      1 eye of newt
-      2 salamanders, diced fine
-    Mix ingredients until salamander starts to smoke
-    <BLANKLINE>
-    Serve warm over rice
-    >>> r.matches_tags(None)
-    True
-    >>> r.matches_tags([])
-    True
-    >>> r.matches_tags(['dinner', 'apprentice'])
-    True
-    >>> r.matches_tags(['dinner', 'dragon'])
-    False
-    >>> pprint.pprint(r.to_yaml())
-    {'author': 'Merlin',
-     'directions': 'Mix ingredients until salamander starts to smoke\\n\\nServe warm over rice',
-     'ingredient_blocks': [{'ingredients': [{'amount': {'alternatives': [],
-                                                        'range_': None,
-                                                        'units': None,
-                                                        'value': '1'},
-                                             'name': 'eye of newt',
-                                             'note': None},
-                                            {'amount': {'alternatives': [],
-                                                        'range_': None,
-                                                        'units': None,
-                                                        'value': '2'},
-                                             'name': 'salamanders',
-                                             'note': 'diced fine'}],
-                            'name': None}],
-     'name': 'Hot Newt Dressing',
-     'source': 'CENSORED',
-     'tags': ['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'],
-     'url': 'http://merlin.uk/recipes/1234/',
-     'yield_': 'enough for one apprentice'}
-    >>> x = Recipe()
-    >>> x.from_yaml(r.to_yaml())
-    >>> print str(x)
-    Recipe: Hot Newt Dressing
-    Yield: enough for one apprentice
-    From: Merlin
-    Source: CENSORED
-    URL: http://merlin.uk/recipes/1234/
-    Ingredients:
-      1 eye of newt
-      2 salamanders, diced fine
-    Mix ingredients until salamander starts to smoke
-    <BLANKLINE>
-    Serve warm over rice
-    """
-    def __init__(self, name=None, ingredient_blocks=None, directions=None,
-                 yield_=None, author=None, source=None, url=None, tags=None):
-        self.name = name
-        self.ingredient_blocks = ingredient_blocks
-        self.directions = directions
-        self.yield_ = yield_
-        self.author = author
-        self.source = source
-        self.url = url
-        self.tags = tags
-        self.path = None
-
-    def clean_name(self, ascii=False):
-        name = self.name
-        for from_,to in [(' ','_'), ('/', '_'), (',', ''), ('&', 'and')]:
-            name = name.replace(from_, to)
-        if ascii == True:
-            return quote_plus(name.encode('utf-8'))
-        return name
-
-    def matches_tags(self, tags):
-        """Return True if this recipe is tagges with each of the tags in tags.
-        """
-        if tags in [None, []]:
-            return True
-        if self.tags == None:
-            return False
-        for t in tags:
-            if t not in self.tags:
-                return False
-        return True
-
-    def __str__(self):
-        return str(self.__unicode__())
-
-    def __unicode__(self):
-        return '\n'.join([
-                'Recipe: %s' % self.name,
-                'Yield: %s' % self.yield_,
-                'From: %s' % self.author,
-                'Source: %s' % self.source,
-                'URL: %s' % self.url,
-                '\n'.join([unicode(ib) for ib in self.ingredient_blocks]),
-                unicode(self.directions),
-                ])
-
-    def to_yaml(self):
-        d = {}
-        for key in ['name', 'ingredient_blocks', 'directions', 'yield_',
-                    'author', 'source', 'url', 'tags']:
-            d[key] = to_yaml_object(getattr(self, key))
-        return d
-
-    def from_yaml(self, dict):
-        for key in ['name', 'yield_', 'author', 'source', 'url', 'tags']:
-            setattr(self, key, dict.get(key, None))
-        self.ingredient_blocks = []
-        for ib in dict.get('ingredient_blocks', []):
-            ingredient_block = IngredientBlock()
-            ingredient_block.from_yaml(ib)
-            self.ingredient_blocks.append(ingredient_block)
-        self.directions = Directions()
-        self.directions.from_yaml(dict.get('directions', None))
-
-    def save(self, stream, path=None):
-        yaml.dump(self.to_yaml(), stream,
-                  default_flow_style=False, allow_unicode=True, width=78)
-        if path != None:
-            self.path = path
-
-    def load(self, stream, path=None):
-        dict = yaml.load(stream)
-        self.from_yaml(dict)
-        if path != None:
-            self.path = path
-
-
-class Cookbook (list):
-    """
-    TODO: doctests
-    """
-    def __init__(self, name="Mom's cookbook", *args, **kwargs):
-        self.name = name
-        self.save_dir = u'recipe'
-        super(Cookbook, self).__init__(*args, **kwargs)
-
-    def save(self):
-        if not os.path.isdir(self.save_dir):
-            os.mkdir(self.save_dir)
-        paths = []
-        for recipe in self:
-            p = self._free_path(recipe, paths)
-            paths.append(p)
-            with open(p, 'w') as f:
-                recipe.save(f, p)
-
-    def load(self):
-        for path in sorted(os.listdir(self.save_dir)):
-            r = Recipe()
-            p = os.path.join(self.save_dir, path)
-            with open(p, 'r') as f:
-                r.load(f, p)
-            self.append(r)
-
-    def make_index(self):
-        self.index = {}
-        paths = [recipe.path for recipe in self]
-        for recipe in self:
-            self.index[recipe.name] = recipe
-            self.index[recipe.clean_name()] = recipe
-            self.index[recipe.clean_name(ascii=True)] = recipe
-            if recipe.path == None:
-                p = self._free_path(recipe, paths)
-                paths.append(p)
-                recipe.path = p
-
-    def _free_path(self, recipe, paths):
-        base_path = os.path.join(self.save_dir, recipe.clean_name())
-        path = base_path
-        i = 2
-        while path in paths:
-            path = '%s_%d' % (base_path, i)
-            i += 1
-        return path
-
-    def tags(self):
-        """List all tags used in this cookbook.
-        """
-        tags = set()
-        for recipe in self:
-            if recipe.tags != None:
-                tags = tags.union(set(recipe.tags))
-        return sorted(tags)
-
-    def tagged(self, tags=None):
-        """Iterate through all recipes matching the given list of tags.
-        """
-        for recipe in self:
-            if recipe.matches_tags(tags):
-                yield recipe
-
-
-def test():
-    import doctest
-    doctest.testmod()
diff --git a/cookbook/models.py b/cookbook/models.py
new file mode 100644 (file)
index 0000000..3d75d57
--- /dev/null
@@ -0,0 +1,111 @@
+from django.db import models
+import markdown
+from taggit.managers import TaggableManager
+
+
+class UnitType (models.Model):
+    "Weight, length, count, time, etc."
+    name = models.CharField(max_length=40)
+
+    def __unicode__(self):
+        return u'{0.name}'.format(self)
+
+class UnitSystem (models.Model):
+    "SI, CGS, British Imperial, US, etc."
+    name = models.CharField(max_length=40)
+
+    def __unicode__(self):
+        return u'{0.name}'.format(self)
+
+class Unit (models.Model):
+    "Kilograms, pounds, liters, gallons, etc."
+    abbrev = models.CharField('abbreviation', max_length=6)
+    name = models.CharField(max_length=40)
+    type = models.ForeignKey(UnitType)
+    system = models.ForeignKey(UnitSystem)
+    si_scale = models.DecimalField(max_digits=30, decimal_places=15)
+    si_offset = models.DecimalField(max_digits=30, decimal_places=15)
+
+    def __unicode__(self):
+        return u'{0.abbrev}'.format(self)
+
+class Amount (models.Model):
+    "1 kg, 2-3 lb., 0.5 (0.3-0.6) gal., etc."
+    unit = models.ForeignKey(Unit)
+    value = models.DecimalField(max_digits=10, decimal_places=4)
+    min_value = models.DecimalField(
+        'minimum value', max_digits=10, decimal_places=4,
+        null=True, blank=True)
+    max_value = models.DecimalField(
+        'maximum value', max_digits=10, decimal_places=4,
+        null=True, blank=True)
+
+    def __unicode__(self):
+        if self.min_value is None and self.max_value is None:
+            value = self.value
+        elif self.min_value is None:
+            value = '{0.value}-{0.max_value}'.format(self)
+        elif self.max_value is None:
+            value = '{0.min_value}-{0.value}'.format(self)
+        else:
+            value = '{0.value} ({0.min_value}-{0.max_value})'.format(self)
+        return u'{0} {1.unit}'.format(value, self)
+
+class Recipe (models.Model):
+    name = models.CharField(max_length=200)
+    directions_markdown = models.TextField(
+        'directions', help_text='Markdown syntax')
+    directions = models.TextField('directions as HTML', blank=True, null=True)
+    # yield is a reserved word
+    x_yield = models.OneToOneField(
+        Amount, verbose_name='yield', db_column='yield', null=True, blank=True)
+    author = models.CharField(max_length=200, null=True, blank=True)
+    source = models.CharField(max_length=200, null=True, blank=True)
+    url = models.URLField('URL', null=True, blank=True)
+    tags = TaggableManager(blank=True)
+
+    class Meta:
+        ordering = ['name']
+
+    def __unicode__(self):
+        return u'{0.name}'.format(self)
+
+    def save(self):
+        # https://code.djangoproject.com/wiki/UsingMarkup
+        self.directions = markdown.markdown(self.directions_markdown)
+        super(Recipe, self).save()
+
+
+class IngredientBlock (models.Model):
+    name = models.CharField(max_length=200)
+    directions_markdown = models.TextField(
+        'directions', help_text='markdown syntax', blank=True, null=True)
+    directions = models.TextField('directions as HTML', blank=True, null=True)
+    recipe = models.ForeignKey(Recipe)
+
+    class Meta:
+        ordering = ['recipe', 'name']
+
+    def __unicode__(self):
+        return u'{0.name}'.format(self)
+
+    def save(self):
+        if self.directions_markdown:
+            self.directions = markdown.markdown(self.directions_markdown)
+        super(IngredientBlock, self).save()
+
+
+class Ingredient (models.Model):
+    "1 kg, 2 lb., 3.4 L, 0.5 gal., etc."
+    amount = models.OneToOneField(Amount, null=True, blank=True)
+    name = models.CharField(max_length=200)
+    note = models.CharField(max_length=200, null=True, blank=True)
+    block = models.ForeignKey(IngredientBlock)
+
+    def __unicode__(self):
+        fmt = '{0.name}'
+        if self.amount:
+            fmt = '{0.amount} ' + fmt
+        if self.note:
+            fmt += u', {0.note}'
+        return fmt.format(self)
diff --git a/cookbook/mom.py b/cookbook/mom.py
deleted file mode 100644 (file)
index 54336cf..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of Cookbook.
-#
-# Cookbook 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.
-#
-# Cookbook 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.
-#
-# You should have received a copy of the GNU General Public License
-# along with Cookbook.  If not, see <http://www.gnu.org/licenses/>.
-
-"""Hack parser for standardizing my Mom's cookbook text.
-"""
-
-from .cookbook import (
-    Cookbook, Recipe, IngredientBlock, Ingredient, Amount, Directions)
-
-
-class MomParser (object):
-    def parse(self, filename):
-        c = Cookbook()
-        consecutive_blanks = 100
-        recipe_lines = None
-        for line in file(filename, 'r'):
-            line = line.strip().decode('utf-8')
-            if line == '':
-                if recipe_lines != None and consecutive_blanks == 0:
-                    recipe_lines.append('')
-                consecutive_blanks += 1
-                continue
-            if consecutive_blanks >= 2:
-                if recipe_lines != None:
-                    c.append(self._parse_recipe(recipe_lines))
-                recipe_lines = [line]
-            else:
-                recipe_lines.append(line)
-            consecutive_blanks = 0
-        return c
-
-    def _parse_recipe(self, lines):
-        name = lines.pop(0)
-        yield_,author,source,url,lines = self._parse_yield_line(lines)
-        ingredient_blocks,lines = self._parse_ingredient_blocks(lines)
-        directions,lines = self._parse_directions(lines)
-        assert len(lines) == 0, lines
-        return Recipe(
-            name=name,
-            ingredient_blocks=ingredient_blocks,
-            directions=directions,
-            yield_=yield_,
-            author=author,
-            source=source,
-            url=url)
-
-    def _parse_yield_line(self, lines):
-        while len(lines) > 0 and lines[0] == '':
-            lines.pop(0)
-        fields = ['yield', 'from', 'source', 'url']
-        yield_ = author = source = url = None
-        matching_line = False
-        for field in fields:
-            if field in lines[0].lower():
-                matching_line = True
-                break
-        if matching_line == True:
-            bits = lines.pop(0).split('\t')
-            for bit in bits:
-                for field in fields:
-                    if bit.lower().startswith(field+':'):
-                        value = bit[len(field+':'):].strip()
-                        if field == 'yield':
-                            yield_ = value.replace('Serving', 'serving')
-                        elif field == 'from':
-                            author = value
-                        elif field == 'source':
-                            source = value
-                        elif field == 'url':
-                            url = value
-                        break
-        return (yield_, author, source, url, lines)
-
-    def _parse_ingredient_blocks(self, lines):
-        ingredient_blocks = []
-        first_block = True
-        while True:
-            while len(lines) > 0 and lines[0] == '': # scroll past blanks
-                lines.pop(0)
-            if (len(lines) == 0
-                or not (first_block == True
-                        or lines[0].endswith(':'))):
-                break
-            if lines[0].endswith(':'):
-                line = lines.pop(0)
-                name = line[:-1].strip()
-            else:
-                name = None
-            block = IngredientBlock(name)
-            while len(lines) > 0 and lines[0] != '':
-                block.append(self._parse_ingredient_line(lines.pop(0)))
-            ingredient_blocks.append(block)
-            first_block = False
-        return (ingredient_blocks, lines)
-
-    def _parse_ingredient_line(self, line):
-        if line.lower().startswith('1 red'):
-            line = '1 # red'+line[len('1 red'):]
-        try:
-            value,units,name = line.split(' ', 2)
-        except ValueError:
-            print line,
-            raise
-        if units == '#':
-            units = None
-        elif units == 'Large':
-            units = 'large'
-        elif units == 'Cloves':
-            units = 'cloves'
-        return Ingredient(name, Amount(value, units))
-
-    def _parse_directions(self, lines):
-        directions = Directions()
-        paragraph = []
-        for line in lines:
-            if line == '':
-                if len(paragraph) > 0:
-                    directions.append('\n'.join(paragraph))
-                    paragraph = []
-            else:
-                paragraph.append(line)
-        return (directions, [])
diff --git a/cookbook/server.py b/cookbook/server.py
deleted file mode 100644 (file)
index e94e769..0000000
+++ /dev/null
@@ -1,440 +0,0 @@
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of Cookbook.
-#
-# Cookbook 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.
-#
-# Cookbook 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.
-#
-# You should have received a copy of the GNU General Public License
-# along with Cookbook.  If not, see <http://www.gnu.org/licenses/>.
-
-"""Serve cookbooks over HTTP.
-"""
-
-from __future__ import absolute_import
-from __future__ import with_statement
-
-import os
-import random
-import re
-import types
-from urllib import urlencode
-import uuid
-from xml.sax import saxutils
-
-import cherrypy
-from jinja2 import Environment, FileSystemLoader
-
-from .cookbook import Recipe, Directions, IngredientBlock, Ingredient, Amount
-
-
-class Server (object):
-    """Cookbook web interface."""
-
-    def __init__(self, cookbook, template_root):
-        self.cookbook = cookbook
-        self.cookbook.make_index()
-        self.env = Environment(loader=FileSystemLoader(template_root))
-        # name regular expressions
-        self._action_ingredient_block_regexp = re.compile(
-            'ingredient block ([0-9]*) ([a-z ]*)')        
-        self._action_ingredient_regexp = re.compile(
-            'ingredient ([0-9]*) ([0-9]*) ([a-z ]*)')        
-        # value regular expressions
-        self._tag_regexp = re.compile('[a-zA-Z./ ].*')  # allowed characters
-        self._action_add_ingredient_block_regexp = re.compile(
-            'add ingredient block')
-        self._action_remove_ingredient_block_regexp = re.compile(
-            'remove ingredient block ([0-9]*)')
-        self._action_add_ingredient_regexp = re.compile(
-            'add ingredient ([0-9]*)')
-        self._action_remove_ingredient_regexp = re.compile(
-            'remove ingredient ([0-9]*) ([0-9]*)')
-
-    def cleanup(self):
-        #self.cookbook.save('new-recipe')
-        pass
-
-    @cherrypy.expose
-    def index(self, tag=None):
-        """Recipe index page.
-
-        Recipes can be filtered by tag.
-        """
-        if isinstance(tag, types.StringTypes):
-            tag = [tag]
-        template = self.env.get_template('recipes.html')
-        return template.render(cookbook=self.cookbook,
-                               recipes=list(self.cookbook.tagged(tag)),
-                               selected_tags=tag)
-
-    @cherrypy.expose
-    def recipe(self, name=None):
-        """Single recipe page.
-        """
-        if name == None:
-            recipe = random.choice(self.cookbook)
-        else:
-            recipe = self.cookbook.index[name]
-        tag_links = [
-            '<a href="./?%s">%s</a>' % (urlencode({'tag':t}), t)
-            for t in (recipe.tags or [])]
-        template = self.env.get_template('recipe.html')
-        return template.render(cookbook=self.cookbook, recipe=recipe,
-                               tag_links=tag_links)
-
-    @cherrypy.expose
-    def add_tag(self, name, tag):
-        """Add a tag to a single recipe."""
-        recipe = self.cookbook.index[name]
-        if recipe.tags == None:
-            recipe.tags = []
-        tag = self._clean_tag(tag)
-        if tag != None and tag not in recipe.tags:
-            recipe.tags.append(tag)
-            with open(recipe.path, 'w') as f:
-                recipe.save(f)
-        raise cherrypy.HTTPRedirect(
-            u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
-
-    @cherrypy.expose
-    def remove_tag(self, name, tag):
-        """Remove a tag from a single recipe."""
-        recipe = self.cookbook.index[name]
-        if recipe.tags == None:
-            return
-        tag = self._clean_tag(tag)
-        if tag != None and tag in recipe.tags:
-            recipe.tags.remove(tag)
-            with open(recipe.path, 'w') as f:
-                recipe.save(f)
-        raise cherrypy.HTTPRedirect(
-            u'recipe?name=%s' % recipe.clean_name(ascii=True), status=302)
-
-    def _clean_tag(self, tag):
-        """Sanitize tag."""
-        if not isinstance(tag, types.StringTypes):
-            if len(tag) == 2 and '' in tag:
-                # User used either dropdown or textbox
-                tag.remove('')
-                tag = tag[0]
-            else:
-                # User used both dropdown and textbox
-                return None
-        m = self._tag_regexp.match(tag)
-        if m != None:
-            return m.group()
-        return None
-
-    @cherrypy.expose
-    def edit(self, name=None, **kwargs):
-        """Remove a tag from a single recipe."""
-        name,recipe,action = self._normalize_edit_params(name, **kwargs)
-        if action == 'remove':
-            recipe = self.cookbook.index[name]
-            os.remove(recipe.path)
-            self.cookbook.remove(recipe)
-            self.cookbook.make_index()
-            raise cherrypy.HTTPRedirect(u'.', status=302)
-        elif action.startswith('edit'):
-            self._update_recipe(name, recipe)
-            if action == 'edit and redirect':  # done editing this recipe
-                raise cherrypy.HTTPRedirect(
-                    u'recipe?name=%s' % recipe.clean_name(ascii=True),
-                    status=302)
-            elif recipe.name != name:
-                raise cherrypy.HTTPRedirect(
-                    u'edit?name=%s' % recipe.clean_name(ascii=True),
-                    status=302)
-        template = self.env.get_template('edit-recipe.html')
-        return template.render(cookbook=self.cookbook,
-                               recipe=recipe,
-                               form='\n'.join(self._recipe_form(recipe)))
-
-    def _update_recipe(self, name, recipe):
-        if len(recipe.ingredient_blocks) == 0:
-            recipe.ingredient_blocks = None
-        if name not in self.cookbook.index:  # new recipe
-            print 'new'
-            self.cookbook.append(recipe)
-            self.cookbook.make_index()
-            self.cookbook.sort(key=lambda r: r.path)
-        elif name != recipe.name:  # renamed recipe
-            print 'rename'
-            os.remove(recipe.path)
-            recipe.path = None
-            self.cookbook.make_index()
-        with open(recipe.path, 'w') as f:
-            recipe.save(f)
-
-    def _normalize_edit_params(self, name, **kwargs):
-        action = 'view form'
-        if name in self.cookbook.index:
-            recipe = self.cookbook.index[name]
-            name = recipe.name  # get the canonical name for this recipe
-        else:
-            # Use uuid4() to generate a random name if name is None
-            recipe = Recipe(name=(name or str(uuid.uuid4())))
-            recipe.path = None
-        # non-ingredient text updates
-        for attr in ['name','directions','yield_','author','source','url']:
-            n = self._recipe_attr_to_name(attr)
-            if n not in kwargs:
-                continue
-            v = kwargs.get(n).strip()
-            if v == '':
-                v = None
-            elif attr == 'directions':
-                d = Directions()
-                v = v.replace('\r\n', '\n').replace('\r', '\n')
-                for p in v.split('\n\n'):
-                    p = p.strip()
-                    if len(p) > 0:
-                        d.append(p)
-                v = d
-            if v != getattr(recipe, attr):
-                setattr(recipe, attr, v)
-                action = 'edit'
-        if recipe.ingredient_blocks == None:
-            recipe.ingredient_blocks = []
-        # protect against index-changing operations
-        ingredient_block_map = dict(
-            [(i,i) for i in range(len(recipe.ingredient_blocks))])
-        ingredient_map = dict(
-            [(i, dict([(j,j) for j in range(len(ib))]))
-             for i,ib in enumerate(recipe.ingredient_blocks)])
-        # ingredient text updates
-        for key,value in kwargs.items():
-            for k in ['ingredient_block', 'ingredient']:
-                regexp = getattr(self, '_action_%s_regexp' % k)
-                m = regexp.match(key)
-                if m != None:
-                    handler = getattr(self, '_action_%s' % k)
-                    kws = {'ingredient_block_map': ingredient_block_map,
-                           'ingredient_map': ingredient_map}
-                    action = handler(recipe, action, value, *m.groups(), **kws)
-                    break
-        # button updates
-        action = kwargs.get('action', action)
-        if action == 'submit':
-            action = 'edit and redirect'
-        for a in ['add_ingredient_block', 'remove_ingredient_block',
-                  'add_ingredient', 'remove_ingredient']:
-            regexp = getattr(self, '_action_%s_regexp' % a)
-            m = regexp.match(action)
-            if m != None:
-                handler = getattr(self, '_action_%s' % a)
-                kws = {'ingredient_block_map': ingredient_block_map,
-                       'ingredient_map': ingredient_map}
-                action = handler(recipe, action, *m.groups(), **kws)
-                break
-        return (name, recipe, action)
-
-    def _action_ingredient_block(self, recipe, action, value,
-                                 block_index, block_action,
-                                 ingredient_block_map, **kwargs):
-        block_index = ingredient_block_map[int(block_index)]
-        ib = recipe.ingredient_blocks[block_index]
-        if value == '':
-            value = None        
-        if block_action == 'position' and value not in [None, '']:
-            value = int(value)
-            if value != block_index:
-                ibs = recipe.ingredient_blocks
-                ibs.insert(value, ibs.pop(block_index))
-                for i in range(min(block_index, value),
-                               max(block_index, value)+1):
-                    ingredient_block_map[i] = i+1
-                ingredient_block_map[block_index] = value
-                return 'edit'
-        elif block_action == 'name':
-            if value != ib.name:
-                ib.name = value
-                return 'edit'
-        return action
-
-    def _action_ingredient(self, recipe, action, value,
-                           block_index, index, ingredient_action,
-                           ingredient_block_map, ingredient_map, **kwargs):
-        block_index = ingredient_block_map[int(block_index)]
-        index = ingredient_map[block_index][int(index)]
-        ingredient = recipe.ingredient_blocks[block_index][index]
-        if value == '':
-            value = None
-        if ingredient_action == 'position' and value != None:
-            value = int(value)
-            if value != block_index:
-                ibs = recipe.ingredient_blocks
-                ibs.insert(value, ibs.pop(block_index))
-                for i in range(min(block_index, value),
-                               max(block_index, value)+1):
-                    ingredient_block_map[i] = i+1
-                ingredient_block_map[block_index] = value
-                return 'edit'
-        elif ingredient_action in ['name', 'note']:
-            if value != getattr(ingredient, ingredient_action):
-                setattr(ingredient, ingredient_action, value)
-                return 'edit'
-        elif ingredient_action in ['value', 'units']:
-            if value != getattr(ingredient.amount, ingredient_action):
-                setattr(ingredient.amount, ingredient_action, value)
-                return 'edit'
-        return action
-
-    def _action_add_ingredient_block(self, recipe, action,
-                                     ingredient_block_map,
-                                     ingredient_map, **kwargs):
-        recipe.ingredient_blocks.append(IngredientBlock())
-        block_index = len(recipe.ingredient_blocks)-1
-        ingredient_block_map[block_index] = block_index
-        ingredient_map[block_index] = {}
-        return 'edit'
-
-    def _action_remove_ingredient_block(self, recipe, action, block_index,
-                                        ingredient_block_map, **kwargs):
-        block_index = ingredient_block_map[int(block_index)]
-        recipe.ingredient_blocks.pop(block_index)
-        for k,v in ingredient_block_map.items():
-            if v >= block_index:
-                ingredient_block_map[k] = v-1
-        return 'edit'
-
-    def _action_add_ingredient(self, recipe, action, block_index,
-                               ingredient_block_map,
-                               ingredient_map, **kwargs):
-        block_index = ingredient_block_map[int(block_index)]
-        recipe.ingredient_blocks[block_index].append(Ingredient(
-                amount=Amount()))
-        index = len(recipe.ingredient_blocks[block_index])-1
-        ingredient_map[block_index][index] = index
-        return 'edit'
-
-    def _action_remove_ingredient(self, recipe, action, block_index, index,
-                                  ingredient_block_map,
-                                  ingredient_map, **kwargs):
-        block_index = ingredient_block_map[int(block_index)]
-        index = ingredient_map[block_index][int(index)]
-        recipe.ingredient_blocks[block_index].pop(index)
-        for k,v in ingredient_map[block_index].items():
-            if v >= index:
-                ingredient_map[block_index][k] = v-1
-        return 'edit'
-
-    def _recipe_form(self, recipe):
-        lines = [
-            '<form action="" method="post">',
-            '<table>',
-            ]
-        for attr in ['name', 'yield_', 'author', 'source', 'url']:
-            lines.extend(self._text_form(
-                    self._recipe_attr_to_name(attr),
-                    self._recipe_attr_to_user(attr),
-                    getattr(recipe, attr)))
-        for i,ib in enumerate(recipe.ingredient_blocks):
-            lines.extend(self._ingredient_block_form(ib, i))
-        lines.extend(self._submit_form('add ingredient block'))
-        lines.extend(self._textarea_form(
-                self._recipe_attr_to_name('directions'),
-                self._recipe_attr_to_user('directions'),
-                '\n\n'.join(recipe.directions or Directions())))
-        lines.extend(self._submit_form('submit'))
-        lines.extend([
-                '</table>',
-                '</form>',
-                ])
-        return lines
-
-    def _ingredient_block_form(self, ingredient_block, index):
-        lines = self._submit_form(
-            'remove ingredient block %d' % index,
-            note='Ingredient block %d' % index)
-        for name,user,value in [
-            ('ingredient block %d position' % index,
-             'Position (%d)' % index,
-             None),
-            ('ingredient block %d name' % index,
-             'Name',
-             ingredient_block.name),
-            ]:
-            lines.extend(self._text_form(name, user, value))
-        for i,ingredient in enumerate(ingredient_block):
-            lines.extend(self._ingredient_form(ingredient, index, i))
-        lines.extend(self._submit_form('add ingredient %d' % index))
-        return lines
-
-    def _ingredient_form(self, ingredient, block_index, index):
-        lines = self._submit_form(
-            'remove ingredient %d %d' % (block_index, index),
-            note='Ingredient %d %d' % (block_index, index))
-        for name,user,value in [
-            ('ingredient %d %d position' % (block_index, index),
-             'Position (%d)' % index,
-             ''),
-            ('ingredient %d %d value' % (block_index, index),
-             'Value',
-             ingredient.amount.value),
-            ('ingredient %d %d units' % (block_index, index),
-             'Units',
-             ingredient.amount.units),
-            ('ingredient %d %d name' % (block_index, index),
-             'Name',
-             ingredient.name),
-            ('ingredient %d %d note' % (block_index, index),
-             'Note',
-             ingredient.note),
-            ]:
-            lines.extend(self._text_form(name, user, value))
-        # TODO: Amount: range_, alternatives
-        return lines
-
-    def _submit_form(self, value, note=''):
-        return [
-            '  <tr>',
-            '    <td>%s</td>' % note,
-            '    <td><input type="submit" name="action" value="%s"/></td>'
-            % value,
-            '  </tr>',
-            ]
-
-    def _text_form(self, name, user, value):
-        return [
-            '  <tr>',
-            '    <td><label for="%s">%s</label></td>' % (name, user),
-            '    <td><input type="text" name="%s" size="60" value="%s"/></td>'
-            % (name, value or ''),
-            '  </tr>',
-            ]
-
-    def _textarea_form(self, name, user, value):
-        return [
-            '  <tr>',
-            '    <td><label for="%s">%s</label></td>' % (name, user),
-            '    <td><textarea name="%s" rows="20" cols="60"/>' % name,
-            value,
-            '        </textarea></td>'
-            '  </tr>',
-            ]
-
-    def _recipe_attr_to_name(self, attr):
-        if attr == 'yield_':
-            return 'yield'
-        elif attr == 'name':
-            return 'new_name'
-        return attr
-
-    def _recipe_attr_to_user(self, attr):
-        if attr == 'yield_':
-            attr = 'yield'
-        return attr.capitalize()
-
-
-def test():
-    import doctest
-    doctest.testmod()
diff --git a/cookbook/template/base.html b/cookbook/template/base.html
deleted file mode 100644 (file)
index cfd1b71..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<html>
-    <head>
-        <title>{{ cookbook.name }}</title>
-<!--
-        <link rel="stylesheet" type="text/css" media="screen"
-              href="/static/style.css"/>
--->
-    </head>
-
-    <body>
-        <div id="main-pane">
-            <div id="header" class="inside-main-pane">
-                <h1><a href="./">{{ cookbook.name }}</a></h1>
-            </div>
-            <div id="content-pane" class="inside-main-pane">
-                <h1>{% block page_title %}{% endblock %}</h1>
-                {% block content %}{% endblock %}
-            </div>
-            <div id="footer" class="inside-main-pane">
-                <p>
-                   Powered by
-                    <a href="http://www.physics.drexel.edu/">Trevor King's</a>
-                   <a href="http://www.physics.drexel.edu/~wking/code/git/git.php?p=cookbook.git">Cookbook</a>.
-                    Built using <a href="http://cherrypy.org/">CherryPy</a>
-                    and <a href="http://jinja.pocoo.org/2/">Jinja2</a>.
-                </p>
-            </div>
-        </div>
-    </body>
-</html>
diff --git a/cookbook/template/edit-recipe.html b/cookbook/template/edit-recipe.html
deleted file mode 100644 (file)
index fcd2a01..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block page_title %}
-    {% if recipe.url %}
-        <a href="{{ recipe.url }}">{{ recipe.name }}</a>
-    {% else %}
-        {{ recipe.name }}
-    {% endif %}
-{% endblock %}
-
-{% block content %}
-    {{ form }}
-{% endblock %}
diff --git a/cookbook/template/recipe.html b/cookbook/template/recipe.html
deleted file mode 100644 (file)
index ac7a90a..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-{% extends "base.html" %}
-
-{% block page_title %}
-    {% if recipe.url %}
-        <a href="{{ recipe.url }}">{{ recipe.name }}</a>
-    {% else %}
-        {{ recipe.name }}
-    {% endif %}
-{% endblock %}
-
-{% block content %}
-    <p id="recipe-details">
-    {% if recipe.yield_ %}
-        <span class="detail-field-header">Yield:</span>
-        <span class="detail-field-contents">{{ recipe.yield_ }}</span><br/>
-    {% endif %}
-    {% if recipe.author %}
-        <span class="detail-field-header">Author:</span>
-        <span class="detail-field-contents">{{ recipe.author }}</span><br/>
-    {% endif %}
-    {% if recipe.source %}
-        <span class="detail-field-header">Source:</span>
-        <span class="detail-field-contents">{{ recipe.source }}</span><br/>
-    {% endif %}
-    {% if recipe.tags %}
-        <span class="detail-field-header">Tags:</span>
-        <span class="detail-field-contents">{{ ', '.join(tag_links) }}
-       </span><br/>
-    {% endif %}
-    </p>
-    <form action="add_tag" method="get">
-      <input type="hidden" name="name" value="{{ recipe.name }}"/>
-      <select name="tag">
-       <option value="">textbox</option>
-       {% for tag in cookbook.tags() %}
-       <option value="{{ tag }}">{{ tag }}</option>
-       {% endfor %}
-      </select>
-      <input type="text" name="tag" value=""/>
-      <input type="submit" value="Add tag"/>
-    </form>
-    {% if recipe.tags %}
-    <form action="remove_tag" method="get">
-      <input type="hidden" name="name" value="{{ recipe.name }}"/>
-      <select name="tag">
-       {% for tag in recipe.tags %}
-       <option value="{{ tag }}">{{ tag }}</option>
-       {% endfor %}
-      </select>
-      <input type="submit" value="Remove tag"/>
-    </form>
-    {% endif %}
-
-    {% if recipe.ingredient_blocks %}
-    <div id="recipe-ingredient-blocks">
-    {% for ingredient_block in recipe.ingredient_blocks %}
-       <h2>{{ ingredient_block.heading() }}</h2>
-       <table>
-        {% for ingredient in ingredient_block %}
-            <tr><td>{{ ingredient.__unicode__() }}</td></tr>
-        {% endfor %}
-       </table>
-    {% endfor %}
-    </div>
-    {% endif %}
-
-    {% if recipe.directions %}
-    <div id="recipe-directions">
-    <h2>Directions</h2>
-    {% for paragraph in recipe.directions.wrapped_paragraphs() %}
-        <p>{{ paragraph }}</p>
-    {% endfor %}
-    </div>
-    {% endif %}
-
-    <ul>
-      <li><a href="edit?name={{ recipe.clean_name(ascii=True) }}">
-         edit receipe</a></li>
-      <li><a href="edit?action=remove&name={{ recipe.clean_name(ascii=True) }}">
-         remove receipe</a></li>
-    </ul>
-{% endblock %}
diff --git a/cookbook/template/recipes.html b/cookbook/template/recipes.html
deleted file mode 100644 (file)
index 06e320e..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-{% extends "base.html" %}
-
-{% block page_title %}
-    Index {% if selected_tags %}({{ ', '.join(selected_tags) }}){% endif %}
-{% endblock %}
-
-{% block content %}
-    <form action="" method="get" style="float:right;">
-      <select name="tag" size="5" multiple="multiple">
-       {% for tag in cookbook.tags() %}
-       <option value="{{ tag }}">{{ tag }}</option>
-       {% endfor %}
-      </select><br/>
-      <input type="submit" value="Show only selected tags"/>
-    </form>
-    <ul id="recipe-list">
-      {% for recipe in recipes %}
-      <li><a href="recipe?name={{ recipe.clean_name(ascii=True) }}">
-             {{ recipe.name }}</li>
-      {% endfor %}
-    </ul>
-
-    <p><a href="edit">add new receipe</a></p>
-{% endblock %}
diff --git a/cookbook/tests.py b/cookbook/tests.py
new file mode 100644 (file)
index 0000000..501deb7
--- /dev/null
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.assertEqual(1 + 1, 2)
diff --git a/cookbook/urls.py b/cookbook/urls.py
new file mode 100644 (file)
index 0000000..146dbcc
--- /dev/null
@@ -0,0 +1,36 @@
+from django.conf import settings
+from django.conf.urls.defaults import patterns, include, url
+from django.views.generic import DetailView, ListView
+import taggit.models
+
+import models
+
+# Uncomment the next two lines to enable the admin:
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    url(r'^$', ListView.as_view(
+            queryset=models.Recipe.objects.all().order_by('name'),
+            context_object_name='recipes',
+            template_name='cookbook/recipes.html'),
+        name='recipes'),
+     url(r'^recipe/(?P<pk>\d+)/$', DetailView.as_view(
+            model=models.Recipe, template_name='cookbook/recipe.html'),
+        name='recipe'),
+     url(r'^tags/$', ListView.as_view(
+            queryset=taggit.models.Tag.objects.all(),
+            context_object_name='tags',
+            template_name='cookbook/tags.html'),
+        name='tags'),
+     url(r'^tag/(?P<pk>\d+)/$', 'cookbook.views.tag', name='tag'),
+
+    # Uncomment the admin/doc line below to enable admin documentation:
+    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+    # Uncomment the next line to enable the admin:
+    url(r'^admin/', include(admin.site.urls), name='admin'),
+
+    url(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to',
+        kwargs={'url': settings.STATIC_URL + 'cookbook.ico'}),
+)
diff --git a/cookbook/views.py b/cookbook/views.py
new file mode 100644 (file)
index 0000000..dbc1bd2
--- /dev/null
@@ -0,0 +1,14 @@
+from django.http import HttpResponse
+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext
+
+from taggit.models import Tag
+from .models import Recipe
+
+
+def tag(request, pk):
+    tag = get_object_or_404(Tag, pk=pk)
+    recipes = Recipe.objects.filter(tags__name__in=[tag.slug])
+    return render_to_response(
+        'cookbook/recipes.html', {'recipes': recipes, 'title': tag.name},
+        RequestContext(request))
diff --git a/doc/abbreviations b/doc/abbreviations
deleted file mode 100644 (file)
index a4e25de..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-Volumes
-=======
-
-t.     teaspoon
-T.     tablespoon
-C.     cup
-pt.    pint
-qt.    quart
-gal.   gallon
-
-Weights
-=======
-
-oz.    ounce
-lb.    pound
-
-Lengths
-=======
-
-”    inch
-
-Counts
-======
-
-doz.   dozen (i.e. 12)
diff --git a/example/__init__.py b/example/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/example/data/static/cookbook.ico b/example/data/static/cookbook.ico
new file mode 100644 (file)
index 0000000..f321568
Binary files /dev/null and b/example/data/static/cookbook.ico differ
diff --git a/example/data/static/style.css b/example/data/static/style.css
new file mode 100644 (file)
index 0000000..69526d5
--- /dev/null
@@ -0,0 +1,109 @@
+/* <body> */
+
+body {
+  background: #eee;
+}
+
+.fullclear {
+  width:100%;
+  height:1px;
+  margin:0;
+  padding:0;
+  clear:both;
+}
+
+/* </body> */
+
+/* <div id="navigation"> */
+
+#navigation {
+  margin: 0;
+  padding: 4px 0 0 0;
+}
+
+#navigation ul {
+  list-style: none;
+  margin: 0;
+  padding: 0 0 0 12px;
+}
+
+#navigation ul li {
+  margin: 0;
+  padding: 0;
+  float: left;
+}
+
+#navigation ul li a {
+  display: block;
+  font-weight: bold;
+  text-decoration: none;
+  margin: 0;
+  padding: 5px 5px;
+  background-color: #eee;
+}
+
+#navigation ul li a:hover {
+  background-color: #ccc;
+}
+
+#navigation ul li.CurrentSection a {
+  background-color: #FFF;
+}
+
+#navigation ul li.CurrentSection a:hover {
+  background-color: #FFF;
+}
+
+#subnavigation ul li.CurrentSection a {
+  border-width: 0 0 1px 0;
+}
+
+/* </div id="navigation"> */
+
+/* <div id="content"> */
+
+#content {
+       width: 100%;
+       margin: 0;
+       padding: 0;
+  padding-top: 1em;
+  padding-bottom: 1em;
+  background: #fff;
+}
+
+table {
+  border-collapse: collapse;
+}
+
+table.centered {
+  margin: auto;
+}
+
+td, th {
+  padding-right: 1em;
+  text-align: left;
+}
+
+table.wide tr:nth-child(odd) {
+  background: #eee;
+}
+
+table.wide thead tr:nth-child(odd) {
+  background: #ccc;
+}
+
+span.number {
+  float: right;            /* so decimal points match up */
+  font-family: monospace;  /* so that n-place digits line up */
+  text-align: '.';  /* should work by itself, but browser support is bad */
+}
+
+span.positive {
+  color: green;
+}
+
+span.negative {
+  color: red;
+}
+
+/* </div id="content"> */
diff --git a/example/data/template/admin/edit_inline/tabular.html b/example/data/template/admin/edit_inline/tabular.html
new file mode 100644 (file)
index 0000000..b0a2c65
--- /dev/null
@@ -0,0 +1,129 @@
+{% load i18n adminmedia admin_modify %}
+{% load url from future %}
+<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
+  <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
+{{ inline_admin_formset.formset.management_form }}
+<fieldset class="module">
+   <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+   {{ inline_admin_formset.formset.non_form_errors }}
+   <table>
+     <thead><tr>
+     {% for field in inline_admin_formset.fields %}
+       {% if not field.widget.is_hidden %}
+         <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
+       {% endif %}
+     {% endfor %}
+     {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
+     </tr></thead>
+
+     <tbody>
+     {% for inline_admin_form in inline_admin_formset %}
+        {% if inline_admin_form.form.non_field_errors %}
+        <tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
+        {% endif %}
+        <tr class="{% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
+             id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+        <td class="original">
+          {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
+          {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_edit_link and inline_admin_form.original %} <a href="{% url adminform.model_admin.admin_site.app_name|add:':'|add:app_label|add:'_'|add:inline_admin_formset.opts.opts.module_name|add:'_changelist' %}{{ inline_admin_form.original.pk }}">Edit</a>{% endif %}{% endif %} 
+          {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
+            </p>{% endif %}
+          {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
+          {{ inline_admin_form.fk_field.field }}
+          {% spaceless %}
+          {% for fieldset in inline_admin_form %}
+            {% for line in fieldset %}
+              {% for field in line %}
+                {% if field.is_hidden %} {{ field.field }} {% endif %}
+              {% endfor %}
+            {% endfor %}
+          {% endfor %}
+          {% endspaceless %}
+        </td>
+        {% for fieldset in inline_admin_form %}
+          {% for line in fieldset %}
+            {% for field in line %}
+              <td class="{{ field.field.name }}">
+              {% if field.is_readonly %}
+                  <p>{{ field.contents }}</p>
+              {% else %}
+                  {{ field.field.errors.as_ul }}
+                  {{ field.field }}
+              {% endif %}
+              </td>
+            {% endfor %}
+          {% endfor %}
+        {% endfor %}
+        {% if inline_admin_formset.formset.can_delete %}
+          <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
+        {% endif %}
+        </tr>
+     {% endfor %}
+     </tbody>
+   </table>
+</fieldset>
+  </div>
+</div>
+
+<script type="text/javascript">
+(function($) {
+    $(document).ready(function($) {
+        var rows = "#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr";
+        var alternatingRows = function(row) {
+            $(rows).not(".add-row").removeClass("row1 row2")
+                .filter(":even").addClass("row1").end()
+                .filter(rows + ":odd").addClass("row2");
+        }
+        var reinitDateTimeShortCuts = function() {
+            // Reinitialize the calendar and clock widgets by force
+            if (typeof DateTimeShortcuts != "undefined") {
+                $(".datetimeshortcuts").remove();
+                DateTimeShortcuts.init();
+            }
+        }
+        var updateSelectFilter = function() {
+            // If any SelectFilter widgets are a part of the new form,
+            // instantiate a new SelectFilter instance for it.
+            if (typeof SelectFilter != "undefined"){
+                $(".selectfilter").each(function(index, value){
+                  var namearr = value.name.split('-');
+                  SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}");
+                });
+                $(".selectfilterstacked").each(function(index, value){
+                  var namearr = value.name.split('-');
+                  SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}");
+                });
+            }
+        }
+        var initPrepopulatedFields = function(row) {
+            row.find('.prepopulated_field').each(function() {
+                var field = $(this);
+                var input = field.find('input, select, textarea');
+                var dependency_list = input.data('dependency_list') || [];
+                var dependencies = [];
+                $.each(dependency_list, function(i, field_name) {
+                  dependencies.push('#' + row.find(field_name).find('input, select, textarea').attr('id'));
+                });
+                if (dependencies.length) {
+                    input.prepopulate(dependencies, input.attr('maxlength'));
+                }
+            });
+        }
+        $(rows).formset({
+            prefix: "{{ inline_admin_formset.formset.prefix }}",
+            addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
+            formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
+            deleteCssClass: "inline-deletelink",
+            deleteText: "{% trans "Remove" %}",
+            emptyCssClass: "empty-form",
+            removed: alternatingRows,
+            added: (function(row) {
+                initPrepopulatedFields(row);
+                reinitDateTimeShortCuts();
+                updateSelectFilter();
+                alternatingRows(row);
+            })
+        });
+    });
+})(django.jQuery);
+</script>
diff --git a/example/data/template/cookbook/base.html b/example/data/template/cookbook/base.html
new file mode 100644 (file)
index 0000000..5e0b6f8
--- /dev/null
@@ -0,0 +1,29 @@
+<html>
+<head>
+    <title>{% block title %}Cookbook{% endblock %}</title>
+    <link rel="stylesheet" href="{{ STATIC_URL }}style.css" />
+    <link rel="shortcut icon" href="{{ STATIC_URL }}cookbook.ico" />
+</head>
+
+<body>
+    <div id="navigation">
+        {% block navigation %}
+        <ul>
+            <li><a href="/">Recipes</a></li>
+            <li><a href="/tags/">Tags</a></li>
+            <li><a href="/admin/">Admin</a></li>
+        </ul>
+        {% endblock %}
+    </div>
+    <div class="fullclear"></div>
+
+    <div id="content">
+        {% block content %}{% endblock %}
+    </div>
+    <div class="fullclear"></div>
+
+    <div id="footer">
+        <p>powered by <a href="http://physics.drexel.edu/~wking/unfolding-disasters/cookbook/">cookbook</a></p>
+    </div>
+</body>
+</html>
diff --git a/example/data/template/cookbook/recipe.html b/example/data/template/cookbook/recipe.html
new file mode 100644 (file)
index 0000000..eb0f5e4
--- /dev/null
@@ -0,0 +1,58 @@
+{% extends "cookbook/base.html" %}
+
+{% block content %}
+<h1>{{ recipe.name }}</h1>
+
+{% block metadata %}
+<table class="metadata">
+  <tr><td>Author</td><td>{{ recipe.author|default:"unknown" }}</td></tr>
+{% if recipe.source or recipe.url %}
+  <tr><td>Source</td><td><a href="{{ recipe.url }}">{{ recipe.source|default:recipe.url }}</a></td></tr>
+{% endif %}
+{% if recipe.x_yield %}
+  <tr><td>Yield</td><td>{{ recipe.x_yield }}</td></tr>
+{% endif %}
+{% if recipe.tags.all %}
+  <tr><td>Tags</td>
+    <td>
+{% for tag in recipe.tags.all %}
+      <a href="{% url tag tag.id %}">{{ tag }}</a>
+{% endfor %}
+    </td>
+  </tr>
+{% endif %}
+</table>
+{% endblock %}
+
+{% block ingredients %}
+<h2>Ingredients</h2>
+
+{% for ingredient_block in recipe.ingredientblock_set.all %}
+<div class="ingredient_block">
+<h3>{{ ingredient_block.name }}</h3>
+<ul>
+{% for ingredient in ingredient_block.ingredient_set.all %}
+  <li>{{ ingredient }}</li>
+{% endfor %}
+</ul>
+{% if ingredient_block.directions %}
+<h4>Directions</h4>
+
+<div class="directions">
+{{ ingredient_block.directions|safe }}
+</div>
+{% endif %}
+
+</div>
+{% endfor %}
+{% endblock %}
+
+{% block directions %}
+<h2>Directions</h2>
+
+<div class="directions">
+{{ recipe.directions|safe }}
+</div>
+{% endblock %}
+
+{% endblock %}
diff --git a/example/data/template/cookbook/recipes.html b/example/data/template/cookbook/recipes.html
new file mode 100644 (file)
index 0000000..4fd2828
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "cookbook/base.html" %}
+
+{% block title %}
+{{ title }}
+{% endblock %}
+
+{% block content %}
+<h1>{{ title }}</h1>
+{% if recipes %}
+    <ul>
+    {% for recipe in recipes %}
+        <li><a href="{% url recipe recipe.id %}">{{ recipe.name }}</a></li>
+    {% endfor %}
+    </ul>
+{% else %}
+    <p>No recipes are available.</p>
+{% endif %}
+{% endblock %}
diff --git a/example/data/template/cookbook/tags.html b/example/data/template/cookbook/tags.html
new file mode 100644 (file)
index 0000000..4a4d97a
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "cookbook/base.html" %}
+
+{% block title %}
+{{ title }}
+{% endblock %}
+
+{% block content %}
+<h1>{{ title }}</h1>
+{% if tags %}
+    <ul>
+    {% for tag in tags %}
+        <li><a href="{% url tag tag.id %}">{{ tag.name }}</a></li>
+    {% endfor %}
+    </ul>
+{% else %}
+    <p>No tags are available.</p>
+{% endif %}
+{% endblock %}
diff --git a/example/manage.py b/example/manage.py
new file mode 100644 (file)
index 0000000..3e4eedc
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+import imp
+try:
+    imp.find_module('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" % __file__)
+    sys.exit(1)
+
+import settings
+
+if __name__ == "__main__":
+    execute_manager(settings)
diff --git a/example/settings.py b/example/settings.py
new file mode 100644 (file)
index 0000000..b253e12
--- /dev/null
@@ -0,0 +1,175 @@
+# Django settings for web project.
+
+import os
+import os.path
+
+
+# This is where we'll put the non-Python portions of the app
+# (e.g. templates) and store state information (e.g. the SQLite
+# database).  This should be an absolute path.
+DEFAULT_DATA_DIRECTORY = os.path.abspath(os.path.join(
+        os.path.dirname(__file__), 'data'))
+DATA_DIRECTORY = os.environ.get('DJANGO_COOKBOOK_DATA', DEFAULT_DATA_DIRECTORY)
+
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    ('W. Trevor King', 'wking@drexel.edu'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+    'default': {
+        #'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(DATA_DIRECTORY, 'sqlite3.db'),  # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = None  # 'America/New_York'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = os.path.join(DATA_DIRECTORY, 'media')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
+MEDIA_URL = '/media/'
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/home/media/media.lawrence.com/static/"
+STATIC_ROOT = ''
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+STATIC_URL = '/static/'
+
+# URL prefix for admin static files -- CSS, JavaScript and images.
+# Make sure to use a trailing slash.
+# Examples: "http://foo.com/static/admin/", "/static/admin/".
+ADMIN_MEDIA_PREFIX = '/static/admin/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+    # Put strings here, like "/home/html/static" or "C:/www/django/static".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    os.path.join(DATA_DIRECTORY, 'media'),
+    os.path.join(DATA_DIRECTORY, 'static'),
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'wq-)fb$0kv*_q%=ufhaqm7$qw(w84=izd7a!nn)5i@@l)!%3x='
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
+#     'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+ROOT_URLCONF = 'cookbook.urls'
+
+TEMPLATE_DIRS = (
+    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    os.path.join(DATA_DIRECTORY, 'template'),
+)
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    # Uncomment the next line to enable the admin:
+    'django.contrib.admin',
+    # Uncomment the next line to enable admin documentation:
+    # 'django.contrib.admindocs',
+    'cookbook',
+    'taggit',
+)
+
+# Required for render_table
+#   http://django-tables2.readthedocs.org/en/latest/#render-table
+TEMPLATE_CONTEXT_PROCESSORS = (
+    "django.contrib.auth.context_processors.auth",
+    "django.core.context_processors.debug",
+    "django.core.context_processors.i18n",
+    "django.core.context_processors.media",
+    "django.core.context_processors.static",
+    "django.contrib.messages.context_processors.messages",
+    "django.core.context_processors.request",
+)
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'class': 'django.utils.log.AdminEmailHandler'
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+    }
+}
diff --git a/example/urls.py b/example/urls.py
new file mode 100644 (file)
index 0000000..7742d01
--- /dev/null
@@ -0,0 +1,17 @@
+from django.conf.urls.defaults import patterns, include, url
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+    # Examples:
+    # url(r'^$', 'web.views.home', name='home'),
+    # url(r'^web/', include('web.foo.urls')),
+
+    # Uncomment the admin/doc line below to enable admin documentation:
+    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+    # Uncomment the next line to enable the admin:
+    # url(r'^admin/', include(admin.site.urls)),
+)
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
index 0000000..d72c5be
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+
+'Recipe (as in food) managing application for Django'
+
+from distutils.core import setup
+import os.path
+
+from cookbook import __version__
+
+
+_this_dir = os.path.dirname(__file__)
+
+setup(
+    name='cookbook',
+    version=__version__,
+    description=__doc__,
+    long_description=open(os.path.join(_this_dir, 'README'), 'r').read(),
+    author='W. Trevor King',
+    author_email='wking@drexel.edu',
+    license='GPL',
+    url='http://physics.drexel.edu/~wking/unfolding-disasters/posts/cookbook/',
+    packages=['cookbook'],
+    classifiers=[
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Internet :: WWW/HTTP',
+        'Topic :: Software Development :: Libraries',
+    ],
+)