Compare commits

..

No commits in common. "master" and "v0.1a13" have entirely different histories.

79 changed files with 1214 additions and 1946 deletions

1
.gitignore vendored
View file

@ -1 +0,0 @@
edbob.egg-info

View file

@ -1,194 +1,4 @@
0.1.2
-----
* Allow config file to prevent logging configuration from happening.
0.1.1
-----
* Some random things required in production at MaMa Jean's...
Specifically this is known to replace occurrences of e.g. ``edbob.User`` with
a more standard (properly imported) reference to ``User``.
0.1a29
------
* Removed ``setup.cfg`` file.
* Changed some logging instances from ``INFO`` to ``DEBUG``.
* Updated ``repr()`` output for model classes.
0.1a28
------
- [bug] Secured daemon PID files. They are no longer readable or writeable
(whoops) by anyone other than the user who owns the process.
- [feature] Added ``--progress`` argument to command system.
- [general] Added initial Fabric script.
0.1a27
------
- [feature] Overhauled file monitors. Added the "process existing" (defaults
to true) and "stop on error" (defaults to false) features to both Linux and
Win32 file monitors. Both features may be overridden in config. The Linux
file monitor was rewritten as an ``edbob.daemon.Daemon`` class.
0.1a26
------
- [feature] Added ``StrippingFieldRenderer``.
- [general] Some style tweaks.
- [bug] Fixed ``graft()`` function when called with module reference.
0.1a25
------
- [feature] Added "sqlerror" tween which, in combination with ``pyramid_tm``,
can allow Pyramid apps to gracefully handle database server restarts. Any
transaction which fails due to a disconnection from the server may be
attempted more than once. The assumption is that the second attempt will
succeed, since the underlying connection pool will have been invalidated upon
the first failure.
- [feature] Added ``PHONE_TYPE`` enumeration.
- [general] Added some ``__unicode__()`` methods to database models.
- [bug] Fixed CSS in the ``progress.mako`` template.
- [feature] Improved the ``load_spec()`` function so that it raises an
``InvalidSpec`` exception if the spec doesn't contain exactly one colon.
0.1a24
------
- [bug] Fixed bug where creating a new ``Role`` with the UI would use default
permissions of the Guest role. Now the default permissions are empty.
- [bug] Fixed ``User.roles`` UI so that the Guest role is never shown.
0.1a23
------
- [feature] Added ``capture_output()`` function to ``win32`` module. This is a
convenience function which works around an issue when attempting to capture
output from a command when the calling application is a Windows GUI app which
was launched via the Windows console (DOS terminal).
- [feature] Updated ``User`` and ``Role`` management views for Pyramid apps.
0.1a22
------
- [feature] Added a view which allows a user to change his/her password from
within Pyramid web apps.
- [general] Tweaked styles for form validation errors.
- [general] Added convenience subclass of ``formencode.Schema``.
- [general] Split CSS styles into ``base.css`` and ``layout.css``, since really
they serve different purposes.
- [feature] Added ``overwriting_move()`` convenience function.
0.1a21
------
- [feature] Added the ability to specify a Mako template for use when
generating error emails. Templates may be referenced in the config file
using either a resource path or an absolute file path. If no template is
specified, the plain text fallback will be used. The ``content_type`` of the
email is also configurable (defaults to ``'text/html'`` if a Mako template is
used; ``'text/plain'`` otherwise).
0.1a20
------
- [feature] Added ability to configure path to the PID file used by file
monitor daemons running on Linux. This was necessary to allow multiple
daemons to run on the same machine.
0.1a19
------
- [general] Added ``active_extensions`` stubs to alembic migration script
template.
- [general] Updated pyramid user views and templates. These were sorely out of
date.
0.1a18
------
- [feature] Added ``engine_from_config()`` function to ``edbob.sqlalchemy``
module. This was done to provide awareness of the ``poolclass`` parameter to
engine configuration.
0.1a17
------
- [feature] Added ``display()`` method to ``AutocompleteView``. This allows
overriding the display text for autocomplete results, in the event that
``str(result)`` isn't good enough.
- [bug] Fixed ``request.has_perm()`` function so that it can check for
permissions on the "guest" role, in the event a user is not logged in.
0.1a16
------
- Add ``alembic/*.py`` to ``MANIFEST.in``.
- Various progress tweaks.
- Add ``OrderedDict`` import to ``util`` module.
0.1a15
------
- Add ``alembic`` to ``db`` dependencies.
- Fix alembic ``env.py`` template.
0.1a14
------
- Slight overhaul of init() system; added ``edbob.init_modules()`` function.
- Added ``read_service()`` method to ``AppConfigParser`` class, for use with
Windows services.
- Added generic ``Service`` class to ``edbob.win32`` module. (File monitor now
inherits from it.)
- Tweaked ``edbob.db`` initialization somewhat. (``Base.metadata`` no longer
binds to ``edbob.db.engine``.)
- Fixed guest role bug in ``edbob.db.auth.has_permission()`` function.
- Added "automagical" enumeration support for database extensions.
- Added ``EMAIL_PREFERENCE`` enum to ``contact`` database extension.
- Tweaked ``edbob.pyramid.includeme()``.
- Tweaked ``people`` Pyramid views.
- Added ``edbob.daemon`` module.
0.1a13 0.1a13
------ ------

View file

@ -19,5 +19,3 @@ recursive-include edbob/pyramid/templates *.mako
recursive-include edbob/scaffolds/edbob *.py recursive-include edbob/scaffolds/edbob *.py
recursive-include edbob/scaffolds/edbob *_tmpl recursive-include edbob/scaffolds/edbob *_tmpl
recursive-include edbob/scaffolds/edbob/+package+/pyramid/templates *.mako recursive-include edbob/scaffolds/edbob/+package+/pyramid/templates *.mako
recursive-include edbob/templates *.mako

View file

@ -28,10 +28,12 @@
from edbob._version import __version__ from edbob._version import __version__
from edbob.enum import *
from edbob.core import * from edbob.core import *
from edbob.time import * from edbob.time import *
from edbob.files import * from edbob.files import *
from edbob.modules import * from edbob.modules import *
from edbob.configuration import * from edbob.configuration import *
from edbob.initialization import * from edbob.initialization import *
inited = False

View file

@ -1 +1 @@
__version__ = '0.1.2' __version__ = '0.1a13'

View file

@ -105,7 +105,6 @@ Options:
Config path (may be specified more than once) Config path (may be specified more than once)
-n, --no-init Don't load config before executing command -n, --no-init Don't load config before executing command
-d, --debug Increase logging level to DEBUG -d, --debug Increase logging level to DEBUG
-P, --progress Show progress indicators (where relevant)
-v, --verbose Increase logging level to INFO -v, --verbose Increase logging level to INFO
-V, --version Display program version and exit -V, --version Display program version and exit
@ -133,7 +132,6 @@ Try '%(name)s help <command>' for more help.""" % self
metavar='PATH') metavar='PATH')
parser.add_argument('-d', '--debug', action='store_true', dest='debug') parser.add_argument('-d', '--debug', action='store_true', dest='debug')
parser.add_argument('-n', '--no-init', action='store_true', default=False) parser.add_argument('-n', '--no-init', action='store_true', default=False)
parser.add_argument('-P', '--progress', action='store_true', default=False)
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose') parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
parser.add_argument('-V', '--version', action='version', parser.add_argument('-V', '--version', action='version',
version="%%(prog)s %s" % self.version) version="%%(prog)s %s" % self.version)
@ -184,7 +182,6 @@ Try '%(name)s help <command>' for more help.""" % self
# And finally, do something of real value... # And finally, do something of real value...
cmd = self.subcommands[cmd](parent=self) cmd = self.subcommands[cmd](parent=self)
cmd.show_progress = args.progress
cmd._run(*(args.command + args.argv)) cmd._run(*(args.command + args.argv))
@ -435,6 +432,11 @@ class FileMonitorCommand(Subcommand):
uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service") uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service")
uninstall.set_defaults(subcommand='remove') uninstall.set_defaults(subcommand='remove')
else:
parser.add_argument('-D', '--dont-daemonize',
action='store_false', dest='daemonize',
help="Don't daemonize when starting")
def get_win32_module(self): def get_win32_module(self):
from edbob.filemon import win32 from edbob.filemon import win32
return win32 return win32
@ -452,10 +454,10 @@ class FileMonitorCommand(Subcommand):
from edbob.filemon import linux as filemon from edbob.filemon import linux as filemon
if args.subcommand == 'start': if args.subcommand == 'start':
filemon.start_daemon(self.appname) filemon.start_daemon(self.appname, daemonize=args.daemonize)
elif args.subcommand == 'stop': elif args.subcommand == 'stop':
filemon.stop_daemon(self.appname) filemon.stop_daemon()
elif sys.platform == 'win32': elif sys.platform == 'win32':
from edbob import win32 from edbob import win32

View file

@ -289,25 +289,6 @@ class AppConfigParser(ConfigParser.SafeConfigParser):
self.paths_loaded.append(path) self.paths_loaded.append(path)
log.info("Read config file: %s" % path) log.info("Read config file: %s" % path)
def read_service(self, service, paths):
"""
"Special" version of :meth:`read()` which will first inspect the
file(s) for a service-specific directive, the presence of which
indicates the *true* config file to be used for the service.
This method is pretty much a hack to get around certain limitations of
Windows service implementations; it is not used otherwise.
"""
config = ConfigParser.SafeConfigParser()
config.read(paths)
if (config.has_section('edbob.service_config')
and config.has_option('edbob.service_config', service)):
paths = eval(config.get('edbob.service_config', service))
self.read(paths, recurse=True)
def require(self, section, option, msg=None): def require(self, section, option, msg=None):
""" """
Convenience method which will raise an exception if the given option Convenience method which will raise an exception if the given option

View file

@ -38,7 +38,7 @@ class Progress(edbob.Object):
""" """
def __init__(self, message, maximum): def __init__(self, message, maximum):
sys.stderr.write("\n%s...(%u total)\n" % (message, maximum)) print >> sys.stderr, "\n%s...(%u total)" % (message, maximum)
widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', progressbar.ETA()] widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', progressbar.ETA()]
self.progress = progressbar.ProgressBar(maxval=maximum, widgets=widgets).start() self.progress = progressbar.ProgressBar(maxval=maximum, widgets=widgets).start()
@ -47,4 +47,4 @@ class Progress(edbob.Object):
return True return True
def destroy(self): def destroy(self):
sys.stderr.write("\n") print >> sys.stderr, ''

View file

@ -112,7 +112,7 @@ def graft(target, source, names=None):
if hasattr(source, '__all__'): if hasattr(source, '__all__'):
names = source.__all__ names = source.__all__
else: else:
names = [x for x in dir(source) if not x.startswith('_')] names = dir(source)
elif isinstance(names, basestring): elif isinstance(names, basestring):
names = [names] names = [names]

View file

@ -1,137 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import
# This code was (mostly, with some tweaks) stolen from:
# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
import sys, os, time, atexit
import stat
from signal import SIGTERM
class Daemon:
"""
A generic daemon class.
Usage: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile
def daemonize(self):
"""
do the UNIX double-fork magic, see Stevens' "Advanced
Programming in the UNIX Environment" for details (ISBN 0201563177)
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError, e:
sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)
# decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError, e:
sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = file(self.stdin, 'r')
so = file(self.stdout, 'a+')
se = file(self.stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
file(self.pidfile,'w+').write("%s\n" % pid)
os.chmod(self.pidfile, stat.S_IRUSR|stat.S_IWUSR)
def delpid(self):
os.remove(self.pidfile)
def start(self):
"""
Start the daemon
"""
# Check for a pidfile to see if the daemon already runs
try:
pf = file(self.pidfile,'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if pid:
message = "pidfile %s already exist. Daemon already running?\n"
sys.stderr.write(message % self.pidfile)
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""
Stop the daemon
"""
# Get the pid from the pidfile
try:
pf = file(self.pidfile,'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None
if not pid:
message = "pidfile %s does not exist. Daemon not running?\n"
sys.stderr.write(message % self.pidfile)
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, SIGTERM)
time.sleep(0.1)
except OSError, err:
err = str(err)
if err.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print str(err)
sys.exit(1)
def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()
def run(self):
"""
You should override this method when you subclass Daemon. It will be called after the process has been
daemonized by start() or restart().
"""

View file

@ -28,16 +28,16 @@
from __future__ import absolute_import from __future__ import absolute_import
from sqlalchemy import MetaData from sqlalchemy import engine_from_config, MetaData
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
import edbob import edbob
from edbob.sqlalchemy import engine_from_config
__all__ = ['engines', 'engine', 'Session', 'get_setting', 'save_setting'] __all__ = ['engines', 'engine', 'Session', 'get_setting', 'save_setting']
inited = False
engines = None engines = None
engine = None engine = None
Session = sessionmaker() Session = sessionmaker()
@ -71,36 +71,36 @@ def init(config):
from edbob.db import enum from edbob.db import enum
from edbob.db.extensions import extend_framework from edbob.db.extensions import extend_framework
global engines, engine global inited, engines, engine
keys = config.get('edbob.db', 'keys') keys = config.get('edbob.db', 'sqlalchemy.keys')
if keys: if keys:
keys = keys.split(',') keys = keys.split()
else: else:
keys = ['default'] keys = ['default']
engines = {} engines = {}
cfg = config.get_dict('edbob.db') cfg = config.get_dict('edbob.db')
for key in keys: for key in keys:
key = key.strip()
try: try:
engines[key] = engine_from_config(cfg, '%s.' % key) engines[key] = engine_from_config(cfg, 'sqlalchemy.%s.' % key)
except KeyError: except KeyError:
if key == 'default': if key == 'default':
try: try:
engines[key] = engine_from_config(cfg, 'sqlalchemy.') engines[key] = engine_from_config(cfg)
except KeyError: except KeyError:
pass pass
engine = engines.get('default') engine = engines.get('default')
if engine: if engine:
Session.configure(bind=engine) Base.metadata.bind = engine
extend_framework() extend_framework()
edbob.graft(edbob, edbob.db) edbob.graft(edbob, edbob.db)
edbob.graft(edbob, model) edbob.graft(edbob, model)
edbob.graft(edbob, enum) edbob.graft(edbob, enum)
inited = True
def get_setting(name, session=None): def get_setting(name, session=None):

View file

@ -85,12 +85,12 @@ def guest_role(session):
""" """
uuid = 'f8a27c98965a11dfaff7001143047286' uuid = 'f8a27c98965a11dfaff7001143047286'
guest = session.query(edbob.Role).get(uuid) admin = session.query(edbob.Role).get(uuid)
if guest: if admin:
return guest return admin
guest = edbob.Role(uuid=uuid, name='Guest') admin = edbob.Role(uuid=uuid, name='Guest')
session.add(guest) session.add(admin)
return guest return admin
def grant_permission(role, permission, session=None): def grant_permission(role, permission, session=None):
@ -105,7 +105,7 @@ def grant_permission(role, permission, session=None):
role.permissions.append(permission) role.permissions.append(permission)
def has_permission(obj, perm, include_guest=True, session=None): def has_permission(obj, perm, session=None):
""" """
Checks the given ``obj`` (which may be either a :class:`edbob.User`` or Checks the given ``obj`` (which may be either a :class:`edbob.User`` or
:class:`edbob.Role` instance), and returns a boolean indicating whether or :class:`edbob.Role` instance), and returns a boolean indicating whether or
@ -114,7 +114,7 @@ def has_permission(obj, perm, include_guest=True, session=None):
""" """
if isinstance(obj, edbob.User): if isinstance(obj, edbob.User):
roles = list(obj.roles) roles = obj.roles
elif isinstance(obj, edbob.Role): elif isinstance(obj, edbob.Role):
roles = [obj] roles = [obj]
elif obj is None: elif obj is None:
@ -124,9 +124,8 @@ def has_permission(obj, perm, include_guest=True, session=None):
if not session: if not session:
session = object_session(obj) session = object_session(obj)
assert session assert session
if include_guest:
roles.append(guest_role(session))
admin = administrator_role(session) admin = administrator_role(session)
roles.append(guest_role(session))
for role in roles: for role in roles:
if role is admin: if role is admin:
return True return True

View file

@ -76,11 +76,6 @@ class Extension(edbob.Object):
# :meth:`Extension.get_models_module()` for more info). # :meth:`Extension.get_models_module()` for more info).
model_module = '' model_module = ''
# You can set this to any dotted module path you like. If unset a default
# will be assumed, of the form ``<path.to.extension>.enum`` (see
# :meth:`Extension.get_enum_module()` for more info).
enum_module = ''
# @property # @property
# @requires_impl(is_property=True) # @requires_impl(is_property=True)
# def name(self): # def name(self):
@ -124,9 +119,6 @@ class Extension(edbob.Object):
""" """
edbob.graft(edbob, self.get_model_module()) edbob.graft(edbob, self.get_model_module())
enum = self.get_enum_module()
if enum:
edbob.graft(edbob, enum)
# def extend_mappers(self, metadata): # def extend_mappers(self, metadata):
# """ # """
@ -152,26 +144,6 @@ class Extension(edbob.Object):
self.populate_metadata(meta, recurse) self.populate_metadata(meta, recurse)
return meta return meta
def get_enum_module(self):
"""
Imports and returns a reference to the Python module providing
enumeration data for the extension (if one exists).
:attr:`Extension.enum_module` is first consulted to determine the
dotted module path. If nothing is found there, a default path is
constructed by appending ``'.enum'`` to the extension module's own
dotted path.
"""
if self.enum_module:
module = self.enum_module
else:
module = str(self.__class__.__module__) + '.enum'
try:
return import_module_path(module)
except ImportError:
return None
def get_model_module(self): def get_model_module(self):
""" """
Imports and returns a reference to the Python module providing schema Imports and returns a reference to the Python module providing schema
@ -366,7 +338,7 @@ def extend_framework():
session.close() session.close()
for name in sorted(extensions, extension_sorter(extensions)): for name in sorted(extensions, extension_sorter(extensions)):
log.debug("Applying active extension: %s" % name) log.info("Applying active extension: %s" % name)
ext = extensions[name] ext = extensions[name]
# merge_extension_metadata(ext) # merge_extension_metadata(ext)
# ext.extend_classes() # ext.extend_classes()

View file

@ -51,11 +51,10 @@ class Permission(Base):
permission = Column(String(50), primary_key=True) permission = Column(String(50), primary_key=True)
def __repr__(self): def __repr__(self):
return "Permission(role_uuid={0}, permission={1})".format( return "<Permission: %s, %s>" % (self.role, self.permission)
repr(self.role_uuid), repr(self.permission))
def __unicode__(self): def __str__(self):
return unicode(self.permission or '') return str(self.permission or '')
class UserRole(Base): class UserRole(Base):
@ -70,7 +69,7 @@ class UserRole(Base):
role_uuid = Column(String(32), ForeignKey('roles.uuid')) role_uuid = Column(String(32), ForeignKey('roles.uuid'))
def __repr__(self): def __repr__(self):
return "UserRole(uuid={0})".format(repr(self.uuid)) return "<UserRole: %s : %s>" % (self.user, self.role)
class Role(Base): class Role(Base):
@ -90,18 +89,16 @@ class Role(Base):
creator=lambda x: Permission(permission=x), creator=lambda x: Permission(permission=x),
getset_factory=getset_factory) getset_factory=getset_factory)
_users = relationship( _users = relationship(UserRole, backref='role')
UserRole, backref='role',
cascade='save-update, merge, delete, delete-orphan')
users = association_proxy('_users', 'user', users = association_proxy('_users', 'user',
creator=lambda x: UserRole(user=x), creator=lambda x: UserRole(user=x),
getset_factory=getset_factory) getset_factory=getset_factory)
def __repr__(self): def __repr__(self):
return "Role(uuid={0})".format(repr(self.uuid)) return "<Role: %s>" % self.name
def __unicode__(self): def __str__(self):
return unicode(self.name or '') return str(self.name or '')
class User(Base): class User(Base):
@ -125,18 +122,17 @@ class User(Base):
getset_factory=getset_factory) getset_factory=getset_factory)
def __repr__(self): def __repr__(self):
return "User(uuid={0})".format(repr(self.uuid)) return "<User: %s>" % self.username
def __unicode__(self): def __str__(self):
return unicode(self.username or '') return str(self.username or '')
@property @property
def display_name(self): def display_name(self):
""" """
Returns :attr:`Person.display_name` if present; otherwise returns Returns the user's ``person.display_name``, if present, otherwise the
:attr:`username`. ``username``.
""" """
if self.person and self.person.display_name: if self.person and self.person.display_name:
return self.person.display_name return self.person.display_name
return self.username return self.username

View file

@ -72,8 +72,7 @@ class PhoneNumber(Base):
__mapper_args__ = {'polymorphic_on': parent_type} __mapper_args__ = {'polymorphic_on': parent_type}
def __repr__(self): def __repr__(self):
return "{0}(uuid={1})".format( return "<%s: %s>" % (self.__class__.__name__, self.number)
self.__class__.__name__, repr(self.uuid))
def __unicode__(self): def __unicode__(self):
return unicode(self.number) return unicode(self.number)
@ -104,8 +103,7 @@ class EmailAddress(Base):
__mapper_args__ = {'polymorphic_on': parent_type} __mapper_args__ = {'polymorphic_on': parent_type}
def __repr__(self): def __repr__(self):
return "{0}(uuid={1})".format( return "<%s: %s>" % (self.__class__.__name__, self.address)
self.__class__.__name__, repr(self.uuid))
def __unicode__(self): def __unicode__(self):
return unicode(self.address) return unicode(self.address)
@ -133,7 +131,7 @@ class Person(Base):
display_name = Column(String(100), default=get_person_display_name) display_name = Column(String(100), default=get_person_display_name)
def __repr__(self): def __repr__(self):
return "Person(uuid={0})".format(repr(self.uuid)) return "<Person: %s>" % self.display_name
def __unicode__(self): def __unicode__(self):
return unicode(self.display_name or '') return unicode(self.display_name or '')

View file

@ -54,7 +54,7 @@ class ActiveExtension(Base):
name = Column(String(50), primary_key=True) name = Column(String(50), primary_key=True)
def __repr__(self): def __repr__(self):
return "ActiveExtension(name={0})".format(repr(self.name)) return "<ActiveExtension: %s>" % self.name
def __str__(self): def __str__(self):
return str(self.name or '') return str(self.name or '')
@ -71,4 +71,4 @@ class Setting(Base):
value = Column(Text) value = Column(Text)
def __repr__(self): def __repr__(self):
return "Setting(name={0})".format(repr(self.name)) return "<Setting: %s>" % self.name

View file

@ -1,51 +0,0 @@
#!/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.enum`` -- Enumerations
"""
EMAIL_PREFERENCE_NONE = 0
EMAIL_PREFERENCE_TEXT = 1
EMAIL_PREFERENCE_HTML = 2
EMAIL_PREFERENCE_MOBILE = 3
EMAIL_PREFERENCE = {
EMAIL_PREFERENCE_NONE : "No Emails",
EMAIL_PREFERENCE_TEXT : "Text",
EMAIL_PREFERENCE_HTML : "HTML",
EMAIL_PREFERENCE_MOBILE : "Mobile",
}
PHONE_TYPE_HOME = 'home'
PHONE_TYPE_MOBILE = 'mobile'
PHONE_TYPE_OTHER = 'other'
PHONE_TYPE = {
PHONE_TYPE_HOME : "Home",
PHONE_TYPE_MOBILE : "Mobile",
PHONE_TYPE_OTHER : "Other",
}

View file

@ -26,7 +26,6 @@
``edbob.errors`` -- Error Alert Emails ``edbob.errors`` -- Error Alert Emails
""" """
import os.path
import sys import sys
import socket import socket
import logging import logging
@ -34,7 +33,6 @@ from traceback import format_exception
from cStringIO import StringIO from cStringIO import StringIO
import edbob import edbob
from edbob.files import resource_path
from edbob.mail import sendmail_with_config from edbob.mail import sendmail_with_config
@ -62,52 +60,15 @@ def email_exception(type=None, value=None, traceback=None):
if not (type and value and traceback): if not (type and value and traceback):
type, value, traceback = sys.exc_info() type, value, traceback = sys.exc_info()
hostname = socket.gethostname()
traceback = ''.join(format_exception(type, value, traceback))
traceback = traceback.strip()
data = {
'host_name': hostname,
'host_ip': socket.gethostbyname(hostname),
'host_time': edbob.local_time(),
'traceback': traceback,
}
body, ctype = render_exception(data)
ctype = edbob.config.get('edbob.errors', 'content_type', default=ctype)
sendmail_with_config('errors', body, content_type=ctype)
def render_exception(data):
"""
Renders the exception data using a Mako template if one is configured;
otherwise as a simple string.
"""
template = edbob.config.get('edbob.errors', 'template')
if template:
template = resource_path(template)
if os.path.exists(template):
# Assume Mako template; render and return.
from mako.template import Template
template = Template(filename=template)
return template.render(**data), 'text/html'
# If not a Mako template, return regular text with substitutions.
body = StringIO() body = StringIO()
data['host_time'] = data['host_time'].strftime('%Y-%m-%d %H:%M:%S %Z%z')
body.write("""\ hostname = socket.gethostname()
An unhandled exception occurred. body.write("An exception occurred.\n")
body.write("\n")
body.write("Machine Name: %s (%s)\n" % (hostname, socket.gethostbyname(hostname)))
body.write("Local Time: %s\n" % (edbob.local_time().strftime('%Y-%m-%d %H:%M:%S %Z%z')))
body.write("\n")
body.write("%s\n" % ''.join(format_exception(type, value, traceback)))
Machine Name: %(host_name)s (%(host_ip)s) sendmail_with_config('errors', body.getvalue())
Machine Time: %(host_time)s
%(traceback)s
""" % data)
b = body.getvalue()
body.close() body.close()
return b, 'text/plain'

View file

@ -73,12 +73,6 @@ class LoadSpecError(Exception):
return None return None
class InvalidSpec(LoadSpecError):
def specifics(self):
return "invalid spec"
class ModuleMissingAttribute(LoadSpecError): class ModuleMissingAttribute(LoadSpecError):
""" """
Raised during :func:`edbob.load_spec()` when the module imported okay but Raised during :func:`edbob.load_spec()` when the module imported okay but

View file

@ -26,18 +26,10 @@
``edbob.filemon`` -- File Monitoring Service ``edbob.filemon`` -- File Monitoring Service
""" """
import os
import os.path import os.path
import sys
import Queue
import logging import logging
import edbob import edbob
from edbob.errors import email_exception
if sys.platform == 'win32':
import win32api
from edbob.win32 import file_is_free
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -73,12 +65,6 @@ class MonitorProfile(object):
self.locks = edbob.config.getboolean( self.locks = edbob.config.getboolean(
'%s.filemon' % appname, '%s.locks' % key, default=False) '%s.filemon' % appname, '%s.locks' % key, default=False)
self.process_existing = edbob.config.getboolean(
'%s.filemon' % appname, '%s.process_existing' % key, default=True)
self.stop_on_error = edbob.config.getboolean(
'%s.filemon' % appname, '%s.stop_on_error' % key, default=False)
def get_monitor_profiles(appname): def get_monitor_profiles(appname):
""" """
@ -124,105 +110,3 @@ def get_monitor_profiles(appname):
del monitored[key] del monitored[key]
return monitored return monitored
def queue_existing(profile, path):
"""
Adds files found in a watched folder to a processing queue. This is called
when the monitor first starts, to handle the case of files which exist
prior to startup.
If files are found, they are first sorted by modification timestamp, using
a lexical sort on the filename as a tie-breaker, and then added to the
queue in that order.
:param profile: Monitor profile for which the folder is to be watched. The
profile is expected to already have a queue attached; any existing files
will be added to this queue.
:type profile: :class:`edbob.filemon.MonitorProfile` instance
:param path: Folder path which is to be checked for files.
:type path: string
:returns: ``None``
"""
def sorter(x, y):
mtime_x = os.path.getmtime(x)
mtime_y = os.path.getmtime(y)
if mtime_x < mtime_y:
return -1
if mtime_x > mtime_y:
return 1
return cmp(x, y)
paths = [os.path.join(path, x) for x in os.listdir(path)]
for path in sorted(paths, cmp=sorter):
# Only process normal files.
if not os.path.isfile(path):
continue
# If using locks, don't process "in transit" files.
if profile.locks and path.endswith('.lock'):
continue
log.debug("queue_existing: queuing existing file for "
"profile '%s': %s" % (profile.key, path))
profile.queue.put(path)
def perform_actions(profile):
"""
Callable target for action threads.
"""
keep_going = True
while keep_going:
try:
path = profile.queue.get_nowait()
except Queue.Empty:
pass
else:
# In some cases, processing one file may cause other related files
# to also be processed. When this happens, a path on the queue may
# point to a file which no longer exists.
if not os.path.exists(path):
log.info("perform_actions: path does not exist: %s" % path)
continue
log.debug("perform_actions: processing file: %s" % path)
if sys.platform == 'win32':
while not file_is_free(path):
win32api.Sleep(0)
for spec, func, args in profile.actions:
log.info("perform_actions: calling function '%s' on file: %s" %
(spec, path))
try:
func(path, *args)
except:
log.exception("perform_actions: exception occurred "
"while processing file: %s" % path)
email_exception()
# Don't process any more files if the profile is so
# configured.
if profile.stop_on_error:
keep_going = False
# Either way this particular file probably shouldn't be
# processed any further.
log.warning("perform_actions: no further processing "
"will be done for file: %s" % path)
break
log.warning("perform_actions: error encountered, and configuration "
"dictates that no more actions will be processed for "
"profile: %s" % profile.key)

View file

@ -27,24 +27,14 @@
""" """
import sys import sys
import os
import os.path import os.path
import threading import signal
import Queue
import logging import logging
import pyinotify
try:
import pyinotify
except ImportError:
# Mock out for testing on Windows.
class Dummy(object):
pass
pyinotify = Dummy()
pyinotify.ProcessEvent = Dummy
import edbob import edbob
from edbob import filemon from edbob.filemon import get_monitor_profiles
from edbob.daemon import Daemon
from edbob.errors import email_exception
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -55,8 +45,9 @@ class EventHandler(pyinotify.ProcessEvent):
Event processor for file monitor daemon. Event processor for file monitor daemon.
""" """
def my_init(self, profile=None, **kwargs): def my_init(self, actions=[], locks=False, **kwargs):
self.profile = profile self.actions = actions
self.locks = locks
def process_IN_ACCESS(self, event): def process_IN_ACCESS(self, event):
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname) log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
@ -66,85 +57,84 @@ class EventHandler(pyinotify.ProcessEvent):
def process_IN_CLOSE_WRITE(self, event): def process_IN_CLOSE_WRITE(self, event):
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname) log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
if not self.profile.locks: if not self.locks:
self.profile.queue.put(event.pathname) self.perform_actions(event.pathname)
def process_IN_CREATE(self, event): def process_IN_CREATE(self, event):
log.debug("EventHandler: IN_CREATE: %s" % event.pathname) log.debug("EventHandler: IN_CREATE: %s" % event.pathname)
def process_IN_DELETE(self, event): def process_IN_DELETE(self, event):
log.debug("EventHandler: IN_DELETE: %s" % event.pathname) log.debug("EventHandler: IN_DELETE: %s" % event.pathname)
if self.profile.locks and event.pathname.endswith('.lock'): if self.locks and event.pathname.endswith('.lock'):
self.profile.queue.put(event.pathname[:-5]) self.perform_actions(event.pathname[:-5])
def process_IN_MODIFY(self, event): def process_IN_MODIFY(self, event):
log.debug("EventHandler: IN_MODIFY: %s" % event.pathname) log.debug("EventHandler: IN_MODIFY: %s" % event.pathname)
def process_IN_MOVED_TO(self, event): def process_IN_MOVED_TO(self, event):
log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname) log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname)
if not self.profile.locks: if not self.locks:
self.profile.queue.put(event.pathname) self.perform_actions(event.pathname)
def perform_actions(self, path):
for spec, func, args in self.actions:
func(path, *args)
class FileMonitorDaemon(Daemon): def get_pid_path():
"""
Returns the path to the PID file for the file monitor daemon.
"""
def run(self): basename = os.path.basename(sys.argv[0])
return '/tmp/%s_filemon.pid' % basename
wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm)
mask = (pyinotify.IN_ACCESS
| pyinotify.IN_ATTRIB
| pyinotify.IN_CLOSE_WRITE
| pyinotify.IN_CREATE
| pyinotify.IN_DELETE
| pyinotify.IN_MODIFY
| pyinotify.IN_MOVED_TO)
monitored = filemon.get_monitor_profiles(self.appname)
for key, profile in monitored.iteritems():
# Create a file queue for the profile.
profile.queue = Queue.Queue()
# Perform setup for each of the watched folders.
for path in profile.dirs:
# Maybe put all pre-existing files in the queue.
if profile.process_existing:
filemon.queue_existing(profile, path)
# Create a watch for the folder.
log.debug("start_daemon: profile '%s' watches folder: %s" % (key, path))
wm.add_watch(path, mask, proc_fun=EventHandler(profile=profile))
# Create an action thread for the profile.
name = 'actions-%s' % key
log.debug("start_daemon: starting action thread: %s" % name)
thread = threading.Thread(target=filemon.perform_actions,
name=name, args=(profile,))
thread.daemon = True
thread.start()
# Fire up the watchers.
notifier.loop()
def get_daemon(appname=None): def start_daemon(appname, daemonize=True):
if appname is None: """
appname = os.path.basename(sys.argv[0]) Starts the file monitor daemon.
pid_path = edbob.config.get('%s.filemon' % appname, 'pid_path') """
if not pid_path:
pid_path = '/tmp/%s_filemon.pid' % appname
monitor = FileMonitorDaemon(pid_path) pid_path = get_pid_path()
monitor.appname = appname if os.path.exists(pid_path):
return monitor print "File monitor is already running"
return
wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm)
monitored = get_monitor_profiles(appname)
mask = (pyinotify.IN_ACCESS | pyinotify.IN_ATTRIB
| pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CREATE
| pyinotify.IN_DELETE | pyinotify.IN_MODIFY
| pyinotify.IN_MOVED_TO)
for profile in monitored.itervalues():
for path in profile.dirs:
wm.add_watch(path, mask, proc_fun=EventHandler(
actions=profile.actions, locks=profile.locks))
if not daemonize:
sys.stderr.write("Starting file monitor. (Press Ctrl+C to quit.)\n")
notifier.loop(daemonize=daemonize, pid_file=pid_path)
def start_daemon(appname): def stop_daemon():
get_daemon(appname).start() """
Stops the file monitor daemon.
"""
pid_path = get_pid_path()
if not os.path.exists(pid_path):
print "File monitor is not running"
return
def stop_daemon(appname): f = open(pid_path)
get_daemon(appname).stop() pid = f.read().strip()
f.close()
if not pid.isdigit():
log.warning("stop_daemon: Found bogus PID (%s) in file: %s" % (pid, pid_path))
return
os.kill(int(pid), signal.SIGKILL)
os.remove(pid_path)

View file

@ -33,9 +33,9 @@ import logging
import threading import threading
import edbob import edbob
from edbob import filemon
from edbob.errors import email_exception from edbob.errors import email_exception
from edbob.win32 import Service, file_is_free from edbob.filemon import get_monitor_profiles
from edbob.win32 import file_is_free
if sys.platform == 'win32': # docs should build for everyone if sys.platform == 'win32': # docs should build for everyone
import win32api import win32api
@ -50,26 +50,39 @@ if sys.platform == 'win32': # docs should build for everyone
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class FileMonitorService(Service): class FileMonitorService(win32serviceutil.ServiceFramework):
""" """
Implements edbob's file monitor Windows service. Implements edbob's file monitor Windows service.
""" """
_svc_name_ = 'EdbobFileMonitor' _svc_name_ = "Edbob File Monitor"
_svc_display_name_ = "Edbob : File Monitoring Service" _svc_display_name_ = "Edbob : File Monitoring Service"
_svc_description_ = ("Monitors one or more folders for incoming files, " _svc_description_ = ("Monitors one or more folders for incoming files, "
"and performs configured actions as new files arrive.") "and performs configured actions as new files arrive.")
appname = 'edbob'
def __init__(self, args):
"""
Constructor.
"""
# super(FileMonitorService, self).__init__(args)
win32serviceutil.ServiceFramework.__init__(self, args)
# Create "wait stop" event, for main worker loop.
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def Initialize(self): def Initialize(self):
""" """
Service initialization. Service initialization.
""" """
if not Service.Initialize(self): # Read configuration file(s).
return False edbob.init(self.appname)
# Read monitor profile(s) from config. # Read monitor profile(s) from config.
self.monitored = filemon.get_monitor_profiles(self.appname) self.monitored = get_monitor_profiles(self.appname)
# Make sure we have something to do. # Make sure we have something to do.
if not self.monitored: if not self.monitored:
@ -79,36 +92,78 @@ class FileMonitorService(Service):
for key, profile in self.monitored.iteritems(): for key, profile in self.monitored.iteritems():
# Create a file queue for the profile. # Create a file queue for the profile.
profile.queue = Queue.Queue() queue = Queue.Queue()
# Perform setup for each of the watched folders. # Create a monitor thread for each folder in profile.
for i, path in enumerate(profile.dirs, 1): for i, path in enumerate(profile.dirs, 1):
# Maybe put all pre-existing files in the queue.
if profile.process_existing:
filemon.queue_existing(profile, path)
# Create a monitor thread for the folder.
name = 'monitor-%s-%u' % (key, i) name = 'monitor-%s-%u' % (key, i)
log.debug("Initialize: Starting '%s' thread for folder: %s" % log.debug("Initialize: Starting '%s' thread for folder: %s" %
(name, path)) (name, path))
thread = threading.Thread(target=monitor_files, thread = threading.Thread(
name=name, args=(profile, path)) target=monitor_files,
name=name,
args=(queue, path, profile))
thread.daemon = True thread.daemon = True
thread.start() thread.start()
# Create an action thread for the profile. # Create an action thread for the profile.
name = 'actions-%s' % key name = 'actions-%s' % key
log.debug("Initialize: Starting '%s' thread" % name) log.debug("Initialize: Starting '%s' thread" % name)
thread = threading.Thread(target=filemon.perform_actions, thread = threading.Thread(
name=name, args=(profile,)) target=perform_actions,
name=name,
args=(queue, profile))
thread.daemon = True thread.daemon = True
thread.start() thread.start()
return True return True
def SvcDoRun(self):
"""
This method is invoked when the service starts.
"""
import servicemanager
# Write start occurrence to Windows Event Log.
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ''))
# Figure out what we're supposed to be doing.
if self.Initialize():
# Wait infinitely for stop request, while threads do their thing.
log.info("SvcDoRun: All threads started; waiting for stop request.")
win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
log.info("SvcDoRun: Stop request received.")
else: # Nothing to be done...
msg = "Nothing to do! No valid monitor profiles found in config."
servicemanager.LogWarningMsg(msg)
log.warning("SvcDoRun: %s" % msg)
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Write stop occurrence to Windows Event Log.
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STOPPED,
(self._svc_name_, ''))
def SvcStop(self):
"""
This method is invoked when the service is requested to stop itself.
"""
# Let the SCM know we're trying to stop.
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Let worker loop know its job is done.
win32event.SetEvent(self.hWaitStop)
def monitor_files(profile, path): def monitor_files(queue, path, profile):
""" """
Callable target for file monitor threads. Callable target for file monitor threads.
""" """
@ -140,7 +195,40 @@ def monitor_files(profile, path):
winnt.FILE_ACTION_RENAMED_NEW_NAME): winnt.FILE_ACTION_RENAMED_NEW_NAME):
log.debug("monitor_files: Queueing '%s' file: %s" % log.debug("monitor_files: Queueing '%s' file: %s" %
(profile.key, fpath)) (profile.key, fpath))
profile.queue.put(fpath) queue.put(fpath)
def perform_actions(queue, profile):
"""
Callable target for action threads.
"""
while True:
try:
path = queue.get_nowait()
except Queue.Empty:
pass
else:
while not file_is_free(path):
win32api.Sleep(0)
for spec, func, args in profile.actions:
log.info("perform_actions: Calling function '%s' on file: %s" %
(spec, path))
try:
func(path, *args)
except:
log.exception("perform_actions: An exception occurred "
"while processing file: %s" % path)
email_exception()
# This file probably shouldn't be processed any further.
break
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -33,8 +33,6 @@ import shutil
import tempfile import tempfile
import lockfile import lockfile
import pkg_resources
__all__ = ['temp_path'] __all__ = ['temp_path']
@ -100,31 +98,6 @@ def count_lines(path):
return lines return lines
def overwriting_move(src, dst):
"""
Convenience function which is equivalent to ``shutil.move()``, except it
will cause the destination file to be overwritten if it exists.
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
if os.path.exists(dst):
os.remove(dst)
shutil.move(src, dst)
def resource_path(path):
"""
Returns a resource file path. ``path`` is assumed either to be a package
resource, or a regular file path. In the latter case it is returned
unchanged.
"""
if not os.path.isabs(path) and ':' in path:
return pkg_resources.resource_filename(*path.split(':'))
return path
def temp_path(suffix='.tmp', prefix='edbob.'): def temp_path(suffix='.tmp', prefix='edbob.'):
""" """
Convenience function to return a temporary file path. The arguments' Convenience function to return a temporary file path. The arguments'

View file

@ -38,9 +38,7 @@ from edbob.configuration import (
from edbob.exceptions import InitError from edbob.exceptions import InitError
__all__ = ['init', 'init_modules', 'inited'] __all__ = ['init']
inited = []
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -82,41 +80,22 @@ def init(appname='edbob', *args, **kwargs):
else: else:
config_paths = default_system_paths(appname) + default_user_paths(appname) config_paths = default_system_paths(appname) + default_user_paths(appname)
service = kwargs.get('service') shell = bool(kwargs.get('shell'))
if service: for paths in config_paths:
config.read_service(service, config_paths) config.read(paths, recurse=not shell)
else: config.configure_logging()
shell = kwargs.get('shell', False)
for paths in config_paths:
config.read(paths, recurse=not shell)
if config.getboolean('edbob', 'configure_logging', default=True):
config.configure_logging()
default_modules = 'edbob.time' default_modules = 'edbob.time'
modules = config.get('edbob', 'init', default=default_modules) modules = config.get('edbob', 'init', default=default_modules)
if modules: if modules:
modules = modules.split(',') for name in modules.split(','):
init_modules(modules, config) name = name.strip()
edbob.graft(edbob, locals(), 'config')
inited.append('edbob')
def init_modules(names, config=None):
"""
Initialize the given modules. ``names`` should be a sequence of strings,
each of which should be a dotted module name. If ``config`` is not
specified, :attr:`edbob.config` is assumed.
"""
if config is None:
config = edbob.config
for name in names:
name = name.strip()
if name not in inited:
module = __import__(name, globals(), locals(), fromlist=['init']) module = __import__(name, globals(), locals(), fromlist=['init'])
if not hasattr(module, 'init'): if not hasattr(module, 'init'):
raise InitError(module) raise InitError(module)
getattr(module, 'init')(config) getattr(module, 'init')(config)
inited.append(name) # config.inited.append(name)
# config.inited.append('edbob')
edbob.graft(edbob, locals(), 'config')
edbob.inited = True

View file

@ -82,9 +82,6 @@ def load_spec(spec):
necessary. necessary.
""" """
if spec.count(':') != 1:
raise exceptions.InvalidSpec(spec)
module_path, obj = spec.split(':') module_path, obj = spec.split(':')
module = import_module_path(module_path) module = import_module_path(module_path)
try: try:

View file

@ -29,8 +29,6 @@
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session
from zope.sqlalchemy import ZopeTransactionExtension from zope.sqlalchemy import ZopeTransactionExtension
import edbob
__all__ = ['Session'] __all__ = ['Session']
@ -58,7 +56,6 @@ def includeme(config):
config.include('pyramid_tm') config.include('pyramid_tm')
# Configure SQLAlchemy session. # Configure SQLAlchemy session.
Session.configure(bind=edbob.engine)
Session.configure(extension=ZopeTransactionExtension()) Session.configure(extension=ZopeTransactionExtension())
# Configure user authentication / authorization. # Configure user authentication / authorization.
@ -72,6 +69,3 @@ def includeme(config):
# Add static views. # Add static views.
config.include('edbob.pyramid.static') config.include('edbob.pyramid.static')
# Add subscriber hooks.
config.include('edbob.pyramid.subscribers')

View file

@ -50,8 +50,7 @@ from edbob.pyramid.forms.formalchemy.renderers import *
__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer', __all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer', 'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
'FieldSet', 'make_fieldset', 'required', 'pretty_datetime', 'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
'AssociationProxyField', 'StrippingFieldRenderer', 'AssociationProxyField', 'YesNoFieldRenderer']
'YesNoFieldRenderer']
class TemplateEngine(formalchemy.templates.TemplateEngine): class TemplateEngine(formalchemy.templates.TemplateEngine):
@ -185,6 +184,7 @@ class DateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
value = edbob.local_time(value) value = edbob.local_time(value)
return value.strftime(self.format) return value.strftime(self.format)
print type(value)
return '' return ''
FieldSet.default_renderers[formalchemy.types.DateTime] = DateTimeFieldRenderer FieldSet.default_renderers[formalchemy.types.DateTime] = DateTimeFieldRenderer

View file

@ -32,7 +32,7 @@ from pyramid.renderers import render
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', __all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
'StrippingFieldRenderer', 'YesNoFieldRenderer'] 'YesNoFieldRenderer']
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'): def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
@ -74,26 +74,17 @@ def EnumFieldRenderer(enum):
return '' return ''
if value in enum: if value in enum:
return enum[value] return enum[value]
return str(value) return value
def render(self, **kwargs): def render(self, **kwargs):
opts = [(enum[x], x) for x in sorted(enum)] opts = []
for value in sorted(enum):
opts.append((enum[value], value))
return formalchemy.fields.SelectFieldRenderer.render(self, opts, **kwargs) return formalchemy.fields.SelectFieldRenderer.render(self, opts, **kwargs)
return Renderer return Renderer
class StrippingFieldRenderer(formalchemy.TextFieldRenderer):
"""
Standard text field renderer, which strips whitespace from either end of
the input value on deserialization.
"""
def deserialize(self):
value = super(StrippingFieldRenderer, self).deserialize()
return value.strip()
class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer): class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
def render_readonly(self, **kwargs): def render_readonly(self, **kwargs):

View file

@ -26,29 +26,15 @@
``edbob.pyramid.forms.simpleform`` -- pyramid_simpleform Forms ``edbob.pyramid.forms.simpleform`` -- pyramid_simpleform Forms
""" """
from pyramid.renderers import render
import formencode
import pyramid_simpleform import pyramid_simpleform
from pyramid.renderers import render
from pyramid_simpleform.renderers import FormRenderer from pyramid_simpleform.renderers import FormRenderer
from edbob.pyramid import helpers from edbob.pyramid import helpers
from edbob.pyramid.forms import Form from edbob.pyramid.forms import Form
__all__ = ['Schema', 'SimpleForm'] __all__ = ['SimpleForm']
class Schema(formencode.Schema):
"""
Subclass of ``formencode.Schema``, which exists only to ignore extra
fields. These normally would cause a schema instance to be deemed invalid,
and pretty much *every* form has a submit button which would be considered
an extra field.
"""
allow_extra_fields = True
filter_extra_fields = True
class SimpleForm(Form): class SimpleForm(Form):

View file

@ -54,9 +54,6 @@ class AlchemyGrid(Grid):
self._formalchemy_grid.prettify = prettify self._formalchemy_grid.prettify = prettify
self.noclick_fields = [] self.noclick_fields = []
def __delattr__(self, attr):
delattr(self._formalchemy_grid, attr)
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self._formalchemy_grid, attr) return getattr(self._formalchemy_grid, attr)

View file

View file

@ -0,0 +1,291 @@
#!/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.handlers.base`` -- Base Handlers
"""
from pyramid.renderers import render_to_response
from pyramid.httpexceptions import HTTPException, HTTPFound, HTTPOk, HTTPUnauthorized
# import sqlahelper
# # import rattail.pyramid.forms.util as util
# from rattail.db.perms import has_permission
# from rattail.pyramid.forms.formalchemy import Grid
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

@ -0,0 +1,72 @@
#!/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.handlers.util`` -- Handler Utilities
"""
from pyramid.httpexceptions import HTTPFound
from edbob.db.perms import has_permission
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

View file

@ -47,32 +47,50 @@ class SessionProgress(object):
def __init__(self, session, key): def __init__(self, session, key):
self.session = get_progress_session(session, key) self.session = get_progress_session(session, key)
self.canceled = False self.cancelled = False
self.clear()
def __call__(self, message, maximum): def __call__(self, message, maximum):
self.clear() self.session['complete'] = False
self.session['message'] = message self.session['message'] = message
self.session['maximum'] = maximum self.session['maximum'] = maximum
self.session['cancelled'] = False
self.session['value'] = 0 self.session['value'] = 0
self.session.save() self.session.save()
return self return self
def clear(self):
self.session.clear()
self.session['complete'] = False
self.session['error'] = False
self.session['canceled'] = False
self.session.save()
def update(self, value): def update(self, value):
self.session.load() self.session.load()
if self.session.get('canceled'): if self.session.get('cancelled'):
self.canceled = True self.cancelled = True
else: else:
self.session['value'] = value self.session['value'] = value
self.session.save() self.session.save()
return not self.canceled return not self.cancelled
def destroy(self):
if not self.cancelled:
self.session['complete'] = True
self.session.save()
def secondary_progress(self):
return SecondarySessionProgress(self)
class SecondarySessionProgress(object):
def __init__(self, parent):
self.parent = parent
self.session = parent.session
def __call__(self, message, maximum):
self.session['message'] = message
self.session['value'] = 0
self.session['maximum'] = maximum
self.session.save()
return self
def update(self, value):
return self.parent.update(value)
def destroy(self): def destroy(self):
pass pass

View file

@ -87,12 +87,81 @@ div.error {
margin-bottom: 10px; margin-bottom: 10px;
} }
ul.error { /* td.right { */
color: #dd6666; /* float: none; */
font-weight: bold; /* } */
padding: 0px;
/* table.wrapper td.right { */
/* vertical-align: bottom; */
/* } */
/******************************
* Main Layout
******************************/
html, body, #container {
height: 100%;
} }
ul.error li { body > #container {
list-style-type: none; height: auto;
min-height: 100%;
}
#container {
margin: 0 auto;
width: 1000px;
}
#header {
border-bottom: 1px solid #000000;
overflow: auto;
}
#body {
padding-top: 15px;
padding-bottom: 5em;
}
#footer {
margin-top: -4em;
text-align: center;
}
/******************************
* Header
******************************/
#header h1 {
margin: 0px 5px 10px 5px;
}
#login {
margin: 8px 20px auto auto;
}
#user-menu {
float: left;
}
#home-link {
font-weight: bold;
}
#header-links {
float: right;
text-align: right;
}
#main-menu {
border-top: 1px solid black;
clear: both;
font-weight: bold;
}
#main-menu li {
display: inline;
margin-right: 15px;
} }

View file

@ -38,18 +38,11 @@ div.fieldset {
div.field-wrapper { div.field-wrapper {
clear: both; clear: both;
min-height: 30px;
overflow: auto; overflow: auto;
padding: 5px; min-height: 30px;
}
div.field-wrapper.error {
background-color: #ddcccc;
border: 2px solid #dd6666;
} }
div.field-wrapper label { div.field-wrapper label {
color: #000000;
display: block; display: block;
float: left; float: left;
width: 140px; width: 140px;
@ -78,15 +71,6 @@ div.field-wrapper div.field textarea {
width: 320px; width: 320px;
} }
label input[type=checkbox] {
margin-right: 8px;
}
/******************************
* Buttons
******************************/
div.buttons { div.buttons {
clear: both; clear: both;
margin-top: 10px; margin-top: 10px;

View file

@ -1,75 +0,0 @@
/******************************
* Main Layout
******************************/
html, body, #container {
height: 100%;
}
body > #container {
height: auto;
min-height: 100%;
}
#container {
margin: 0 auto;
width: 1000px;
}
#header {
border-bottom: 1px solid #000000;
overflow: auto;
}
#body {
padding-top: 15px;
padding-bottom: 5em;
}
#footer {
clear: both;
margin-top: -4em;
text-align: center;
}
/******************************
* Header
******************************/
#header h1 {
margin: 0px 5px 10px 5px;
}
#login {
margin: 8px 20px auto auto;
}
#login a.username {
font-weight: bold;
}
#user-menu {
float: left;
}
#home-link {
font-weight: bold;
}
#header-links {
float: right;
text-align: right;
}
#main-menu {
border-top: 1px solid black;
clear: both;
font-weight: bold;
}
#main-menu li {
display: inline;
margin-right: 15px;
}

View file

@ -1,33 +1,17 @@
/****************************** /******************************
* Permission Lists * perms.css
******************************/ ******************************/
div.field-wrapper.permissions div.field div.group { div.field-couple.permissions div.field p.group {
margin-bottom: 10px;
}
div.field-wrapper.permissions div.field div.group p {
font-weight: bold; font-weight: bold;
} }
div.field-wrapper.permissions div.field label { div.field-couple.permissions div.field label {
float: none; float: none;
font-weight: normal; font-weight: normal;
} }
div.field-wrapper.permissions div.field label input { div.field-couple.permissions div.field label input {
margin-left: 15px;
margin-right: 10px;
}
div.field-wrapper.permissions div.field div.group p.perm {
font-weight: normal;
margin-left: 15px;
}
div.field-wrapper.permissions div.field div.group p.perm span {
font-family: monospace;
/* font-weight: bold; */
margin-right: 10px; margin-right: 10px;
} }

View file

@ -76,16 +76,11 @@ def context_found(event):
request.user = Session.query(edbob.User).get(uuid) request.user = Session.query(edbob.User).get(uuid)
def has_perm(perm): def has_perm(perm):
return has_permission(request.user, perm, session=Session()) if not request.user:
return False
return has_permission(request.user, perm)
request.has_perm = has_perm request.has_perm = has_perm
def has_any_perm(perms):
for perm in perms:
if has_permission(request.user, perm, session=Session()):
return True
return False
request.has_any_perm = has_any_perm
def get_referrer(default=None): def get_referrer(default=None):
if request.params.get('referrer'): if request.params.get('referrer'):
return request.params['referrer'] return request.params['referrer']

View file

@ -1,15 +0,0 @@
<%inherit file="/base.mako" />
<%def name="title()">Change Password</%def>
<div class="form">
${h.form(url('change_password'))}
${form.referrer_field()}
${form.field_div('current_password', form.password('current_password'))}
${form.field_div('new_password', form.password('new_password'))}
${form.field_div('confirm_password', form.password('confirm_password'))}
<div class="buttons">
${h.submit('submit', "Change Password")}
</div>
${h.end_form()}
</div>

View file

@ -1,18 +1,3 @@
<%inherit file="/form.mako" /> <%inherit file="/edbob/crud.mako" />
<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+h.literal(str(form.fieldset.model))}</%def>
<%def name="head_tags()">
${parent.head_tags()}
<script type="text/javascript">
$(function() {
$('a.delete').click(function() {
if (! confirm("Do you really wish to delete this object?")) {
return false;
}
});
});
</script>
</%def>
${parent.body()} ${parent.body()}

View file

@ -18,8 +18,7 @@
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.autocomplete.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.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/grids.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/grids.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/filters.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/filters.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/forms.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/forms.css'))}
@ -38,7 +37,7 @@
<h1 class="left">${self.title()}</h1> <h1 class="left">${self.title()}</h1>
<div id="login" class="right"> <div id="login" class="right">
% if request.user: % if request.user:
${h.link_to(request.user.display_name, url('change_password'), class_='username')} logged in as <strong>${request.user.display_name}</strong>
(${h.link_to("logout", url('logout'))}) (${h.link_to("logout", url('logout'))})
% else: % else:
${h.link_to("login", url('login'))} ${h.link_to("login", url('login'))}

View file

@ -0,0 +1,5 @@
<%inherit file="/form.mako" />
<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+str(form.fieldset.model)}</%def>
${parent.body()}

View file

@ -1,7 +1,7 @@
<div class="fieldset"> <div class="fieldset">
% for field in fieldset.render_fields.itervalues(): % for field in fieldset.render_fields.itervalues():
% if field.requires_label: % if field.requires_label:
<div class="field-wrapper ${field.name}"> <div class="field-wrapper">
${field.label_tag()|n} ${field.label_tag()|n}
<div class="field"> <div class="field">
${field.render_readonly()} ${field.render_readonly()}

View file

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

View file

@ -1,28 +0,0 @@
<%inherit file="/crud.mako" />
<%def name="crud_name()">Person</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to People", url('people'))}</li>
</%def>
${parent.body()}
## % if fieldset.edit:
## <h2>User Info</h2>
## % if user:
## ${user.render()|n}
## <div class="buttons">
## <button type="button" onclick="location.href = '${url('user.edit', uuid=user.model.uuid)}';">Edit User</button>
## </div>
## % else:
## <p>This person does not have a user account.</p>
## ${h.form(url('user.new'))}
## ${h.hidden('User--person_uuid', value=fieldset.model.uuid)}
## ${h.hidden('User--username')}
## <div class="buttons">
## ${h.submit('submit', "Create User")}
## </div>
## ${h.end_form()}
## % endif
## % endif

View file

@ -1,11 +1,12 @@
<%inherit file="/grid.mako" /> <%inherit file="/people/base.mako" />
<%inherit file="/index.mako" />
<%def name="title()">People</%def> <%def name="title()">People</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
## % if request.has_perm('people.create'): % if request.has_perm('people.create'):
## <li>${h.link_to("Create a new Person", url('person.new'))}</li> <li>${h.link_to("Create a new Person", url('person.new'))}</li>
## % endif % endif
</%def> </%def>
${parent.body()} ${parent.body()}

View file

@ -0,0 +1,29 @@
<%inherit file="/people/base.mako" />
<%inherit file="/crud.mako" />
<%def name="crud_name()">Person</%def>
<%def name="menu()">
<p>${h.link_to("Back to People", url('people.list'))}</p>
</%def>
${parent.body()}
% if fieldset.edit:
<h2>User Info</h2>
% if user:
${user.render()|n}
<div class="buttons">
<button type="button" onclick="location.href = '${url('user.edit', uuid=user.model.uuid)}';">Edit User</button>
</div>
% else:
<p>This person does not have a user account.</p>
${h.form(url('user.new'))}
${h.hidden('User--person_uuid', value=fieldset.model.uuid)}
${h.hidden('User--username')}
<div class="buttons">
${h.submit('submit', "Create User")}
</div>
${h.end_form()}
% endif
% endif

View file

@ -5,8 +5,7 @@
<title>Working...</title> <title>Working...</title>
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))} ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))} ${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))}
<style type="text/css"> <style type="text/css">
#container { #container {
@ -62,29 +61,25 @@
$.ajax({ $.ajax({
url: '${url('progress', key=key)}', url: '${url('progress', key=key)}',
success: function(data) { success: function(data) {
if (data.complete || data.maximum) { $('#message').html(data.message);
$('#message').html(data.message); $('#total').html('('+data.maximum+' total)');
$('#total').html('('+data.maximum+' total)'); $('#cancel button').show();
$('#cancel button').show(); if (data.complete) {
if (data.complete) { clearInterval(updater);
clearInterval(updater); $('#cancel button').hide();
$('#cancel button').hide(); $('#total').html('done!');
$('#total').html('done!'); $('#complete').css('width', '100%');
$('#complete').css('width', '100%'); $('#remaining').hide();
$('#remaining').hide(); $('#percentage').html('100%');
$('#percentage').html('100%'); location.href = data.success_url;
location.href = data.success_url; } else {
} else { var width = parseInt(data.value) / parseInt(data.maximum);
var width = parseInt(data.value) / parseInt(data.maximum); width = Math.round(100 * width);
width = Math.round(100 * width); if (width > 0) {
if (width > 0) { $('#complete').css('width', width+'%');
$('#complete').css('width', width+'%'); $('#remaining').css('width', 'auto');
$('#remaining').css('width', 'auto');
}
$('#percentage').html(width+'%');
} }
} else if (data.error) { $('#percentage').html(width+'%');
location.href = '${cancel_url}';
} }
}, },
}); });
@ -117,7 +112,7 @@
<div id="wrapper"> <div id="wrapper">
<p><span id="message">${initial_msg or "Working"} (please wait)</span> ... <span id="total"></span></p> <p><span id="message">Working</span> ... <span id="total"></span></p>
<table id="progress-wrapper"> <table id="progress-wrapper">
<tr> <tr>

View file

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

View file

@ -1,18 +0,0 @@
<%inherit file="edbob.pyramid:templates/crud.mako" />
<%def name="head_tags()">
${parent.head_tags()}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/perms.css'))}
</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Roles", url('roles'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Role", url('role.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Role", url('role.read', uuid=form.fieldset.model.uuid))}</li>
% endif
<li>${h.link_to("Delete this Role", url('role.delete', uuid=form.fieldset.model.uuid), class_='delete')}</li>
</%def>
${parent.body()}

View file

@ -1,11 +1,10 @@
<%inherit file="/grid.mako" /> <%inherit file="/roles/base.mako" />
<%inherit file="/index.mako" />
<%def name="title()">Roles</%def> <%def name="title()">Roles</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
% if request.has_perm('roles.create'): <li>${h.link_to("Create a new Role", url('role.new'))}</li>
<li>${h.link_to("Create a new Role", url('role.create'))}</li>
% endif
</%def> </%def>
${parent.body()} ${parent.body()}

View file

@ -0,0 +1,15 @@
<%inherit file="/roles/base.mako" />
<%inherit file="/crud.mako" />
<%def name="crud_name()">Role</%def>
<%def name="head_tags()">
${parent.head_tags()}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/perms.css'))}
</%def>
<%def name="menu()">
<p>${h.link_to("Back to Roles", url('roles.list'))}</p>
</%def>
${parent.body()}

View file

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

View file

@ -1,12 +1,10 @@
<%inherit file="/users/base.mako" />
<%inherit file="/crud.mako" /> <%inherit file="/crud.mako" />
<%def name="crud_name()">User</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
<li>${h.link_to("Back to Users", url('users'))}</li> <li>${h.link_to("Back to Users", url('users.list'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def> </%def>
${parent.body()} ${parent.body()}

View file

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

View file

@ -1,11 +1,10 @@
<%inherit file="/grid.mako" /> <%inherit file="/users/base.mako" />
<%inherit file="/index.mako" />
<%def name="title()">Users</%def> <%def name="title()">Users</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
% if request.has_perm('users.create'): <li>${h.link_to("Create a new User", url('user.new'))}</li>
<li>${h.link_to("Create a new User", url('user.create'))}</li>
% endif
</%def> </%def>
${parent.body()} ${parent.body()}

View file

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

View file

@ -1,59 +0,0 @@
#!/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.tweens`` -- Tween Factories
"""
import sqlalchemy.exc
from transaction.interfaces import TransientError
def sqlerror_tween_factory(handler, registry):
"""
Produces a tween which will convert ``sqlalchemy.exc.OperationalError``
instances (caused by database server restart) into a retryable
``transaction.interfaces.TransientError`` instance, so that a second
attempt may be made to connect to the database before really giving up.
.. note::
This tween alone is not enough to cause the transaction to be retried;
it only marks the error as being *retryable*. If you wish more than one
attempt to be made, you must define the ``tm.attempts`` setting within
your Pyramid app configuration. See `Retrying
<http://docs.pylonsproject.org/projects/pyramid_tm/en/latest/#retrying>`_
for more information.
"""
def sqlerror_tween(request):
try:
response = handler(request)
except sqlalchemy.exc.OperationalError, error:
if error.connection_invalidated:
raise TransientError(str(error))
raise
return response
return sqlerror_tween

View file

@ -26,50 +26,17 @@
``edbob.pyramid.views.auth`` -- Auth Views ``edbob.pyramid.views.auth`` -- Auth Views
""" """
import formencode
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget from pyramid.security import remember, forget
import formencode
from pyramid_simpleform import Form from pyramid_simpleform import Form
import pyramid_simpleform.renderers from pyramid_simpleform.renderers import FormRenderer
from webhelpers.html import tags
from webhelpers.html.builder import HTML
import edbob import edbob
from edbob.db.auth import authenticate_user, set_user_password from edbob.db.auth import authenticate_user
from edbob.pyramid import Session from edbob.pyramid import Session
from edbob.util import prettify
class FormRenderer(pyramid_simpleform.renderers.FormRenderer):
"""
Customized form renderer. Provides some extra methods for convenience.
"""
# Note that as of this writing, this renderer is used only by the
# ``change_password`` view. This should probably change, and this class
# definition should be moved elsewhere.
def field_div(self, name, field, label=None):
errors = self.errors_for(name)
if errors:
errors = [HTML.tag('div', class_='field-error', c=x) for x in errors]
errors = tags.literal('').join(errors)
label = HTML.tag('label', for_=name, c=label or prettify(name))
inner = HTML.tag('div', class_='field', c=field)
outer_class = 'field-wrapper'
if errors:
outer_class += ' error'
outer = HTML.tag('div', class_=outer_class, c=(errors or '') + label + inner)
return outer
def referrer_field(self):
return self.hidden('referrer', value=self.form.request.get_referrer())
class UserLogin(formencode.Schema): class UserLogin(formencode.Schema):
allow_extra_fields = True allow_extra_fields = True
filter_extra_fields = True filter_extra_fields = True
@ -125,47 +92,6 @@ def logout(request):
return HTTPFound(location=referrer, headers=headers) return HTTPFound(location=referrer, headers=headers)
class CurrentPasswordCorrect(formencode.validators.FancyValidator):
def _to_python(self, value, state):
user = state
if not authenticate_user(user.username, value, session=Session()):
raise formencode.Invalid("The password is incorrect.", value, state)
return value
class ChangePassword(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
current_password = formencode.All(
formencode.validators.NotEmpty(),
CurrentPasswordCorrect())
new_password = formencode.validators.NotEmpty()
confirm_password = formencode.validators.NotEmpty()
chained_validators = [formencode.validators.FieldsMatch(
'new_password', 'confirm_password')]
def change_password(request):
"""
Allows a user to change his or her password.
"""
if not request.user:
return HTTPFound(location=request.route_url('home'))
form = Form(request, schema=ChangePassword, state=request.user)
if form.validate():
set_user_password(request.user, form.data['new_password'])
return HTTPFound(location=request.get_referrer())
return {'form': FormRenderer(form)}
def includeme(config): def includeme(config):
config.add_route('login', '/login') config.add_route('login', '/login')
@ -173,6 +99,3 @@ def includeme(config):
config.add_route('logout', '/logout') config.add_route('logout', '/logout')
config.add_view(logout, route_name='logout') config.add_view(logout, route_name='logout')
config.add_route('change_password', '/change-password')
config.add_view(change_password, route_name='change_password', renderer='/change_password.mako')

View file

@ -59,15 +59,12 @@ class AutocompleteView(View):
def query(self, query): def query(self, query):
return self.make_query(query) return self.make_query(query)
def display(self, instance):
return getattr(instance, self.fieldname)
def __call__(self): def __call__(self):
query = self.request.params['query'] query = self.request.params['query']
objs = self.query(query).all() objs = self.query(query).all()
data = dict( data = dict(
query=query, query=query,
suggestions=[self.display(x) for x in objs], suggestions=[getattr(x, self.fieldname) for x in objs],
data=[x.uuid for x in objs], data=[x.uuid for x in objs],
) )
return data return data

View file

@ -26,240 +26,155 @@
``edbob.pyramid.views.people`` -- Person Views ``edbob.pyramid.views.people`` -- Person Views
""" """
from sqlalchemy import and_ import transaction
from pyramid.httpexceptions import HTTPFound
# import transaction from formalchemy import Field
# from pyramid.httpexceptions import HTTPFound
# from formalchemy import Field import edbob
from edbob.pyramid import filters
# from edbob.pyramid import filters from edbob.pyramid import forms
# from edbob.pyramid import forms from edbob.pyramid import grids
# from edbob.pyramid import grids from edbob.pyramid import Session
# from edbob.pyramid import Session
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
from edbob.db.extensions.contact.model import (
Person, PersonEmailAddress, PersonPhoneNumber)
class PeopleGrid(SearchableAlchemyGridView): def filter_map():
return filters.get_filter_map(
edbob.Person,
ilike=['first_name', 'last_name', 'display_name'])
mapped_class = Person def search_config(request, fmap):
config_prefix = 'people' return filters.get_search_config(
sort = 'first_name' 'people.list', request, fmap,
include_filter_display_name=True,
filter_type_display_name='lk')
def join_map(self): def search_form(config):
return { return filters.get_search_form(config)
'email':
lambda q: q.outerjoin(PersonEmailAddress, and_(
PersonEmailAddress.parent_uuid == Person.uuid,
PersonEmailAddress.preference == 1)),
'phone':
lambda q: q.outerjoin(PersonPhoneNumber, and_(
PersonPhoneNumber.parent_uuid == Person.uuid,
PersonPhoneNumber.preference == 1)),
}
def filter_map(self): def grid_config(request, search, fmap):
return self.make_filter_map( return grids.get_grid_config(
ilike=['first_name', 'last_name'], 'people.list', request, search,
email=self.filter_ilike(PersonEmailAddress.address), filter_map=fmap, sort='display_name')
phone=self.filter_ilike(PersonPhoneNumber.number))
def filter_config(self): def sort_map():
return self.make_filter_config( return grids.get_sort_map(
include_filter_first_name=True, edbob.Person,
filter_type_first_name='lk', ['first_name', 'last_name', 'display_name'])
include_filter_last_name=True,
filter_type_last_name='lk',
filter_label_phone="Phone Number",
filter_label_email="Email Address")
def sort_map(self): def query(config):
return self.make_sort_map( smap = sort_map()
'first_name', 'last_name', q = Session.query(edbob.Person)
email=self.sorter(PersonEmailAddress.address), q = filters.filter_query(q, config)
phone=self.sorter(PersonPhoneNumber.number)) q = grids.sort_query(q, config, smap)
return q
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.first_name,
g.last_name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
g.clickable = True
g.click_route_name = 'person.read'
return g
class PersonCrud(CrudView): def people(context, request):
mapped_class = Person fmap = filter_map()
home_route = 'people' config = search_config(request, fmap)
search = search_form(config)
config = grid_config(request, search, fmap)
people = grids.get_pager(query, config)
def fieldset(self, model): g = forms.AlchemyGrid(
fs = self.make_fieldset(model) edbob.Person, people, config,
fs.configure( gridurl=request.route_url('people.list'),
include=[ objurl='person.edit')
fs.first_name,
fs.last_name, g.configure(
fs.phone.label("Phone Number"), include=[
fs.email.label("Email Address"), g.first_name,
]) g.last_name,
return fs g.display_name,
],
readonly=True)
grid = g.render(class_='clickable people')
return grids.render_grid(request, grid, search)
# def filter_map(): def person_fieldset(person, request):
# return filters.get_filter_map( fs = forms.make_fieldset(person, url=request.route_url,
# edbob.Person, url_action=request.current_route_url(),
# ilike=['first_name', 'last_name', 'display_name']) route_name='people.list')
fs.configure(
# def search_config(request, fmap): include=[
# return filters.get_search_config( fs.first_name,
# 'people.list', request, fmap, fs.last_name,
# include_filter_display_name=True, fs.display_name,
# filter_type_display_name='lk') ])
return fs
# def search_form(config):
# return filters.get_search_form(config)
# def grid_config(request, search, fmap):
# return grids.get_grid_config(
# 'people.list', request, search,
# filter_map=fmap, sort='display_name')
# def sort_map():
# return grids.get_sort_map(
# edbob.Person,
# ['first_name', 'last_name', 'display_name'])
# def query(config):
# smap = sort_map()
# q = Session.query(edbob.Person)
# q = filters.filter_query(q, config)
# q = grids.sort_query(q, config, smap)
# return q
# def people(context, request): def new_person(context, request):
# fmap = filter_map() fs = person_fieldset(edbob.Person, request)
# config = search_config(request, fmap) if not fs.readonly and request.POST:
# search = search_form(config) fs.rebind(data=request.params)
# config = grid_config(request, search, fmap) if fs.validate():
# people = grids.get_pager(query, config)
# g = forms.AlchemyGrid( with transaction.manager:
# edbob.Person, people, config, fs.sync()
# gridurl=request.route_url('people.list'), Session.add(fs.model)
# objurl='person.edit') Session.flush()
request.session.flash("%s \"%s\" has been %s." % (
fs.crud_title, fs.get_display_text(),
'updated' if fs.edit else 'created'))
# g.configure( return HTTPFound(location=request.route_url('people.list'))
# include=[
# g.first_name,
# g.last_name,
# g.display_name,
# ],
# readonly=True)
# grid = g.render(class_='clickable people') return {'fieldset': fs, 'crud': True}
# return grids.render_grid(request, grid, search)
# def person_fieldset(person, request): def edit_person(request):
# fs = forms.make_fieldset(person, url=request.route_url, """
# url_action=request.current_route_url(), View for editing a :class:`edbob.Person` instance.
# route_name='people.list') """
# fs.configure(
# include=[
# fs.first_name,
# fs.last_name,
# fs.display_name,
# ])
# return fs
from edbob.pyramid.views.users import user_fieldset
# def new_person(context, request): uuid = request.matchdict['uuid']
person = Session.query(edbob.Person).get(uuid) if uuid else None
assert person
# fs = person_fieldset(edbob.Person, request) fs = person_fieldset(person, request)
# if not fs.readonly and request.POST: if request.POST:
# fs.rebind(data=request.params) fs.rebind(data=request.params)
# if fs.validate(): if fs.validate():
# with transaction.manager: with transaction.manager:
# fs.sync() fs.sync()
# Session.add(fs.model) fs.model = Session.merge(fs.model)
# Session.flush() request.session.flash("%s \"%s\" has been %s." % (
# request.session.flash("%s \"%s\" has been %s." % ( fs.crud_title, fs.get_display_text(),
# fs.crud_title, fs.get_display_text(), 'updated' if fs.edit else 'created'))
# 'updated' if fs.edit else 'created')) home = request.route_url('people.list')
# return HTTPFound(location=request.route_url('people.list')) return HTTPFound(location=home)
# return {'fieldset': fs, 'crud': True} user = fs.model.user
if user:
user = user_fieldset(user, request)
user.readonly = True
del user.person
del user.password
del user.confirm_password
return {'fieldset': fs, 'crud': True, 'user': user}
# def edit_person(request):
# """
# View for editing a :class:`edbob.Person` instance.
# """
# from edbob.pyramid.views.users import user_fieldset
# uuid = request.matchdict['uuid']
# person = Session.query(edbob.Person).get(uuid) if uuid else None
# assert person
# fs = person_fieldset(person, request)
# if request.POST:
# fs.rebind(data=request.params)
# if fs.validate():
# with transaction.manager:
# fs.sync()
# fs.model = Session.merge(fs.model)
# request.session.flash("%s \"%s\" has been %s." % (
# fs.crud_title, fs.get_display_text(),
# 'updated' if fs.edit else 'created'))
# home = request.route_url('people.list')
# return HTTPFound(location=home)
# user = fs.model.user
# if user:
# user = user_fieldset(user, request)
# user.readonly = True
# del user.person
# del user.password
# del user.confirm_password
# return {'fieldset': fs, 'crud': True, 'user': user}
def includeme(config): def includeme(config):
config.add_route('people', '/people') config.add_route('people.list', '/people')
config.add_view(PeopleGrid, route_name='people', config.add_view(people, route_name='people.list', renderer='/people/index.mako',
renderer='/people/index.mako', permission='people.list', http_cache=0)
permission='people.list')
# config.add_route('people.list', '/people') config.add_route('person.new', '/people/new')
# config.add_view(people, route_name='people.list', renderer='/people/index.mako', config.add_view(new_person, route_name='person.new', renderer='/people/person.mako',
# permission='people.list', http_cache=0) permission='people.create', http_cache=0)
config.add_route('person.read', '/people/{uuid}') config.add_route('person.edit', '/people/{uuid}/edit')
config.add_view(PersonCrud, attr='read', route_name='person.read', config.add_view(edit_person, route_name='person.edit', renderer='/people/person.mako',
renderer='/people/crud.mako', permission='people.edit', http_cache=0)
permission='people.read')
# config.add_route('person.new', '/people/new')
# config.add_view(new_person, route_name='person.new', renderer='/people/person.mako',
# permission='people.create', http_cache=0)
# config.add_route('person.edit', '/people/{uuid}/edit')
# config.add_view(edit_person, route_name='person.edit', renderer='/people/person.mako',
# permission='people.edit', http_cache=0)

View file

@ -32,10 +32,8 @@ from edbob.pyramid.progress import get_progress_session
def progress(request): def progress(request):
key = request.matchdict['key'] key = request.matchdict['key']
session = get_progress_session(request.session, key) session = get_progress_session(request.session, key)
if session.get('complete'): if session.get('complete') and session.get('success_msg'):
request.session.flash(session.get('success_msg', "The process has completed successfully.")) request.session.flash(session['success_msg'])
elif session.get('error'):
request.session.flash(session.get('error_msg', "An unspecified error occurred."), 'error')
return session return session
@ -43,9 +41,9 @@ def cancel(request):
key = request.matchdict['key'] key = request.matchdict['key']
session = get_progress_session(request.session, key) session = get_progress_session(request.session, key)
session.clear() session.clear()
session['canceled'] = True session['cancelled'] = True
session.save() session.save()
msg = request.params.get('cancel_msg', "The operation was canceled.") msg = request.params.get('cancel_msg', "The operation was cancelled.")
request.session.flash(msg) request.session.flash(msg)
return {} return {}

View file

@ -26,83 +26,75 @@
``edbob.pyramid.views.roles`` -- Role Views ``edbob.pyramid.views.roles`` -- Role Views
""" """
import transaction
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
import formalchemy from formalchemy import Field, FieldRenderer
from webhelpers.html import tags from webhelpers.html import literal
from webhelpers.html.builder import HTML from webhelpers.html.tags import checkbox, hidden
from edbob.db import auth import edbob
from edbob.db.auth import administrator_role, has_permission
from edbob.pyramid import filters
from edbob.pyramid import forms
from edbob.pyramid import grids
from edbob.pyramid import Session from edbob.pyramid import Session
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
from edbob.db.extensions.auth.model import Role
default_permissions = [ def filter_map():
return filters.get_filter_map(
edbob.Role,
ilike=['name'])
("People", [ def search_config(request, fmap):
('people.list', "List People"), return filters.get_search_config(
('people.read', "View Person"), 'roles.list', request, fmap,
('people.create', "Create Person"), include_filter_name=True,
('people.update', "Edit Person"), filter_type_name='lk')
('people.delete', "Delete Person"),
]),
("Roles", [ def search_form(config):
('roles.list', "List Roles"), return filters.get_search_form(config)
('roles.read', "View Role"),
('roles.create', "Create Role"),
('roles.update', "Edit Role"),
('roles.delete', "Delete Role"),
]),
("Users", [ def grid_config(request, search, fmap):
('users.list', "List Users"), return grids.get_grid_config(
('users.read', "View User"), 'roles.list', request, search,
('users.create', "Create User"), filter_map=fmap, sort='name')
('users.update', "Edit User"),
('users.delete', "Delete User"), def sort_map():
]), return grids.get_sort_map(edbob.Role, ['name'])
]
def query(config):
smap = sort_map()
q = Session.query(edbob.Role)
q = filters.filter_query(q, config)
q = grids.sort_query(q, config, smap)
return q
class RolesGrid(SearchableAlchemyGridView): def roles(request):
mapped_class = Role fmap = filter_map()
config_prefix = 'roles' config = search_config(request, fmap)
sort = 'name' search = search_form(config)
config = grid_config(request, search, fmap)
roles = grids.get_pager(query, config)
def filter_map(self): g = forms.AlchemyGrid(
return self.make_filter_map(ilike=['name']) edbob.Role, roles, config,
gridurl=request.route_url('roles.list'),
objurl='role.edit')
def filter_config(self): g.configure(
return self.make_filter_config( include=[
include_filter_name=True, g.name,
filter_type_name='lk') ],
readonly=True)
def sort_map(self): grid = g.render(class_='clickable roles')
return self.make_sort_map('name') return grids.render_grid(request, grid, search)
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.name,
],
readonly=True)
if self.request.has_perm('roles.read'):
g.clickable = True
g.click_route_name = 'role.read'
if self.request.has_perm('roles.update'):
g.editable = True
g.edit_route_name = 'role.update'
if self.request.has_perm('roles.delete'):
g.deletable = True
g.delete_route_name = 'role.delete'
return g
class PermissionsField(formalchemy.Field): class PermissionsField(Field):
def sync(self): def sync(self):
if not self.is_readonly(): if not self.is_readonly():
@ -110,108 +102,154 @@ class PermissionsField(formalchemy.Field):
role.permissions = self.renderer.deserialize() role.permissions = self.renderer.deserialize()
def PermissionsFieldRenderer(permissions, *args, **kwargs): class PermissionsFieldRenderer(FieldRenderer):
perms = permissions available_permissions = [
("Batches", [
('batches.list', "List Batches"),
('batches.edit', "Edit Batch"),
('batches.create', "Create Batch"),
]),
("Roles", [
('roles.list', "List Roles"),
('roles.edit', "Edit Role"),
('roles.create', "Create Role"),
]),
]
def deserialize(self):
perms = []
i = len(self.name) + 1
for key in self.params:
if key.startswith(self.name):
perms.append(key[i:])
return perms
def _render(self, readonly=False, **kwargs):
# result = literal('')
# for group_name, group_label, perm_list in self.field.model_value:
# rendered_group_name = literal('<p class="permission-group">' + group_label + '</p>\n')
# if readonly:
# result += literal('<tr><td colspan="2">') + rendered_group_name + literal('</td></tr>')
# else:
# result += rendered_group_name
# result += literal('<div>')
# for perm_name, perm_label, checked in perm_list:
# if readonly:
# result += literal('<tr>'
# + '<td class="permission">' + ('[X]' if checked else '[&nbsp; ]') + '</td>'
# + '<td class="permission-label">' + perm_label + '</td>'
# + '</tr>\n')
# else:
# name = '.'.join((self.name, group_name, perm_name))
# result += check_box(name, label=perm_label, checked=checked)
# if not readonly:
# result += literal('</div>')
# if readonly:
# return literal('<table class="permissions">') + result + literal('</table>')
# return literal('<div class="permissions">') + result + literal('</div>')
role = self.field.model
if role is administrator_role(Session()):
res = literal('<p>This is the administrative role; '
'it has full access to the entire system.</p>')
if not readonly:
res += hidden(self.name, value='') # ugly hack..or good idea?
else:
res = ''
for group, perms in self.available_permissions:
res += literal('<p class="group">%s</p>' % group)
for perm, title in perms:
if readonly:
res += literal('<p>%s</p>' % title)
else:
checked = has_permission(role, perm)
res += checkbox(self.name + '-' + perm,
checked=checked, label=title)
return res
def render(self, **kwargs):
return self._render(**kwargs)
def render_readonly(self, **kwargs):
return self._render(readonly=True, **kwargs)
def role_fieldset(role, request):
fs = forms.make_fieldset(role, url=request.route_url,
url_action=request.current_route_url(),
route_name='roles.list')
class PermissionsFieldRenderer(formalchemy.FieldRenderer): fs.append(PermissionsField('permissions',
renderer=PermissionsFieldRenderer))
permissions = perms fs.configure(
include=[
fs.name,
fs.permissions,
])
def deserialize(self): if not fs.edit:
perms = [] del fs.permissions
i = len(self.name) + 1
for key in self.params:
if key.startswith(self.name):
perms.append(key[i:])
return perms
def _render(self, readonly=False, **kwargs): return fs
role = self.field.model
admin = auth.administrator_role(Session())
if role is admin:
html = HTML.tag('p', c="This is the administrative role; "
"it has full access to the entire system.")
if not readonly:
html += tags.hidden(self.name, value='') # ugly hack..or good idea?
else:
html = ''
for group, perms in self.permissions:
inner = HTML.tag('p', c=group)
for perm, title in perms:
checked = auth.has_permission(
role, perm, include_guest=False, session=Session())
if readonly:
span = HTML.tag('span', c="[X]" if checked else "[ ]")
inner += HTML.tag('p', class_='perm', c=span + ' ' + title)
else:
inner += tags.checkbox(self.name + '-' + perm,
checked=checked, label=title)
html += HTML.tag('div', class_='group', c=inner)
return html
def render(self, **kwargs):
return self._render(**kwargs)
def render_readonly(self, **kwargs):
return self._render(readonly=True, **kwargs)
return PermissionsFieldRenderer
class RoleCrud(CrudView): def new_role(request):
mapped_class = Role fs = role_fieldset(edbob.Role, request)
home_route = 'roles' if request.POST:
permissions = default_permissions fs.rebind(data=request.params)
if fs.validate():
def fieldset(self, role): with transaction.manager:
fs = self.make_fieldset(role) fs.sync()
fs.append(PermissionsField( fs.model = Session.merge(fs.model)
'permissions', request.session.flash("%s \"%s\" has been %s." % (
renderer=PermissionsFieldRenderer(self.permissions))) fs.crud_title, fs.get_display_text(),
fs.configure( 'updated' if fs.edit else 'created'))
include=[ home = request.route_url('roles.list')
fs.name,
fs.permissions,
])
return fs
def pre_delete(self, model): return HTTPFound(location=home)
admin = auth.administrator_role(Session())
guest = auth.guest_role(Session()) return {'fieldset': fs, 'crud': True}
if model in (admin, guest):
self.request.session.flash("You may not delete the %s role." % str(model), 'error')
return HTTPFound(location=self.request.get_referrer()) def edit_role(request):
uuid = request.matchdict['uuid']
role = Session.query(edbob.Role).get(uuid) if uuid else None
assert role
fs = role_fieldset(role, request)
if request.POST:
fs.rebind(data=request.params)
if fs.validate():
with transaction.manager:
Session.add(fs.model)
fs.sync()
request.session.flash("%s \"%s\" has been %s." % (
fs.crud_title, fs.get_display_text(),
'updated' if fs.edit else 'created'))
home = request.route_url('roles.list')
return HTTPFound(location=home)
return {'fieldset': fs, 'crud': True}
def includeme(config): def includeme(config):
config.add_route('roles', '/roles')
config.add_view(RolesGrid, route_name='roles',
renderer='/roles/index.mako',
permission='roles.list')
settings = config.get_settings() config.add_route('roles.list', '/roles')
perms = settings.get('edbob.permissions') config.add_view(roles, route_name='roles.list', renderer='/roles/index.mako',
if perms: permission='roles.list', http_cache=0)
RoleCrud.permissions = perms
config.add_route('role.create', '/roles/new') config.add_route('role.new', '/roles/new')
config.add_view(RoleCrud, attr='create', route_name='role.create', config.add_view(new_role, route_name='role.new', renderer='/roles/role.mako',
renderer='/roles/crud.mako', permission='roles.create', http_cache=0)
permission='roles.create')
config.add_route('role.read', '/roles/{uuid}') config.add_route('role.edit', '/roles/{uuid}/edit')
config.add_view(RoleCrud, attr='read', route_name='role.read', config.add_view(edit_role, route_name='role.edit', renderer='/roles/role.mako',
renderer='/roles/crud.mako', permission='roles.edit', http_cache=0)
permission='roles.read')
config.add_route('role.update', '/roles/{uuid}/edit')
config.add_view(RoleCrud, attr='update', route_name='role.update',
renderer='/roles/crud.mako',
permission='roles.update')
config.add_route('role.delete', '/roles/{uuid}/delete')
config.add_view(RoleCrud, attr='delete', route_name='role.delete',
permission='roles.delete')

View file

@ -26,35 +26,35 @@
``edbob.pyramid.views.users`` -- User Views ``edbob.pyramid.views.users`` -- User Views
""" """
from webhelpers.html import tags from webhelpers.html import literal, tags
from webhelpers.html.builder import HTML
import formalchemy import formalchemy
from formalchemy.fields import SelectFieldRenderer from formalchemy.fields import SelectFieldRenderer
from edbob.db import auth import edbob
from edbob.db.auth import set_user_password
from edbob.pyramid import Session from edbob.pyramid import Session
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView from edbob.pyramid.views import SearchableAlchemyGridView
from edbob.db.extensions.auth.model import User, Role from edbob.pyramid.views.crud import Crud
from edbob.db.extensions.contact.model import Person
class UsersGrid(SearchableAlchemyGridView): class UsersGrid(SearchableAlchemyGridView):
mapped_class = User mapped_class = edbob.User
config_prefix = 'users' route_name = 'users'
route_url = '/users'
sort = 'username' sort = 'username'
def join_map(self): def join_map(self):
return { return {
'person': 'person':
lambda q: q.outerjoin(Person), lambda q: q.outerjoin(edbob.Person),
} }
def filter_map(self): def filter_map(self):
return self.make_filter_map( return self.make_filter_map(
ilike=['username'], ilike=['username'],
person=self.filter_ilike(Person.display_name)) person=self.filter_ilike(edbob.Person.display_name))
def filter_config(self): def filter_config(self):
return self.make_filter_config( return self.make_filter_config(
@ -66,7 +66,7 @@ class UsersGrid(SearchableAlchemyGridView):
def sort_map(self): def sort_map(self):
return self.make_sort_map( return self.make_sort_map(
'username', 'username',
person=self.sorter(Person.display_name)) person=self.sorter(edbob.Person.display_name))
def grid(self): def grid(self):
g = self.make_grid() g = self.make_grid()
@ -76,34 +76,25 @@ class UsersGrid(SearchableAlchemyGridView):
g.person, g.person,
], ],
readonly=True) readonly=True)
if self.request.has_perm('users.read'):
g.clickable = True
g.click_route_name = 'user.read'
if self.request.has_perm('users.update'):
g.editable = True
g.edit_route_name = 'user.update'
if self.request.has_perm('users.delete'):
g.deletable = True
g.delete_route_name = 'user.delete'
return g return g
class _RolesFieldRenderer(SelectFieldRenderer):
def render_readonly(self, **kwargs):
roles = Session.query(edbob.Role)
res = literal('<ul>')
for uuid in self.value:
role = roles.get(uuid)
res += literal('<li>%s</li>' % (
tags.link_to(role.name,
self.request.route_url('role.edit', uuid=role.uuid))))
res += literal('</ul>')
return res
def RolesFieldRenderer(request): def RolesFieldRenderer(request):
return type('RolesFieldRenderer', (_RolesFieldRenderer,), {'request': request})
class RolesFieldRenderer(SelectFieldRenderer):
def render_readonly(self, **kwargs):
roles = Session.query(Role)
html = ''
for uuid in self.value:
role = roles.get(uuid)
link = tags.link_to(
role.name, request.route_url('role.read', uuid=role.uuid))
html += HTML.tag('li', c=link)
html = HTML.tag('ul', c=html)
return html
return RolesFieldRenderer
class RolesField(formalchemy.Field): class RolesField(formalchemy.Field):
@ -118,15 +109,14 @@ class RolesField(formalchemy.Field):
return [x.uuid for x in user.roles] return [x.uuid for x in user.roles]
def get_options(self): def get_options(self):
q = Session.query(Role.name, Role.uuid) q = Session.query(edbob.Role.name, edbob.Role.uuid)
q = q.filter(Role.uuid != auth.guest_role(Session()).uuid) q = q.order_by(edbob.Role.name)
q = q.order_by(Role.name)
return q.all() return q.all()
def sync(self): def sync(self):
if not self.is_readonly(): if not self.is_readonly():
user = self.model user = self.model
roles = Session.query(Role) roles = Session.query(edbob.Role)
data = self.renderer.deserialize() data = self.renderer.deserialize()
user.roles = [roles.get(x) for x in data] user.roles = [roles.get(x) for x in data]
@ -141,7 +131,7 @@ class _ProtectedPersonRenderer(formalchemy.FieldRenderer):
def ProtectedPersonRenderer(uuid): def ProtectedPersonRenderer(uuid):
person = Session.query(Person).get(uuid) person = Session.query(edbob.Person).get(uuid)
assert person assert person
return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,), return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,),
{'person': person}) {'person': person})
@ -183,13 +173,13 @@ class PasswordField(formalchemy.Field):
if not self.is_readonly(): if not self.is_readonly():
password = self.renderer.deserialize() password = self.renderer.deserialize()
if password: if password:
auth.set_user_password(self.model, password) set_user_password(self.model, password)
class UserCrud(CrudView): class UserCrud(Crud):
mapped_class = User mapped_class = edbob.User
home_route = 'users' home_route = 'users.list'
def fieldset(self, user): def fieldset(self, user):
fs = self.make_fieldset(user) fs = self.make_fieldset(user)
@ -209,12 +199,8 @@ class UserCrud(CrudView):
fs.roles, fs.roles,
]) ])
if self.readonly:
del fs.password
del fs.confirm_password
# if fs.edit and user.person: # if fs.edit and user.person:
if isinstance(user, User) and user.person: if isinstance(user, edbob.User) and user.person:
fs.person.set(readonly=True, fs.person.set(readonly=True,
renderer=LinkedPersonRenderer(self.request)) renderer=LinkedPersonRenderer(self.request))
@ -227,27 +213,5 @@ class UserCrud(CrudView):
def includeme(config): def includeme(config):
UsersGrid.add_route(config)
config.add_route('users', '/users') UserCrud.add_routes(config)
config.add_view(UsersGrid, route_name='users',
renderer='/users/index.mako',
permission='users.list')
config.add_route('user.create', '/users/new')
config.add_view(UserCrud, attr='create', route_name='user.create',
renderer='/users/crud.mako',
permission='users.create')
config.add_route('user.read', '/users/{uuid}')
config.add_view(UserCrud, attr='read', route_name='user.read',
renderer='/users/crud.mako',
permission='users.read')
config.add_route('user.update', '/users/{uuid}/edit')
config.add_view(UserCrud, attr='update', route_name='user.update',
renderer='/users/crud.mako',
permission='users.update')
config.add_route('user.delete', '/users/{uuid}/delete')
config.add_view(UserCrud, attr='delete', route_name='user.delete',
permission='users.delete')

