save point (see note)
Added initial alembic skeleton, improved base pyramid templates, added auth db extension, etc...
This commit is contained in:
		
							parent
							
								
									727b9a5fa7
								
							
						
					
					
						commit
						b1e6b12b71
					
				
					 43 changed files with 2293 additions and 347 deletions
				
			
		
							
								
								
									
										5
									
								
								CHANGES.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								CHANGES.txt
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
 | 
			
		||||
0.1a1
 | 
			
		||||
-----
 | 
			
		||||
 | 
			
		||||
-  Initial version
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,2 @@
 | 
			
		|||
include COPYING.txt
 | 
			
		||||
include *.txt
 | 
			
		||||
include ez_setup.py
 | 
			
		||||
# recursive-include edbob/data *
 | 
			
		||||
# recursive-include edbob/db/schema *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,8 +37,6 @@ The following functions are considered "core" to ``edbob``:
 | 
			
		|||
 | 
			
		||||
.. autofunction:: basic_logging
 | 
			
		||||
 | 
			
		||||
.. autofunction:: entry_point_map
 | 
			
		||||
 | 
			
		||||
.. autofunction:: get_uuid
 | 
			
		||||
 | 
			
		||||
.. autofunction:: graft
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ import subprocess
 | 
			
		|||
import logging
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
from edbob.util import requires_impl
 | 
			
		||||
from edbob.util import entry_point_map, requires_impl
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArgumentParser(argparse.ArgumentParser):
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ See the file COPYING.txt for more information.
 | 
			
		|||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        edbob.Object.__init__(self, **kwargs)
 | 
			
		||||
        self.subcommands = edbob.entry_point_map('%s.commands' % self.name)
 | 
			
		||||
        self.subcommands = entry_point_map('%s.commands' % self.name)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return str(self.name)
 | 
			
		||||
