successfully ported everything to a flesh shabti template. shortnames work now,...
authorNick Retallack <nickretallack@gmil.com>
Sun, 4 Oct 2009 10:15:07 +0000 (03:15 -0700)
committerNick Retallack <nickretallack@gmil.com>
Sun, 4 Oct 2009 10:15:07 +0000 (03:15 -0700)
20 files changed:
README.txt
floof/config/environment.py
floof/forms/__init__.py [new file with mode: 0644]
floof/forms/validators/__init__.py [new file with mode: 0644]
floof/forms/validators/unique.py [new file with mode: 0644]
floof/lib/base.py
floof/lib/fixtures.py [new file with mode: 0644]
floof/lib/helpers.py
floof/model/__init__.py
floof/model/meta.py
floof/public/bg.png [new file with mode: 0644]
floof/public/favicon.ico [new file with mode: 0644]
floof/public/pylons-logo.gif [new file with mode: 0644]
floof/tests/__init__.py
floof/tests/functional/test_appserver.py [new file with mode: 0644]
floof/tests/functional/test_elixir.py [new file with mode: 0644]
floof/tests/test_models.py
floof/websetup.py
setup.py
test.ini

index a1f2914..68db46c 100644 (file)
@@ -17,3 +17,25 @@ Tweak the config file as appropriate and then setup the application::
     paster setup-app config.ini
 
 Then you are ready to go.
+
+Creating models
+======================
+
+To create a new model class, type::
+
+    paster model mymodel
+
+Once you have defined your model classes in mymodel, import them in floof/models/__init__.py::
+
+    from mymodel import MyEntity
+
+To create tables use create_sql::
+
+    paster create_sql
+
+To drop tables use drop_sql::
+
+    paster drop_sql
+
+Note that you must first import your classes into floof/models/__init__.py in order for the database commands to work !
+
index 20683db..3470f2a 100644 (file)
@@ -3,13 +3,12 @@ import os
 
 from mako.lookup import TemplateLookup
 from pylons import config
-from pylons.error import handle_mako_error
 from sqlalchemy import engine_from_config
 
 import floof.lib.app_globals as app_globals
 import floof.lib.helpers
-from floof import model
 from floof.config.routing import make_map
+import floof.model as model
 
 def load_environment(global_conf, app_conf):
     """Configure the Pylons environment via the ``pylons.config``
@@ -32,22 +31,21 @@ def load_environment(global_conf, app_conf):
     # Create the Mako TemplateLookup, with the default auto-escaping
     config['pylons.app_globals'].mako_lookup = TemplateLookup(
         directories=paths['templates'],
-        error_handler=handle_mako_error,
         module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
-        input_encoding='utf-8', default_filters=['escape'],
-        imports=['from webhelpers.html import escape'])
-
-    # Setup the SQLAlchemy database engine
+        input_encoding='utf-8', output_encoding='utf-8',
+        imports=['from webhelpers.html import escape'],
+        default_filters=['escape'])
+    
+    # Setup the SQLAlchemy^W Elixir database engine
     engine = engine_from_config(config, 'sqlalchemy.')
-
     if model.elixir.options_defaults.get('autoload'):
+        # Reflected tables
         model.elixir.bind = engine
         model.metadata.bind = engine
         model.elixir.setup_all()
     else:
+        # Non-reflected tables
         model.init_model(engine)
-
-    model.meta.engine = engine
-
+    
     # CONFIGURATION OPTIONS HERE (note: all config options will override
     # any Pylons config options)
diff --git a/floof/forms/__init__.py b/floof/forms/__init__.py
new file mode 100644 (file)
index 0000000..53f1f19
--- /dev/null
@@ -0,0 +1 @@
+# Import your forms here
diff --git a/floof/forms/validators/__init__.py b/floof/forms/validators/__init__.py
new file mode 100644 (file)
index 0000000..53f1f19
--- /dev/null
@@ -0,0 +1 @@
+# Import your forms here
diff --git a/floof/forms/validators/unique.py b/floof/forms/validators/unique.py
new file mode 100644 (file)
index 0000000..4fb3c8f
--- /dev/null
@@ -0,0 +1,52 @@
+from formencode import *
+from formencode import validators 
+import pylons
+
+_ = validators._ # dummy translation string
+
+# Custom schemas
+
+class FilteringSchema(Schema):
+    "Schema with extra fields filtered by default"
+    filter_extra_fields = True
+    allow_extra_fields = True
+
+# Model-based validators
+
+class Unique(validators.FancyValidator):
+    
+    """
+    Checks if given value is unique to the model.Will check the state: if state object
+    is the same as the instance, or the state contains a property with the same name
+    as the context name. For example:
+    
+    validator = validators.Unique(model.NewsItem, "title", context_name="news_item")
+    
+    This will check if there is an existing instance with the same "title". If there
+    is a matching instance, will check if the state passed into the validator is the
+    same instance, or if the state contains a property "news_item" which is the same
+    instance.
+    """
+    
+    __unpackargs__ = ('model', 'attr', "model_name", "context_name", "attribute_name")
+    messages = {
+        'notUnique' : _("%(modelName)s already exists with this %(attrName)s"),
+    }
+    
+    model_name = "Item"
+    attribute_name = None
+    context_name = None
+    
+    def validate_python(self, value, state):
+        instance = self.model.get_by(**{self.attr : value})
+        if instance:
+            context_name = self.context_name or self.model.__name__.lower()
+            if state != instance and \
+                getattr(state, context_name, None) != instance:
+                attr_name = self.attribute_name or self.attr
+                raise Invalid(self.message('notUnique', state, 
+                                           modelName=self.model_name,
+                                           attrName=attr_name), 
+                              value, state)
+validators.Unique = Unique
index 4de5e19..cb7c746 100644 (file)
@@ -2,14 +2,17 @@
 
 Provides the BaseController class for subclassing.
 """
-from pylons import session, tmpl_context as c
 from pylons.controllers import WSGIController
 from pylons.templating import render_mako as render
+from pylons import config
+from floof import model
 
+from pylons import session, tmpl_context as c
 from floof.model.users import User
 
 class BaseController(WSGIController):
 
+    # NOTE: This could have been implemented as a middleware =]
     def __before__(self, action, **params):
         # Fetch current user object
         try:
@@ -22,4 +25,10 @@ class BaseController(WSGIController):
         # WSGIController.__call__ dispatches to the Controller method
         # the request is routed to. This routing information is
         # available in environ['pylons.routes_dict']
-        return WSGIController.__call__(self, environ, start_response)
+
+        # Insert any code to be run per request here.
+
+        try:
+            return WSGIController.__call__(self, environ, start_response)
+        finally:
+            model.Session.remove()
diff --git a/floof/lib/fixtures.py b/floof/lib/fixtures.py
new file mode 100644 (file)
index 0000000..0b2f3ec
--- /dev/null
@@ -0,0 +1,139 @@
+import types
+import sys
+import os
+import simplejson
+
+from floof import model as model
+
+"""
+This module can be used for loading data into your models, for example when setting up default application data,
+unit tests, JSON export/import and importing/exporting legacy data. Data is serialized to and from the JSON format. 
+"""
+
+VALID_FIXTURE_FILE_EXTENSIONS = ['.json']
+
+def load_data(model, filename=None, base_dir=None):
+    """Installs provided fixture files into given model. Filename may be directory, file or list of dirs or files. If filename is 
+    None, assumes that source file is located in fixtures/model_module_name/model_tablename.yaml of your application directory, 
+    for example MyProject/fixtures/news/newsitems.yaml. The base_dir argument is the top package of the application unless 
+    specified. You can also pass the name of a table instead of a model class."""
+
+    if type(model) is types.StringType:
+        return load_data_to_table(model, filename, base_dir)
+    else:
+        if filename is None:
+            filename = _default_fixture_path_for_model(model, base_dir)
+        return _load_data_from_file(model, filename)
+
+def load_data_to_table(table, filename=None, base_dir=None):
+    """Installs data directly into a table. Useful if table does not have a corresponding model, for example a many-to-many join table.
+    """
+    
+    if filename is None:
+        filename = _default_fixture_path_for_table(table, base_dir)
+    _load_data_to_table(table, filename)
+    
+def dump_data(model, filename=None, **params):
+    """Dumps data to given destination. Params are optional arguments for selecting data. If filename is None, assumes that destination 
+    file is located in fixtures/model_module_name/model_name_lowercase.yaml of your application directory, for example 
+    MyProject/fixtures/news/newsitem.yaml.
+    """
+    
+    if filename is None:
+        filename = _default_fixture_path_for_model(model)
+    _dump_data_to_file(model, filename, **params)
+
+_base_dir = os.path.dirname(os.path.dirname(__file__))
+
+def _default_fixture_path_for_model(model, base_dir=None):
+    if base_dir is None:
+        base_dir = _base_dir
+    path = os.path.join(base_dir, 'fixtures')
+    module_dirs = model.__module__.split('.', 2)[-1].split('.')
+    for dir in module_dirs:
+        path = os.path.join(path, dir)
+    return os.path.join(path, model.table.name + '.json')    
+
+def _default_fixture_path_for_table(table, base_dir=None):
+    if base_dir is None:
+        base_dir = _base_dir
+    module_dirs = table.split('.')
+    path = os.path.join(base_dir, 'fixtures')
+    for name in module_dirs:
+        path = os.path.join(path, name)
+    return path + ".json"
+
+def _is_fixture_file(filename):
+    basename, ext = os.path.splitext(filename)
+    return (ext.lower() in VALID_FIXTURE_FILE_EXTENSIONS)
+    
+def _load_data_from_dir(model, dirname):
+    for dirpath, dirnames, filenames in os.walk(dirname):
+        for filename in filenames:
+            _load_data_from_file(model, filename)
+    
+def _load_data_from_file(model, filename):
+    if not _is_fixture_file(filename): 
+        return
+    fp = file(filename, 'r')
+    data = simplejson.load(fp)
+    fp.close()
+    retval = None
+    if type(data) is types.ListType:
+        retval = []
+        for item in data:
+            retval.append(_load_instance_from_dict(model, item))
+    elif type(data) is types.DictType:
+        retval = {}
+        for key, item in data.iteritems():
+            retval[key] = _load_instance_from_dict(model, item)    
+    return retval
+
+def _load_data_to_table(tablename, filename):
+    if not _is_fixture_file(filename): 
+        return
+    fp = file(filename, 'r')
+    data = simplejson.load(fp)
+    fp.close()
+    tablename = tablename.split(".")[-1]
+    table = model.context.metadata.tables[tablename]
+    if type(data) is types.ListType:
+        for item in data:
+            table.insert(item).execute()
+    elif type(data) is types.DictType:
+        for key, item in data.iteritems():
+            table.insert(item).execute()
+    return data
+    
+def _dump_data_to_file(model, filename, **params):
+    if params:
+        queryset = model.select_by(**params)
+    else:
+        queryset = model.select()
+    data = []
+    for instance in queryset:
+        data.append(_dump_instance_to_dict(instance))
+    fp = file(filename, 'w')
+    simplejson.dump(data, fp)
+    fp.close()
+    
+def _load_instance_from_dict(model, dict):
+    if not dict: return 
+    instance = model()
+    fields = model._descriptor.fields.keys()
+    for k, v in dict.iteritems():
+        if k in fields:
+            setattr(instance, k, v)
+    instance.flush()
+    return instance
+
+def _dump_instance_to_dict(instance):
+    if hasattr(instance, 'to_json'):
+        return instance.to_json()
+    d = {}
+    fields = instance._descriptor.fields.keys()
+    for field in fields:
+        d[field] = getattr(instance, field)
+    return d
+       
+__all__ = ['load_data', 'dump_data']
index ff55eb6..84c3394 100644 (file)
@@ -1,15 +1,32 @@
+# -*- coding: utf-8 -*-
 """Helper functions
 
 Consists of functions to typically be used within templates, but also
-available to Controllers. This module is available to templates as 'h'.
+available to Controllers. This module is available to both as 'h'.
 """
 # Import helpers as desired, or define your own, ie:
-#from webhelpers.html.tags import checkbox, password
+# from webhelpers.html.tags import checkbox, password
 from webhelpers import *
 from routes import url_for, redirect_to
 
+# Scaffolding helper imports
 from webhelpers.html.tags import *
 from webhelpers.html import literal
 from webhelpers.pylonslib import Flash
 import sqlalchemy.types as types
 flash = Flash()
+# End of.
+
+def get_object_or_404(model, **kw):
+    from pylons.controllers.util import abort
+    """
+    Returns object, or raises a 404 Not Found is object is not in db.
+    Uses elixir-specific `get_by()` convenience function (see elixir source: 
+    http://elixir.ematia.de/trac/browser/elixir/trunk/elixir/entity.py#L1082)
+    Example: user = get_object_or_404(model.User, id = 1)
+    """
+    obj = model.get_by(**kw)
+    if obj is None:
+        abort(404)
+    return obj
+
index 5e963e2..9cacd18 100644 (file)
@@ -1,28 +1,33 @@
-#
-#   floof/floof/model/__init__.py
-#
-#   Copyright (c) 2009 Scribblr
-#
-#   See: http://bel-epa.com/docs/elixir_pylons/
-#
-
 """The application's model objects"""
-
-from floof.model import art, users
-from floof.model import meta
 import elixir
+from floof.model import meta
 
-elixir.options_defaults.update({ 'autoload': True, 'shortnames': True })
-
+Session = elixir.session = meta.Session
+# Uncomment if using reflected tables
+# elixir.options_defaults.update({'autoload': True})
+elixir.options_defaults.update({'shortnames':True})
 metadata = elixir.metadata
 
+# this will be called in config/environment.py
+# if not using reflected tables
 def init_model(engine):
-    elixir.session.configure(bind=engine)
+    """Call me before using any of the tables or classes in the model"""
+    meta.Session.configure(bind=engine)
     metadata.bind = engine
 
-    if elixir.options_defaults.get('autoload', False):
-        if not metadata.is_bound():
-            elixir.delay_setup = True
-    else:
-        elixir.setup_all(True)
+# Delay the setup if using reflected tables
+if elixir.options_defaults.get('autoload', False) \
+    and not metadata.is_bound():
+    elixir.delay_setup = True
+
+# # import other entities here, e.g.
+# from floof.model.blog import BlogEntry, BlogComment
+from floof.model.art import Art
+from floof.model.users import User, IdentityURL
+
+# Finally, call elixir to set up the tables.
+# but not if using reflected tables
+if not elixir.options_defaults.get('autoload', False):
+    elixir.setup_all()
+
 
index 7992ce1..1a20aa7 100644 (file)
@@ -1,11 +1,15 @@
-"""SQLAlchemy Metadata object"""
+"""SQLAlchemy Metadata and Session object"""
 from sqlalchemy import MetaData
 from sqlalchemy.orm import scoped_session, sessionmaker
 
-__all__ = ['engine', 'metadata']
+__all__ = ['Session', 'engine', 'metadata']
 
 # SQLAlchemy database engine. Updated by model.init_model()
 engine = None
 
-metadata = MetaData()
+# SQLAlchemy session manager. Updated by model.init_model()
+Session = scoped_session(sessionmaker())
 
+# Global metadata. If you have multiple databases with overlapping table
+# names, you'll need a metadata for each database
+metadata = MetaData()
diff --git a/floof/public/bg.png b/floof/public/bg.png
new file mode 100644 (file)
index 0000000..69c1798
Binary files /dev/null and b/floof/public/bg.png differ
diff --git a/floof/public/favicon.ico b/floof/public/favicon.ico
new file mode 100644 (file)
index 0000000..21e215e
Binary files /dev/null and b/floof/public/favicon.ico differ
diff --git a/floof/public/pylons-logo.gif b/floof/public/pylons-logo.gif
new file mode 100644 (file)
index 0000000..61b2d9a
Binary files /dev/null and b/floof/public/pylons-logo.gif differ
index d51f6f2..68cb322 100644 (file)
@@ -16,14 +16,68 @@ from routes.util import URLGenerator
 from webtest import TestApp
 
 import pylons.test
+from elixir import *
+from floof.model import *
+from floof.model import meta
+from floof import model as model
+from sqlalchemy import engine_from_config
+
+__all__ = ['environ', 'url', 'TestController', 'TestModel']
 
-__all__ = ['environ', 'url', 'TestController']
 
 # Invoke websetup with the current config file
-SetupCommand('setup-app').run([config['__file__']])
+# SetupCommand('setup-app').run([config['__file__']])
+
+# additional imports ...
+import os
+from paste.deploy import appconfig
+from floof.config.environment import load_environment
 
+here_dir = os.path.dirname(__file__)
+conf_dir = os.path.dirname(os.path.dirname(here_dir))
+
+test_file = os.path.join(conf_dir, 'test.ini')
+conf = appconfig('config:' + test_file)
+load_environment(conf.global_conf, conf.local_conf)
 environ = {}
 
+engine = engine_from_config(config, 'sqlalchemy.')
+model.init_model(engine)
+metadata = elixir.metadata
+Session = elixir.session = meta.Session
+
+class Individual(Entity):
+    """Table 'Individual'.
+
+    >>> me = Individual('Groucho')
+
+    # 'name' field is converted to lowercase
+    >>> me.name
+    'groucho'
+    """
+    name = Field(String(20), unique=True)
+    favorite_color = Field(String(20))
+
+    def __init__(self, name, favorite_color=None):
+        self.name = str(name).lower()
+        self.favorite_color = favorite_color
+
+setup_all()
+
+def setup():
+    pass
+
+def teardown():
+    pass
+
+class TestModel(TestCase):
+    def setUp(self):
+        setup_all(True)
+    
+    def tearDown(self):
+        drop_all(engine)
+
+
 class TestController(TestCase):
 
     def __init__(self, *args, **kwargs):
diff --git a/floof/tests/functional/test_appserver.py b/floof/tests/functional/test_appserver.py
new file mode 100644 (file)
index 0000000..03ce23a
--- /dev/null
@@ -0,0 +1,7 @@
+from floof.tests import *
+
+class TestAppServer(TestController):
+    def test_index(self):
+        response = self.app.get('/')
+        # Test response...
+        assert '<span style="color:lime">Elixir DSL</span>' in response
diff --git a/floof/tests/functional/test_elixir.py b/floof/tests/functional/test_elixir.py
new file mode 100644 (file)
index 0000000..bd0228b
--- /dev/null
@@ -0,0 +1,13 @@
+from floof.tests import *
+from floof.tests import Session, metadata
+
+class TestElixir(TestModel):
+    def setUp(self):
+        TestModel.setUp(self)
+
+    def test_metadata(self):
+        assert 'A collection of Tables and their associated schema constructs.' in metadata.__doc__
+
+    def test_session(self):
+        assert Session.connection().dialect.name is 'sqlite'
+    
index e69de29..9b27a5c 100644 (file)
@@ -0,0 +1,22 @@
+from sqlalchemy.exceptions import IntegrityError
+from floof.tests import *
+from floof.tests import Session, metadata, Individual, create_all, drop_all
+
+class TestMyModel(TestModel):
+
+    def test_simpleassert(self):
+        """test description
+        """
+        einstein = Individual('einstein')
+
+        Session.commit()
+
+        ind1 = Individual.get_by(name = 'einstein')
+        assert ind1 == einstein
+
+    def test_exception(self):
+        me = Individual('giuseppe')
+        me_again = Individual('giuseppe')
+        self.assertRaises(IntegrityError, Session.commit)
+        Session.rollback()
+
index fa97149..7caeed0 100644 (file)
@@ -1,25 +1,31 @@
+# -*- coding: utf-8 -*-
 """Setup the floof application"""
-import elixir
 import logging
 
 from floof.config.environment import load_environment
-from floof.model import meta
-from floof.model.users import IdentityURL, User
 
 log = logging.getLogger(__name__)
 
+from pylons import config
+from elixir import *
+from floof import model as model
+
 def setup_app(command, conf, vars):
     """Place any commands to setup floof here"""
     load_environment(conf.global_conf, conf.local_conf)
+    model.metadata.create_all()
 
-    # Create the tables if they don't already exist
-    elixir.create_all(bind=meta.engine, checkfirst=False)
-
-    ### Load some basic data
+    # Initialisation here ... this sort of stuff:
 
     # Users
     identity_url = IdentityURL(url=u'http://eevee.livejournal.com/')
     user = User(name=u'Eevee')
     user.identity_urls.append(identity_url)
 
-    elixir.session.commit()
+
+    # some_entity = model.Session.query(model.<modelfile>.<Some_Entity>).get(1)
+    # e.g. foo = model.Session.query(model.identity.User).get(1)
+    # from datetime import datetime
+    # some_entity.poked_on = datetime.now()
+    # model.Session.add(some_entity)
+    model.Session.commit()
index ebbed71..30bf697 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ setup(
     #        ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
     #        ('public/**', 'ignore', None)]},
     zip_safe=False,
-    paster_plugins=['PasteScript', 'Pylons'],
+    paster_plugins=['Elixir', 'PasteScript', 'Pylons', 'Shabti'],
     entry_points="""
     [paste.app_factory]
     main = floof.config.middleware:make_app
index 80fbd20..a16fbdc 100644 (file)
--- a/test.ini
+++ b/test.ini
@@ -19,3 +19,5 @@ port = 5000
 use = config:development.ini
 
 # Add additional test specific configuration options as necessary.
+sqlalchemy.url = sqlite:///%(here)s/nosetest.db
+sqlalchemy.echo = False
\ No newline at end of file