View file

@ -42,7 +42,7 @@ def run_migrations_offline():
""" """
url = edbob.config.require('edbob.db', 'default.url') url = edbob.config.require('edbob.db', 'sqlalchemy.url')
context.configure(url=url) context.configure(url=url)
with context.begin_transaction(): with context.begin_transaction():

View file

@ -24,20 +24,8 @@ active_extensions = sa.Table(
def upgrade(): def upgrade():
${upgrades if upgrades else 'pass'} ${upgrades if upgrades else "pass"}
# active extensions
# op.execute(
# active_extensions.insert().values(
# name='dummy'))
def downgrade(): def downgrade():
${downgrades if downgrades else 'pass'} ${downgrades if downgrades else "pass"}
# active extensions
# op.execute(
# active_extensions.delete().where(
# active_extensions.c.name == 'dummy'))

View file

@ -7,8 +7,10 @@
import os.path import os.path
from pyramid.config import Configurator from pyramid.config import Configurator
from pyramid.authentication import SessionAuthenticationPolicy
import edbob import edbob
from edbob.pyramid.auth import EdbobAuthorizationPolicy
def main(global_config, **settings): def main(global_config, **settings):
@ -25,18 +27,24 @@ def main(global_config, **settings):
# * Raise an exception if a setting is missing or invalid. # * Raise an exception if a setting is missing or invalid.
# * Convert values from strings to their intended type. # * Convert values from strings to their intended type.
settings.setdefault('mako.directories', [ settings['mako.directories'] = [
'{{package}}.pyramid:templates', '{{package}}.pyramid:templates',
'edbob.pyramid:templates', 'edbob.pyramid:templates',
]) ]
# Make two attempts when "retryable" errors happen during transactions.
settings.setdefault('tm.attempts', 2)
config = Configurator(settings=settings) config = Configurator(settings=settings)
# Configure edbob # Configure edbob
edbob.init('{{package}}', os.path.abspath(settings['edbob.config'])) edbob.init('{{package}}', os.path.abspath(settings['edbob.config']))
# Configure session
config.include('pyramid_beaker')
# Configure auth
config.set_authentication_policy(SessionAuthenticationPolicy())
config.set_authorization_policy(EdbobAuthorizationPolicy())
# Include "core" stuff provided by edbob.
config.include('edbob.pyramid') config.include('edbob.pyramid')
# Additional config is defined elsewhere within {{project}}. This includes # Additional config is defined elsewhere within {{project}}. This includes
@ -45,8 +53,4 @@ def main(global_config, **settings):
config.include('{{package}}.pyramid.subscribers') config.include('{{package}}.pyramid.subscribers')
config.include('{{package}}.pyramid.views') config.include('{{package}}.pyramid.views')
# Consider PostgreSQL server restart errors to be "retryable."
config.add_tween('edbob.pyramid.tweens.sqlerror_tween_factory',
under='pyramid_tm.tm_tween_factory')
return config.make_wsgi_app() return config.make_wsgi_app()

View file

@ -1,3 +1,2 @@
include *.txt *.ini *.cfg *.rst include *.txt *.ini *.cfg *.rst
recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
recursive-include {{package}}/alembic *.py

View file

@ -69,6 +69,7 @@ setup(
], ],
install_requires = requires, install_requires = requires,
tests_require = requires,
packages = find_packages(), packages = find_packages(),
include_package_data = True, include_package_data = True,

View file

@ -29,64 +29,23 @@
from __future__ import absolute_import from __future__ import absolute_import
from sqlalchemy import Table, Column, String from sqlalchemy import Table, Column, String
from sqlalchemy import engine_from_config as _engine_from_config
from edbob.core import get_uuid from edbob.core import get_uuid
from edbob.modules import load_spec
from edbob.time import utc_time from edbob.time import utc_time
__all__ = ['getset_factory', 'table_with_uuid', 'current_time'] __all__ = ['getset_factory', 'table_with_uuid', 'current_time']
def current_time(context):
"""
This function may be provided to the ``default`` parameter of a
:class:`sqlalchemy.Column` class definition. Doing so will ensure the
column's default value will be the current time in UTC.
"""
return utc_time(naive=True)
def engine_from_config(config, prefix='sqlalchemy.', **kwargs):
"""
Slightly enhanced version of the :func:`sqlalchemy.engine_from_config()`
function. This version is aware of the ``poolclass`` configuration
parameter, and will coerce it via :func:`edbob.load_spec()`.
Note that if a pool class is specified, the class should be represented
using the "spec" format and *not* pure dotted path notation, e.g.:
Correct::
[edbob.db]
default.poolclass = sqlqlchemy.pool:NullPool
Incorrect::
[edbob.db]
default.poolclass = sqlalchemy.pool.NullPool
"""
if config.has_key(prefix + 'poolclass'):
config[prefix + 'poolclass'] = load_spec(config[prefix + 'poolclass'])
return _engine_from_config(config, prefix=prefix, **kwargs)
def getset_factory(collection_class, proxy): def getset_factory(collection_class, proxy):
""" """
Get/set factory for SQLAlchemy association proxy attributes. Helper function, useful for SQLAlchemy's "association proxy" configuration.
""" """
def getter(obj): def getter(obj):
if obj is None: if obj is None: return None
return None
return getattr(obj, proxy.value_attr) return getattr(obj, proxy.value_attr)
setter = lambda obj, val: setattr(obj, proxy.value_attr, val)
def setter(obj, val):
setattr(obj, proxy.value_attr, val)
return getter, setter return getter, setter
@ -114,3 +73,13 @@ def table_with_uuid(name, metadata, *args, **kwargs):
return Table(name, metadata, return Table(name, metadata,
Column('uuid', String(32), primary_key=True, default=get_uuid), Column('uuid', String(32), primary_key=True, default=get_uuid),
*args, **kwargs) *args, **kwargs)
def current_time(context):
"""
This function may be provided to the ``default`` parameter of a
:class:`sqlalchemy.Column` class definition. Doing so will ensure the
column's default value will be the current time in UTC.
"""
return utc_time(naive=True)

View file

@ -1,9 +0,0 @@
An unhandled exception occurred.
*Machine Name:* ${host_name} (${host_ip})
*Machine Time:* ${host_time.strftime('%Y-%m-%d %H:%M:%S %Z%z')}
<pre>
${traceback}
</pre>

View file

@ -1,19 +0,0 @@
import unittest
from pyramid import testing
class TestCase(unittest.TestCase):
"""
Base class for all test suites.
"""
def setUp(self):
self.config = testing.setUp()
def tearDown(self):
testing.tearDown()
def test_something(self):
self.assertTrue(1)

View file

@ -75,7 +75,7 @@ def init(config):
tz = config.get('edbob.time', key) tz = config.get('edbob.time', key)
if tz: if tz:
key = key[5:] key = key[5:]
log.debug("'%s' timezone set to '%s'" % (key, tz)) log.info("'%s' timezone set to '%s'" % (key, tz))
set_timezone(tz, key) set_timezone(tz, key)
if 'local' not in timezones: if 'local' not in timezones:

View file

@ -31,13 +31,6 @@ from pkg_resources import iter_entry_points
import edbob import edbob
# Import OrderedDict for the sake of other modules.
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
def entry_point_map(key): def entry_point_map(key):
""" """
Convenience function to retrieve a dictionary of entry points, keyed by Convenience function to retrieve a dictionary of entry points, keyed by

View file

@ -28,104 +28,16 @@
import sys import sys
import subprocess import subprocess
import logging
if sys.platform == 'win32': # docs should build for everyone if sys.platform == 'win32': # docs should build for everyone
import pywintypes import pywintypes
import win32api import win32api
import win32con import win32con
import win32event
import win32file import win32file
import win32print import win32print
import win32service import win32service
import winerror import winerror
try:
import win32serviceutil
except ImportError:
# Mock out for testing on Linux.
class Object(object):
pass
win32serviceutil = Object()
win32serviceutil.ServiceFramework = Object
import edbob
log = logging.getLogger(__name__)
class Service(win32serviceutil.ServiceFramework):
"""
Base class for Windows service implementations.
"""
appname = 'edbob'
def __init__(self, args):
"""
Constructor.
"""
win32serviceutil.ServiceFramework.__init__(self, args)
# Create "wait stop" event, for main worker loop.
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def Initialize(self):
"""
Service initialization.
"""
# Read configuration file(s).
edbob.init(self.appname, service=self._svc_name_)
return True
def SvcDoRun(self):
"""
This method is invoked when the service starts.
"""
import servicemanager
# Write start occurrence to Windows Event Log.
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, ''))
# Figure out what we're supposed to be doing.
if self.Initialize():
# Wait infinitely for stop request, while threads do their thing.
log.info("SvcDoRun: All threads started; waiting for stop request.")
win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
log.info("SvcDoRun: Stop request received.")
else: # Nothing to be done...
msg = "Nothing to do! (Initialization failed.)"
servicemanager.LogWarningMsg(msg)
log.warning("SvcDoRun: %s" % msg)
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Write stop occurrence to Windows Event Log.
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STOPPED,
(self._svc_name_, ''))
def SvcStop(self):
"""
This method is invoked when the service is requested to stop itself.
"""
# Let the SCM know we're trying to stop.
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# Let worker loop know its job is done.
win32event.SetEvent(self.hWaitStop)
def RegDeleteTree(key, subkey): def RegDeleteTree(key, subkey):
""" """
@ -171,20 +83,6 @@ def RegDeleteTree(key, subkey):
pass pass
def capture_output(command):
"""
Runs ``command`` and returns any output it produces.
"""
# We *need* to pipe ``stdout`` because that's how we capture the output of
# the ``hg`` command. However, we must pipe *all* handles in order to
# prevent issues when running as a GUI but *from* the Windows console.
# See also: http://bugs.python.org/issue3905
kwargs = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = subprocess.Popen(command, **kwargs).communicate()[0]
return output
def delayed_auto_start_service(name): def delayed_auto_start_service(name):
""" """
Configures the Windows service named ``name`` such that its startup type is Configures the Windows service named ``name`` such that its startup type is

36
fabfile.py vendored
View file

@ -1,36 +0,0 @@
#!/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/>.
#
################################################################################
import shutil
from fabric.api import *
@task
def release():
"""
Release a new version of 'edbob'.
"""
shutil.rmtree('edbob.egg-info')
local('python setup.py sdist --formats=gztar register upload')

View file

@ -1,7 +1,2 @@
[nosetests] [egg_info]
nocapture = 1 tag_build = .dev
cover-package = edbob
cover-erase = 1
cover-inclusive = 1
cover-html = 1
cover-html-dir = htmlcov

View file

@ -73,10 +73,7 @@ requires = [
'decorator', # 3.3.2 'decorator', # 3.3.2
'lockfile', # 0.9.1 'lockfile', # 0.9.1
'progressbar', # 2.3 'progressbar', # 2.3
'pytz', # 2012b
# Hardcode ``pytz`` minimum since apparently it isn't (any longer?) enough
# to simply require the library.
'pytz>=2013b', # 2013b
] ]
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
@ -97,7 +94,6 @@ extras = {
# #
# package # low high # package # low high
'alembic', # 0.3.1
'SQLAlchemy', # 0.7.6 'SQLAlchemy', # 0.7.6
# 'Tempita', # 0.5.1 # 'Tempita', # 0.5.1
], ],
@ -212,8 +208,6 @@ setup(
install_requires = requires, install_requires = requires,
extras_require = extras, extras_require = extras,
tests_require = requires + ['nose'],
test_suite = 'nose.collector',
packages = find_packages(), packages = find_packages(),
include_package_data = True, include_package_data = True,