| 
						 | 
				
			
			@ -259,8 +259,74 @@ class DatabaseCommand(Subcommand):
 | 
			
		|||
    def add_parser_args(self, parser):
 | 
			
		||||
        parser.add_argument('-D', '--database', metavar='URL',
 | 
			
		||||
                            help="Database engine (default is edbob.db.engine)")
 | 
			
		||||
        parser.add_argument('command', choices=['upgrade', 'extensions', 'activate', 'deactivate'],
 | 
			
		||||
                            help="Command to execute against database")
 | 
			
		||||
        # parser.add_argument('command', choices=['upgrade', 'extensions', 'activate', 'deactivate'],
 | 
			
		||||
        #                     help="Command to execute against database")
 | 
			
		||||
        subparsers = parser.add_subparsers(title='subcommands')
 | 
			
		||||
 | 
			
		||||
        extensions = subparsers.add_parser('extensions',
 | 
			
		||||
                                           help="Display current extension status for the database")
 | 
			
		||||
        extensions.set_defaults(func=self.extensions)
 | 
			
		||||
 | 
			
		||||
        activate = subparsers.add_parser('activate',
 | 
			
		||||
                                         help="Activate an extension within the database")
 | 
			
		||||
        activate.add_argument('extension', help="Name of extension to activate")
 | 
			
		||||
        activate.set_defaults(func=self.activate)
 | 
			
		||||
 | 
			
		||||
        deactivate = subparsers.add_parser('deactivate',
 | 
			
		||||
                                           help="Deactivate an extension within the database")
 | 
			
		||||
        deactivate.add_argument('extension', help="Name of extension to deactivate")
 | 
			
		||||
        deactivate.set_defaults(func=self.deactivate)
 | 
			
		||||
 | 
			
		||||
    def activate(self, engine, args):
 | 
			
		||||
        from edbob.db.extensions import (
 | 
			
		||||
            available_extensions,
 | 
			
		||||
            extension_active,
 | 
			
		||||
            activate_extension,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if args.extension in available_extensions():
 | 
			
		||||
            if not extension_active(args.extension, engine):
 | 
			
		||||
                activate_extension(args.extension, engine)
 | 
			
		||||
                print "Activated extension '%s' in database:" % args.extension
 | 
			
		||||
                print '  %s' % engine.url
 | 
			
		||||
            else:
 | 
			
		||||
                print >> sys.stderr, "Extension already active: %s" % args.extension
 | 
			
		||||
        else:
 | 
			
		||||
            print >> sys.stderr, "Extension unknown: %s" % args.extension
 | 
			
		||||
 | 
			
		||||
    def deactivate(self, engine, args):
 | 
			
		||||
        from edbob.db.extensions import (
 | 
			
		||||
            available_extensions,
 | 
			
		||||
            extension_active,
 | 
			
		||||
            deactivate_extension,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if args.extension in available_extensions():
 | 
			
		||||
            if extension_active(args.extension, engine):
 | 
			
		||||
                deactivate_extension(args.extension, engine)
 | 
			
		||||
                print "Deactivated extension '%s' in database:" % args.extension
 | 
			
		||||
                print '  %s' % engine.url
 | 
			
		||||
            else:
 | 
			
		||||
                print >> sys.stderr, "Extension already inactive: %s" % args.extension
 | 
			
		||||
        else:
 | 
			
		||||
            print >> sys.stderr, "Extension unknown: %s" % args.extension
 | 
			
		||||
 | 
			
		||||
    def extensions(self, engine, args):
 | 
			
		||||
        from edbob.db.extensions import (
 | 
			
		||||
            available_extensions,
 | 
			
		||||
            extension_active,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        print "Extensions for database:"
 | 
			
		||||
        print '  %s' % engine.url
 | 
			
		||||
        print ''
 | 
			
		||||
        print " Name            Active?"
 | 
			
		||||
        print "------------------------"
 | 
			
		||||
        for name in sorted(available_extensions()):
 | 
			
		||||
            print " %-16s  %s" % (
 | 
			
		||||
                name, 'Yes' if extension_active(name, engine) else 'No')
 | 
			
		||||
        print ''
 | 
			
		||||
        print "Use 'edbob db [de]activate <extension>' to change."
 | 
			
		||||
 | 
			
		||||
    def run(self, args):
 | 
			
		||||
        if args.database:
 | 
			
		||||
| 
						 | 
				
			
			@ -276,9 +342,7 @@ class DatabaseCommand(Subcommand):
 | 
			
		|||
        if not engine:
 | 
			
		||||
            print >> sys.stderr, "Database not configured; please change that or specify -D URL"
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if args.command == 'upgrade':
 | 
			
		||||
            print 'got upgrade ..'
 | 
			
		||||
        args.func(engine, args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# class ExtensionsCommand(RattailCommand):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,10 +29,9 @@
 | 
			
		|||
 | 
			
		||||
import logging
 | 
			
		||||
import uuid
 | 
			
		||||
from pkg_resources import iter_entry_points
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Object', 'basic_logging', 'entry_point_map', 'get_uuid', 'graft']
 | 
			
		||||
__all__ = ['Object', 'basic_logging', 'get_uuid', 'graft']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Object(object):
 | 
			
		||||
| 
						 | 
				
			
			@ -75,21 +74,6 @@ def basic_logging():
 | 
			
		|||
    logging.getLogger().addHandler(handler)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def entry_point_map(key):
 | 
			
		||||
    """
 | 
			
		||||
    Convenience function to retrieve a dictionary of entry points, keyed by
 | 
			
		||||
    name.
 | 
			
		||||
 | 
			
		||||
    ``key`` must be the "section name" for the entry points you're after, e.g.
 | 
			
		||||
    ``'edbob.commands'``.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    epmap = {}
 | 
			
		||||
    for ep in iter_entry_points(key):
 | 
			
		||||
        epmap[ep.name] = ep.load()
 | 
			
		||||
    return epmap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_uuid():
 | 
			
		||||
    """
 | 
			
		||||
    Generates a universally-unique identifier and returns its 32-character hex
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,20 +26,22 @@
 | 
			
		|||
``edbob.db`` -- Database Framework
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import engine_from_config
 | 
			
		||||
from sqlalchemy import engine_from_config, MetaData
 | 
			
		||||
from sqlalchemy.orm import sessionmaker
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['engines', 'engine', 'Session', 'metadata',
 | 
			
		||||
           'get_setting', 'save_setting']
 | 
			
		||||
# __all__ = ['engines', 'engine', 'Session', 'metadata',
 | 
			
		||||
#            'get_setting', 'save_setting']
 | 
			
		||||
 | 
			
		||||
__all__ = ['engines', 'engine', 'Session', 'get_setting', 'save_setting']
 | 
			
		||||
 | 
			
		||||
inited = False
 | 
			
		||||
engines = None
 | 
			
		||||
engine = None
 | 
			
		||||
Session = sessionmaker()
 | 
			
		||||
metadata = None
 | 
			
		||||
# metadata = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init(config):
 | 
			
		||||
| 
						 | 
				
			
			@ -65,13 +67,16 @@ def init(config):
 | 
			
		|||
    """
 | 
			
		||||
 | 
			
		||||
    import edbob.db
 | 
			
		||||
    from edbob.db import classes
 | 
			
		||||
    # from edbob.db import classes
 | 
			
		||||
    from edbob.db import model
 | 
			
		||||
    from edbob.db import enum
 | 
			
		||||
    from edbob.db.model import get_metadata
 | 
			
		||||
    from edbob.db.mappers import make_mappers
 | 
			
		||||
    from edbob.db.extensions import extend_framework
 | 
			
		||||
    from edbob.db.model import Base
 | 
			
		||||
    # from edbob.db.model import get_metadata
 | 
			
		||||
    # from edbob.db.mappers import make_mappers
 | 
			
		||||
    # from edbob.db.extensions import extend_framework
 | 
			
		||||
 | 
			
		||||
    global inited, engines, engine, metadata
 | 
			
		||||
    # global inited, engines, engine, metadata
 | 
			
		||||
    global inited, engines, engine
 | 
			
		||||
 | 
			
		||||
    keys = config.get('edbob.db', 'sqlalchemy.keys')
 | 
			
		||||
    if keys:
 | 
			
		||||
| 
						 | 
				
			
			@ -94,13 +99,15 @@ def init(config):
 | 
			
		|||
    engine = engines.get('default')
 | 
			
		||||
    if engine:
 | 
			
		||||
        Session.configure(bind=engine)
 | 
			
		||||
        Base.metadata.bind = engine
 | 
			
		||||
    
 | 
			
		||||
    metadata = get_metadata(bind=engine)
 | 
			
		||||
    make_mappers(metadata)
 | 
			
		||||
    extend_framework()
 | 
			
		||||
    # metadata = get_metadata(bind=engine)
 | 
			
		||||
    # make_mappers(metadata)
 | 
			
		||||
    # extend_framework()
 | 
			
		||||
 | 
			
		||||
    edbob.graft(edbob, edbob.db)
 | 
			
		||||
    edbob.graft(edbob, classes)
 | 
			
		||||
    # edbob.graft(edbob, classes)
 | 
			
		||||
    edbob.graft(edbob, model)
 | 
			
		||||
    edbob.graft(edbob, enum)
 | 
			
		||||
    inited = True
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -139,19 +146,35 @@ def save_setting(name, value, session=None):
 | 
			
		|||
        session.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_core_metadata():
 | 
			
		||||
    """
 | 
			
		||||
    Returns a :class:`sqlalchemy.MetaData` instance containing only those
 | 
			
		||||
    :class:`sqlalchemy.Table`s which are part of the core ``edbob`` schema.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    from edbob.db import model
 | 
			
		||||
 | 
			
		||||
    meta = MetaData()
 | 
			
		||||
    for name in model.__all__:
 | 
			
		||||
        if name != 'Base':
 | 
			
		||||
            obj = getattr(model, name)
 | 
			
		||||
            if isinstance(obj, type) and issubclass(obj, model.Base):
 | 
			
		||||
                obj.__table__.tometadata(meta)
 | 
			
		||||
    return meta
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def needs_session(func):
 | 
			
		||||
    """
 | 
			
		||||
    Decorator which adds helpful session handling.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def wrapped(*args, **kwargs):
 | 
			
		||||
        session = kwargs.get('session')
 | 
			
		||||
        _session = session
 | 
			
		||||
        session = kwargs.pop('session', None)
 | 
			
		||||
        _orig_session = session
 | 
			
		||||
        if not session:
 | 
			
		||||
            session = Session()
 | 
			
		||||
        kwargs['session'] = session
 | 
			
		||||
        res = func(session, *args, **kwargs)
 | 
			
		||||
        if not _session:
 | 
			
		||||
        if not _orig_session:
 | 
			
		||||
            session.commit()
 | 
			
		||||
            session.close()
 | 
			
		||||
        return res
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								edbob/db/alembic.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								edbob/db/alembic.ini
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
# A generic, single database configuration.
 | 
			
		||||
 | 
			
		||||
[alembic]
 | 
			
		||||
# path to migration scripts
 | 
			
		||||
script_location = schema
 | 
			
		||||
 | 
			
		||||
# template used to generate migration files
 | 
			
		||||
# file_template = %%(rev)s_%%(slug)s
 | 
			
		||||
 | 
			
		||||
sqlalchemy.url = driver://user:pass@localhost/dbname
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Logging configuration
 | 
			
		||||
[loggers]
 | 
			
		||||
keys = root,sqlalchemy,alembic
 | 
			
		||||
 | 
			
		||||
[handlers]
 | 
			
		||||
keys = console
 | 
			
		||||
 | 
			
		||||
[formatters]
 | 
			
		||||
keys = generic
 | 
			
		||||
 | 
			
		||||
[logger_root]
 | 
			
		||||
level = WARN
 | 
			
		||||
handlers = console
 | 
			
		||||
qualname =
 | 
			
		||||
 | 
			
		||||
[logger_sqlalchemy]
 | 
			
		||||
level = WARN
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = sqlalchemy.engine
 | 
			
		||||
 | 
			
		||||
[logger_alembic]
 | 
			
		||||
level = INFO
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = alembic
 | 
			
		||||
 | 
			
		||||
[handler_console]
 | 
			
		||||
class = StreamHandler
 | 
			
		||||
args = (sys.stderr,)
 | 
			
		||||
level = NOTSET
 | 
			
		||||
formatter = generic
 | 
			
		||||
 | 
			
		||||
[formatter_generic]
 | 
			
		||||
format = %(levelname)-5.5s [%(name)s] %(message)s
 | 
			
		||||
datefmt = %H:%M:%S
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ import logging
 | 
			
		|||
# from pkg_resources import iter_entry_points
 | 
			
		||||
 | 
			
		||||
import sqlalchemy.exc
 | 
			
		||||
from sqlalchemy import MetaData
 | 
			
		||||
# from sqlalchemy.orm import clear_mappers
 | 
			
		||||
 | 
			
		||||
import migrate.versioning.api
 | 
			
		||||
| 
						 | 
				
			
			@ -47,20 +48,19 @@ import edbob
 | 
			
		|||
import edbob.db
 | 
			
		||||
from edbob.db import exceptions
 | 
			
		||||
from edbob.db import Session
 | 
			
		||||
from edbob.db.classes import ActiveExtension
 | 
			
		||||
# from edbob.db.classes import ActiveExtension
 | 
			
		||||
from edbob.db.model import Base, ActiveExtension
 | 
			
		||||
from edbob.db.util import (
 | 
			
		||||
    get_database_version,
 | 
			
		||||
    get_repository_path,
 | 
			
		||||
    get_repository_version,
 | 
			
		||||
    )
 | 
			
		||||
from edbob.util import requires_impl
 | 
			
		||||
from edbob.modules import import_module_path
 | 
			
		||||
from edbob.util import entry_point_map, requires_impl
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
available_extensions = edbob.entry_point_map('edbob.db.extensions')
 | 
			
		||||
active_extensions = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Extension(edbob.Object):
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			@ -71,22 +71,27 @@ class Extension(edbob.Object):
 | 
			
		|||
    # derived class.
 | 
			
		||||
    required_extensions = []
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    @requires_impl(is_property=True)
 | 
			
		||||
    def name(self):
 | 
			
		||||
        """
 | 
			
		||||
        The name of the extension.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
    # You can set this to any dotted module path you like.  If unset a default
 | 
			
		||||
    # will be assumed, of the form ``<path.to.extension>.model`` (see
 | 
			
		||||
    # :meth:`Extension.get_models_module()` for more info).
 | 
			
		||||
    model_module = ''
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    @requires_impl(is_property=True)
 | 
			
		||||
    def schema(self):
 | 
			
		||||
        """
 | 
			
		||||
        Should return a reference to the extension's ``schema`` module, which
 | 
			
		||||
        is assumed to be a SQLAlchemy-Migrate repository.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
    # @property
 | 
			
		||||
    # @requires_impl(is_property=True)
 | 
			
		||||
    # def name(self):
 | 
			
		||||
    #     """
 | 
			
		||||
    #     The name of the extension.
 | 
			
		||||
    #     """
 | 
			
		||||
    #     pass
 | 
			
		||||
 | 
			
		||||
    # @property
 | 
			
		||||
    # @requires_impl(is_property=True)
 | 
			
		||||
    # def schema(self):
 | 
			
		||||
    #     """
 | 
			
		||||
    #     Should return a reference to the extension's ``schema`` module, which
 | 
			
		||||
    #     is assumed to be a SQLAlchemy-Migrate repository.
 | 
			
		||||
    #     """
 | 
			
		||||
    #     pass
 | 
			
		||||
 | 
			
		||||
    def add_class(self, cls):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -119,13 +124,54 @@ class Extension(edbob.Object):
 | 
			
		|||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def get_metadata(self):
 | 
			
		||||
    def get_metadata(self, recurse=False):
 | 
			
		||||
        """
 | 
			
		||||
        Should return a :class:`sqlalchemy.MetaData` instance containing the
 | 
			
		||||
        schema definition for the extension, or ``None``.
 | 
			
		||||
        Returns a :class:`sqlalchemy.MetaData` instance containing the schema
 | 
			
		||||
        definition for the extension.
 | 
			
		||||
 | 
			
		||||
        If ``recurse`` evaluates to true, then tables from any extensions upon
 | 
			
		||||
        which this one relies will be included as well.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
        meta = MetaData()
 | 
			
		||||
        self.populate_metadata(meta, recurse)
 | 
			
		||||
        return meta
 | 
			
		||||
 | 
			
		||||
    def get_model_module(self):
 | 
			
		||||
        """
 | 
			
		||||
        Imports and returns a reference to the Python module providing schema
 | 
			
		||||
        definition for the extension.
 | 
			
		||||
 | 
			
		||||
        :attr:`Extension.model_module` is first consulted to determine the
 | 
			
		||||
        dotted module path.  If nothing is found there, a default path is
 | 
			
		||||
        constructed by appending ``'.model'`` to the extension module's own
 | 
			
		||||
        dotted path.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if self.model_module:
 | 
			
		||||
            module = self.model_module
 | 
			
		||||
        else:
 | 
			
		||||
            module = str(self.__class__.__module__) + '.model'
 | 
			
		||||
        return import_module_path(module)
 | 
			
		||||
 | 
			
		||||
    def populate_metadata(self, metadata, recurse=False):
 | 
			
		||||
        """
 | 
			
		||||
        Populates ``metadata`` with tables provided by the extension.
 | 
			
		||||
 | 
			
		||||
        If ``recurse`` evaluates to true, then tables for any extension upon
 | 
			
		||||
        which this one relies will also be included.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if recurse:
 | 
			
		||||
            for name in self.required_extensions:
 | 
			
		||||
                ext = get_extension(name)
 | 
			
		||||
                ext.populate_metadata(metadata, True)
 | 
			
		||||
 | 
			
		||||
        model = self.get_model_module()
 | 
			
		||||
        for name in model.__all__:
 | 
			
		||||
            obj = getattr(model, name)
 | 
			
		||||
            if isinstance(obj, type) and issubclass(obj, model.Base):
 | 
			
		||||
                obj.__table__.tometadata(metadata)
 | 
			
		||||
 | 
			
		||||
    def remove_class(self, name):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -164,54 +210,81 @@ def activate_extension(extension, engine=None):
 | 
			
		|||
    if not isinstance(extension, Extension):
 | 
			
		||||
        extension = get_extension(extension)
 | 
			
		||||
 | 
			
		||||
    log.info("Activating extension: %s" % extension.name)
 | 
			
		||||
    # Skip all this if already active.
 | 
			
		||||
    if extension_active(extension, engine):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    log.debug("Activating extension: %s" % extension.name)
 | 
			
		||||
 | 
			
		||||
    # Activate all required extensions first.
 | 
			
		||||
    for name in extension.required_extensions:
 | 
			
		||||
        activate_extension(name, engine)
 | 
			
		||||
 | 
			
		||||
    # Install schema for this extension.
 | 
			
		||||
    install_extension_schema(extension, engine)
 | 
			
		||||
 | 
			
		||||
    # Add ActiveExtension record for this extension.
 | 
			
		||||
    session = Session(bind=engine)
 | 
			
		||||
    if not session.query(ActiveExtension).get(extension.name):
 | 
			
		||||
        session.add(ActiveExtension(name=extension.name))
 | 
			
		||||
        session.commit()
 | 
			
		||||
    session.close()
 | 
			
		||||
 | 
			
		||||
    merge_extension_metadata(extension)
 | 
			
		||||
    extension.extend_classes()
 | 
			
		||||
    extension.extend_mappers(rattail.metadata)
 | 
			
		||||
    active_extensions[extension.name] = extension
 | 
			
		||||
    # merge_extension_metadata(extension)
 | 
			
		||||
    # extension.extend_classes()
 | 
			
		||||
    # extension.extend_mappers(Base.metadata)
 | 
			
		||||
 | 
			
		||||
    # Add extension to in-memory active extensions tracker.
 | 
			
		||||
    active_extensions(engine).append(extension.name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def deactivate_extension(extension, engine=None):
 | 
			
		||||
#     """
 | 
			
		||||
#     Uninstalls an extension's schema from the primary database, and immediately
 | 
			
		||||
#     requests it to restore the ORM API.
 | 
			
		||||
_available_extensions = None
 | 
			
		||||
def available_extensions():
 | 
			
		||||
    """
 | 
			
		||||
    Returns the map of available :class:`Extension` classes, as determined by
 | 
			
		||||
    ``'edbob.db.extensions'`` entry points..
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
#     If ``engine`` is not provided, then ``rattail.engine`` is assumed.
 | 
			
		||||
#     """
 | 
			
		||||
    global _available_extensions
 | 
			
		||||
 | 
			
		||||
#     if engine is None:
 | 
			
		||||
#         engine = rattail.engine
 | 
			
		||||
        
 | 
			
		||||
#     if not isinstance(extension, RattailExtension):
 | 
			
		||||
#         extension = get_extension(extension)
 | 
			
		||||
    if _available_extensions is None:
 | 
			
		||||
        _available_extensions = entry_point_map('edbob.db.extensions')
 | 
			
		||||
    return _available_extensions
 | 
			
		||||
 | 
			
		||||
#     log.info("Deactivating extension: %s" % extension.name)
 | 
			
		||||
#     if extension.name in _active_extensions:
 | 
			
		||||
#         del _active_extensions[extension.name]
 | 
			
		||||
 | 
			
		||||
#     session = Session()
 | 
			
		||||
#     ext = session.query(ActiveExtension).get(extension.name)
 | 
			
		||||
#     if ext:
 | 
			
		||||
#         session.delete(ext)
 | 
			
		||||
#         session.commit()
 | 
			
		||||
#     session.close()
 | 
			
		||||
def deactivate_extension(extension, engine=None):
 | 
			
		||||
    """
 | 
			
		||||
    Uninstalls an extension's schema from a database.
 | 
			
		||||
 | 
			
		||||
#     uninstall_extension_schema(extension, engine)
 | 
			
		||||
#     unmerge_extension_metadata(extension)
 | 
			
		||||
#     extension.restore_classes()
 | 
			
		||||
    If ``engine`` is not provided, :attr:`edbob.db.engine` is assumed.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
#     clear_mappers()
 | 
			
		||||
#     make_mappers(rattail.metadata)
 | 
			
		||||
#     for name in sorted(_active_extensions, extension_sorter(_active_extensions)):
 | 
			
		||||
#         _active_extensions[name].extend_mappers(rattail.metadata)
 | 
			
		||||
    if engine is None:
 | 
			
		||||
        engine = edbob.db.engine
 | 
			
		||||
 | 
			
		||||
    if not isinstance(extension, Extension):
 | 
			
		||||
        extension = get_extension(extension)
 | 
			
		||||
 | 
			
		||||
    log.debug("Deactivating extension: %s" % extension.name)
 | 
			
		||||
    active = active_extensions(engine)
 | 
			
		||||
    if extension.name in active:
 | 
			
		||||
        active.remove(extension.name)
 | 
			
		||||
 | 
			
		||||
    session = Session(bind=engine)
 | 
			
		||||
    ext = session.query(ActiveExtension).get(extension.name)
 | 
			
		||||
    if ext:
 | 
			
		||||
        session.delete(ext)
 | 
			
		||||
        session.commit()
 | 
			
		||||
    session.close()
 | 
			
		||||
 | 
			
		||||
    uninstall_extension_schema(extension, engine)
 | 
			
		||||
    # unmerge_extension_metadata(extension)
 | 
			
		||||
    # extension.restore_classes()
 | 
			
		||||
 | 
			
		||||
    # clear_mappers()
 | 
			
		||||
    # make_mappers(rattail.metadata)
 | 
			
		||||
    # for name in sorted(_active_extensions, extension_sorter(_active_extensions)):
 | 
			
		||||
    #     _active_extensions[name].extend_mappers(rattail.metadata)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extend_framework():
 | 
			
		||||
| 
						 | 
				
			
			@ -232,11 +305,11 @@ def extend_framework():
 | 
			
		|||
    except sqlalchemy.exc.OperationalError:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Check database version to see if core schema is installed.
 | 
			
		||||
    try:
 | 
			
		||||
        db_version = get_database_version(engine)
 | 
			
		||||
    except exceptions.CoreSchemaNotInstalled:
 | 
			
		||||
        return
 | 
			
		||||
    # # Check database version to see if core schema is installed.
 | 
			
		||||
    # try:
 | 
			
		||||
    #     db_version = get_database_version(engine)
 | 
			
		||||
    # except exceptions.CoreSchemaNotInstalled:
 | 
			
		||||
    #     return
 | 
			
		||||
 | 
			
		||||
    # Since extensions may depend on one another, we must first retrieve the
 | 
			
		||||
    # list of active extensions' names from the database and *then* sort them
 | 
			
		||||
| 
						 | 
				
			
			@ -264,15 +337,20 @@ def extend_framework():
 | 
			
		|||
        active_extensions[name] = ext
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def extension_active(extension):
 | 
			
		||||
#     """
 | 
			
		||||
#     Returns boolean indicating whether or not the given ``extension`` is active
 | 
			
		||||
#     within the current database.
 | 
			
		||||
#     """
 | 
			
		||||
def extension_active(extension, engine=None):
 | 
			
		||||
    """
 | 
			
		||||
    Returns boolean indicating whether or not the given ``extension`` is active
 | 
			
		||||
    within a database.
 | 
			
		||||
 | 
			
		||||
#     if not isinstance(extension, RattailExtension):
 | 
			
		||||
#         extension = get_extension(extension)
 | 
			
		||||
#     return extension.name in _active_extensions
 | 
			
		||||
    If ``engine`` is not provided, :attr:`edbob.db.engine` is assumed.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    if not engine:
 | 
			
		||||
        engine = edbob.db.engine
 | 
			
		||||
 | 
			
		||||
    if not isinstance(extension, Extension):
 | 
			
		||||
        extension = get_extension(extension)
 | 
			
		||||
    return extension.name in active_extensions(engine)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extension_sorter(extensions):
 | 
			
		||||
| 
						 | 
				
			
			@ -306,8 +384,9 @@ def get_extension(name):
 | 
			
		|||
    raised if the extension cannot be found.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    if name in available_extensions:
 | 
			
		||||
        return available_extensions[name]()
 | 
			
		||||
    extensions = available_extensions()
 | 
			
		||||
    if name in extensions:
 | 
			
		||||
        return extensions[name]()
 | 
			
		||||
    raise exceptions.ExtensionNotFound(name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -322,23 +401,38 @@ def install_extension_schema(extension, engine=None):
 | 
			
		|||
    if engine is None:
 | 
			
		||||
        engine = edbob.db.engine
 | 
			
		||||
 | 
			
		||||
    # Extensions aren't required to provide metadata...
 | 
			
		||||
    ext_meta = extension.get_metadata()
 | 
			
		||||
    if not ext_meta:
 | 
			
		||||
        return
 | 
			
		||||
    # # Extensions aren't required to provide metadata...
 | 
			
		||||
    # ext_meta = extension.get_metadata()
 | 
			
		||||
    # if not ext_meta:
 | 
			
		||||
    #     return
 | 
			
		||||
 | 
			
		||||
    # ...but if they do they must also provide a SQLAlchemy-Migrate repository.
 | 
			
		||||
    assert extension.schema, "Extension does not implement 'schema': %s" % extension.name
 | 
			
		||||
    # # ...but if they do they must also provide a SQLAlchemy-Migrate repository.
 | 
			
		||||
    # assert extension.schema, "Extension does not implement 'schema': %s" % extension.name
 | 
			
		||||
        
 | 
			
		||||
    meta = edbob.db.metadata
 | 
			
		||||
    for table in meta.sorted_tables:
 | 
			
		||||
        table.tometadata(ext_meta)
 | 
			
		||||
    for table in ext_meta.sorted_tables:
 | 
			
		||||
        if table.name not in meta.tables:
 | 
			
		||||
            table.create(bind=engine, checkfirst=True)
 | 
			
		||||
    # meta = edbob.db.metadata
 | 
			
		||||
    # for table in meta.sorted_tables:
 | 
			
		||||
    #     table.tometadata(ext_meta)
 | 
			
		||||
    # for table in ext_meta.sorted_tables:
 | 
			
		||||
    #     if table.name not in meta.tables:
 | 
			
		||||
    #         table.create(bind=engine, checkfirst=True)
 | 
			
		||||
 | 
			
		||||
    migrate.versioning.api.version_control(
 | 
			
		||||
        str(engine.url), get_repository_path(extension), get_repository_version(extension))
 | 
			
		||||
    # TODO: This sucks, please fix.
 | 
			
		||||
    # edbob.db.Base.metadata.create_all(engine)
 | 
			
		||||
 | 
			
		||||
    # meta = MetaData(engine)
 | 
			
		||||
    # for tables in (edbob.db.iter_tables(), extension.iter_tables()):
 | 
			
		||||
    #     for table in tables:
 | 
			
		||||
    #         table.tometadata(meta)
 | 
			
		||||
    # meta.create_all()
 | 
			
		||||
 | 
			
		||||
    core_meta = edbob.db.get_core_metadata()
 | 
			
		||||
    ext_meta = extension.get_metadata(recurse=True)
 | 
			
		||||
    for table in ext_meta.sorted_tables:
 | 
			
		||||
        table.tometadata(core_meta)
 | 
			
		||||
    core_meta.create_all(engine)
 | 
			
		||||
 | 
			
		||||
    # migrate.versioning.api.version_control(
 | 
			
		||||
    #     str(engine.url), get_repository_path(extension), get_repository_version(extension))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def merge_extension_metadata(ext):
 | 
			
		||||
| 
						 | 
				
			
			@ -353,7 +447,7 @@ def merge_extension_metadata(ext):
 | 
			
		|||
    ext_meta = ext.get_metadata()
 | 
			
		||||
    if not ext_meta:
 | 
			
		||||
        return
 | 
			
		||||
    meta = edbob.db.metadata
 | 
			
		||||
    meta = Base.metadata
 | 
			
		||||
    for table in meta.sorted_tables:
 | 
			
		||||
        table.tometadata(ext_meta)
 | 
			
		||||
    for table in ext_meta.sorted_tables:
 | 
			
		||||
| 
						 | 
				
			
			@ -361,30 +455,53 @@ def merge_extension_metadata(ext):
 | 
			
		|||
            table.tometadata(meta)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def uninstall_extension_schema(extension, engine=None):
 | 
			
		||||
#     """
 | 
			
		||||
#     Uninstalls an extension's tables from the database represented by
 | 
			
		||||
#     ``engine`` (or ``rattail.engine`` if none is provided), and removes
 | 
			
		||||
#     SQLAlchemy-Migrate version control for the extension.
 | 
			
		||||
#     """
 | 
			
		||||
def uninstall_extension_schema(extension, engine=None):
 | 
			
		||||
    """
 | 
			
		||||
    Uninstalls an extension's tables from the database represented by
 | 
			
		||||
    ``engine`` (or :attr:`edbob.db.engine` if none is provided), and removes
 | 
			
		||||
    SQLAlchemy-Migrate version control for the extension.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
#     if engine is None:
 | 
			
		||||
#         engine = rattail.engine
 | 
			
		||||
    if engine is None:
 | 
			
		||||
        engine = edbob.db.engine
 | 
			
		||||
    
 | 
			
		||||
#     ext_meta = extension.get_metadata()
 | 
			
		||||
#     if not ext_meta:
 | 
			
		||||
#         return
 | 
			
		||||
    # ext_meta = extension.get_metadata()
 | 
			
		||||
    # if not ext_meta:
 | 
			
		||||
    #     return
 | 
			
		||||
    
 | 
			
		||||
#     schema = ControlledSchema(engine, get_repository_path(extension))
 | 
			
		||||
#     engine.execute(schema.table.delete().where(
 | 
			
		||||
#             schema.table.c.repository_id == schema.repository.id))
 | 
			
		||||
    # schema = ControlledSchema(engine, get_repository_path(extension))
 | 
			
		||||
    # engine.execute(schema.table.delete().where(
 | 
			
		||||
    #         schema.table.c.repository_id == schema.repository.id))
 | 
			
		||||
 | 
			
		||||
#     meta = get_metadata()
 | 
			
		||||
#     for table in meta.sorted_tables:
 | 
			
		||||
#         table.tometadata(ext_meta)
 | 
			
		||||
#     for table in reversed(ext_meta.sorted_tables):
 | 
			
		||||
#         if table.name not in meta.tables:
 | 
			
		||||
#             table.drop(bind=engine)
 | 
			
		||||
    # meta = get_metadata()
 | 
			
		||||
    # for table in meta.sorted_tables:
 | 
			
		||||
    #     table.tometadata(ext_meta)
 | 
			
		||||
    # for table in reversed(ext_meta.sorted_tables):
 | 
			
		||||
    #     if table.name not in meta.tables:
 | 
			
		||||
    #         table.drop(bind=engine)
 | 
			
		||||
 | 
			
		||||
    # core_meta = edbob.db.get_core_metadata()
 | 
			
		||||
    # ext_meta = extension.get_metadata()
 | 
			
		||||
    # for table in ext_meta.sorted_tables:
 | 
			
		||||
    #     table.tometadata(core_meta)
 | 
			
		||||
    # core_meta.create_all(engine)
 | 
			
		||||
 | 
			
		||||
    # core_meta = edbob.db.get_core_metadata()
 | 
			
		||||
    # ext_meta = extension.get_metadata()
 | 
			
		||||
    # for table in ext_meta.sorted_tables:
 | 
			
		||||
    #     table.tometadata(core_meta)
 | 
			
		||||
    # for table in reversed(core_meta.sorted_tables):
 | 
			
		||||
    #     if table in ext_meta:
 | 
			
		||||
    #         table.drop(engine)
 | 
			
		||||
 | 
			
		||||
    core_meta = edbob.db.get_core_metadata()
 | 
			
		||||
    ext_fullmeta = extension.get_metadata(True)
 | 
			
		||||
    for table in ext_fullmeta.sorted_tables:
 | 
			
		||||
        table.tometadata(core_meta)
 | 
			
		||||
    ext_meta = extension.get_metadata()
 | 
			
		||||
    for table in reversed(core_meta.sorted_tables):
 | 
			
		||||
        if table in ext_meta:
 | 
			
		||||
            table.drop(engine)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def unmerge_extension_metadata(extension):
 | 
			
		||||
| 
						 | 
				
			
			@ -433,3 +550,27 @@ def merge_extension_metadata(ext):
 | 
			
		|||
# #                     # Extensions may override permission display names.
 | 
			
		||||
# #                     if ext_perms[perm_name][1]:
 | 
			
		||||
# #                         perms[perm_name][1] = ext_perms[perm_name][1]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_active_extensions = {}
 | 
			
		||||
def active_extensions(engine=None):
 | 
			
		||||
    """
 | 
			
		||||
    Returns a list of names for extensions which are active within a database.
 | 
			
		||||
 | 
			
		||||
    If ``engine`` is not provided, ``edbob.db.engine`` is assumed.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    if not engine:
 | 
			
		||||
        engine = edbob.db.engine
 | 
			
		||||
 | 
			
		||||
    exts = _active_extensions.get(engine.url)
 | 
			
		||||
    if exts:
 | 
			
		||||
        return exts
 | 
			
		||||
 | 
			
		||||
    session = Session()
 | 
			
		||||
    q = session.query(ActiveExtension.name)
 | 
			
		||||
    exts = [x[0] for x in q]
 | 
			
		||||
    session.close()
 | 
			
		||||
 | 
			
		||||
    _active_extensions[engine.url] = exts
 | 
			
		||||
    return exts
 | 
			
		||||
							
								
								
									
										36
									
								
								edbob/db/extensions/auth/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								edbob/db/extensions/auth/__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
# -*- coding: utf-8  -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  edbob -- Pythonic Software Framework
 | 
			
		||||
#  Copyright © 2010-2012 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of edbob.
 | 
			
		||||
#
 | 
			
		||||
#  edbob is free software: you can redistribute it and/or modify it under the
 | 
			
		||||
#  terms of the GNU Affero General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  edbob 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 Affero General Public License for
 | 
			
		||||
#  more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
#  along with edbob.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``edbob.db.extensions.auth`` -- 'auth' Extension
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import MetaData
 | 
			
		||||
 | 
			
		||||
from edbob.db.extensions import Extension
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthExtension(Extension):
 | 
			
		||||
 | 
			
		||||
    name = 'auth'
 | 
			
		||||
							
								
								
									
										89
									
								
								edbob/db/extensions/auth/model.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								edbob/db/extensions/auth/model.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
# -*- coding: utf-8  -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  edbob -- Pythonic Software Framework
 | 
			
		||||
#  Copyright © 2010-2012 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of edbob.
 | 
			
		||||
#
 | 
			
		||||
#  edbob is free software: you can redistribute it and/or modify it under the
 | 
			
		||||
#  terms of the GNU Affero General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  edbob 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 Affero General Public License for
 | 
			
		||||
#  more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
#  along with edbob.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``edbob.db.extensions.auth.model`` -- Schema Definition
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import *
 | 
			
		||||
from sqlalchemy.orm import relationship
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
from edbob.db.model import Base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Person', 'User']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_person_display_name(context):
 | 
			
		||||
    first_name = context.current_parameters['first_name']
 | 
			
		||||
    last_name = context.current_parameters['last_name']
 | 
			
		||||
    if not (first_name or last_name):
 | 
			
		||||
        return None
 | 
			
		||||
    return '%(first_name)s %(last_name)s' % locals()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Person(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Represents a real, living and breathing person.  (Or, at least was
 | 
			
		||||
    previously living and breathing, in the case of the deceased.)
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    __tablename__ = 'people'
 | 
			
		||||
 | 
			
		||||
    uuid = Column(String(32), primary_key=True, default=edbob.get_uuid)
 | 
			
		||||
    first_name = Column(String(50))
 | 
			
		||||
    last_name = Column(String(50))
 | 
			
		||||
    display_name = Column(String(100), default=get_person_display_name)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<Person: %s>" % self.display_name
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return str(self.display_name or '')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Represents a user of the system.  This may or may not correspond to a real
 | 
			
		||||
    person, i.e. some users may exist solely for automated tasks.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    __tablename__ = 'users'
 | 
			
		||||
 | 
			
		||||
    uuid = Column(String(32), primary_key=True, default=edbob.get_uuid)
 | 
			
		||||
    username = Column(String(25), nullable=False, unique=True)
 | 
			
		||||
    person_uuid = Column(String(32), ForeignKey('people.uuid'))
 | 
			
		||||
 | 
			
		||||
    person = relationship(Person, backref='user')
 | 
			
		||||
 | 
			
		||||
    # roles = association_proxy('_roles', 'role',
 | 
			
		||||
    #                           creator=lambda x: UserRole(role=x),
 | 
			
		||||
    #                           getset_factory=getset_factory)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<User: %s>" % self.username
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return str(self.username or '')
 | 
			
		||||
| 
						 | 
				
			
			@ -26,80 +26,134 @@
 | 
			
		|||
``edbob.db.model`` -- Core Schema Definition
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import *
 | 
			
		||||
from sqlalchemy import Column, String, Text
 | 
			
		||||
from sqlalchemy.ext.declarative import declarative_base
 | 
			
		||||
 | 
			
		||||
from edbob.sqlalchemy import table_with_uuid
 | 
			
		||||
import edbob
 | 
			
		||||
# from edbob import Object, get_uuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_metadata(*args, **kwargs):
 | 
			
		||||
__all__ = ['ActiveExtension', 'Setting']
 | 
			
		||||
 | 
			
		||||
Base = declarative_base()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# class ClassWithUuid(Object):
 | 
			
		||||
#     """
 | 
			
		||||
#     Simple mixin class which defines a ``uuid`` column as primary key.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     Column('uuid', String(32), primary_key=True, default=get_uuid)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def uuid_column(*args):
 | 
			
		||||
    """
 | 
			
		||||
    Returns the core ``edbob`` schema definition.
 | 
			
		||||
 | 
			
		||||
    Note that when :func:`edbob.init()` is called, the ``sqlalchemy.MetaData``
 | 
			
		||||
    instance which is returned from this function will henceforth be available
 | 
			
		||||
    as ``edbob.metadata``.  However, ``edbob.init()`` may extend
 | 
			
		||||
    ``edbob.metadata`` as well, depending on which extensions are activated
 | 
			
		||||
    within the primary database.
 | 
			
		||||
 | 
			
		||||
    This function then serves two purposes: First, it provides the core
 | 
			
		||||
    metadata instance.  Secondly, it allows edbob to always know what its core
 | 
			
		||||
    schema looks like, as opposed to what's held in the current
 | 
			
		||||
    ``edbob.metadata`` instance, which may have been extended locally.  (The
 | 
			
		||||
    latter use is necessary in order for edbob to properly manage its
 | 
			
		||||
    extensions.)
 | 
			
		||||
 | 
			
		||||
    All arguments (positional and keyword) are passed directly to the
 | 
			
		||||
    ``sqlalchemy.MetaData()`` constructor.
 | 
			
		||||
    Convenience function which returns a ``uuid`` column for use as a table's
 | 
			
		||||
    primary key.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    metadata = MetaData(*args, **kwargs)
 | 
			
		||||
    return Column(String(32), primary_key=True, default=edbob.get_uuid, *args)
 | 
			
		||||
 | 
			
		||||
    active_extensions = Table(
 | 
			
		||||
        'active_extensions', metadata,
 | 
			
		||||
        Column('name', String(50), primary_key=True),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_person_display_name(context):
 | 
			
		||||
        first_name = context.current_parameters['first_name']
 | 
			
		||||
        last_name = context.current_parameters['last_name']
 | 
			
		||||
        if not (first_name or last_name):
 | 
			
		||||
            return None
 | 
			
		||||
        return '%(first_name)s %(last_name)s' % locals()
 | 
			
		||||
class ActiveExtension(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Represents an extension which has been activated within a database.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    people = table_with_uuid(
 | 
			
		||||
        'people', metadata,
 | 
			
		||||
        Column('first_name', String(50)),
 | 
			
		||||
        Column('last_name', String(50)),
 | 
			
		||||
        Column('display_name', String(100), default=get_person_display_name),
 | 
			
		||||
        )
 | 
			
		||||
    __tablename__ = 'active_extensions'
 | 
			
		||||
 | 
			
		||||
    permissions = Table(
 | 
			
		||||
        'permissions', metadata,
 | 
			
		||||
        Column('role_uuid', String(32), ForeignKey('roles.uuid'), primary_key=True),
 | 
			
		||||
        Column('permission', String(50), primary_key=True),
 | 
			
		||||
        )
 | 
			
		||||
    name = Column(String(50), primary_key=True)
 | 
			
		||||
 | 
			
		||||
    roles = table_with_uuid(
 | 
			
		||||
        'roles', metadata,
 | 
			
		||||
        Column('name', String(25), nullable=False, unique=True),
 | 
			
		||||
        )
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<ActiveExtension: %s>" % self.name
 | 
			
		||||
 | 
			
		||||
    settings = Table(
 | 
			
		||||
        'settings', metadata,
 | 
			
		||||
        Column('name', String(255), primary_key=True),
 | 
			
		||||
        Column('value', Text),
 | 
			
		||||
        )
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return str(self.name or '')
 | 
			
		||||
 | 
			
		||||
    users = table_with_uuid(
 | 
			
		||||
        'users', metadata,
 | 
			
		||||
        Column('username', String(25), nullable=False, unique=True),
 | 
			
		||||
        Column('person_uuid', String(32), ForeignKey('people.uuid')),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    users_roles = table_with_uuid(
 | 
			
		||||
        'users_roles', metadata,
 | 
			
		||||
        Column('user_uuid', String(32), ForeignKey('users.uuid')),
 | 
			
		||||
        Column('role_uuid', String(32), ForeignKey('roles.uuid')),
 | 
			
		||||
        )
 | 
			
		||||
class Setting(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Represents a setting stored within the database.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    return metadata
 | 
			
		||||
    __tablename__ = 'settings'
 | 
			
		||||
 | 
			
		||||
    name = Column(String(255), primary_key=True)
 | 
			
		||||
    value = Column(Text)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<Setting: %s>" % self.name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def get_metadata(*args, **kwargs):
 | 
			
		||||
#     """
 | 
			
		||||
#     Returns the core ``edbob`` schema definition.
 | 
			
		||||
 | 
			
		||||
#     Note that when :func:`edbob.init()` is called, the ``sqlalchemy.MetaData``
 | 
			
		||||
#     instance which is returned from this function will henceforth be available
 | 
			
		||||
#     as ``edbob.metadata``.  However, ``edbob.init()`` may extend
 | 
			
		||||
#     ``edbob.metadata`` as well, depending on which extensions are activated
 | 
			
		||||
#     within the primary database.
 | 
			
		||||
 | 
			
		||||
#     This function then serves two purposes: First, it provides the core
 | 
			
		||||
#     metadata instance.  Secondly, it allows edbob to always know what its core
 | 
			
		||||
#     schema looks like, as opposed to what's held in the current
 | 
			
		||||
#     ``edbob.metadata`` instance, which may have been extended locally.  (The
 | 
			
		||||
#     latter use is necessary in order for edbob to properly manage its
 | 
			
		||||
#     extensions.)
 | 
			
		||||
 | 
			
		||||
#     All arguments (positional and keyword) are passed directly to the
 | 
			
		||||
#     ``sqlalchemy.MetaData()`` constructor.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     metadata = MetaData(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
#     active_extensions = Table(
 | 
			
		||||
#         'active_extensions', metadata,
 | 
			
		||||
#         Column('name', String(50), primary_key=True),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     def get_person_display_name(context):
 | 
			
		||||
#         first_name = context.current_parameters['first_name']
 | 
			
		||||
#         last_name = context.current_parameters['last_name']
 | 
			
		||||
#         if not (first_name or last_name):
 | 
			
		||||
#             return None
 | 
			
		||||
#         return '%(first_name)s %(last_name)s' % locals()
 | 
			
		||||
 | 
			
		||||
#     people = table_with_uuid(
 | 
			
		||||
#         'people', metadata,
 | 
			
		||||
#         Column('first_name', String(50)),
 | 
			
		||||
#         Column('last_name', String(50)),
 | 
			
		||||
#         Column('display_name', String(100), default=get_person_display_name),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     permissions = Table(
 | 
			
		||||
#         'permissions', metadata,
 | 
			
		||||
#         Column('role_uuid', String(32), ForeignKey('roles.uuid'), primary_key=True),
 | 
			
		||||
#         Column('permission', String(50), primary_key=True),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     roles = table_with_uuid(
 | 
			
		||||
#         'roles', metadata,
 | 
			
		||||
#         Column('name', String(25), nullable=False, unique=True),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     settings = Table(
 | 
			
		||||
#         'settings', metadata,
 | 
			
		||||
#         Column('name', String(255), primary_key=True),
 | 
			
		||||
#         Column('value', Text),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     users = table_with_uuid(
 | 
			
		||||
#         'users', metadata,
 | 
			
		||||
#         Column('username', String(25), nullable=False, unique=True),
 | 
			
		||||
#         Column('person_uuid', String(32), ForeignKey('people.uuid')),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     users_roles = table_with_uuid(
 | 
			
		||||
#         'users_roles', metadata,
 | 
			
		||||
#         Column('user_uuid', String(32), ForeignKey('users.uuid')),
 | 
			
		||||
#         Column('role_uuid', String(32), ForeignKey('roles.uuid')),
 | 
			
		||||
#         )
 | 
			
		||||
 | 
			
		||||
#     return metadata
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								edbob/db/schema/README
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								edbob/db/schema/README
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
Generic single-database configuration.
 | 
			
		||||
							
								
								
									
										71
									
								
								edbob/db/schema/env.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								edbob/db/schema/env.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
from __future__ import with_statement
 | 
			
		||||
from alembic import context
 | 
			
		||||
from sqlalchemy import engine_from_config, pool
 | 
			
		||||
from logging.config import fileConfig
 | 
			
		||||
 | 
			
		||||
# this is the Alembic Config object, which provides
 | 
			
		||||
# access to the values within the .ini file in use.
 | 
			
		||||
config = context.config
 | 
			
		||||
 | 
			
		||||
# Interpret the config file for Pyhton logging. 
 | 
			
		||||
# This line sets up loggers basically.
 | 
			
		||||
fileConfig(config.config_file_name)
 | 
			
		||||
 | 
			
		||||
# add your model's MetaData object here
 | 
			
		||||
# for 'autogenerate' support
 | 
			
		||||
# from myapp import mymodel
 | 
			
		||||
# target_metadata = mymodel.Base.metadata
 | 
			
		||||
target_metadata = None
 | 
			
		||||
 | 
			
		||||
# other values from the config, defined by the needs of env.py,
 | 
			
		||||
# can be acquired:
 | 
			
		||||
# my_important_option = config.get_main_option("my_important_option")
 | 
			
		||||
# ... etc.
 | 
			
		||||
 | 
			
		||||
def run_migrations_offline():
 | 
			
		||||
    """Run migrations in 'offline' mode.
 | 
			
		||||
 | 
			
		||||
    This configures the context with just a URL
 | 
			
		||||
    and not an Engine, though an Engine is acceptable
 | 
			
		||||
    here as well.  By skipping the Engine creation
 | 
			
		||||
    we don't even need a DBAPI to be available.
 | 
			
		||||
    
 | 
			
		||||
    Calls to context.execute() here emit the given string to the
 | 
			
		||||
    script output.
 | 
			
		||||
    
 | 
			
		||||
    """
 | 
			
		||||
    url = config.get_main_option("sqlalchemy.url")
 | 
			
		||||
    context.configure(url=url)
 | 
			
		||||
 | 
			
		||||
    with context.begin_transaction():
 | 
			
		||||
        context.run_migrations()
 | 
			
		||||
 | 
			
		||||
def run_migrations_online():
 | 
			
		||||
    """Run migrations in 'online' mode.
 | 
			
		||||
 | 
			
		||||
    In this scenario we need to create an Engine
 | 
			
		||||
    and associate a connection with the context.
 | 
			
		||||
    
 | 
			
		||||
    """
 | 
			
		||||
    engine = engine_from_config(
 | 
			
		||||
                config.get_section(config.config_ini_section), 
 | 
			
		||||
                prefix='sqlalchemy.', 
 | 
			
		||||
                poolclass=pool.NullPool)
 | 
			
		||||
 | 
			
		||||
    connection = engine.connect()
 | 
			
		||||
    context.configure(
 | 
			
		||||
                connection=connection, 
 | 
			
		||||
                target_metadata=target_metadata
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        with context.begin_transaction():
 | 
			
		||||
            context.run_migrations()
 | 
			
		||||
    finally:
 | 
			
		||||
        connection.close()
 | 
			
		||||
 | 
			
		||||
if context.is_offline_mode():
 | 
			
		||||
    run_migrations_offline()
 | 
			
		||||
else:
 | 
			
		||||
    run_migrations_online()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								edbob/db/schema/script.py.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								edbob/db/schema/script.py.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
"""${message}
 | 
			
		||||
 | 
			
		||||
Revision ID: ${up_revision}
 | 
			
		||||
Revises: ${down_revision}
 | 
			
		||||
Create Date: ${create_date}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = ${repr(up_revision)}
 | 
			
		||||
down_revision = ${repr(down_revision)}
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
${imports if imports else ""}
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    ${upgrades if upgrades else "pass"}
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    ${downgrades if downgrades else "pass"}
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +41,8 @@ import migrate.exceptions
 | 
			
		|||
 | 
			
		||||
import edbob.db
 | 
			
		||||
from edbob.db import exceptions
 | 
			
		||||
from edbob.db.model import get_metadata
 | 
			
		||||
from edbob.db.model import Base
 | 
			
		||||
# from edbob.db.model import get_metadata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def core_schema_installed(engine=None):
 | 
			
		||||
| 
						 | 
				
			
			@ -121,25 +122,27 @@ def install_core_schema(engine=None):
 | 
			
		|||
    if not engine:
 | 
			
		||||
        engine = edbob.db.engine
 | 
			
		||||
 | 
			
		||||
    # Try to connect in order to force an error, if applicable.
 | 
			
		||||
    # Attempt connection in order to force an error, if applicable.
 | 
			
		||||
    conn = engine.connect()
 | 
			
		||||
    conn.close()
 | 
			
		||||
 | 
			
		||||
    # Check DB version to see if core schema is already installed.
 | 
			
		||||
    try:
 | 
			
		||||
        db_version = get_database_version(engine)
 | 
			
		||||
    except exceptions.CoreSchemaNotInstalled:
 | 
			
		||||
        pass
 | 
			
		||||
    else:
 | 
			
		||||
        raise exceptions.CoreSchemaAlreadyInstalled(db_version)
 | 
			
		||||
    # # Check DB version to see if core schema is already installed.
 | 
			
		||||
    # try:
 | 
			
		||||
    #     db_version = get_database_version(engine)
 | 
			
		||||
    # except exceptions.CoreSchemaNotInstalled:
 | 
			
		||||
    #     pass
 | 
			
		||||
    # else:
 | 
			
		||||
    #     raise exceptions.CoreSchemaAlreadyInstalled(db_version)
 | 
			
		||||
 | 
			
		||||
    # Create tables for core schema.
 | 
			
		||||
    metadata = get_metadata()
 | 
			
		||||
    metadata.create_all(bind=engine)
 | 
			
		||||
    # metadata = get_metadata()
 | 
			
		||||
    # Base.metadata.create_all(engine)
 | 
			
		||||
    meta = edbob.db.get_core_metadata()
 | 
			
		||||
    meta.create_all(engine)
 | 
			
		||||
 | 
			
		||||
    # Add versioning for core schema.
 | 
			
		||||
    migrate.versioning.api.version_control(
 | 
			
		||||
        str(engine.url), get_repository_path(), get_repository_version())
 | 
			
		||||
    # # Add versioning for core schema.
 | 
			
		||||
    # migrate.versioning.api.version_control(
 | 
			
		||||
    #     str(engine.url), get_repository_path(), get_repository_version())
 | 
			
		||||
 | 
			
		||||
    # WTF
 | 
			
		||||
    # session = Session(bind=engine)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,17 @@
 | 
			
		|||
``edbob.pyramid`` -- Pyramid Framework
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.orm import scoped_session
 | 
			
		||||
from zope.sqlalchemy import ZopeTransactionExtension
 | 
			
		||||
 | 
			
		||||
import edbob.db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Session']
 | 
			
		||||
 | 
			
		||||
Session = scoped_session(edbob.db.Session)
 | 
			
		||||
Session.configure(extension=ZopeTransactionExtension())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def includeme(config):
 | 
			
		||||
    config.include('edbob.pyramid.static')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										218
									
								
								edbob/pyramid/filters.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								edbob/pyramid/filters.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,218 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
# -*- coding: utf-8  -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  edbob -- Pythonic Software Framework
 | 
			
		||||
#  Copyright © 2010-2012 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of edbob.
 | 
			
		||||
#
 | 
			
		||||
#  edbob is free software: you can redistribute it and/or modify it under the
 | 
			
		||||
#  terms of the GNU Affero General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  edbob 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 Affero General Public License for
 | 
			
		||||
#  more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
#  along with edbob.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``edbob.pyramid.filters`` -- Search Filters
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from pyramid_simpleform import Form
 | 
			
		||||
from pyramid_simpleform.renderers import FormRenderer
 | 
			
		||||
from webhelpers.html import tags
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
from edbob.util import prettify
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['SearchFilter', 'SearchForm']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SearchFilter(edbob.Object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, label=None, **kwargs):
 | 
			
		||||
        edbob.Object.__init__(self, **kwargs)
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.label = label or prettify(name)
 | 
			
		||||
 | 
			
		||||
    def types_select(self):
 | 
			
		||||
        types = [
 | 
			
		||||
            ('is', 'is'),
 | 
			
		||||
            ('nt', 'is not'),
 | 
			
		||||
            ('lk', 'contains'),
 | 
			
		||||
            ('nl', 'doesn\'t contain'),
 | 
			
		||||
            ]
 | 
			
		||||
        options = []
 | 
			
		||||
        filter_map = self.search.config['filter_map'][self.name]
 | 
			
		||||
        for value, label in types:
 | 
			
		||||
            if value in filter_map:
 | 
			
		||||
                options.append((value, label))
 | 
			
		||||
        return tags.select('filter_type_'+self.name,
 | 
			
		||||
                           self.search.config.get('filter_type_'+self.name),
 | 
			
		||||
                           options, class_='filter-type')
 | 
			
		||||
 | 
			
		||||
    def value_control(self):
 | 
			
		||||
        return tags.text(self.name, self.search.config.get(self.name))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SearchForm(Form):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, request, filters, config, *args, **kwargs):
 | 
			
		||||
        Form.__init__(self, request, *args, **kwargs)
 | 
			
		||||
        self.filters = filters
 | 
			
		||||
        for f in filters:
 | 
			
		||||
            filters[f].search = self
 | 
			
		||||
        self.config = config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SearchFormRenderer(FormRenderer):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, form, *args, **kwargs):
 | 
			
		||||
        FormRenderer.__init__(self, form, *args, **kwargs)
 | 
			
		||||
        self.filters = form.filters
 | 
			
		||||
        self.config = form.config
 | 
			
		||||
 | 
			
		||||
    def checkbox(self, name, checked=None, *args, **kwargs):
 | 
			
		||||
        if name.startswith('include_filter_'):
 | 
			
		||||
            if checked is None:
 | 
			
		||||
                checked = self.config[name]
 | 
			
		||||
            return tags.checkbox(name, checked=checked, *args, **kwargs)
 | 
			
		||||
        if checked is None:
 | 
			
		||||
            checked = False
 | 
			
		||||
        return FormRenderer.checkbox(self, name, checked=checked, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def text(self, name, *args, **kwargs):
 | 
			
		||||
        return tags.text(name, value=self.config.get(name), *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def sorted_filters(self):
 | 
			
		||||
        return sorted(self.filters, key=lambda x: self.filters[x].label)
 | 
			
		||||
 | 
			
		||||
    def add_filter(self, visible):
 | 
			
		||||
        options = ['add a filter']
 | 
			
		||||
        for f in sorted(self.filters):
 | 
			
		||||
            f = self.filters[f]
 | 
			
		||||
            options.append((f.name, f.label))
 | 
			
		||||
        return self.select('add-filter', options,
 | 
			
		||||
                           style='display: none;' if len(visible) == len(self.filters) else None)
 | 
			
		||||
 | 
			
		||||
    def render(self, **kwargs):
 | 
			
		||||
        from formalchemy import config
 | 
			
		||||
        return config.engine('filterset', search=self, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_exact(field):
 | 
			
		||||
    """
 | 
			
		||||
    Returns a filter map entry, with typical logic built in for "exact match"
 | 
			
		||||
    queries applied to ``field``.
 | 
			
		||||
    """
 | 
			
		||||
    return {
 | 
			
		||||
        'is':
 | 
			
		||||
            lambda q, v: q.filter(field == v) if v else q,
 | 
			
		||||
        'nt':
 | 
			
		||||
            lambda q, v: q.filter(field != v) if v else q,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_ilike(field):
 | 
			
		||||
    """
 | 
			
		||||
    Returns a filter map entry, with typical logic built in for ILIKE queries
 | 
			
		||||
    applied to ``field``.
 | 
			
		||||
    """
 | 
			
		||||
    return {
 | 
			
		||||
        'lk':
 | 
			
		||||
            lambda q, v: q.filter(field.ilike('%%%s%%' % v)) if v else q,
 | 
			
		||||
        'nl':
 | 
			
		||||
            lambda q, v: q.filter(~field.ilike('%%%s%%' % v)) if v else q,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_query(query, config, join_map={}):
 | 
			
		||||
    filter_map = config['filter_map']
 | 
			
		||||
    if config.get('search'):
 | 
			
		||||
        search = config['search'].config
 | 
			
		||||
        joins = config.setdefault('joins', [])
 | 
			
		||||
        include_filter = re.compile(r'^include_filter_(.*)$')
 | 
			
		||||
        for key in search:
 | 
			
		||||
            m = include_filter.match(key)
 | 
			
		||||
            if m and search[key]:
 | 
			
		||||
                field = m.group(1)
 | 
			
		||||
                if field in join_map and field not in joins:
 | 
			
		||||
                    query = join_map[field](query)
 | 
			
		||||
                    joins.append(field)
 | 
			
		||||
                value = search.get(field)
 | 
			
		||||
                if value:
 | 
			
		||||
                    f = filter_map[field][search['filter_type_'+field]]
 | 
			
		||||
                    query = f(query, value)
 | 
			
		||||
    return query
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_filter_map(cls, exact=[], ilike=[], **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Convenience function which returns a filter map for ``cls``.  All fields
 | 
			
		||||
    represented by ``names`` will be included in the map.
 | 
			
		||||
 | 
			
		||||
    Each field's entry will use the :func:`filter_ilike()` function unless the
 | 
			
		||||
    field's name is also found within ``exact``, in which case the
 | 
			
		||||
    :func:`filter_exact()` function will be used instead.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    fmap = {}
 | 
			
		||||
    for name in exact:
 | 
			
		||||
        fmap[name] = filter_exact(getattr(cls, name))
 | 
			
		||||
    for name in ilike:
 | 
			
		||||
        fmap[name] = filter_ilike(getattr(cls, name))
 | 
			
		||||
    fmap.update(kwargs)
 | 
			
		||||
    return fmap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_search_config(name, request, filter_map, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Returns a dictionary of configuration options for a search form.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    config = {}
 | 
			
		||||
    for field in filter_map:
 | 
			
		||||
        config['include_filter_'+field] = False
 | 
			
		||||
    config.update(kwargs)
 | 
			
		||||
 | 
			
		||||
    def update_config(dict_, prefix='', exclude_by_default=False):
 | 
			
		||||
        for field in filter_map:
 | 
			
		||||
            if prefix+'include_filter_'+field in dict_:
 | 
			
		||||
                include = dict_[prefix+'include_filter_'+field]
 | 
			
		||||
                include = bool(include) and include != '0'
 | 
			
		||||
                config['include_filter_'+field] = include
 | 
			
		||||
            elif exclude_by_default:
 | 
			
		||||
                config['include_filter_'+field] = False
 | 
			
		||||
            if prefix+'filter_type_'+field in dict_:
 | 
			
		||||
                config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
 | 
			
		||||
            if prefix+field in dict_:
 | 
			
		||||
                config[field] = dict_[prefix+field]
 | 
			
		||||
 | 
			
		||||
    update_config(request.session, prefix=name+'.')
 | 
			
		||||
    if request.params.get('filters'):
 | 
			
		||||
        update_config(request.params, exclude_by_default=True)
 | 
			
		||||
    for key in config:
 | 
			
		||||
        if not key.startswith('filter_factory_'):
 | 
			
		||||
            request.session[name+'.'+key] = config[key]
 | 
			
		||||
    config['request'] = request
 | 
			
		||||
    config['filter_map'] = filter_map
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_search_form(config, **labels):
 | 
			
		||||
    filters = {}
 | 
			
		||||
    for field in config['filter_map']:
 | 
			
		||||
        factory = config.get('filter_factory_%s' % field, SearchFilter)
 | 
			
		||||
        filters[field] = factory(field, label=labels.get(field))
 | 
			
		||||
    return SearchForm(config['request'], filters, config)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
# -*- coding: utf-8  -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  edbob -- Pythonic Software Framework
 | 
			
		||||
#  Copyright © 2010-2012 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of edbob.
 | 
			
		||||
#
 | 
			
		||||
#  edbob is free software: you can redistribute it and/or modify it under the
 | 
			
		||||
#  terms of the GNU Affero General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  edbob 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 Affero General Public License for
 | 
			
		||||
#  more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
#  along with edbob.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``edbob.pyramid.forms`` -- Forms
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from edbob.pyramid.forms.formalchemy import *
 | 
			
		||||
| 
						 | 
				
			
			@ -26,3 +26,303 @@
 | 
			
		|||
``edbob.pyramid.forms.formalchemy`` -- FormAlchemy Interface
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from __future__ import absolute_import
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from pyramid.renderers import render
 | 
			
		||||
from webhelpers import paginate
 | 
			
		||||
from webhelpers.html.builder import format_attrs
 | 
			
		||||
from webhelpers.html.tags import literal
 | 
			
		||||
 | 
			
		||||
import formalchemy
 | 
			
		||||
from formalchemy.validators import accepts_none
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
from edbob.lib import pretty
 | 
			
		||||
from edbob.util import prettify
 | 
			
		||||
from edbob.pyramid import Session
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['AlchemyGrid', 'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer',
 | 
			
		||||
           'make_fieldset', 'required']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateEngine(formalchemy.templates.TemplateEngine):
 | 
			
		||||
    """
 | 
			
		||||
    Mako template engine for FormAlchemy.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def render(self, template, prefix='/forms/', suffix='.mako', **kwargs):
 | 
			
		||||
        template = ''.join((prefix, template, suffix))
 | 
			
		||||
        return render(template, kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Make our TemplateEngine the default.
 | 
			
		||||
engine = TemplateEngine()
 | 
			
		||||
formalchemy.config.engine = engine
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FieldSet(formalchemy.FieldSet):
 | 
			
		||||
    """
 | 
			
		||||
    Adds a little magic to the ``FieldSet`` class.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    prettify = staticmethod(prettify)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, model, class_name=None, crud_title=None, url=None,
 | 
			
		||||
                 route_name=None, url_action=None, url_cancel=None, **kwargs):
 | 
			
		||||
        formalchemy.FieldSet.__init__(self, model, **kwargs)
 | 
			
		||||
        self.class_name = class_name or self._original_cls.__name__.lower()
 | 
			
		||||
        self.crud_title = crud_title or prettify(self.class_name)
 | 
			
		||||
        self.edit = isinstance(model, self._original_cls)
 | 
			
		||||
        self.route_name = route_name or (self.class_name + 's')
 | 
			
		||||
        self.url_action = url_action or url(self.route_name)
 | 
			
		||||
        self.url_cancel = url_cancel or url(self.route_name)
 | 
			
		||||
 | 
			
		||||
    def get_display_text(self):
 | 
			
		||||
        return str(self.model)
 | 
			
		||||
 | 
			
		||||
    def render(self, **kwargs):
 | 
			
		||||
        kwargs.setdefault('class_', self.class_name)
 | 
			
		||||
        return formalchemy.FieldSet.render(self, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlchemyGrid(formalchemy.Grid):
 | 
			
		||||
    """
 | 
			
		||||
    This class defines the basic grid which you see in pretty much all
 | 
			
		||||
    Rattail/Pyramid apps.
 | 
			
		||||
 | 
			
		||||
    .. todo::
 | 
			
		||||
       This needs to be documented more fully, along with the rest of
 | 
			
		||||
       rattail.pyramid I suppose...
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    prettify = staticmethod(prettify)
 | 
			
		||||
 | 
			
		||||
    # uuid_key = None
 | 
			
		||||
 | 
			
		||||
    # def __init__(self, cls, instances, config, url_kwargs={}, *args, **kwargs):
 | 
			
		||||
    #     formalchemy.Grid.__init__(self, cls, instances, *args, **kwargs)
 | 
			
		||||
    #     self.pager = instances if isinstance(instances, paginate.Page) else None
 | 
			
		||||
    #     self.config = config
 | 
			
		||||
    #     self.url_kwargs = url_kwargs
 | 
			
		||||
    #     self.sortable = config.get('sortable', False)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, cls, instances, config, url_grid, url_object=None,
 | 
			
		||||
                 url_delete=None, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Grid constructor.
 | 
			
		||||
 | 
			
		||||
        ``url`` must be the URL used to access the grid itself.  This url/view
 | 
			
		||||
        must accept a GET query string parameter of "partial=True", which will
 | 
			
		||||
        indicate that the grid *only* is being requested, as opposed to the
 | 
			
		||||
        full page in which the grid normally resides.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        formalchemy.Grid.__init__(self, cls, instances, **kwargs)
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.url_grid = url_grid
 | 
			
		||||
        self.url_object = url_object
 | 
			
		||||
        self.url_delete = url_delete
 | 
			
		||||
        self.sortable = config.get('sortable', False)
 | 
			
		||||
        self.deletable = config.get('deletable', False)
 | 
			
		||||
        self.pager = instances if isinstance(instances, paginate.Page) else None
 | 
			
		||||
 | 
			
		||||
    def field_name(self, field):
 | 
			
		||||
        return field.name
 | 
			
		||||
 | 
			
		||||
    def iter_fields(self):
 | 
			
		||||
        for field in self.render_fields.itervalues():
 | 
			
		||||
            yield field
 | 
			
		||||
 | 
			
		||||
    def render_field(self, field, readonly):
 | 
			
		||||
        if readonly:
 | 
			
		||||
            return field.render_readonly()
 | 
			
		||||
        return field.render()
 | 
			
		||||
 | 
			
		||||
    def row_attrs(self, i):
 | 
			
		||||
        return format_attrs(
 | 
			
		||||
            uuid=self.model.uuid,
 | 
			
		||||
            class_='even' if i % 2 else 'odd',
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def url_attrs(self):
 | 
			
		||||
        return format_attrs(url=self.url_grid,
 | 
			
		||||
                            objurl=self.url_object,
 | 
			
		||||
                            delurl=self.url_delete)
 | 
			
		||||
 | 
			
		||||
    # def render(self, class_=None, **kwargs):
 | 
			
		||||
    #     """
 | 
			
		||||
    #     Renders the grid into HTML, and returns the result.
 | 
			
		||||
 | 
			
		||||
    #     ``class_`` (if provided) is used to define the class of the ``<div>``
 | 
			
		||||
    #     (wrapper) and ``<table>`` elements of the grid.
 | 
			
		||||
 | 
			
		||||
    #     Any remaining ``kwargs`` are passed directly to the underlying
 | 
			
		||||
    #     ``formalchemy.Grid.render()`` method.
 | 
			
		||||
    #     """
 | 
			
		||||
 | 
			
		||||
    #     kwargs['class_'] = class_
 | 
			
		||||
    #     # kwargs.setdefault('get_uuid', self.get_uuid)
 | 
			
		||||
    #     kwargs.setdefault('checkboxes', False)
 | 
			
		||||
    #     return formalchemy.Grid.render(self, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def render(self, **kwargs):
 | 
			
		||||
        engine = self.engine or formalchemy.config.engine
 | 
			
		||||
        if self.readonly:
 | 
			
		||||
            return engine('grid_readonly', grid=self, **kwargs)
 | 
			
		||||
        kwargs.setdefault('request', self._request)
 | 
			
		||||
        return engine('grid', grid=self, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def th_sortable(self, field):
 | 
			
		||||
        class_ = ''
 | 
			
		||||
        label = field.label()
 | 
			
		||||
        if self.sortable and field.key in self.config.get('sort_map', {}):
 | 
			
		||||
            class_ = 'sortable'                
 | 
			
		||||
            if field.key == self.config['sort']:
 | 
			
		||||
                class_ += ' sorted ' + self.config['dir']
 | 
			
		||||
            label = literal('<a href="#">') + label + literal('</a>')
 | 
			
		||||
        if class_:
 | 
			
		||||
            class_ = ' class="%s"' % class_
 | 
			
		||||
        return literal('<th' + class_ + ' field="' + field.key + '">') + label + literal('</th>')
 | 
			
		||||
 | 
			
		||||
    # def url(self):
 | 
			
		||||
    #     # TODO: Probably clean this up somehow...
 | 
			
		||||
    #     if self.pager is not None:
 | 
			
		||||
    #         u = self.pager._url_generator(self.pager.page, partial=True)
 | 
			
		||||
    #     else:
 | 
			
		||||
    #         u = self._url or ''
 | 
			
		||||
    #     qs = self.query_string()
 | 
			
		||||
    #     if qs:
 | 
			
		||||
    #         if '?' not in u:
 | 
			
		||||
    #             u += '?'
 | 
			
		||||
    #         u += qs
 | 
			
		||||
    #     elif '?' not in u:
 | 
			
		||||
    #         u += '?partial=True'
 | 
			
		||||
    #     return u
 | 
			
		||||
 | 
			
		||||
    # def query_string(self):
 | 
			
		||||
    #     # TODO: Probably clean this up somehow...
 | 
			
		||||
    #     qs = ''
 | 
			
		||||
    #     if self.url_kwargs:
 | 
			
		||||
    #         for k, v in self.url_kwargs.items():
 | 
			
		||||
    #             qs += '&%s=%s' % (urllib.quote_plus(k), urllib.quote_plus(v))
 | 
			
		||||
    #     return qs
 | 
			
		||||
 | 
			
		||||
    # def get_actions(self):
 | 
			
		||||
 | 
			
		||||
    #     def get_class(text):
 | 
			
		||||
    #         c = text.lower()
 | 
			
		||||
    #         c = c.replace(' ', '-')
 | 
			
		||||
    #         return c
 | 
			
		||||
 | 
			
		||||
    #     res = ''
 | 
			
		||||
    #     for action in self.config['actions']:
 | 
			
		||||
    #         if isinstance(action, basestring):
 | 
			
		||||
    #             text = action
 | 
			
		||||
    #             class_ = get_class(text)
 | 
			
		||||
    #         else:
 | 
			
		||||
    #             text = action[0]
 | 
			
		||||
    #             if len(action) > 1:
 | 
			
		||||
    #                 class_ = action[1]
 | 
			
		||||
    #             else:
 | 
			
		||||
    #                 class_ = get_class(text)
 | 
			
		||||
    #         res += literal('<td class="action%s"><a href="#">%s</a></td>' %
 | 
			
		||||
    #                        (' ' + class_ if class_ else '', text))
 | 
			
		||||
    #     return res
 | 
			
		||||
 | 
			
		||||
    # def get_uuid(self):
 | 
			
		||||
    #     """
 | 
			
		||||
    #     .. highlight:: none
 | 
			
		||||
 | 
			
		||||
    #     Returns a unique identifier for a given record, in the form of an HTML
 | 
			
		||||
    #     attribute for direct inclusion in a ``<tr>`` element within a template.
 | 
			
		||||
    #     An example of what this function might return would be the string::
 | 
			
		||||
 | 
			
		||||
    #        'uuid="420"'
 | 
			
		||||
 | 
			
		||||
    #     Rattail itself will tend to use *universally-unique* IDs (true UUIDs),
 | 
			
		||||
    #     but this method may be overridden to support legacy databases with
 | 
			
		||||
    #     auto-increment IDs, etc.  Really the only important thing is that the
 | 
			
		||||
    #     value returned be unique across the relevant data set.
 | 
			
		||||
 | 
			
		||||
    #     If the concept is unsupported, the method should return an empty
 | 
			
		||||
    #     string.
 | 
			
		||||
    #     """
 | 
			
		||||
 | 
			
		||||
    #     def uuid():
 | 
			
		||||
    #         if self.uuid_key and hasattr(self.model, self.uuid_key):
 | 
			
		||||
    #             return getattr(self.model, self.uuid_key)
 | 
			
		||||
    #         if hasattr(self.model, 'uuid'):
 | 
			
		||||
    #             return getattr(self.model, 'uuid')
 | 
			
		||||
    #         if hasattr(self.model, 'id'):
 | 
			
		||||
    #             return getattr(self.model, 'id')
 | 
			
		||||
 | 
			
		||||
    #     uuid = uuid()
 | 
			
		||||
    #     if uuid:
 | 
			
		||||
    #         return literal('uuid="%s"' % uuid)
 | 
			
		||||
    #     return ''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_fieldset(model, **kwargs):
 | 
			
		||||
    kwargs.setdefault('session', Session())
 | 
			
		||||
    return FieldSet(model, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@accepts_none
 | 
			
		||||
def required(value, field=None):
 | 
			
		||||
    if value is None or value == '':
 | 
			
		||||
        msg = "Please provide a value"
 | 
			
		||||
        if field:
 | 
			
		||||
            msg = "You must provide a value for %s" % field.label()
 | 
			
		||||
        raise formalchemy.ValidationError(msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def EnumFieldRenderer(enum):
 | 
			
		||||
    """
 | 
			
		||||
    Adds support for enumeration fields.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    class Renderer(formalchemy.fields.SelectFieldRenderer):
 | 
			
		||||
        
 | 
			
		||||
        def render_readonly(self, **kwargs):
 | 
			
		||||
            value = self.raw_value
 | 
			
		||||
            if value is None:
 | 
			
		||||
                return ''
 | 
			
		||||
            if value in enum:
 | 
			
		||||
                return enum[value]
 | 
			
		||||
            return value
 | 
			
		||||
 | 
			
		||||
        def render(self, **kwargs):
 | 
			
		||||
            opts = []
 | 
			
		||||
            for value in sorted(enum):
 | 
			
		||||
                opts.append((enum[value], value))
 | 
			
		||||
            return formalchemy.fields.SelectFieldRenderer.render(self, opts, **kwargs)
 | 
			
		||||
 | 
			
		||||
    return Renderer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pretty_datetime(value):
 | 
			
		||||
    """
 | 
			
		||||
    Formats a ``datetime.datetime`` instance and returns a "pretty"
 | 
			
		||||
    human-readable string from it, e.g. "42 minutes ago".  ``value`` is
 | 
			
		||||
    rendered directly as a string if no date/time can be parsed from it.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    if not isinstance(value, datetime.datetime):
 | 
			
		||||
        return str(value) if value else ''
 | 
			
		||||
    value = edbob.local_time(value)
 | 
			
		||||
    fmt = formalchemy.fields.DateTimeFieldRenderer.format
 | 
			
		||||
    return literal('<span title="%s">%s</span>' % (
 | 
			
		||||
            value.strftime(fmt),
 | 
			
		||||
            pretty.date(value)))    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PrettyDateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
 | 
			
		||||
    """
 | 
			
		||||
    Adds "pretty" date/time support for FormAlchemy.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def render_readonly(self, **kwargs):
 | 
			
		||||
        return pretty_datetime(self.raw_value)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										190
									
								
								edbob/pyramid/grids.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								edbob/pyramid/grids.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,190 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
# -*- coding: utf-8  -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  edbob -- Pythonic Software Framework
 | 
			
		||||
#  Copyright © 2010-2012 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of edbob.
 | 
			
		||||
#
 | 
			
		||||
#  edbob is free software: you can redistribute it and/or modify it under the
 | 
			
		||||
#  terms of the GNU Affero General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  edbob 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 Affero General Public License for
 | 
			
		||||
#  more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
#  along with edbob.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``edbob.pyramid.grids`` -- Grid Tables
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from collections import OrderedDict
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.orm import Query
 | 
			
		||||
 | 
			
		||||
from pyramid.renderers import render
 | 
			
		||||
from pyramid.response import Response
 | 
			
		||||
from webhelpers import paginate
 | 
			
		||||
from webhelpers.html import literal
 | 
			
		||||
from webhelpers.html.builder import format_attrs
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
from edbob.pyramid.filters import SearchFormRenderer
 | 
			
		||||
from edbob.util import prettify
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasicGrid(edbob.Object):
 | 
			
		||||
    """
 | 
			
		||||
    Basic grid class for those times when SQLAlchemy is not needed.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, columns, rows, config, url, sortable=True, deletable=False, **kwargs):
 | 
			
		||||
        edbob.Object.__init__(self, **kwargs)
 | 
			
		||||
        self.rows = rows
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.url = url
 | 
			
		||||
        self.sortable = sortable
 | 
			
		||||
        self.deletable = deletable
 | 
			
		||||
        self.columns = OrderedDict()
 | 
			
		||||
        for col in columns:
 | 
			
		||||
            if isinstance(col, (tuple, list)):
 | 
			
		||||
                if len(col) == 2:
 | 
			
		||||
                    self.columns[col[0]] = col[1]
 | 
			
		||||
                    continue
 | 
			
		||||
            elif isinstance(col, basestring):
 | 
			
		||||
                self.columns[col] = prettify(col)
 | 
			
		||||
                continue
 | 
			
		||||
            raise ValueError("Column element must be either a string or 2-tuple")                
 | 
			
		||||
 | 
			
		||||
    def _set_active(self, row):
 | 
			
		||||
        self.model = {}
 | 
			
		||||
        for i, col in enumerate(self.columns.keys()):
 | 
			
		||||
            if i >= len(row):
 | 
			
		||||
                break
 | 
			
		||||
            self.model[col] = row[i]
 | 
			
		||||
 | 
			
		||||
    def field_label(self, name):
 | 
			
		||||
        return self.columns[name]
 | 
			
		||||
 | 
			
		||||
    def field_name(self, field):
 | 
			
		||||
        return field
 | 
			
		||||
 | 
			
		||||
    def iter_fields(self):
 | 
			
		||||
        for col in self.columns.keys():
 | 
			
		||||
            yield col
 | 
			
		||||
 | 
			
		||||
    def render(self, **kwargs):
 | 
			
		||||
        kwargs['grid'] = self
 | 
			
		||||
        return render('forms/grid_readonly.mako', kwargs)
 | 
			
		||||
 | 
			
		||||
    def render_field(self, field, readonly):
 | 
			
		||||
        return self.model[field]
 | 
			
		||||
 | 
			
		||||
    def row_attrs(self, i):
 | 
			
		||||
        return format_attrs(class_='even' if i % 2 else 'odd')
 | 
			
		||||
 | 
			
		||||
    def th_sortable(self, field):
 | 
			
		||||
        class_ = ''
 | 
			
		||||
        label = self.field_label(field)
 | 
			
		||||
        if self.sortable and field in self.config.get('sort_map', {}):
 | 
			
		||||
            class_ = 'sortable'                
 | 
			
		||||
            if field == self.config['sort']:
 | 
			
		||||
                class_ += ' sorted ' + self.config['dir']
 | 
			
		||||
            label = literal('<a href="#">') + label + literal('</a>')
 | 
			
		||||
        if class_:
 | 
			
		||||
            class_ = ' class="%s"' % class_
 | 
			
		||||
        return literal('<th' + class_ + ' field="' + field + '">') + label + literal('</th>')
 | 
			
		||||
 | 
			
		||||
    def url_attrs(self):
 | 
			
		||||
        return format_attrs(url=self.url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_grid_config(name, request, search=None, url=None, **kwargs):
 | 
			
		||||
    config = {
 | 
			
		||||
        'actions': [],
 | 
			
		||||
        'per_page': 20,
 | 
			
		||||
        'page': 1,
 | 
			
		||||
        'sortable': True,
 | 
			
		||||
        'dir': 'asc',
 | 
			
		||||
        'object_url': '',
 | 
			
		||||
        'deletable': False,
 | 
			
		||||
        'delete_url': '',
 | 
			
		||||
        'use_dialog': False,
 | 
			
		||||
        }
 | 
			
		||||
    config.update(kwargs)
 | 
			
		||||
    # words = name.split('.')
 | 
			
		||||
    # if len(words) == 2:
 | 
			
		||||
    #     config.setdefault('object_url', request.route_url(words[0], action='crud'))
 | 
			
		||||
    #     config.setdefault('delete_url', config['object_url'])
 | 
			
		||||
    for key in config:
 | 
			
		||||
        full_key = name+'_'+key
 | 
			
		||||
        if request.params.get(key):
 | 
			
		||||
            value = request.params[key]
 | 
			
		||||
            config[key] = value
 | 
			
		||||
            request.session[full_key] = value
 | 
			
		||||
        elif request.session.get(full_key):
 | 
			
		||||
            value = request.session[full_key]
 | 
			
		||||
            config[key] = value
 | 
			
		||||
    config['search'] = search
 | 
			
		||||
    config['url'] = url
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_pager(query, config):
 | 
			
		||||
    query = query(config)
 | 
			
		||||
    count = None
 | 
			
		||||
    if isinstance(query, Query):
 | 
			
		||||
        count = query.count()
 | 
			
		||||
    return paginate.Page(
 | 
			
		||||
        query, item_count=count,
 | 
			
		||||
        items_per_page=int(config['per_page']),
 | 
			
		||||
        page=int(config['page']),
 | 
			
		||||
        url=paginate.PageURL(config['url'], {}),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_sort_map(cls, names, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Convenience function which returns a sort map.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    smap = {}
 | 
			
		||||
    for name in names:
 | 
			
		||||
        smap[name] = sorter(getattr(cls, name))
 | 
			
		||||
    smap.update(kwargs)
 | 
			
		||||
    return smap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def render_grid(request, grid, search=None, **kwargs):
 | 
			
		||||
    if request.params.get('partial'):
 | 
			
		||||
        return Response(body=grid, content_type='text/html')
 | 
			
		||||
    kwargs['grid'] = grid
 | 
			
		||||
    if search:
 | 
			
		||||
        kwargs['search'] = SearchFormRenderer(search)
 | 
			
		||||
    return kwargs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sort_query(query, config, sort_map, join_map={}):
 | 
			
		||||
    field = config['sort']
 | 
			
		||||
    joins = config.setdefault('joins', [])
 | 
			
		||||
    if field in join_map and field not in joins:
 | 
			
		||||
        query = join_map[field](query)
 | 
			
		||||
        joins.append(field)
 | 
			
		||||
    config['sort_map'] = sort_map
 | 
			
		||||
    return sort_map[field](query, config['dir'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sorter(field):
 | 
			
		||||
    """
 | 
			
		||||
    Returns a function suitable for a sort map callable, with typical
 | 
			
		||||
    logic built in for sorting applied to ``field``.
 | 
			
		||||
    """
 | 
			
		||||
    return lambda q, d: q.order_by(getattr(field, d)())
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``{{package}}.db`` -- Database Stuff
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.orm import scoped_session, sessionmaker
 | 
			
		||||
from zope.sqlalchemy import ZopeTransactionExtension
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Session']
 | 
			
		||||
 | 
			
		||||
Session = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
<%inherit file="edbob/base.mako" />
 | 
			
		||||
<%inherit file="/edbob/base.mako" />
 | 
			
		||||
<%def name="global_title()">{{project}}</%def>
 | 
			
		||||
<%def name="footer()">
 | 
			
		||||
  {{project}} v${{{package}}.__version__} powered by
 | 
			
		||||
  {{project}} v${ {{package}}.__version__} powered by
 | 
			
		||||
  ${h.link_to("edbob", 'http://edbob.org/', target='_blank')} v${edbob.__version__}
 | 
			
		||||
</%def>
 | 
			
		||||
${parent.body()}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
<%inherit file="base.mako" />
 | 
			
		||||
<%inherit file="/base.mako" />
 | 
			
		||||
 | 
			
		||||
<h1>Welcome to {{project}}</h1>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ whatever = you like
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
####################
 | 
			
		||||
# Pyramid
 | 
			
		||||
# pyramid
 | 
			
		||||
####################
 | 
			
		||||
 | 
			
		||||
[app:main]
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +36,20 @@ host = 0.0.0.0
 | 
			
		|||
port = 6543
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
####################
 | 
			
		||||
# alembic
 | 
			
		||||
####################
 | 
			
		||||
 | 
			
		||||
[alembic]
 | 
			
		||||
# path to migration scripts
 | 
			
		||||
script_location = schema
 | 
			
		||||
 | 
			
		||||
# template used to generate migration files
 | 
			
		||||
# file_template = %%(rev)s_%%(slug)s
 | 
			
		||||
 | 
			
		||||
sqlalchemy.url = postgresql://user:pass@localhost/{{package}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
####################
 | 
			
		||||
# edbob
 | 
			
		||||
####################
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,25 +37,9 @@ requires = [
 | 
			
		|||
    # outside the lines with regard to these soft limits.  If bugs are
 | 
			
		||||
    # encountered then they should be filed as such.
 | 
			
		||||
    #
 | 
			
		||||
    # package                   # low                   high
 | 
			
		||||
    # package                           # low                   high
 | 
			
		||||
 | 
			
		||||
    # Beaker dependency included here because 'pyramid_beaker' uses incorrect
 | 
			
		||||
    # case in its requirement declaration.
 | 
			
		||||
    'Beaker',                   # 1.6.3
 | 
			
		||||
 | 
			
		||||
    'decorator',                # 3.3.2
 | 
			
		||||
    'edbob',                    # 0.1a1
 | 
			
		||||
    'Mako',                     # 0.6.2
 | 
			
		||||
    'pyramid',                  # 1.3b2
 | 
			
		||||
    'pyramid_beaker',           # 0.6.1
 | 
			
		||||
    'pyramid_debugtoolbar',     # 1.0
 | 
			
		||||
    'pyramid_tm',               # 0.3
 | 
			
		||||
    'SQLAlchemy',               # 0.7.6
 | 
			
		||||
    'Tempita',                  # 0.5.1
 | 
			
		||||
    'transaction',              # 1.2.0
 | 
			
		||||
    'waitress',                 # 0.8.1
 | 
			
		||||
    'WebHelpers',               # 1.3
 | 
			
		||||
    'zope.sqlalchemy',          # 0.7
 | 
			
		||||
    'edbob[db,pyramid]',                # 0.1a1.dev
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,15 @@ li {
 | 
			
		|||
    line-height: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wrapper {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.wrapper {
 | 
			
		||||
    /* border: 1px solid black; */
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.left {
 | 
			
		||||
    float: left;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +43,15 @@ li {
 | 
			
		|||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
td.right {
 | 
			
		||||
    float: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.wrapper td.right {
 | 
			
		||||
    vertical-align: bottom;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/******************************
 | 
			
		||||
 * Main Layout
 | 
			
		||||
 ******************************/
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +128,7 @@ h1 {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
h2 {
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    margin: 20px auto 10px auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
| 
						 | 
				
			
			@ -166,6 +184,10 @@ div.dialog {
 | 
			
		|||
 * Filters
 | 
			
		||||
 ******************************/
 | 
			
		||||
 | 
			
		||||
div.filterset {
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.filters {
 | 
			
		||||
    /* margin-bottom: 10px; */
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -214,23 +236,24 @@ table.search-wrapper td.grid-mgmt {
 | 
			
		|||
 * Grids
 | 
			
		||||
 ******************************/
 | 
			
		||||
 | 
			
		||||
a.add-object {
 | 
			
		||||
    display: block;
 | 
			
		||||
    float: right;
 | 
			
		||||
}
 | 
			
		||||
/* a.add-object { */
 | 
			
		||||
/*     display: block; */
 | 
			
		||||
/*     float: right; */
 | 
			
		||||
/* } */
 | 
			
		||||
 | 
			
		||||
ul.grid-menu {
 | 
			
		||||
    display: block;
 | 
			
		||||
    float: right;
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
/* ul.grid-menu { */
 | 
			
		||||
/*     display: block; */
 | 
			
		||||
/*     float: right; */
 | 
			
		||||
/*     list-style-type: none; */
 | 
			
		||||
/*     margin-bottom: 5px; */
 | 
			
		||||
/* } */
 | 
			
		||||
 | 
			
		||||
div.grid {
 | 
			
		||||
    clear: both;
 | 
			
		||||
    /* margin-top: 8px; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid {
 | 
			
		||||
div.grid table {
 | 
			
		||||
    border-top: 1px solid black;
 | 
			
		||||
    border-left: 1px solid black;
 | 
			
		||||
    border-collapse: collapse;
 | 
			
		||||
| 
						 | 
				
			
			@ -239,47 +262,47 @@ table.grid {
 | 
			
		|||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid th,
 | 
			
		||||
table.grid td {
 | 
			
		||||
div.grid table th,
 | 
			
		||||
div.grid table td {
 | 
			
		||||
    border-right: 1px solid black;
 | 
			
		||||
    border-bottom: 1px solid black;
 | 
			
		||||
    padding: 2px 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid th.sortable a {
 | 
			
		||||
div.grid table th.sortable a {
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding-right: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid th.sorted {
 | 
			
		||||
div.grid table th.sorted {
 | 
			
		||||
    background-position: right center;
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid th.sorted.asc {
 | 
			
		||||
div.grid table th.sorted.asc {
 | 
			
		||||
    background-image: url(../img/sort_arrow_up.png);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid th.sorted.desc {
 | 
			
		||||
div.grid table th.sorted.desc {
 | 
			
		||||
    background-image: url(../img/sort_arrow_down.png);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid tr.even {
 | 
			
		||||
div.grid table tr.even {
 | 
			
		||||
    background-color: #e0e0e0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid thead th.checkbox,
 | 
			
		||||
table.grid tbody td.checkbox {
 | 
			
		||||
div.grid table thead th.checkbox,
 | 
			
		||||
div.grid table tbody td.checkbox {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    width: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid td.action {
 | 
			
		||||
div.grid table td.action {
 | 
			
		||||
    cursor: default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid td.delete {
 | 
			
		||||
div.grid table td.delete {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    background-image: url(../img/delete.png);
 | 
			
		||||
| 
						 | 
				
			
			@ -288,25 +311,25 @@ table.grid td.delete {
 | 
			
		|||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid tbody tr.hovering {
 | 
			
		||||
div.grid table tbody tr.hovering {
 | 
			
		||||
    background-color: #bbbbbb;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid.hoverable tbody tr {
 | 
			
		||||
div.grid table.hoverable tbody tr {
 | 
			
		||||
    cursor: default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid.clickable tbody tr {
 | 
			
		||||
div.grid.clickable table tbody tr {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid.selectable tbody tr,
 | 
			
		||||
table.grid.checkable tbody tr {
 | 
			
		||||
div.grid table.selectable tbody tr,
 | 
			
		||||
div.grid table.checkable tbody tr {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table.grid.selectable tbody tr.selected,
 | 
			
		||||
table.grid.checkable tbody tr.selected {
 | 
			
		||||
div.grid table.selectable tbody tr.selected,
 | 
			
		||||
div.grid table.checkable tbody tr.selected {
 | 
			
		||||
    background-color: #666666;
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -361,7 +384,7 @@ div.field-couple div.field {
 | 
			
		|||
 | 
			
		||||
div.field-couple div.field input[type=text],
 | 
			
		||||
div.field-couple div.field select {
 | 
			
		||||
    width: 180px;
 | 
			
		||||
    width: 320px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.checkbox {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								edbob/pyramid/static/img/delete.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								edbob/pyramid/static/img/delete.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 641 B  | 
							
								
								
									
										
											BIN
										
									
								
								edbob/pyramid/static/img/sort_arrow_down.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								edbob/pyramid/static/img/sort_arrow_down.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 158 B  | 
							
								
								
									
										
											BIN
										
									
								
								edbob/pyramid/static/img/sort_arrow_up.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								edbob/pyramid/static/img/sort_arrow_up.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 169 B  | 
| 
						 | 
				
			
			@ -174,7 +174,7 @@ $(function() {
 | 
			
		|||
	return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid th.sortable a').live('click', function() {
 | 
			
		||||
    $('div.grid table th.sortable a').live('click', function() {
 | 
			
		||||
	var div = $(this).parents('div.grid:first');
 | 
			
		||||
	var th = $(this).parents('th:first');
 | 
			
		||||
	var dir = 'asc';
 | 
			
		||||
| 
						 | 
				
			
			@ -185,43 +185,44 @@ $(function() {
 | 
			
		|||
	var url = div.attr('url');
 | 
			
		||||
	url += url.match(/\?/) ? '&' : '?';
 | 
			
		||||
	url += 'sort=' + th.attr('field') + '&dir=' + dir;
 | 
			
		||||
	url += '&partial=true';
 | 
			
		||||
	div.load(url);
 | 
			
		||||
	return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.hoverable tbody tr').live('mouseenter', function() {
 | 
			
		||||
    $('div.grid.hoverable table tbody tr').live('mouseenter', function() {
 | 
			
		||||
	$(this).addClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.hoverable tbody tr').live('mouseleave', function() {
 | 
			
		||||
    $('div.grid.hoverable table tbody tr').live('mouseleave', function() {
 | 
			
		||||
	$(this).removeClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.clickable tbody tr').live('mouseenter', function() {
 | 
			
		||||
    $('div.grid.clickable table tbody tr').live('mouseenter', function() {
 | 
			
		||||
	$(this).addClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.clickable tbody tr').live('mouseleave', function() {
 | 
			
		||||
    $('div.grid.clickable table tbody tr').live('mouseleave', function() {
 | 
			
		||||
	$(this).removeClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.selectable tbody tr').live('mouseenter', function() {
 | 
			
		||||
    $('div.grid.selectable table tbody tr').live('mouseenter', function() {
 | 
			
		||||
	$(this).addClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.selectable tbody tr').live('mouseleave', function() {
 | 
			
		||||
    $('div.grid.selectable table tbody tr').live('mouseleave', function() {
 | 
			
		||||
	$(this).removeClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.checkable tbody tr').live('mouseenter', function() {
 | 
			
		||||
    $('div.grid.checkable table tbody tr').live('mouseenter', function() {
 | 
			
		||||
	$(this).addClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.checkable tbody tr').live('mouseleave', function() {
 | 
			
		||||
    $('div.grid.checkable table tbody tr').live('mouseleave', function() {
 | 
			
		||||
	$(this).removeClass('hovering');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.clickable tbody tr').live('click', function() {
 | 
			
		||||
    $('div.grid.clickable table tbody tr').live('click', function() {
 | 
			
		||||
	var div = $(this).parents('div.grid:first');
 | 
			
		||||
	if (div.attr('usedlg') == 'True') {
 | 
			
		||||
	    var dlg = get_dialog('grid-object');
 | 
			
		||||
| 
						 | 
				
			
			@ -240,9 +241,9 @@ $(function() {
 | 
			
		|||
	}
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.checkable thead th.checkbox input[type=checkbox]').live('click', function() {
 | 
			
		||||
    $('div.grid.checkable table thead th.checkbox input[type=checkbox]').live('click', function() {
 | 
			
		||||
	var checked = $(this).is(':checked');
 | 
			
		||||
	var table = $(this).parents('table.grid:first');
 | 
			
		||||
	var table = $(this).parents('table:first');
 | 
			
		||||
	table.find('tbody tr').each(function() {
 | 
			
		||||
	    $(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
 | 
			
		||||
	    if (checked) {
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +254,7 @@ $(function() {
 | 
			
		|||
	});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.selectable tbody tr').live('click', function() {
 | 
			
		||||
    $('div.grid.selectable table tbody tr').live('click', function() {
 | 
			
		||||
	var table = $(this).parents('table:first');
 | 
			
		||||
	if (! table.hasClass('multiple')) {
 | 
			
		||||
	    table.find('tbody tr').removeClass('selected');
 | 
			
		||||
| 
						 | 
				
			
			@ -261,12 +262,22 @@ $(function() {
 | 
			
		|||
	$(this).addClass('selected');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('table.grid.checkable tbody tr').live('click', function() {
 | 
			
		||||
    $('div.grid.checkable table tbody tr').live('click', function() {
 | 
			
		||||
	var checkbox = $(this).find('td:first input[type=checkbox]');
 | 
			
		||||
	checkbox.attr('checked', !checkbox.is(':checked'));
 | 
			
		||||
	$(this).toggleClass('selected');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('div.grid td.delete').live('click', function() {
 | 
			
		||||
	if (confirm("Do you really wish to delete this object?")) {
 | 
			
		||||
	    var grid = $(this).parents('div.grid:first');
 | 
			
		||||
	    var url = grid.attr('delurl');
 | 
			
		||||
//	    alert(url + '?uuid=' + get_uuid(this) + '&delete=true');
 | 
			
		||||
	    location.href = url + '?uuid=' + get_uuid(this) + '&delete=true';
 | 
			
		||||
	}
 | 
			
		||||
	return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('#grid-page-count').live('change', function() {
 | 
			
		||||
	var div = $(this).parents('div.grid:first');
 | 
			
		||||
	loading(div);
 | 
			
		||||
| 
						 | 
				
			
			@ -320,7 +331,7 @@ $(function() {
 | 
			
		|||
 | 
			
		||||
    $('div.dialog.lookup button.ok').live('click', function() {
 | 
			
		||||
	var dialog = $(this).parents('div.dialog.lookup:first');
 | 
			
		||||
	var tr = dialog.find('table.grid tbody tr.selected');
 | 
			
		||||
	var tr = dialog.find('div.grid table tbody tr.selected');
 | 
			
		||||
	if (! tr.length) {
 | 
			
		||||
	    alert("You haven't selected anything.");
 | 
			
		||||
	    return false;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,8 +26,8 @@
 | 
			
		|||
``edbob.pyramid.subscribers`` -- Subscribers
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from pyramid import threadlocal
 | 
			
		||||
from pyramid.security import authenticated_userid
 | 
			
		||||
# from sqlahelper import get_session
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
from edbob.db.auth import has_permission
 | 
			
		||||
| 
						 | 
				
			
			@ -43,9 +43,11 @@ def before_render(event):
 | 
			
		|||
       * ``edbob``
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    request = event.get('request') or threadlocal.get_current_request()
 | 
			
		||||
 | 
			
		||||
    renderer_globals = event
 | 
			
		||||
    renderer_globals['h'] = helpers
 | 
			
		||||
    renderer_globals['url'] = event['request'].route_url
 | 
			
		||||
    renderer_globals['url'] = request.route_url
 | 
			
		||||
    renderer_globals['edbob'] = edbob
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								edbob/pyramid/templates/crud.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								edbob/pyramid/templates/crud.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
<%inherit file="/edbob/crud.mako" />
 | 
			
		||||
${parent.body()}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
<%def name="global_title()">edbob</%def>
 | 
			
		||||
<%def name="title()"></%def>
 | 
			
		||||
<%def name="title()">${(fieldset.crud_title+' : '+fieldset.get_display_text() if fieldset.edit else 'New '+fieldset.crud_title) if crud else ''}</%def>
 | 
			
		||||
<%def name="head_tags()"></%def>
 | 
			
		||||
<%def name="home_link()"><h1 class="right">${h.link_to("Home", url('home'))}</h1></%def>
 | 
			
		||||
<%def name="footer()">
 | 
			
		||||
  powered by ${h.link_to('edbob', 'http://edbob.org', target='_blank')} v${edbob.__version__}
 | 
			
		||||
</%def>
 | 
			
		||||
| 
						 | 
				
			
			@ -10,14 +11,14 @@
 | 
			
		|||
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
 | 
			
		||||
    <title>${self.global_title()}${' : ' + capture(self.title) if capture(self.title) else ''}</title>
 | 
			
		||||
 | 
			
		||||
    ${h.javascript_link('edbob/js/jquery.js')}
 | 
			
		||||
    ${h.javascript_link('edbob/js/jquery.ui.js')}
 | 
			
		||||
    ${h.javascript_link('edbob/js/jquery.loading.js')}
 | 
			
		||||
    ${h.javascript_link('edbob/js/jquery.autocomplete.js')}
 | 
			
		||||
    ${h.javascript_link('edbob/js/edbob.js')}
 | 
			
		||||
    ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
 | 
			
		||||
    ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.ui.js'))}
 | 
			
		||||
    ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.loading.js'))}
 | 
			
		||||
    ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.autocomplete.js'))}
 | 
			
		||||
    ${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
 | 
			
		||||
 | 
			
		||||
    ${h.stylesheet_link('edbob/css/smoothness/jquery-ui-1.8.2.custom.css')}
 | 
			
		||||
    ${h.stylesheet_link('edbob/css/edbob.css')}
 | 
			
		||||
    ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/smoothness/jquery-ui-1.8.2.custom.css'))}
 | 
			
		||||
    ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}
 | 
			
		||||
 | 
			
		||||
    ${self.head_tags()}
 | 
			
		||||
  </head>
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +28,7 @@
 | 
			
		|||
      <div id="main">
 | 
			
		||||
 | 
			
		||||
	<div id="header">
 | 
			
		||||
	  <h1 class="right">${h.link_to("Home", url('home'))}</h1>
 | 
			
		||||
	  ${self.home_link()}
 | 
			
		||||
	  <h1 class="left">${self.title()}</h1>
 | 
			
		||||
	  <div id="login" class="left">
 | 
			
		||||
##	    <% user = request.current_user %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								edbob/pyramid/templates/edbob/crud.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								edbob/pyramid/templates/edbob/crud.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<%inherit file="/base.mako" />
 | 
			
		||||
 | 
			
		||||
<div class="wrapper">
 | 
			
		||||
 | 
			
		||||
  <div class="right">
 | 
			
		||||
    ${self.menu()}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="left">
 | 
			
		||||
    ${fieldset.render()|n}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										14
									
								
								edbob/pyramid/templates/edbob/index.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								edbob/pyramid/templates/edbob/index.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
<%inherit file="/base.mako" />
 | 
			
		||||
 | 
			
		||||
<div class="wrapper">
 | 
			
		||||
 | 
			
		||||
  <div class="right">
 | 
			
		||||
    ${self.menu()|n}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="left">
 | 
			
		||||
    ${search.render()|n}
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
${grid|n}
 | 
			
		||||
							
								
								
									
										51
									
								
								edbob/pyramid/templates/forms/fieldset.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								edbob/pyramid/templates/forms/fieldset.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
<% _focus_rendered = False %>
 | 
			
		||||
 | 
			
		||||
<div class="fieldset-form ${class_}">
 | 
			
		||||
  ${h.form(fieldset.url_action+('?uuid='+fieldset.model.uuid) if fieldset.edit else '')}
 | 
			
		||||
 | 
			
		||||
  % for error in fieldset.errors.get(None, []):
 | 
			
		||||
      <div class="fieldset-error">${error}</div>
 | 
			
		||||
  % endfor
 | 
			
		||||
 | 
			
		||||
  % for field in fieldset.render_fields.itervalues():
 | 
			
		||||
 | 
			
		||||
      <div class="field-couple ${field.name}">
 | 
			
		||||
	% for error in field.errors:
 | 
			
		||||
	    <div class="field-error">${error}</div>
 | 
			
		||||
	% endfor
 | 
			
		||||
        ${field.label_tag()|n}
 | 
			
		||||
	<div class="field">
 | 
			
		||||
	  ${field.render()|n}
 | 
			
		||||
	</div>
 | 
			
		||||
	% if 'instructions' in field.metadata:
 | 
			
		||||
	    <span class="instructions">${field.metadata['instructions']}</span>
 | 
			
		||||
	% endif
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      % if (fieldset.focus == field or fieldset.focus is True) and not _focus_rendered:
 | 
			
		||||
	  % if not field.is_readonly():
 | 
			
		||||
	      <script language="javascript" type="text/javascript">
 | 
			
		||||
		$(function() {
 | 
			
		||||
		    $('#${field.renderer.name}').focus();
 | 
			
		||||
		});
 | 
			
		||||
	      </script>
 | 
			
		||||
	      <% _focus_rendered = True %>
 | 
			
		||||
	  % endif
 | 
			
		||||
      % endif
 | 
			
		||||
 | 
			
		||||
  % endfor
 | 
			
		||||
 | 
			
		||||
  <div class="buttons">
 | 
			
		||||
    ${h.submit('submit', "Save")}
 | 
			
		||||
    <button type="button" class="cancel">Cancel</button>
 | 
			
		||||
  </div>
 | 
			
		||||
  ${h.end_form()}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script language="javascript" type="text/javascript">
 | 
			
		||||
$(function() {
 | 
			
		||||
    $('button.cancel').click(function() {
 | 
			
		||||
	location.href = '${fieldset.url_cancel}';
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										12
									
								
								edbob/pyramid/templates/forms/fieldset_readonly.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								edbob/pyramid/templates/forms/fieldset_readonly.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
<table class="fieldset ${class_}">
 | 
			
		||||
  <tbody>
 | 
			
		||||
    %for field in fieldset.render_fields.itervalues():
 | 
			
		||||
	%if field.requires_label:
 | 
			
		||||
	    <tr class="${field.key}">
 | 
			
		||||
	      <td class="label">${field.label()|h}</td>
 | 
			
		||||
	      <td>${field.render_readonly()|n}</td>
 | 
			
		||||
	    </tr>
 | 
			
		||||
	%endif
 | 
			
		||||
    %endfor
 | 
			
		||||
  </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
							
								
								
									
										36
									
								
								edbob/pyramid/templates/forms/filterset.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								edbob/pyramid/templates/forms/filterset.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
<div class="filterset">
 | 
			
		||||
  ${search.begin()}
 | 
			
		||||
  <% visible = [] %>
 | 
			
		||||
  % for f in search.sorted_filters():
 | 
			
		||||
      <% f = search.filters[f] %>
 | 
			
		||||
      <div class="filter" id="filter-${f.name}"${' style="display: none;"' if not search.config.get('include_filter_'+f.name) else ''}>
 | 
			
		||||
	${search.checkbox('include_filter_'+f.name)}
 | 
			
		||||
	<label for="${f.name}">${f.label}</label>
 | 
			
		||||
	${f.types_select()}
 | 
			
		||||
	${f.value_control()}
 | 
			
		||||
      </div>
 | 
			
		||||
      % if search.config.get('include_filter_'+f.name):
 | 
			
		||||
          <% visible.append(f.name) %>
 | 
			
		||||
      % endif
 | 
			
		||||
  % endfor
 | 
			
		||||
  <div class="buttons">
 | 
			
		||||
    ${search.add_filter(visible)}
 | 
			
		||||
    ${search.submit('submit', "Search", style='display: none;' if not visible else None)}
 | 
			
		||||
    <button type="reset"${' style="display: none;"' if not visible else ''}>Reset</button>
 | 
			
		||||
  </div>
 | 
			
		||||
  ${search.end()}
 | 
			
		||||
  % if visible:
 | 
			
		||||
      <script language="javascript" type="text/javascript">
 | 
			
		||||
	var filters_to_disable = [
 | 
			
		||||
	    % for field in visible:
 | 
			
		||||
		'${field}',
 | 
			
		||||
	    % endfor
 | 
			
		||||
	];
 | 
			
		||||
	% if not dialog:
 | 
			
		||||
	    $(function() {
 | 
			
		||||
		disable_filter_options();
 | 
			
		||||
	    });
 | 
			
		||||
	% endif
 | 
			
		||||
      </script>
 | 
			
		||||
  % endif
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										57
									
								
								edbob/pyramid/templates/forms/grid_readonly.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								edbob/pyramid/templates/forms/grid_readonly.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
<div class="grid${' '+class_ if class_ else ''}" ${grid.url_attrs()|n}>
 | 
			
		||||
 | 
			
		||||
##url="${grid.url_grid}"
 | 
			
		||||
##     objurl="${grid.url_object}" delurl="${grid.url_object}"
 | 
			
		||||
##     usedlg="${grid.config['use_dialog']}">
 | 
			
		||||
 | 
			
		||||
  <table>
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
	% if checkboxes:
 | 
			
		||||
	    <th class="checkbox">${h.checkbox('check-all')}</th>
 | 
			
		||||
	% endif
 | 
			
		||||
        % for field in grid.iter_fields():
 | 
			
		||||
	    ${grid.th_sortable(field)|n}
 | 
			
		||||
	% endfor
 | 
			
		||||
##	% for i in range(len(grid.config['actions'])):
 | 
			
		||||
##	    <th> </th>
 | 
			
		||||
##	% endfor
 | 
			
		||||
	% if grid.deletable:
 | 
			
		||||
	    <th> </th>
 | 
			
		||||
	% endif
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
 | 
			
		||||
    <tbody>
 | 
			
		||||
      % for i, row in enumerate(grid.rows):
 | 
			
		||||
	  <% grid._set_active(row) %>
 | 
			
		||||
##	  <tr uuid="${grid.model.uuid}" class="${'even' if i % 2 else 'odd'}">
 | 
			
		||||
          <tr ${grid.row_attrs(i)|n}>
 | 
			
		||||
	    % if checkboxes:
 | 
			
		||||
		<td class="checkbox">${h.checkbox('check-'+grid.model.uuid, disabled=True)}</td>
 | 
			
		||||
	    % endif
 | 
			
		||||
            % for field in grid.iter_fields():
 | 
			
		||||
	        <td class="${grid.field_name(field)}">${grid.render_field(field, True)|n}</td>
 | 
			
		||||
	    % endfor
 | 
			
		||||
##	    ${grid.get_actions()}
 | 
			
		||||
	    %if grid.deletable:
 | 
			
		||||
	        <td class="delete"> </td>
 | 
			
		||||
	    %endif
 | 
			
		||||
	  </tr>
 | 
			
		||||
      % endfor
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
  % if hasattr(grid, 'pager') and grid.pager:
 | 
			
		||||
      <div class="pager">
 | 
			
		||||
	<p class="showing">
 | 
			
		||||
	  showing
 | 
			
		||||
	  ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
 | 
			
		||||
	</p>
 | 
			
		||||
	<p class="page-links">
 | 
			
		||||
	  ${h.select('grid-page-count', grid.pager.items_per_page, (5, 10, 20, 50, 100))}
 | 
			
		||||
	  per page: 
 | 
			
		||||
	  ${grid.pager.pager('~3~', onclick='return grid_navigate_page($(this));')}
 | 
			
		||||
	</p>
 | 
			
		||||
      </div>
 | 
			
		||||
  % endif
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -39,17 +39,17 @@ from edbob.db.auth import authenticate_user
 | 
			
		|||
_here = os.path.join(os.path.dirname(__file__), os.pardir)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_favicon = open(os.path.join(_here, 'static', 'favicon.ico'), 'rb').read()
 | 
			
		||||
_favicon_response = Response(content_type='image/x-icon', body=_favicon)
 | 
			
		||||
# _favicon = open(os.path.join(_here, 'static', 'favicon.ico'), 'rb').read()
 | 
			
		||||
# _favicon_response = Response(content_type='image/x-icon', body=_favicon)
 | 
			
		||||
 | 
			
		||||
@view_config(route_name='favicon.ico')
 | 
			
		||||
def favicon_ico(context, request):
 | 
			
		||||
    return _favicon_response
 | 
			
		||||
# @view_config(route_name='favicon.ico')
 | 
			
		||||
# def favicon_ico(context, request):
 | 
			
		||||
#     return _favicon_response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@view_config(route_name='home', renderer='home.mako')
 | 
			
		||||
def home(context, request):
 | 
			
		||||
    return {}
 | 
			
		||||
# @view_config(route_name='home', renderer='/home.mako')
 | 
			
		||||
# def home(context, request):
 | 
			
		||||
#     return {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@view_config(route_name='login', renderer='login.mako')
 | 
			
		||||
| 
						 | 
				
			
			@ -80,17 +80,17 @@ def login(context, request):
 | 
			
		|||
    return {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_robots = open(os.path.join(_here, 'static', 'robots.txt')).read()
 | 
			
		||||
_robots_response = Response(content_type='text/plain', body=_robots)
 | 
			
		||||
# _robots = open(os.path.join(_here, 'static', 'robots.txt')).read()
 | 
			
		||||
# _robots_response = Response(content_type='text/plain', body=_robots)
 | 
			
		||||
 | 
			
		||||
@view_config(route_name='robots.txt')
 | 
			
		||||
def robots_txt(context, request):
 | 
			
		||||
    return _robots_response
 | 
			
		||||
# @view_config(route_name='robots.txt')
 | 
			
		||||
# def robots_txt(context, request):
 | 
			
		||||
#     return _robots_response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def includeme(config):
 | 
			
		||||
    config.add_route('home', '/')
 | 
			
		||||
    config.add_route('favicon.ico', '/favicon.ico')
 | 
			
		||||
    config.add_route('robots.txt', '/robots.txt')
 | 
			
		||||
    # config.add_route('home', '/')
 | 
			
		||||
    # config.add_route('favicon.ico', '/favicon.ico')
 | 
			
		||||
    # config.add_route('robots.txt', '/robots.txt')
 | 
			
		||||
    config.add_route('login', '/login')
 | 
			
		||||
    config.scan()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										405
									
								
								edbob/pyramid/views/crud.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								edbob/pyramid/views/crud.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,405 @@
 | 
			
		|||
#!/usr/bin/env python
 | 
			
		||||
# -*- coding: utf-8  -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  edbob -- Pythonic Software Framework
 | 
			
		||||
#  Copyright © 2010-2012 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of edbob.
 | 
			
		||||
#
 | 
			
		||||
#  edbob is free software: you can redistribute it and/or modify it under the
 | 
			
		||||
#  terms of the GNU Affero General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  edbob 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 Affero General Public License for
 | 
			
		||||
#  more details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
#  along with edbob.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
``edbob.pyramid.views.crud`` -- CRUD View Function
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# from pyramid.renderers import render_to_response
 | 
			
		||||
# from pyramid.httpexceptions import HTTPException, HTTPFound, HTTPOk, HTTPUnauthorized
 | 
			
		||||
import transaction
 | 
			
		||||
from pyramid.httpexceptions import HTTPFound, HTTPException
 | 
			
		||||
 | 
			
		||||
# import sqlahelper
 | 
			
		||||
 | 
			
		||||
# # import rattail.pyramid.forms.util as util
 | 
			
		||||
# from rattail.db.perms import has_permission
 | 
			
		||||
# from rattail.pyramid.forms.formalchemy import Grid
 | 
			
		||||
 | 
			
		||||
from edbob.pyramid import Session
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def crud(request, cls, fieldset_factory, home=None, delete=None, post_sync=None, pre_render=None):
 | 
			
		||||
    """
 | 
			
		||||
    Adds a common CRUD mechanism for objects.
 | 
			
		||||
 | 
			
		||||
    ``cls`` should be a SQLAlchemy-mapped class, presumably deriving from
 | 
			
		||||
    :class:`edbob.Object`.
 | 
			
		||||
 | 
			
		||||
    ``fieldset_factory`` must be a callable which accepts the fieldset's
 | 
			
		||||
    "model" as its only positional argument.
 | 
			
		||||
 | 
			
		||||
    ``home`` will be used as the redirect location once a form is fully
 | 
			
		||||
    validated and data saved.  If you do not speficy this parameter, the
 | 
			
		||||
    user will be redirected to be the CRUD page for the new object (e.g. so
 | 
			
		||||
    an object may be created before certain properties may be edited).
 | 
			
		||||
 | 
			
		||||
    ``delete`` may either be a string containing a URL to which the user
 | 
			
		||||
    should be redirected after the object has been deleted, or else a
 | 
			
		||||
    callback which will be executed *instead of* the normal algorithm
 | 
			
		||||
    (which is merely to delete the object via the Session).
 | 
			
		||||
 | 
			
		||||
    ``post_sync`` may be a callback which will be executed immediately
 | 
			
		||||
    after :meth:`FieldSet.sync()` is called, i.e. after validation as well.
 | 
			
		||||
 | 
			
		||||
    ``pre_render`` may be a callback which will be executed after any POST
 | 
			
		||||
    processing has occured, but just before rendering.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    uuid = request.params.get('uuid')
 | 
			
		||||
    obj = Session.query(cls).get(uuid) if uuid else cls
 | 
			
		||||
    assert obj
 | 
			
		||||
 | 
			
		||||
    if request.params.get('delete'):
 | 
			
		||||
        if delete:
 | 
			
		||||
            if isinstance(delete, basestring):
 | 
			
		||||
                with transaction.manager:
 | 
			
		||||
                    Session.delete(obj)
 | 
			
		||||
                return HTTPFound(location=delete)
 | 
			
		||||
            with transaction.manager:
 | 
			
		||||
                res = delete(obj)
 | 
			
		||||
            if res:
 | 
			
		||||
                return res
 | 
			
		||||
        else:
 | 
			
		||||
            with transaction.manager:
 | 
			
		||||
                Session.delete(obj)
 | 
			
		||||
            if not home:
 | 
			
		||||
                raise ValueError("Must specify 'home' or 'delete' url "
 | 
			
		||||
                                 "in call to crud()")
 | 
			
		||||
            return HTTPFound(location=home)
 | 
			
		||||
 | 
			
		||||
    fs = fieldset_factory(obj)
 | 
			
		||||
 | 
			
		||||
    # if not fs.readonly and self.request.params.get('fieldset'):
 | 
			
		||||
    #     fs.rebind(data=self.request.params)
 | 
			
		||||
    #     if fs.validate():
 | 
			
		||||
    #         fs.sync()
 | 
			
		||||
    #         if post_sync:
 | 
			
		||||
    #             res = post_sync(fs)
 | 
			
		||||
    #             if isinstance(res, HTTPFound):
 | 
			
		||||
    #                 return res
 | 
			
		||||
    #         if self.request.params.get('partial'):
 | 
			
		||||
    #             self.Session.flush()
 | 
			
		||||
    #             return self.json_success(uuid=fs.model.uuid)
 | 
			
		||||
    #         return HTTPFound(location=self.request.route_url(objects, action='index'))
 | 
			
		||||
 | 
			
		||||
    if not fs.readonly and request.POST:
 | 
			
		||||
        # print self.request.POST
 | 
			
		||||
        fs.rebind(data=request.params)
 | 
			
		||||
        if fs.validate():
 | 
			
		||||
            with transaction.manager:
 | 
			
		||||
                fs.sync()
 | 
			
		||||
                if post_sync:
 | 
			
		||||
                    res = post_sync(fs)
 | 
			
		||||
                    if res:
 | 
			
		||||
                        return res
 | 
			
		||||
 | 
			
		||||
            if request.params.get('partial'):
 | 
			
		||||
                # Session.flush()
 | 
			
		||||
                # return self.json_success(uuid=fs.model.uuid)
 | 
			
		||||
                assert False, "need to fix this"
 | 
			
		||||
 | 
			
		||||
            # Session.commit()
 | 
			
		||||
            if not home:
 | 
			
		||||
                # FIXME
 | 
			
		||||
                # home = request.route_url.current() + '?uuid=' + fs.model.uuid
 | 
			
		||||
                # home = request.route_url('home')
 | 
			
		||||
                fs.model = Session.merge(fs.model)
 | 
			
		||||
                home = request.current_route_url() + '?uuid=' + fs.model.uuid
 | 
			
		||||
                request.session.flash("%s \"%s\" has been %s." % (
 | 
			
		||||
                        fs.crud_title, fs.get_display_text(),
 | 
			
		||||
                        'updated' if fs.edit else 'created'))
 | 
			
		||||
            return HTTPFound(location=home)
 | 
			
		||||
 | 
			
		||||
    data = {'fieldset': fs, 'crud': True}
 | 
			
		||||
 | 
			
		||||
    if pre_render:
 | 
			
		||||
        res = pre_render(fs)
 | 
			
		||||
        if res:
 | 
			
		||||
            if isinstance(res, HTTPException):
 | 
			
		||||
                return res
 | 
			
		||||
            data.update(res)
 | 
			
		||||
 | 
			
		||||
    # data = {'fieldset':fs}
 | 
			
		||||
    # if self.request.params.get('partial'):
 | 
			
		||||
    #     return render_to_response('/%s/crud_partial.mako' % objects,
 | 
			
		||||
    #                               data, request=self.request)
 | 
			
		||||
    # return data
 | 
			
		||||
 | 
			
		||||
    return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# class needs_perm(object):
 | 
			
		||||
#     """
 | 
			
		||||
#     Decorator to be used for handler methods which should restrict access based
 | 
			
		||||
#     on the current user's permissions.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     def __init__(self, permission, **kwargs):
 | 
			
		||||
#         self.permission = permission
 | 
			
		||||
#         self.kwargs = kwargs
 | 
			
		||||
 | 
			
		||||
#     def __call__(self, fn):
 | 
			
		||||
#         permission = self.permission
 | 
			
		||||
#         kw = self.kwargs
 | 
			
		||||
#         def wrapped(self):
 | 
			
		||||
#             if not self.request.current_user:
 | 
			
		||||
#                 self.request.session['referrer'] = self.request.url_generator.current()
 | 
			
		||||
#                 self.request.session.flash("You must be logged in to do that.", 'error')
 | 
			
		||||
#                 return HTTPFound(location=self.request.route_url('login'))
 | 
			
		||||
#             if not has_permission(self.request.current_user, permission):
 | 
			
		||||
#                 self.request.session.flash("You do not have permission to do that.", 'error')
 | 
			
		||||
#                 home = kw.get('redirect', self.request.route_url('home'))
 | 
			
		||||
#                 return HTTPFound(location=home)
 | 
			
		||||
#             return fn(self)
 | 
			
		||||
#         return wrapped
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def needs_user(fn):
 | 
			
		||||
#     """
 | 
			
		||||
#     Decorator for handler methods which require simply that a user be currently
 | 
			
		||||
#     logged in.
 | 
			
		||||
#     """
 | 
			
		||||
 | 
			
		||||
#     def wrapped(self):
 | 
			
		||||
#         if not self.request.current_user:
 | 
			
		||||
#             self.request.session['referrer'] = self.request.url_generator.current()
 | 
			
		||||
#             self.request.session.flash("You must be logged in to do that.", 'error')
 | 
			
		||||
#             return HTTPFound(location=self.request.route_url('login'))
 | 
			
		||||
#         return fn(self)
 | 
			
		||||
#     return wrapped
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# class Handler(object):
 | 
			
		||||
 | 
			
		||||
#     def __init__(self, request):
 | 
			
		||||
#         self.request = request
 | 
			
		||||
#         self.Session = sqlahelper.get_session()
 | 
			
		||||
 | 
			
		||||
#     # def json_response(self, data={}):
 | 
			
		||||
#     #     response = render_to_response('json', data, request=self.request)
 | 
			
		||||
#     #     response.headers['Content-Type'] = 'application/json'
 | 
			
		||||
#     #     return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# class CrudHandler(Handler):
 | 
			
		||||
#     # """
 | 
			
		||||
#     # This handler provides all the goodies typically associated with general
 | 
			
		||||
#     # CRUD functionality, e.g. search filters and grids.
 | 
			
		||||
#     # """
 | 
			
		||||
 | 
			
		||||
#     def crud(self, cls, fieldset_factory, home=None, delete=None, post_sync=None, pre_render=None):
 | 
			
		||||
#         """
 | 
			
		||||
#         Adds a common CRUD mechanism for objects.
 | 
			
		||||
 | 
			
		||||
#         ``cls`` should be a SQLAlchemy-mapped class, presumably deriving from
 | 
			
		||||
#         :class:`rattail.Object`.
 | 
			
		||||
 | 
			
		||||
#         ``fieldset_factory`` must be a callable which accepts the fieldset's
 | 
			
		||||
#         "model" as its only positional argument.
 | 
			
		||||
 | 
			
		||||
#         ``home`` will be used as the redirect location once a form is fully
 | 
			
		||||
#         validated and data saved.  If you do not speficy this parameter, the
 | 
			
		||||
#         user will be redirected to be the CRUD page for the new object (e.g. so
 | 
			
		||||
#         an object may be created before certain properties may be edited).
 | 
			
		||||
 | 
			
		||||
#         ``delete`` may either be a string containing a URL to which the user
 | 
			
		||||
#         should be redirected after the object has been deleted, or else a
 | 
			
		||||
#         callback which will be executed *instead of* the normal algorithm
 | 
			
		||||
#         (which is merely to delete the object via the Session).
 | 
			
		||||
 | 
			
		||||
#         ``post_sync`` may be a callback which will be executed immediately
 | 
			
		||||
#         after ``FieldSet.sync()`` is called, i.e. after validation as well.
 | 
			
		||||
 | 
			
		||||
#         ``pre_render`` may be a callback which will be executed after any POST
 | 
			
		||||
#         processing has occured, but just before rendering.
 | 
			
		||||
#         """
 | 
			
		||||
 | 
			
		||||
#         uuid = self.request.params.get('uuid')
 | 
			
		||||
#         obj = self.Session.query(cls).get(uuid) if uuid else cls
 | 
			
		||||
#         assert obj
 | 
			
		||||
 | 
			
		||||
#         if self.request.params.get('delete'):
 | 
			
		||||
#             if delete:
 | 
			
		||||
#                 if isinstance(delete, basestring):
 | 
			
		||||
#                     self.Session.delete(obj)
 | 
			
		||||
#                     return HTTPFound(location=delete)
 | 
			
		||||
#                 res = delete(obj)
 | 
			
		||||
#                 if res:
 | 
			
		||||
#                     return res
 | 
			
		||||
#             else:
 | 
			
		||||
#                 self.Session.delete(obj)
 | 
			
		||||
#                 if not home:
 | 
			
		||||
#                     raise ValueError("Must specify 'home' or 'delete' url "
 | 
			
		||||
#                                      "in call to CrudHandler.crud()")
 | 
			
		||||
#                 return HTTPFound(location=home)
 | 
			
		||||
 | 
			
		||||
#         fs = fieldset_factory(obj)
 | 
			
		||||
 | 
			
		||||
#         # if not fs.readonly and self.request.params.get('fieldset'):
 | 
			
		||||
#         #     fs.rebind(data=self.request.params)
 | 
			
		||||
#         #     if fs.validate():
 | 
			
		||||
#         #         fs.sync()
 | 
			
		||||
#         #         if post_sync:
 | 
			
		||||
#         #             res = post_sync(fs)
 | 
			
		||||
#         #             if isinstance(res, HTTPFound):
 | 
			
		||||
#         #                 return res
 | 
			
		||||
#         #         if self.request.params.get('partial'):
 | 
			
		||||
#         #             self.Session.flush()
 | 
			
		||||
#         #             return self.json_success(uuid=fs.model.uuid)
 | 
			
		||||
#         #         return HTTPFound(location=self.request.route_url(objects, action='index'))
 | 
			
		||||
 | 
			
		||||
#         if not fs.readonly and self.request.POST:
 | 
			
		||||
#             # print self.request.POST
 | 
			
		||||
#             fs.rebind(data=self.request.params)
 | 
			
		||||
#             if fs.validate():
 | 
			
		||||
#                 fs.sync()
 | 
			
		||||
#                 if post_sync:
 | 
			
		||||
#                     res = post_sync(fs)
 | 
			
		||||
#                     if res:
 | 
			
		||||
#                         return res
 | 
			
		||||
#                 if self.request.params.get('partial'):
 | 
			
		||||
#                     self.Session.flush()
 | 
			
		||||
#                     return self.json_success(uuid=fs.model.uuid)
 | 
			
		||||
 | 
			
		||||
#                 if not home:
 | 
			
		||||
#                     self.Session.flush()
 | 
			
		||||
#                     home = self.request.url_generator.current() + '?uuid=' + fs.model.uuid
 | 
			
		||||
#                     self.request.session.flash("%s \"%s\" has been %s." % (
 | 
			
		||||
#                             fs.crud_title, fs.get_display_text(),
 | 
			
		||||
#                             'updated' if fs.edit else 'created'))
 | 
			
		||||
#                 return HTTPFound(location=home)
 | 
			
		||||
 | 
			
		||||
#         data = {'fieldset': fs, 'crud': True}
 | 
			
		||||
 | 
			
		||||
#         if pre_render:
 | 
			
		||||
#             res = pre_render(fs)
 | 
			
		||||
#             if res:
 | 
			
		||||
#                 if isinstance(res, HTTPException):
 | 
			
		||||
#                     return res
 | 
			
		||||
#                 data.update(res)
 | 
			
		||||
 | 
			
		||||
#         # data = {'fieldset':fs}
 | 
			
		||||
#         # if self.request.params.get('partial'):
 | 
			
		||||
#         #     return render_to_response('/%s/crud_partial.mako' % objects,
 | 
			
		||||
#         #                               data, request=self.request)
 | 
			
		||||
#         # return data
 | 
			
		||||
 | 
			
		||||
#         return data
 | 
			
		||||
 | 
			
		||||
#     def grid(self, *args, **kwargs):
 | 
			
		||||
#         """
 | 
			
		||||
#         Convenience function which returns a grid.  The only functionality this
 | 
			
		||||
#         method adds is the ``session`` parameter.
 | 
			
		||||
#         """
 | 
			
		||||
 | 
			
		||||
#         return Grid(session=self.Session(), *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
#     # def get_grid(self, name, grid, query, search=None, url=None, **defaults):
 | 
			
		||||
#     #     """
 | 
			
		||||
#     #     Convenience function for obtaining the configuration for a grid,
 | 
			
		||||
#     #     and then obtaining the grid itself.
 | 
			
		||||
 | 
			
		||||
#     #     ``name`` is essentially the config key, e.g. ``'products.lookup'``, and
 | 
			
		||||
#     #     in fact is expected to take that precise form (where the first part is
 | 
			
		||||
#     #     considered the handler name and the second part the action name).
 | 
			
		||||
 | 
			
		||||
#     #     ``grid`` must be a callable with a signature of ``grid(query,
 | 
			
		||||
#     #     config)``, and ``query`` will be passed directly to the ``grid``
 | 
			
		||||
#     #     callable.  ``search`` will be used to inform the grid of the search in
 | 
			
		||||
#     #     effect, if any. ``defaults`` will be used to customize the grid config.
 | 
			
		||||
#     #     """
 | 
			
		||||
 | 
			
		||||
#     #     if not url:
 | 
			
		||||
#     #         handler, action = name.split('.')
 | 
			
		||||
#     #         url = self.request.route_url(handler, action=action)
 | 
			
		||||
#     #     config = util.get_grid_config(name, self.request, search,
 | 
			
		||||
#     #                                   url=url, **defaults)
 | 
			
		||||
#     #     return grid(query, config)
 | 
			
		||||
 | 
			
		||||
#     # def get_search_form(self, name, labels={}, **defaults):
 | 
			
		||||
#     #     """
 | 
			
		||||
#     #     Convenience function for obtaining the configuration for a search form,
 | 
			
		||||
#     #     and then obtaining the form itself.
 | 
			
		||||
 | 
			
		||||
#     #     ``name`` is essentially the config key, e.g. ``'products.lookup'``.
 | 
			
		||||
#     #     The ``labels`` dictionary can be used to override the default labels
 | 
			
		||||
#     #     displayed for the various search fields.  The ``defaults`` dictionary
 | 
			
		||||
#     #     is used to customize the search config.
 | 
			
		||||
#     #     """
 | 
			
		||||
 | 
			
		||||
#     #     config = util.get_search_config(name, self.request,
 | 
			
		||||
#     #                                     self.filter_map(), **defaults)
 | 
			
		||||
#     #     form = util.get_search_form(config, **labels)
 | 
			
		||||
#     #     return form
 | 
			
		||||
 | 
			
		||||
#     # def object_crud(self, cls, objects=None, post_sync=None):
 | 
			
		||||
#     #     """
 | 
			
		||||
#     #     This method is a desperate attempt to encapsulate shared CRUD logic
 | 
			
		||||
#     #     which is useful across all editable data objects.
 | 
			
		||||
 | 
			
		||||
#     #     ``objects``, if provided, should be the plural name for the class as
 | 
			
		||||
#     #     used in internal naming, e.g. ``'products'``.  A default will be used
 | 
			
		||||
#     #     if you do not provide this value.
 | 
			
		||||
 | 
			
		||||
#     #     ``post_sync``, if provided, should be a callable which accepts a
 | 
			
		||||
#     #     ``formalchemy.Fieldset`` instance as its only argument.  It will be
 | 
			
		||||
#     #     called immediately after the fieldset is synced.
 | 
			
		||||
#     #     """
 | 
			
		||||
 | 
			
		||||
#     #     if not objects:
 | 
			
		||||
#     #         objects = cls.__name__.lower() + 's'
 | 
			
		||||
 | 
			
		||||
#     #     uuid = self.request.params.get('uuid')
 | 
			
		||||
#     #     obj = self.Session.query(cls).get(uuid) if uuid else cls
 | 
			
		||||
#     #     assert obj
 | 
			
		||||
 | 
			
		||||
#     #     fs = self.fieldset(obj)
 | 
			
		||||
 | 
			
		||||
#     #     if not fs.readonly and self.request.params.get('fieldset'):
 | 
			
		||||
#     #         fs.rebind(data=self.request.params)
 | 
			
		||||
#     #         if fs.validate():
 | 
			
		||||
#     #             fs.sync()
 | 
			
		||||
#     #             if post_sync:
 | 
			
		||||
#     #                 res = post_sync(fs)
 | 
			
		||||
#     #                 if isinstance(res, HTTPFound):
 | 
			
		||||
#     #                     return res
 | 
			
		||||
#     #             if self.request.params.get('partial'):
 | 
			
		||||
#     #                 self.Session.flush()
 | 
			
		||||
#     #                 return self.json_success(uuid=fs.model.uuid)
 | 
			
		||||
#     #             return HTTPFound(location=self.request.route_url(objects, action='index'))
 | 
			
		||||
 | 
			
		||||
#     #     data = {'fieldset':fs}
 | 
			
		||||
#     #     if self.request.params.get('partial'):
 | 
			
		||||
#     #         return render_to_response('/%s/crud_partial.mako' % objects,
 | 
			
		||||
#     #                                   data, request=self.request)
 | 
			
		||||
#     #     return data
 | 
			
		||||
 | 
			
		||||
#     # def render_grid(self, grid, search=None, **kwargs):
 | 
			
		||||
#     #     """
 | 
			
		||||
#     #     Convenience function to render a standard grid.  Really just calls
 | 
			
		||||
#     #     :func:`dtail.forms.util.render_grid()`.
 | 
			
		||||
#     #     """
 | 
			
		||||
 | 
			
		||||
#     #     return util.render_grid(self.request, grid, search, **kwargs)
 | 
			
		||||
| 
						 | 
				
			
			@ -26,9 +26,42 @@
 | 
			
		|||
``edbob.util`` -- Utilities
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from pkg_resources import iter_entry_points
 | 
			
		||||
 | 
			
		||||
import edbob
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def entry_point_map(key):
 | 
			
		||||
    """
 | 
			
		||||
    Convenience function to retrieve a dictionary of entry points, keyed by
 | 
			
		||||
    name.
 | 
			
		||||
 | 
			
		||||
    ``key`` must be the "section name" for the entry points you're after, e.g.
 | 
			
		||||
    ``'edbob.commands'``.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    epmap = {}
 | 
			
		||||
    for ep in iter_entry_points(key):
 | 
			
		||||
        epmap[ep.name] = ep.load()
 | 
			
		||||
    return epmap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def prettify(text):
 | 
			
		||||
    """
 | 
			
		||||
    Returns a "prettified" version of ``text``, which is more or less assumed
 | 
			
		||||
    to be a Pythonic representation of an (singular or plural) entity name.  It
 | 
			
		||||
    splits the text into capitalized words, e.g. "purchase_orders" becomes
 | 
			
		||||
    "Purchase Orders".
 | 
			
		||||
 | 
			
		||||
    .. note::
 | 
			
		||||
       No attempt is made to handle pluralization; the spelling of ``text`` is
 | 
			
		||||
       always preserved.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    words = text.replace('_', ' ').split()
 | 
			
		||||
    return ' '.join([x.capitalize() for x in words])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class requires_impl(edbob.Object):
 | 
			
		||||
    """
 | 
			
		||||
    Decorator for properties or methods defined on parent classes only for
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										41
									
								
								setup.py
									
										
									
									
									
								
							
							
						
						
									
										41
									
								
								setup.py
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -37,7 +37,8 @@ from setuptools import setup, find_packages
 | 
			
		|||
 | 
			
		||||
here = os.path.abspath(os.path.dirname(__file__))
 | 
			
		||||
execfile(os.path.join(here, 'edbob', '_version.py'))
 | 
			
		||||
readme = open(os.path.join(here, 'README.txt')).read()
 | 
			
		||||
README = open(os.path.join(here, 'README.txt')).read()
 | 
			
		||||
CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
requires = [
 | 
			
		||||
| 
						 | 
				
			
			@ -67,10 +68,11 @@ requires = [
 | 
			
		|||
    # outside the lines with regard to these soft limits.  If bugs are
 | 
			
		||||
    # encountered then they should be filed as such.
 | 
			
		||||
    #
 | 
			
		||||
    # package                       # low                   high
 | 
			
		||||
    # package                           # low                   high
 | 
			
		||||
 | 
			
		||||
    'progressbar',                  # 2.3
 | 
			
		||||
    'pytz',                         # 2012b
 | 
			
		||||
    'decorator',                        # 3.3.2
 | 
			
		||||
    'progressbar',                      # 2.3
 | 
			
		||||
    'pytz',                             # 2012b
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
if sys.version_info < (2, 7):
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +81,7 @@ if sys.version_info < (2, 7):
 | 
			
		|||
    requires += [
 | 
			
		||||
        #
 | 
			
		||||
        # package                       # low                   high
 | 
			
		||||
        #
 | 
			
		||||
 | 
			
		||||
        'argparse',                     # 1.2.1
 | 
			
		||||
        ]
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +94,7 @@ setup(
 | 
			
		|||
    url = "http://edbob.org/",
 | 
			
		||||
    license = "GNU Affero GPL v3",
 | 
			
		||||
    description = "Pythonic Software Framework",
 | 
			
		||||
    long_description = readme,
 | 
			
		||||
    long_description = README + '\n\n' +  CHANGES,
 | 
			
		||||
 | 
			
		||||
    classifiers = [
 | 
			
		||||
        'Development Status :: 3 - Alpha',
 | 
			
		||||
| 
						 | 
				
			
			@ -117,30 +119,44 @@ setup(
 | 
			
		|||
        'db': [
 | 
			
		||||
            #
 | 
			
		||||
            # package                   # low                   high
 | 
			
		||||
            #
 | 
			
		||||
 | 
			
		||||
            'alembic',                  # 0.2.1
 | 
			
		||||
            'decorator',                # 3.3.2
 | 
			
		||||
            'py-bcrypt',                # 0.2
 | 
			
		||||
            'SQLAlchemy',               # 0.7.6
 | 
			
		||||
            # 'sqlalchemy-migrate',       # 0.7.2
 | 
			
		||||
            'Tempita',                  # 0.5.1
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
        'docs': [
 | 
			
		||||
            #
 | 
			
		||||
            # package                   # low                   high
 | 
			
		||||
            #
 | 
			
		||||
 | 
			
		||||
            'Sphinx',                   # 1.1.3
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
        'pyramid': [
 | 
			
		||||
            #
 | 
			
		||||
            # package                   # low                   high
 | 
			
		||||
            #
 | 
			
		||||
            
 | 
			
		||||
            # Beaker dependency included here because 'pyramid_beaker' uses incorrect
 | 
			
		||||
            # case in its requirement declaration.
 | 
			
		||||
            'Beaker',                   # 1.6.3
 | 
			
		||||
 | 
			
		||||
            # Pyramid 1.3 introduced 'pcreate' command (and friends) to replace
 | 
			
		||||
            # deprecated 'paster create' (and friends).
 | 
			
		||||
            'pyramid>=1.3a1',           #                       1.3b2
 | 
			
		||||
 | 
			
		||||
            'FormAlchemy',              # 1.4.2
 | 
			
		||||
            'FormEncode',               # 1.2.4
 | 
			
		||||
            'Mako',                     # 0.6.2
 | 
			
		||||
            'pyramid_beaker',           # 0.6.1
 | 
			
		||||
            'pyramid_debugtoolbar',     # 1.0
 | 
			
		||||
            'pyramid_simpleform',       # 0.6.1
 | 
			
		||||
            'pyramid_tm',               # 0.3
 | 
			
		||||
            'Tempita',                  # 0.5.1
 | 
			
		||||
            'transaction',              # 1.2.0
 | 
			
		||||
            'waitress',                 # 0.8.1
 | 
			
		||||
            'WebHelpers',               # 1.3
 | 
			
		||||
            'zope.sqlalchemy',          # 0.7
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -164,5 +180,8 @@ db = edbob.commands:DatabaseCommand
 | 
			
		|||
shell = edbob.commands:ShellCommand
 | 
			
		||||
uuid = edbob.commands:UuidCommand
 | 
			
		||||
 | 
			
		||||
[edbob.db.extensions]
 | 
			
		||||
auth = edbob.db.extensions.auth:AuthExtension
 | 
			
		||||
 | 
			
		||||
""",
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue