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
|
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:: basic_logging
|
||||||
|
|
||||||
.. autofunction:: entry_point_map
|
|
||||||
|
|
||||||
.. autofunction:: get_uuid
|
.. autofunction:: get_uuid
|
||||||
|
|
||||||
.. autofunction:: graft
|
.. autofunction:: graft
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
from edbob.util import requires_impl
|
from edbob.util import entry_point_map, requires_impl
|
||||||
|
|
||||||
|
|
||||||
class ArgumentParser(argparse.ArgumentParser):
|
class ArgumentParser(argparse.ArgumentParser):
|
||||||
|
|
@ -75,7 +75,7 @@ See the file COPYING.txt for more information.
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
edbob.Object.__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):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
@ -259,8 +259,74 @@ class DatabaseCommand(Subcommand):
|
||||||
def add_parser_args(self, parser):
|
def add_parser_args(self, parser):
|
||||||
parser.add_argument('-D', '--database', metavar='URL',
|
parser.add_argument('-D', '--database', metavar='URL',
|
||||||
help="Database engine (default is edbob.db.engine)")
|
help="Database engine (default is edbob.db.engine)")
|
||||||
parser.add_argument('command', choices=['upgrade', 'extensions', 'activate', 'deactivate'],
|
# parser.add_argument('command', choices=['upgrade', 'extensions', 'activate', 'deactivate'],
|
||||||
help="Command to execute against database")
|
# 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):
|
def run(self, args):
|
||||||
if args.database:
|
if args.database:
|
||||||
|
|
@ -276,9 +342,7 @@ class DatabaseCommand(Subcommand):
|
||||||
if not engine:
|
if not engine:
|
||||||
print >> sys.stderr, "Database not configured; please change that or specify -D URL"
|
print >> sys.stderr, "Database not configured; please change that or specify -D URL"
|
||||||
return
|
return
|
||||||
|
args.func(engine, args)
|
||||||
if args.command == 'upgrade':
|
|
||||||
print 'got upgrade ..'
|
|
||||||
|
|
||||||
|
|
||||||
# class ExtensionsCommand(RattailCommand):
|
# class ExtensionsCommand(RattailCommand):
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,9 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
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):
|
class Object(object):
|
||||||
|
|
@ -75,21 +74,6 @@ def basic_logging():
|
||||||
logging.getLogger().addHandler(handler)
|
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():
|
def get_uuid():
|
||||||
"""
|
"""
|
||||||
Generates a universally-unique identifier and returns its 32-character hex
|
Generates a universally-unique identifier and returns its 32-character hex
|
||||||
|
|
|
||||||
|
|
@ -26,20 +26,22 @@
|
||||||
``edbob.db`` -- Database Framework
|
``edbob.db`` -- Database Framework
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config, MetaData
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['engines', 'engine', 'Session', 'metadata',
|
# __all__ = ['engines', 'engine', 'Session', 'metadata',
|
||||||
'get_setting', 'save_setting']
|
# 'get_setting', 'save_setting']
|
||||||
|
|
||||||
|
__all__ = ['engines', 'engine', 'Session', 'get_setting', 'save_setting']
|
||||||
|
|
||||||
inited = False
|
inited = False
|
||||||
engines = None
|
engines = None
|
||||||
engine = None
|
engine = None
|
||||||
Session = sessionmaker()
|
Session = sessionmaker()
|
||||||
metadata = None
|
# metadata = None
|
||||||
|
|
||||||
|
|
||||||
def init(config):
|
def init(config):
|
||||||
|
|
@ -65,13 +67,16 @@ def init(config):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import edbob.db
|
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 import enum
|
||||||
from edbob.db.model import get_metadata
|
from edbob.db.model import Base
|
||||||
from edbob.db.mappers import make_mappers
|
# from edbob.db.model import get_metadata
|
||||||
from edbob.db.extensions import extend_framework
|
# 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')
|
keys = config.get('edbob.db', 'sqlalchemy.keys')
|
||||||
if keys:
|
if keys:
|
||||||
|
|
@ -94,13 +99,15 @@ def init(config):
|
||||||
engine = engines.get('default')
|
engine = engines.get('default')
|
||||||
if engine:
|
if engine:
|
||||||
Session.configure(bind=engine)
|
Session.configure(bind=engine)
|
||||||
|
Base.metadata.bind = engine
|
||||||
|
|
||||||
metadata = get_metadata(bind=engine)
|
# metadata = get_metadata(bind=engine)
|
||||||
make_mappers(metadata)
|
# make_mappers(metadata)
|
||||||
extend_framework()
|
# extend_framework()
|
||||||
|
|
||||||
edbob.graft(edbob, edbob.db)
|
edbob.graft(edbob, edbob.db)
|
||||||
edbob.graft(edbob, classes)
|
# edbob.graft(edbob, classes)
|
||||||
|
edbob.graft(edbob, model)
|
||||||
edbob.graft(edbob, enum)
|
edbob.graft(edbob, enum)
|
||||||
inited = True
|
inited = True
|
||||||
|
|
||||||
|
|
@ -139,19 +146,35 @@ def save_setting(name, value, session=None):
|
||||||
session.close()
|
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):
|
def needs_session(func):
|
||||||
"""
|
"""
|
||||||
Decorator which adds helpful session handling.
|
Decorator which adds helpful session handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
session = kwargs.get('session')
|
session = kwargs.pop('session', None)
|
||||||
_session = session
|
_orig_session = session
|
||||||
if not session:
|
if not session:
|
||||||
session = Session()
|
session = Session()
|
||||||
kwargs['session'] = session
|
|
||||||
res = func(session, *args, **kwargs)
|
res = func(session, *args, **kwargs)
|
||||||
if not _session:
|
if not _orig_session:
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
return res
|
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
|
# from pkg_resources import iter_entry_points
|
||||||
|
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
|
from sqlalchemy import MetaData
|
||||||
# from sqlalchemy.orm import clear_mappers
|
# from sqlalchemy.orm import clear_mappers
|
||||||
|
|
||||||
import migrate.versioning.api
|
import migrate.versioning.api
|
||||||
|
|
@ -47,20 +48,19 @@ import edbob
|
||||||
import edbob.db
|
import edbob.db
|
||||||
from edbob.db import exceptions
|
from edbob.db import exceptions
|
||||||
from edbob.db import Session
|
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 (
|
from edbob.db.util import (
|
||||||
get_database_version,
|
get_database_version,
|
||||||
get_repository_path,
|
get_repository_path,
|
||||||
get_repository_version,
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
available_extensions = edbob.entry_point_map('edbob.db.extensions')
|
|
||||||
active_extensions = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Extension(edbob.Object):
|
class Extension(edbob.Object):
|
||||||
"""
|
"""
|
||||||
|
|
@ -71,22 +71,27 @@ class Extension(edbob.Object):
|
||||||
# derived class.
|
# derived class.
|
||||||
required_extensions = []
|
required_extensions = []
|
||||||
|
|
||||||
@property
|
# You can set this to any dotted module path you like. If unset a default
|
||||||
@requires_impl(is_property=True)
|
# will be assumed, of the form ``<path.to.extension>.model`` (see
|
||||||
def name(self):
|
# :meth:`Extension.get_models_module()` for more info).
|
||||||
"""
|
model_module = ''
|
||||||
The name of the extension.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
@requires_impl(is_property=True)
|
# @requires_impl(is_property=True)
|
||||||
def schema(self):
|
# def name(self):
|
||||||
"""
|
# """
|
||||||
Should return a reference to the extension's ``schema`` module, which
|
# The name of the extension.
|
||||||
is assumed to be a SQLAlchemy-Migrate repository.
|
# """
|
||||||
"""
|
# pass
|
||||||
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):
|
def add_class(self, cls):
|
||||||
"""
|
"""
|
||||||
|
|
@ -119,13 +124,54 @@ class Extension(edbob.Object):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_metadata(self):
|
def get_metadata(self, recurse=False):
|
||||||
"""
|
"""
|
||||||
Should return a :class:`sqlalchemy.MetaData` instance containing the
|
Returns a :class:`sqlalchemy.MetaData` instance containing the schema
|
||||||
schema definition for the extension, or ``None``.
|
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):
|
def remove_class(self, name):
|
||||||
"""
|
"""
|
||||||
|
|
@ -164,54 +210,81 @@ def activate_extension(extension, engine=None):
|
||||||
if not isinstance(extension, Extension):
|
if not isinstance(extension, Extension):
|
||||||
extension = get_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)
|
install_extension_schema(extension, engine)
|
||||||
|
|
||||||
|
# Add ActiveExtension record for this extension.
|
||||||
session = Session(bind=engine)
|
session = Session(bind=engine)
|
||||||
if not session.query(ActiveExtension).get(extension.name):
|
if not session.query(ActiveExtension).get(extension.name):
|
||||||
session.add(ActiveExtension(name=extension.name))
|
session.add(ActiveExtension(name=extension.name))
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
merge_extension_metadata(extension)
|
# merge_extension_metadata(extension)
|
||||||
extension.extend_classes()
|
# extension.extend_classes()
|
||||||
extension.extend_mappers(rattail.metadata)
|
# extension.extend_mappers(Base.metadata)
|
||||||
active_extensions[extension.name] = extension
|
|
||||||
|
# Add extension to in-memory active extensions tracker.
|
||||||
|
active_extensions(engine).append(extension.name)
|
||||||
|
|
||||||
|
|
||||||
# def deactivate_extension(extension, engine=None):
|
_available_extensions = None
|
||||||
# """
|
def available_extensions():
|
||||||
# Uninstalls an extension's schema from the primary database, and immediately
|
"""
|
||||||
# requests it to restore the ORM API.
|
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:
|
if _available_extensions is None:
|
||||||
# engine = rattail.engine
|
_available_extensions = entry_point_map('edbob.db.extensions')
|
||||||
|
return _available_extensions
|
||||||
|
|
||||||
# if not isinstance(extension, RattailExtension):
|
|
||||||
# extension = get_extension(extension)
|
|
||||||
|
|
||||||
# log.info("Deactivating extension: %s" % extension.name)
|
def deactivate_extension(extension, engine=None):
|
||||||
# if extension.name in _active_extensions:
|
"""
|
||||||
# del _active_extensions[extension.name]
|
Uninstalls an extension's schema from a database.
|
||||||
|
|
||||||
# session = Session()
|
If ``engine`` is not provided, :attr:`edbob.db.engine` is assumed.
|
||||||
# ext = session.query(ActiveExtension).get(extension.name)
|
"""
|
||||||
# if ext:
|
|
||||||
# session.delete(ext)
|
|
||||||
# session.commit()
|
|
||||||
# session.close()
|
|
||||||
|
|
||||||
# uninstall_extension_schema(extension, engine)
|
if engine is None:
|
||||||
# unmerge_extension_metadata(extension)
|
engine = edbob.db.engine
|
||||||
# extension.restore_classes()
|
|
||||||
|
|
||||||
# clear_mappers()
|
if not isinstance(extension, Extension):
|
||||||
# make_mappers(rattail.metadata)
|
extension = get_extension(extension)
|
||||||
# for name in sorted(_active_extensions, extension_sorter(_active_extensions)):
|
|
||||||
# _active_extensions[name].extend_mappers(rattail.metadata)
|
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():
|
def extend_framework():
|
||||||
|
|
@ -232,11 +305,11 @@ def extend_framework():
|
||||||
except sqlalchemy.exc.OperationalError:
|
except sqlalchemy.exc.OperationalError:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check database version to see if core schema is installed.
|
# # Check database version to see if core schema is installed.
|
||||||
try:
|
# try:
|
||||||
db_version = get_database_version(engine)
|
# db_version = get_database_version(engine)
|
||||||
except exceptions.CoreSchemaNotInstalled:
|
# except exceptions.CoreSchemaNotInstalled:
|
||||||
return
|
# return
|
||||||
|
|
||||||
# Since extensions may depend on one another, we must first retrieve the
|
# Since extensions may depend on one another, we must first retrieve the
|
||||||
# list of active extensions' names from the database and *then* sort them
|
# list of active extensions' names from the database and *then* sort them
|
||||||
|
|
@ -264,15 +337,20 @@ def extend_framework():
|
||||||
active_extensions[name] = ext
|
active_extensions[name] = ext
|
||||||
|
|
||||||
|
|
||||||
# def extension_active(extension):
|
def extension_active(extension, engine=None):
|
||||||
# """
|
"""
|
||||||
# Returns boolean indicating whether or not the given ``extension`` is active
|
Returns boolean indicating whether or not the given ``extension`` is active
|
||||||
# within the current database.
|
within a database.
|
||||||
# """
|
|
||||||
|
|
||||||
# if not isinstance(extension, RattailExtension):
|
If ``engine`` is not provided, :attr:`edbob.db.engine` is assumed.
|
||||||
# extension = get_extension(extension)
|
"""
|
||||||
# return extension.name in _active_extensions
|
|
||||||
|
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):
|
def extension_sorter(extensions):
|
||||||
|
|
@ -306,8 +384,9 @@ def get_extension(name):
|
||||||
raised if the extension cannot be found.
|
raised if the extension cannot be found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if name in available_extensions:
|
extensions = available_extensions()
|
||||||
return available_extensions[name]()
|
if name in extensions:
|
||||||
|
return extensions[name]()
|
||||||
raise exceptions.ExtensionNotFound(name)
|
raise exceptions.ExtensionNotFound(name)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -322,23 +401,38 @@ def install_extension_schema(extension, engine=None):
|
||||||
if engine is None:
|
if engine is None:
|
||||||
engine = edbob.db.engine
|
engine = edbob.db.engine
|
||||||
|
|
||||||
# Extensions aren't required to provide metadata...
|
# # Extensions aren't required to provide metadata...
|
||||||
ext_meta = extension.get_metadata()
|
# ext_meta = extension.get_metadata()
|
||||||
if not ext_meta:
|
# if not ext_meta:
|
||||||
return
|
# return
|
||||||
|
|
||||||
# ...but if they do they must also provide a SQLAlchemy-Migrate repository.
|
# # ...but if they do they must also provide a SQLAlchemy-Migrate repository.
|
||||||
assert extension.schema, "Extension does not implement 'schema': %s" % extension.name
|
# assert extension.schema, "Extension does not implement 'schema': %s" % extension.name
|
||||||
|
|
||||||
meta = edbob.db.metadata
|
# meta = edbob.db.metadata
|
||||||
for table in meta.sorted_tables:
|
# for table in meta.sorted_tables:
|
||||||
table.tometadata(ext_meta)
|
# table.tometadata(ext_meta)
|
||||||
|
# for table in ext_meta.sorted_tables:
|
||||||
|
# if table.name not in meta.tables:
|
||||||
|
# table.create(bind=engine, checkfirst=True)
|
||||||
|
|
||||||
|
# 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:
|
for table in ext_meta.sorted_tables:
|
||||||
if table.name not in meta.tables:
|
table.tometadata(core_meta)
|
||||||
table.create(bind=engine, checkfirst=True)
|
core_meta.create_all(engine)
|
||||||
|
|
||||||
migrate.versioning.api.version_control(
|
# migrate.versioning.api.version_control(
|
||||||
str(engine.url), get_repository_path(extension), get_repository_version(extension))
|
# str(engine.url), get_repository_path(extension), get_repository_version(extension))
|
||||||
|
|
||||||
|
|
||||||
def merge_extension_metadata(ext):
|
def merge_extension_metadata(ext):
|
||||||
|
|
@ -353,7 +447,7 @@ def merge_extension_metadata(ext):
|
||||||
ext_meta = ext.get_metadata()
|
ext_meta = ext.get_metadata()
|
||||||
if not ext_meta:
|
if not ext_meta:
|
||||||
return
|
return
|
||||||
meta = edbob.db.metadata
|
meta = Base.metadata
|
||||||
for table in meta.sorted_tables:
|
for table in meta.sorted_tables:
|
||||||
table.tometadata(ext_meta)
|
table.tometadata(ext_meta)
|
||||||
for table in ext_meta.sorted_tables:
|
for table in ext_meta.sorted_tables:
|
||||||
|
|
@ -361,30 +455,53 @@ def merge_extension_metadata(ext):
|
||||||
table.tometadata(meta)
|
table.tometadata(meta)
|
||||||
|
|
||||||
|
|
||||||
# def uninstall_extension_schema(extension, engine=None):
|
def uninstall_extension_schema(extension, engine=None):
|
||||||
# """
|
"""
|
||||||
# Uninstalls an extension's tables from the database represented by
|
Uninstalls an extension's tables from the database represented by
|
||||||
# ``engine`` (or ``rattail.engine`` if none is provided), and removes
|
``engine`` (or :attr:`edbob.db.engine` if none is provided), and removes
|
||||||
# SQLAlchemy-Migrate version control for the extension.
|
SQLAlchemy-Migrate version control for the extension.
|
||||||
# """
|
"""
|
||||||
|
|
||||||
# if engine is None:
|
if engine is None:
|
||||||
# engine = rattail.engine
|
engine = edbob.db.engine
|
||||||
|
|
||||||
# ext_meta = extension.get_metadata()
|
# ext_meta = extension.get_metadata()
|
||||||
# if not ext_meta:
|
# if not ext_meta:
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# schema = ControlledSchema(engine, get_repository_path(extension))
|
# schema = ControlledSchema(engine, get_repository_path(extension))
|
||||||
# engine.execute(schema.table.delete().where(
|
# engine.execute(schema.table.delete().where(
|
||||||
# schema.table.c.repository_id == schema.repository.id))
|
# schema.table.c.repository_id == schema.repository.id))
|
||||||
|
|
||||||
# meta = get_metadata()
|
# meta = get_metadata()
|
||||||
# for table in meta.sorted_tables:
|
# for table in meta.sorted_tables:
|
||||||
# table.tometadata(ext_meta)
|
# table.tometadata(ext_meta)
|
||||||
# for table in reversed(ext_meta.sorted_tables):
|
# for table in reversed(ext_meta.sorted_tables):
|
||||||
# if table.name not in meta.tables:
|
# if table.name not in meta.tables:
|
||||||
# table.drop(bind=engine)
|
# 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):
|
# def unmerge_extension_metadata(extension):
|
||||||
|
|
@ -433,3 +550,27 @@ def merge_extension_metadata(ext):
|
||||||
# # # Extensions may override permission display names.
|
# # # Extensions may override permission display names.
|
||||||
# # if ext_perms[perm_name][1]:
|
# # if ext_perms[perm_name][1]:
|
||||||
# # perms[perm_name][1] = 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
|
``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.
|
Convenience function which returns a ``uuid`` column for use as a table's
|
||||||
|
primary key.
|
||||||
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)
|
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):
|
class ActiveExtension(Base):
|
||||||
first_name = context.current_parameters['first_name']
|
"""
|
||||||
last_name = context.current_parameters['last_name']
|
Represents an extension which has been activated within a database.
|
||||||
if not (first_name or last_name):
|
"""
|
||||||
return None
|
|
||||||
return '%(first_name)s %(last_name)s' % locals()
|
|
||||||
|
|
||||||
people = table_with_uuid(
|
__tablename__ = 'active_extensions'
|
||||||
'people', metadata,
|
|
||||||
Column('first_name', String(50)),
|
|
||||||
Column('last_name', String(50)),
|
|
||||||
Column('display_name', String(100), default=get_person_display_name),
|
|
||||||
)
|
|
||||||
|
|
||||||
permissions = Table(
|
name = Column(String(50), primary_key=True)
|
||||||
'permissions', metadata,
|
|
||||||
Column('role_uuid', String(32), ForeignKey('roles.uuid'), primary_key=True),
|
|
||||||
Column('permission', String(50), primary_key=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
roles = table_with_uuid(
|
def __repr__(self):
|
||||||
'roles', metadata,
|
return "<ActiveExtension: %s>" % self.name
|
||||||
Column('name', String(25), nullable=False, unique=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
settings = Table(
|
def __str__(self):
|
||||||
'settings', metadata,
|
return str(self.name or '')
|
||||||
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(
|
class Setting(Base):
|
||||||
'users_roles', metadata,
|
"""
|
||||||
Column('user_uuid', String(32), ForeignKey('users.uuid')),
|
Represents a setting stored within the database.
|
||||||
Column('role_uuid', String(32), ForeignKey('roles.uuid')),
|
"""
|
||||||
)
|
|
||||||
|
|
||||||
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
|
import edbob.db
|
||||||
from edbob.db import exceptions
|
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):
|
# def core_schema_installed(engine=None):
|
||||||
|
|
@ -121,25 +122,27 @@ def install_core_schema(engine=None):
|
||||||
if not engine:
|
if not engine:
|
||||||
engine = edbob.db.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 = engine.connect()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# Check DB version to see if core schema is already installed.
|
# # Check DB version to see if core schema is already installed.
|
||||||
try:
|
# try:
|
||||||
db_version = get_database_version(engine)
|
# db_version = get_database_version(engine)
|
||||||
except exceptions.CoreSchemaNotInstalled:
|
# except exceptions.CoreSchemaNotInstalled:
|
||||||
pass
|
# pass
|
||||||
else:
|
# else:
|
||||||
raise exceptions.CoreSchemaAlreadyInstalled(db_version)
|
# raise exceptions.CoreSchemaAlreadyInstalled(db_version)
|
||||||
|
|
||||||
# Create tables for core schema.
|
# Create tables for core schema.
|
||||||
metadata = get_metadata()
|
# metadata = get_metadata()
|
||||||
metadata.create_all(bind=engine)
|
# Base.metadata.create_all(engine)
|
||||||
|
meta = edbob.db.get_core_metadata()
|
||||||
|
meta.create_all(engine)
|
||||||
|
|
||||||
# Add versioning for core schema.
|
# # Add versioning for core schema.
|
||||||
migrate.versioning.api.version_control(
|
# migrate.versioning.api.version_control(
|
||||||
str(engine.url), get_repository_path(), get_repository_version())
|
# str(engine.url), get_repository_path(), get_repository_version())
|
||||||
|
|
||||||
# WTF
|
# WTF
|
||||||
# session = Session(bind=engine)
|
# session = Session(bind=engine)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,17 @@
|
||||||
``edbob.pyramid`` -- Pyramid Framework
|
``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):
|
def includeme(config):
|
||||||
config.include('edbob.pyramid.static')
|
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
|
``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="global_title()">{{project}}</%def>
|
||||||
<%def name="footer()">
|
<%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__}
|
${h.link_to("edbob", 'http://edbob.org/', target='_blank')} v${edbob.__version__}
|
||||||
</%def>
|
</%def>
|
||||||
${parent.body()}
|
${parent.body()}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<%inherit file="base.mako" />
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
<h1>Welcome to {{project}}</h1>
|
<h1>Welcome to {{project}}</h1>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ whatever = you like
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Pyramid
|
# pyramid
|
||||||
####################
|
####################
|
||||||
|
|
||||||
[app:main]
|
[app:main]
|
||||||
|
|
@ -36,6 +36,20 @@ host = 0.0.0.0
|
||||||
port = 6543
|
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
|
# edbob
|
||||||
####################
|
####################
|
||||||
|
|
|
||||||
|
|
@ -39,23 +39,7 @@ requires = [
|
||||||
#
|
#
|
||||||
# package # low high
|
# package # low high
|
||||||
|
|
||||||
# Beaker dependency included here because 'pyramid_beaker' uses incorrect
|
'edbob[db,pyramid]', # 0.1a1.dev
|
||||||
# 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
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ li {
|
||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.wrapper {
|
||||||
|
/* border: 1px solid black; */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
float: left;
|
float: left;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -34,6 +43,15 @@ li {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.right {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.wrapper td.right {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/******************************
|
/******************************
|
||||||
* Main Layout
|
* Main Layout
|
||||||
******************************/
|
******************************/
|
||||||
|
|
@ -110,7 +128,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 10px;
|
margin: 20px auto 10px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
@ -166,6 +184,10 @@ div.dialog {
|
||||||
* Filters
|
* Filters
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
|
div.filterset {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
div.filters {
|
div.filters {
|
||||||
/* margin-bottom: 10px; */
|
/* margin-bottom: 10px; */
|
||||||
}
|
}
|
||||||
|
|
@ -214,23 +236,24 @@ table.search-wrapper td.grid-mgmt {
|
||||||
* Grids
|
* Grids
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
a.add-object {
|
/* a.add-object { */
|
||||||
display: block;
|
/* display: block; */
|
||||||
float: right;
|
/* float: right; */
|
||||||
}
|
/* } */
|
||||||
|
|
||||||
ul.grid-menu {
|
/* ul.grid-menu { */
|
||||||
display: block;
|
/* display: block; */
|
||||||
float: right;
|
/* float: right; */
|
||||||
list-style-type: none;
|
/* list-style-type: none; */
|
||||||
margin-bottom: 5px;
|
/* margin-bottom: 5px; */
|
||||||
}
|
/* } */
|
||||||
|
|
||||||
div.grid {
|
div.grid {
|
||||||
clear: both;
|
clear: both;
|
||||||
|
/* margin-top: 8px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid {
|
div.grid table {
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid black;
|
||||||
border-left: 1px solid black;
|
border-left: 1px solid black;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
@ -239,47 +262,47 @@ table.grid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid th,
|
div.grid table th,
|
||||||
table.grid td {
|
div.grid table td {
|
||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
padding: 2px 3px;
|
padding: 2px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid th.sortable a {
|
div.grid table th.sortable a {
|
||||||
display: block;
|
display: block;
|
||||||
padding-right: 18px;
|
padding-right: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid th.sorted {
|
div.grid table th.sorted {
|
||||||
background-position: right center;
|
background-position: right center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid th.sorted.asc {
|
div.grid table th.sorted.asc {
|
||||||
background-image: url(../img/sort_arrow_up.png);
|
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);
|
background-image: url(../img/sort_arrow_down.png);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid tr.even {
|
div.grid table tr.even {
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid thead th.checkbox,
|
div.grid table thead th.checkbox,
|
||||||
table.grid tbody td.checkbox {
|
div.grid table tbody td.checkbox {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid td.action {
|
div.grid table td.action {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid td.delete {
|
div.grid table td.delete {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
background-image: url(../img/delete.png);
|
background-image: url(../img/delete.png);
|
||||||
|
|
@ -288,25 +311,25 @@ table.grid td.delete {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid tbody tr.hovering {
|
div.grid table tbody tr.hovering {
|
||||||
background-color: #bbbbbb;
|
background-color: #bbbbbb;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid.hoverable tbody tr {
|
div.grid table.hoverable tbody tr {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid.clickable tbody tr {
|
div.grid.clickable table tbody tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid.selectable tbody tr,
|
div.grid table.selectable tbody tr,
|
||||||
table.grid.checkable tbody tr {
|
div.grid table.checkable tbody tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.grid.selectable tbody tr.selected,
|
div.grid table.selectable tbody tr.selected,
|
||||||
table.grid.checkable tbody tr.selected {
|
div.grid table.checkable tbody tr.selected {
|
||||||
background-color: #666666;
|
background-color: #666666;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +384,7 @@ div.field-couple div.field {
|
||||||
|
|
||||||
div.field-couple div.field input[type=text],
|
div.field-couple div.field input[type=text],
|
||||||
div.field-couple div.field select {
|
div.field-couple div.field select {
|
||||||
width: 180px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.checkbox {
|
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;
|
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 div = $(this).parents('div.grid:first');
|
||||||
var th = $(this).parents('th:first');
|
var th = $(this).parents('th:first');
|
||||||
var dir = 'asc';
|
var dir = 'asc';
|
||||||
|
|
@ -185,43 +185,44 @@ $(function() {
|
||||||
var url = div.attr('url');
|
var url = div.attr('url');
|
||||||
url += url.match(/\?/) ? '&' : '?';
|
url += url.match(/\?/) ? '&' : '?';
|
||||||
url += 'sort=' + th.attr('field') + '&dir=' + dir;
|
url += 'sort=' + th.attr('field') + '&dir=' + dir;
|
||||||
|
url += '&partial=true';
|
||||||
div.load(url);
|
div.load(url);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.hoverable tbody tr').live('mouseenter', function() {
|
$('div.grid.hoverable table tbody tr').live('mouseenter', function() {
|
||||||
$(this).addClass('hovering');
|
$(this).addClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.hoverable tbody tr').live('mouseleave', function() {
|
$('div.grid.hoverable table tbody tr').live('mouseleave', function() {
|
||||||
$(this).removeClass('hovering');
|
$(this).removeClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.clickable tbody tr').live('mouseenter', function() {
|
$('div.grid.clickable table tbody tr').live('mouseenter', function() {
|
||||||
$(this).addClass('hovering');
|
$(this).addClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.clickable tbody tr').live('mouseleave', function() {
|
$('div.grid.clickable table tbody tr').live('mouseleave', function() {
|
||||||
$(this).removeClass('hovering');
|
$(this).removeClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.selectable tbody tr').live('mouseenter', function() {
|
$('div.grid.selectable table tbody tr').live('mouseenter', function() {
|
||||||
$(this).addClass('hovering');
|
$(this).addClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.selectable tbody tr').live('mouseleave', function() {
|
$('div.grid.selectable table tbody tr').live('mouseleave', function() {
|
||||||
$(this).removeClass('hovering');
|
$(this).removeClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.checkable tbody tr').live('mouseenter', function() {
|
$('div.grid.checkable table tbody tr').live('mouseenter', function() {
|
||||||
$(this).addClass('hovering');
|
$(this).addClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('table.grid.checkable tbody tr').live('mouseleave', function() {
|
$('div.grid.checkable table tbody tr').live('mouseleave', function() {
|
||||||
$(this).removeClass('hovering');
|
$(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');
|
var div = $(this).parents('div.grid:first');
|
||||||
if (div.attr('usedlg') == 'True') {
|
if (div.attr('usedlg') == 'True') {
|
||||||
var dlg = get_dialog('grid-object');
|
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 checked = $(this).is(':checked');
|
||||||
var table = $(this).parents('table.grid:first');
|
var table = $(this).parents('table:first');
|
||||||
table.find('tbody tr').each(function() {
|
table.find('tbody tr').each(function() {
|
||||||
$(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
|
$(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
|
||||||
if (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');
|
var table = $(this).parents('table:first');
|
||||||
if (! table.hasClass('multiple')) {
|
if (! table.hasClass('multiple')) {
|
||||||
table.find('tbody tr').removeClass('selected');
|
table.find('tbody tr').removeClass('selected');
|
||||||
|
|
@ -261,12 +262,22 @@ $(function() {
|
||||||
$(this).addClass('selected');
|
$(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]');
|
var checkbox = $(this).find('td:first input[type=checkbox]');
|
||||||
checkbox.attr('checked', !checkbox.is(':checked'));
|
checkbox.attr('checked', !checkbox.is(':checked'));
|
||||||
$(this).toggleClass('selected');
|
$(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() {
|
$('#grid-page-count').live('change', function() {
|
||||||
var div = $(this).parents('div.grid:first');
|
var div = $(this).parents('div.grid:first');
|
||||||
loading(div);
|
loading(div);
|
||||||
|
|
@ -320,7 +331,7 @@ $(function() {
|
||||||
|
|
||||||
$('div.dialog.lookup button.ok').live('click', function() {
|
$('div.dialog.lookup button.ok').live('click', function() {
|
||||||
var dialog = $(this).parents('div.dialog.lookup:first');
|
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) {
|
if (! tr.length) {
|
||||||
alert("You haven't selected anything.");
|
alert("You haven't selected anything.");
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
``edbob.pyramid.subscribers`` -- Subscribers
|
``edbob.pyramid.subscribers`` -- Subscribers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from pyramid import threadlocal
|
||||||
from pyramid.security import authenticated_userid
|
from pyramid.security import authenticated_userid
|
||||||
# from sqlahelper import get_session
|
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
from edbob.db.auth import has_permission
|
from edbob.db.auth import has_permission
|
||||||
|
|
@ -43,9 +43,11 @@ def before_render(event):
|
||||||
* ``edbob``
|
* ``edbob``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
request = event.get('request') or threadlocal.get_current_request()
|
||||||
|
|
||||||
renderer_globals = event
|
renderer_globals = event
|
||||||
renderer_globals['h'] = helpers
|
renderer_globals['h'] = helpers
|
||||||
renderer_globals['url'] = event['request'].route_url
|
renderer_globals['url'] = request.route_url
|
||||||
renderer_globals['edbob'] = edbob
|
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="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="head_tags()"></%def>
|
||||||
|
<%def name="home_link()"><h1 class="right">${h.link_to("Home", url('home'))}</h1></%def>
|
||||||
<%def name="footer()">
|
<%def name="footer()">
|
||||||
powered by ${h.link_to('edbob', 'http://edbob.org', target='_blank')} v${edbob.__version__}
|
powered by ${h.link_to('edbob', 'http://edbob.org', target='_blank')} v${edbob.__version__}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
@ -10,14 +11,14 @@
|
||||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||||
<title>${self.global_title()}${' : ' + capture(self.title) if capture(self.title) else ''}</title>
|
<title>${self.global_title()}${' : ' + capture(self.title) if capture(self.title) else ''}</title>
|
||||||
|
|
||||||
${h.javascript_link('edbob/js/jquery.js')}
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
|
||||||
${h.javascript_link('edbob/js/jquery.ui.js')}
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.ui.js'))}
|
||||||
${h.javascript_link('edbob/js/jquery.loading.js')}
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.loading.js'))}
|
||||||
${h.javascript_link('edbob/js/jquery.autocomplete.js')}
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.autocomplete.js'))}
|
||||||
${h.javascript_link('edbob/js/edbob.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(request.static_url('edbob.pyramid:static/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/edbob.css'))}
|
||||||
|
|
||||||
${self.head_tags()}
|
${self.head_tags()}
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<h1 class="right">${h.link_to("Home", url('home'))}</h1>
|
${self.home_link()}
|
||||||
<h1 class="left">${self.title()}</h1>
|
<h1 class="left">${self.title()}</h1>
|
||||||
<div id="login" class="left">
|
<div id="login" class="left">
|
||||||
## <% user = request.current_user %>
|
## <% 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)
|
_here = os.path.join(os.path.dirname(__file__), os.pardir)
|
||||||
|
|
||||||
|
|
||||||
_favicon = open(os.path.join(_here, 'static', 'favicon.ico'), 'rb').read()
|
# _favicon = open(os.path.join(_here, 'static', 'favicon.ico'), 'rb').read()
|
||||||
_favicon_response = Response(content_type='image/x-icon', body=_favicon)
|
# _favicon_response = Response(content_type='image/x-icon', body=_favicon)
|
||||||
|
|
||||||
@view_config(route_name='favicon.ico')
|
# @view_config(route_name='favicon.ico')
|
||||||
def favicon_ico(context, request):
|
# def favicon_ico(context, request):
|
||||||
return _favicon_response
|
# return _favicon_response
|
||||||
|
|
||||||
|
|
||||||
@view_config(route_name='home', renderer='home.mako')
|
# @view_config(route_name='home', renderer='/home.mako')
|
||||||
def home(context, request):
|
# def home(context, request):
|
||||||
return {}
|
# return {}
|
||||||
|
|
||||||
|
|
||||||
@view_config(route_name='login', renderer='login.mako')
|
@view_config(route_name='login', renderer='login.mako')
|
||||||
|
|
@ -80,17 +80,17 @@ def login(context, request):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
_robots = open(os.path.join(_here, 'static', 'robots.txt')).read()
|
# _robots = open(os.path.join(_here, 'static', 'robots.txt')).read()
|
||||||
_robots_response = Response(content_type='text/plain', body=_robots)
|
# _robots_response = Response(content_type='text/plain', body=_robots)
|
||||||
|
|
||||||
@view_config(route_name='robots.txt')
|
# @view_config(route_name='robots.txt')
|
||||||
def robots_txt(context, request):
|
# def robots_txt(context, request):
|
||||||
return _robots_response
|
# return _robots_response
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.add_route('home', '/')
|
# config.add_route('home', '/')
|
||||||
config.add_route('favicon.ico', '/favicon.ico')
|
# config.add_route('favicon.ico', '/favicon.ico')
|
||||||
config.add_route('robots.txt', '/robots.txt')
|
# config.add_route('robots.txt', '/robots.txt')
|
||||||
config.add_route('login', '/login')
|
config.add_route('login', '/login')
|
||||||
config.scan()
|
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
|
``edbob.util`` -- Utilities
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from pkg_resources import iter_entry_points
|
||||||
|
|
||||||
import edbob
|
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):
|
class requires_impl(edbob.Object):
|
||||||
"""
|
"""
|
||||||
Decorator for properties or methods defined on parent classes only for
|
Decorator for properties or methods defined on parent classes only for
|
||||||
|
|
|
||||||
35
setup.py
35
setup.py
|
|
@ -37,7 +37,8 @@ from setuptools import setup, find_packages
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
execfile(os.path.join(here, 'edbob', '_version.py'))
|
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 = [
|
requires = [
|
||||||
|
|
@ -69,6 +70,7 @@ requires = [
|
||||||
#
|
#
|
||||||
# package # low high
|
# package # low high
|
||||||
|
|
||||||
|
'decorator', # 3.3.2
|
||||||
'progressbar', # 2.3
|
'progressbar', # 2.3
|
||||||
'pytz', # 2012b
|
'pytz', # 2012b
|
||||||
]
|
]
|
||||||
|
|
@ -79,7 +81,7 @@ if sys.version_info < (2, 7):
|
||||||
requires += [
|
requires += [
|
||||||
#
|
#
|
||||||
# package # low high
|
# package # low high
|
||||||
#
|
|
||||||
'argparse', # 1.2.1
|
'argparse', # 1.2.1
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -92,7 +94,7 @@ setup(
|
||||||
url = "http://edbob.org/",
|
url = "http://edbob.org/",
|
||||||
license = "GNU Affero GPL v3",
|
license = "GNU Affero GPL v3",
|
||||||
description = "Pythonic Software Framework",
|
description = "Pythonic Software Framework",
|
||||||
long_description = readme,
|
long_description = README + '\n\n' + CHANGES,
|
||||||
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 3 - Alpha',
|
||||||
|
|
@ -117,30 +119,44 @@ setup(
|
||||||
'db': [
|
'db': [
|
||||||
#
|
#
|
||||||
# package # low high
|
# package # low high
|
||||||
#
|
|
||||||
'alembic', # 0.2.1
|
'alembic', # 0.2.1
|
||||||
'decorator', # 3.3.2
|
|
||||||
'py-bcrypt', # 0.2
|
'py-bcrypt', # 0.2
|
||||||
'SQLAlchemy', # 0.7.6
|
'SQLAlchemy', # 0.7.6
|
||||||
# 'sqlalchemy-migrate', # 0.7.2
|
|
||||||
'Tempita', # 0.5.1
|
'Tempita', # 0.5.1
|
||||||
],
|
],
|
||||||
|
|
||||||
'docs': [
|
'docs': [
|
||||||
#
|
#
|
||||||
# package # low high
|
# package # low high
|
||||||
#
|
|
||||||
'Sphinx', # 1.1.3
|
'Sphinx', # 1.1.3
|
||||||
],
|
],
|
||||||
|
|
||||||
'pyramid': [
|
'pyramid': [
|
||||||
#
|
#
|
||||||
# package # low high
|
# 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
|
# Pyramid 1.3 introduced 'pcreate' command (and friends) to replace
|
||||||
# deprecated 'paster create' (and friends).
|
# deprecated 'paster create' (and friends).
|
||||||
'pyramid>=1.3a1', # 1.3b2
|
'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
|
shell = edbob.commands:ShellCommand
|
||||||
uuid = edbob.commands:UuidCommand
|
uuid = edbob.commands:UuidCommand
|
||||||
|
|
||||||
|
[edbob.db.extensions]
|
||||||
|
auth = edbob.db.extensions.auth:AuthExtension
|
||||||
|
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue