From 364935709467c654cc1c079212ec939438935d6f Mon Sep 17 00:00:00 2001
From: "W. Trevor King"
Date: Thu, 4 Aug 2011 15:39:18 -0400
Subject: [PATCH] Rewrite to Django from scratch. Now it's much more
user-friendly.
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
---
.gitignore | 6 +-
.htaccess | 1 -
README | 67 +-
bin/cook.py | 149 -----
cookbook/__init__.py | 12 +-
cookbook/admin.py | 55 ++
cookbook/cookbook.py | 578 ------------------
cookbook/models.py | 111 ++++
cookbook/mom.py | 136 -----
cookbook/server.py | 440 -------------
cookbook/template/base.html | 33 -
cookbook/template/edit-recipe.html | 13 -
cookbook/template/recipe.html | 82 ---
cookbook/template/recipes.html | 24 -
cookbook/tests.py | 16 +
cookbook/urls.py | 36 ++
cookbook/views.py | 14 +
doc/abbreviations | 25 -
example/__init__.py | 0
example/data/static/cookbook.ico | Bin 0 -> 4286 bytes
example/data/static/style.css | 109 ++++
.../template/admin/edit_inline/tabular.html | 129 ++++
example/data/template/cookbook/base.html | 29 +
example/data/template/cookbook/recipe.html | 58 ++
example/data/template/cookbook/recipes.html | 18 +
example/data/template/cookbook/tags.html | 18 +
example/manage.py | 14 +
example/settings.py | 175 ++++++
example/urls.py | 17 +
setup.py | 33 +
30 files changed, 894 insertions(+), 1504 deletions(-)
delete mode 100644 .htaccess
delete mode 100755 bin/cook.py
create mode 100644 cookbook/admin.py
delete mode 100644 cookbook/cookbook.py
create mode 100644 cookbook/models.py
delete mode 100644 cookbook/mom.py
delete mode 100644 cookbook/server.py
delete mode 100644 cookbook/template/base.html
delete mode 100644 cookbook/template/edit-recipe.html
delete mode 100644 cookbook/template/recipe.html
delete mode 100644 cookbook/template/recipes.html
create mode 100644 cookbook/tests.py
create mode 100644 cookbook/urls.py
create mode 100644 cookbook/views.py
delete mode 100644 doc/abbreviations
create mode 100644 example/__init__.py
create mode 100644 example/data/static/cookbook.ico
create mode 100644 example/data/static/style.css
create mode 100644 example/data/template/admin/edit_inline/tabular.html
create mode 100644 example/data/template/cookbook/base.html
create mode 100644 example/data/template/cookbook/recipe.html
create mode 100644 example/data/template/cookbook/recipes.html
create mode 100644 example/data/template/cookbook/tags.html
create mode 100644 example/manage.py
create mode 100644 example/settings.py
create mode 100644 example/urls.py
create mode 100755 setup.py
diff --git a/.gitignore b/.gitignore
index 58e2130..f5d4d3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+MANIFEST
+sqlite3.db
+dist
+build
*.pyc
-recipe
-mom*
diff --git a/.htaccess b/.htaccess
deleted file mode 100644
index 50c2d6b..0000000
--- a/.htaccess
+++ /dev/null
@@ -1 +0,0 @@
-guest:cookbook:8eb3d4841608f011af3dd4bdc4c8b340
diff --git a/README b/README
index 152440d..e751563 100644
--- 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 .
+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
index a832e33..0000000
--- a/bin/cook.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/python
-
-# Copyright (C) 2010 W. Trevor King
-#
-# 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 .
-
-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()
diff --git a/cookbook/__init__.py b/cookbook/__init__.py
index 920f373..b650ceb 100644
--- a/cookbook/__init__.py
+++ b/cookbook/__init__.py
@@ -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
index 0000000..cb6a1b1
--- /dev/null
+++ b/cookbook/admin.py
@@ -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
index 24a70fe..0000000
--- a/cookbook/cookbook.py
+++ /dev/null
@@ -1,578 +0,0 @@
-#!/usr/bin/python
-# -*- encoding: utf-8 -*-
-#
-# Copyright (C) 2010 W. Trevor King
-#
-# 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 .
-
-"""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
-
- paragraph 2
-
- 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
-
- paragraph 2
-
- 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
-
- 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
-
- 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
index 0000000..3d75d57
--- /dev/null
+++ b/cookbook/models.py
@@ -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
index 54336cf..0000000
--- a/cookbook/mom.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# Copyright (C) 2010 W. Trevor King
-#
-# 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 .
-
-"""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
index e94e769..0000000
--- a/cookbook/server.py
+++ /dev/null
@@ -1,440 +0,0 @@
-# Copyright (C) 2010 W. Trevor King
-#
-# 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 .
-
-"""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 = [
- '%s' % (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 = [
- '',
- ])
- 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 [
- ' ',
- ' %s | ' % note,
- ' | '
- % value,
- '
',
- ]
-
- def _text_form(self, name, user, value):
- return [
- ' ',
- ' | ' % (name, user),
- ' | '
- % (name, value or ''),
- '
',
- ]
-
- def _textarea_form(self, name, user, value):
- return [
- ' ',
- ' | ' % (name, user),
- ' ' % name,
- value,
- ' | '
- '
',
- ]
-
- 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
index cfd1b71..0000000
--- a/cookbook/template/base.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
- {{ cookbook.name }}
-
-
-
-
-
-
-
-
{% block page_title %}{% endblock %}
- {% block content %}{% endblock %}
-
-
-
-
-
diff --git a/cookbook/template/edit-recipe.html b/cookbook/template/edit-recipe.html
deleted file mode 100644
index fcd2a01..0000000
--- a/cookbook/template/edit-recipe.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block page_title %}
- {% if recipe.url %}
- {{ recipe.name }}
- {% 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
index ac7a90a..0000000
--- a/cookbook/template/recipe.html
+++ /dev/null
@@ -1,82 +0,0 @@
-{% extends "base.html" %}
-
-{% block page_title %}
- {% if recipe.url %}
- {{ recipe.name }}
- {% else %}
- {{ recipe.name }}
- {% endif %}
-{% endblock %}
-
-{% block content %}
-
- {% if recipe.yield_ %}
-
- {{ recipe.yield_ }}
- {% endif %}
- {% if recipe.author %}
-
- {{ recipe.author }}
- {% endif %}
- {% if recipe.source %}
-
- {{ recipe.source }}
- {% endif %}
- {% if recipe.tags %}
-
- {{ ', '.join(tag_links) }}
-
- {% endif %}
-
-
- {% if recipe.tags %}
-
- {% endif %}
-
- {% if recipe.ingredient_blocks %}
-
- {% for ingredient_block in recipe.ingredient_blocks %}
-
{{ ingredient_block.heading() }}
-
- {% for ingredient in ingredient_block %}
- {{ ingredient.__unicode__() }} |
- {% endfor %}
-
- {% endfor %}
-
- {% endif %}
-
- {% if recipe.directions %}
-
-
Directions
- {% for paragraph in recipe.directions.wrapped_paragraphs() %}
-
{{ paragraph }}
- {% endfor %}
-
- {% endif %}
-
-
-{% endblock %}
diff --git a/cookbook/template/recipes.html b/cookbook/template/recipes.html
deleted file mode 100644
index 06e320e..0000000
--- a/cookbook/template/recipes.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{% extends "base.html" %}
-
-{% block page_title %}
- Index {% if selected_tags %}({{ ', '.join(selected_tags) }}){% endif %}
-{% endblock %}
-
-{% block content %}
-
-
-
- add new receipe
-{% endblock %}
diff --git a/cookbook/tests.py b/cookbook/tests.py
new file mode 100644
index 0000000..501deb7
--- /dev/null
+++ b/cookbook/tests.py
@@ -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
index 0000000..146dbcc
--- /dev/null
+++ b/cookbook/urls.py
@@ -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\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\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
index 0000000..dbc1bd2
--- /dev/null
+++ b/cookbook/views.py
@@ -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
index a4e25de..0000000
--- a/doc/abbreviations
+++ /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
index 0000000..e69de29
diff --git a/example/data/static/cookbook.ico b/example/data/static/cookbook.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f321568eca2ceef65d57aeb505b47273795b57e3
GIT binary patch
literal 4286
zcmd5H!M^QheDC#8wGDSra;C$#s)}H_2Lp+?IC;<4s
zfq36a>T&pYt@BA-K=31i=LvpJ@WK@Qgj_#M@H7E(=iC>On73llbI)#%SQK`6{gUWY
z8-I1^!j@&}nOk4U&l9|M7m+J?tpnqgsFw@VHZRLK@!`_cFW3JvMiBnu8^3*VA=zeq#_zpHEfCRW;Ces$Q`UFRGbR(kwS6U_y@+<)NVe0E*H
zrS0|T+N+$3lFP$)PQ*wHJ`Zb5k9zf3^1Dlxz5K$$8STf}Ex%ciEeJbamawjIpz4~&
z?=S>{P6J~Ts~K&MmeZUvaH=E=r%AAKn$2!dMLmFl1OwN%*T5-H4RY#h{j8&_m!X|H
z#!qS@8rsr?bxqdTi-Vs2p_KX9v
z{J4m#=rLUlxpAvM=@cI7qTO~jeRnZ)IJ^vUBf6kzts1&NFhJkOHW(zn#+Wg%#reP)
zKOTxk!5Eo;Kx(j%8it^L4XGhQ3Kzrb;LFvyAUv7H&?7dMXiLyzI{)Im?_L`-s>Xz=
z#a!I$xp3@_8t6$g!TqlsApfHs)KP9Q{KX6QU4C%yqrsENgrY6p4|-CAD#`^te=tMq
zds?WDRDdvC3i^38e}DszD$
zeH8jPyI^RW2h2Oi!MTTq5#l|zp9w|Bo&cD3_+ap}QBaUQ>RhjbhPRcFy{ZMi3NHe|
zipy~J`~`A9=zZuho&VKc>y~JmZ>bdG4(5y1`EWY20UBd8FxpOoI@%4oXfIge{NNz|
z?!+LtlNfN3=-5qzDQ+CJ+en?8oT0N4uN{EvtJ@%T|}9@F_Z|9u-(
zo+=OqN34NM3Aed3zrO<|YZYKG^MO_91vTDpf>AKWdB74+YT4xlbG!%i+ebkwaD(y_
z8}xi+hK}`G(tkhPh-`(_h+@b*lEb1VYNN+={y2Yi??;Q}WkRW4qh!+FuZ7!dr6B*S
z9^9Qi=!qTz=_W16KOF`|6!|V}9R_*S5XiO+f@Gr#+BWn<<2pIiua!ad8VTvY3C?_2
zz>Vtr8Pr7WCwWhn>zyUr+gLLGAgZ
zV%lvAG#_r@Zf@v;#*`s2H+!IcrwMxY*r7l10jT%8K%3%*p=5F{iRAZS?*kZ)cfe4r
z71}@Ug_fgL3~CUqvh;mho|)92Nz6GQST4DB<*whVWy|-8x$I5dpsH|yjO@W3!63B7
z>Y$tS+Y@gBIjN~{hZ&UH&2T@)2r7XQdXM*TL8q3zcRQma`#^N~OgyJ@TCW_WM9023
zqL(#kX`__U-YyBKD%v^QfQfHS6LSrx8~BE_M!w}j3*UMEE-y`Q<7C(CIAwl4J6Ks7
z2pH}$n2Q>yHC212*O&LlJX3x?InS-{cLj%}=KH(Z<8Ph$+-&;8TSP%|!F$Z%|
zV=m9)oTIycU7vNbD&R7V)2dpUR@c$2rHA#)3j+S$DZsJ(W`I`J(8$Fc%tdc}&9vu7
zd2Khop$bkX$KEZ@Y@Qf3v9z*;4)hk%w6c`cQo{r_^$f<)c%Xvj#!PJcEn(BmQ_1m|
z`>5V*#$WG_o?mh?HAB%2id}5`R{a
zd0fPe=r~$YNc`)VkiYdi;@?I5>lo5IhaBWir!$xN%B!bEywA!975NM^Si?-1I@k$o
z4~G%Q{J4Wh&Rjg6lv|a3PQ-gHe6aTh!)hwn31b^KVUchcam@SdJaV3-F`KoAcQ8d&
zUB5v5%{(p7VHkBe%NkmVe>aB_$Gp$RBWIF7m-~Kdos)XidFdhlpgfCV)urr&p_%x1
zaTsxo{>Yi+|F8Wk^D;z1x1RTOXE0uci1n)KIIp&a^J(fiUtbY<#u|C#d|&>>!n6{n
zPQe=*&N8~zELPW9#Od#qbNcRL&UiPQb?TKoa=s6L;ko0>Yl?2PIQ93zt(Ea!>mV?`
z5tyKT5OA4cR43 */
+
+body {
+ background: #eee;
+}
+
+.fullclear {
+ width:100%;
+ height:1px;
+ margin:0;
+ padding:0;
+ clear:both;
+}
+
+/*
+
+ {% block navigation %}
+
+ {% endblock %}
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+ */
+
+#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;
+}
+
+/*
*/
+
+#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;
+}
+
+/*
*/
diff --git a/example/data/template/admin/edit_inline/tabular.html b/example/data/template/admin/edit_inline/tabular.html
new file mode 100644
index 0000000..b0a2c65
--- /dev/null
+++ b/example/data/template/admin/edit_inline/tabular.html
@@ -0,0 +1,129 @@
+{% load i18n adminmedia admin_modify %}
+{% load url from future %}
+
+
+
diff --git a/example/data/template/cookbook/base.html b/example/data/template/cookbook/base.html
new file mode 100644
index 0000000..5e0b6f8
--- /dev/null
+++ b/example/data/template/cookbook/base.html
@@ -0,0 +1,29 @@
+
+