save point (see note)

Added initial alembic skeleton, improved base pyramid templates, added auth db
extension, etc...
This commit is contained in:
Lance Edgar 2012-04-09 11:18:34 -05:00
parent 727b9a5fa7
commit b1e6b12b71
43 changed files with 2293 additions and 347 deletions

5
CHANGES.txt Normal file
View file

@ -0,0 +1,5 @@
0.1a1
-----
- Initial version

View file

@ -1,4 +1,2 @@
include COPYING.txt
include *.txt
include ez_setup.py
# recursive-include edbob/data *
# recursive-include edbob/db/schema *

View file

@ -37,8 +37,6 @@ The following functions are considered "core" to ``edbob``:
.. autofunction:: basic_logging
.. autofunction:: entry_point_map
.. autofunction:: get_uuid
.. autofunction:: graft

View file

@ -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):

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View 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'

View 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 '')

View file

@ -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
View file

@ -0,0 +1 @@
Generic single-database configuration.

71
edbob/db/schema/env.py Normal file
View 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()

View 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"}

View file

@ -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)

View file

@ -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
View 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)

View file

@ -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 *

View file

@ -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
View 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)())

View file

@ -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()))

View file

@ -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()}

View file

@ -1,4 +1,4 @@
<%inherit file="base.mako" />
<%inherit file="/base.mako" />
<h1>Welcome to {{project}}</h1>

View file

@ -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
####################

View file

@ -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
]

View file

@ -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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

View file

@ -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;

View file

@ -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

View file

@ -0,0 +1,2 @@
<%inherit file="/edbob/crud.mako" />
${parent.body()}

View file

@ -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 %>

View 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>

View 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}

View 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>

View 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>

View 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>

View 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>&nbsp;</th>
## % endfor
% if grid.deletable:
<th>&nbsp;</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">&nbsp;</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:&nbsp;
${grid.pager.pager('~3~', onclick='return grid_navigate_page($(this));')}
</p>
</div>
% endif
</div>

View file

@ -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
View 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)

View file

@ -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

View file

@ -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
""",
)