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