From: Nick Retallack Date: Sun, 4 Oct 2009 10:15:07 +0000 (-0700) Subject: successfully ported everything to a flesh shabti template. shortnames work now,... X-Git-Url: http://git.veekun.com/zzz-floof.git/commitdiff_plain/c03a740f65ea24ba6567b0ef75f3785faae37d42?ds=inline;hp=--cc successfully ported everything to a flesh shabti template. shortnames work now, and so does all your stuff. Enjoy --- c03a740f65ea24ba6567b0ef75f3785faae37d42 diff --git a/README.txt b/README.txt index a1f2914..68db46c 100644 --- a/README.txt +++ b/README.txt @@ -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 ! + diff --git a/floof/config/environment.py b/floof/config/environment.py index 20683db..3470f2a 100644 --- a/floof/config/environment.py +++ b/floof/config/environment.py @@ -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 index 0000000..53f1f19 --- /dev/null +++ b/floof/forms/__init__.py @@ -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 index 0000000..53f1f19 --- /dev/null +++ b/floof/forms/validators/__init__.py @@ -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 index 0000000..4fb3c8f --- /dev/null +++ b/floof/forms/validators/unique.py @@ -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 diff --git a/floof/lib/base.py b/floof/lib/base.py index 4de5e19..cb7c746 100644 --- a/floof/lib/base.py +++ b/floof/lib/base.py @@ -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 index 0000000..0b2f3ec --- /dev/null +++ b/floof/lib/fixtures.py @@ -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'] diff --git a/floof/lib/helpers.py b/floof/lib/helpers.py index ff55eb6..84c3394 100644 --- a/floof/lib/helpers.py +++ b/floof/lib/helpers.py @@ -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 + diff --git a/floof/model/__init__.py b/floof/model/__init__.py index 5e963e2..9cacd18 100644 --- a/floof/model/__init__.py +++ b/floof/model/__init__.py @@ -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() + diff --git a/floof/model/meta.py b/floof/model/meta.py index 7992ce1..1a20aa7 100644 --- a/floof/model/meta.py +++ b/floof/model/meta.py @@ -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 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 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 index 0000000..61b2d9a Binary files /dev/null and b/floof/public/pylons-logo.gif differ diff --git a/floof/tests/__init__.py b/floof/tests/__init__.py index d51f6f2..68cb322 100644 --- a/floof/tests/__init__.py +++ b/floof/tests/__init__.py @@ -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 index 0000000..03ce23a --- /dev/null +++ b/floof/tests/functional/test_appserver.py @@ -0,0 +1,7 @@ +from floof.tests import * + +class TestAppServer(TestController): + def test_index(self): + response = self.app.get('/') + # Test response... + assert 'Elixir DSL' in response diff --git a/floof/tests/functional/test_elixir.py b/floof/tests/functional/test_elixir.py new file mode 100644 index 0000000..bd0228b --- /dev/null +++ b/floof/tests/functional/test_elixir.py @@ -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' + diff --git a/floof/tests/test_models.py b/floof/tests/test_models.py index e69de29..9b27a5c 100644 --- a/floof/tests/test_models.py +++ b/floof/tests/test_models.py @@ -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() + diff --git a/floof/websetup.py b/floof/websetup.py index fa97149..7caeed0 100644 --- a/floof/websetup.py +++ b/floof/websetup.py @@ -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..).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() diff --git a/setup.py b/setup.py index ebbed71..30bf697 100644 --- 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 diff --git a/test.ini b/test.ini index 80fbd20..a16fbdc 100644 --- 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