Compare commits

..

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

62 changed files with 906 additions and 1205 deletions

View file

@ -1,135 +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
------

View file

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

View file

@ -28,7 +28,6 @@
from edbob._version import __version__
from edbob.enum import *
from edbob.core import *
from edbob.time import *
from edbob.files import *

View file

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

View file

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

View file

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

View file

@ -3,11 +3,10 @@
from __future__ import absolute_import
# This code was (mostly, with some tweaks) stolen from:
# This code was 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:
@ -66,7 +65,6 @@ class Daemon:
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)

View file

@ -105,7 +105,7 @@ def grant_permission(role, permission, session=None):
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
:class:`edbob.Role` instance), and returns a boolean indicating whether or
@ -124,9 +124,8 @@ def has_permission(obj, perm, include_guest=True, session=None):
if not session:
session = object_session(obj)
assert session
if include_guest:
roles.append(guest_role(session))
admin = administrator_role(session)
roles.append(guest_role(session))
for role in roles:
if role is admin:
return True

View file

@ -366,7 +366,7 @@ def extend_framework():
session.close()
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]
# merge_extension_metadata(ext)
# ext.extend_classes()

View file

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

View file

@ -23,7 +23,7 @@
################################################################################
"""
``edbob.enum`` -- Enumerations
``edbob.db.extensions.contact.enum`` -- Enumerations
"""
@ -38,14 +38,3 @@ EMAIL_PREFERENCE = {
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

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

View file

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

View file

@ -26,7 +26,6 @@
``edbob.errors`` -- Error Alert Emails
"""
import os.path
import sys
import socket
import logging
@ -34,7 +33,6 @@ from traceback import format_exception
from cStringIO import StringIO
import edbob
from edbob.files import resource_path
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):
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()
data['host_time'] = data['host_time'].strftime('%Y-%m-%d %H:%M:%S %Z%z')
body.write("""\
An unhandled exception occurred.
hostname = socket.gethostname()
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)
Machine Time: %(host_time)s
%(traceback)s
""" % data)
b = body.getvalue()
sendmail_with_config('errors', body.getvalue())
body.close()
return b, 'text/plain'

View file

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

View file

@ -26,18 +26,10 @@
``edbob.filemon`` -- File Monitoring Service
"""
import os
import os.path
import sys
import Queue
import logging
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__)
@ -73,12 +65,6 @@ class MonitorProfile(object):
self.locks = edbob.config.getboolean(
'%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):
"""
@ -124,105 +110,3 @@ def get_monitor_profiles(appname):
del monitored[key]
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 os
import os.path
import threading
import Queue
import signal
import logging
try:
import pyinotify
except ImportError:
# Mock out for testing on Windows.
class Dummy(object):
pass
pyinotify = Dummy()
pyinotify.ProcessEvent = Dummy
import edbob
from edbob import filemon
from edbob.daemon import Daemon
from edbob.errors import email_exception
from edbob.filemon import get_monitor_profiles
log = logging.getLogger(__name__)
@ -55,8 +45,9 @@ class EventHandler(pyinotify.ProcessEvent):
Event processor for file monitor daemon.
"""
def my_init(self, profile=None, **kwargs):
self.profile = profile
def my_init(self, actions=[], locks=False, **kwargs):
self.actions = actions
self.locks = locks
def process_IN_ACCESS(self, event):
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
@ -66,85 +57,84 @@ class EventHandler(pyinotify.ProcessEvent):
def process_IN_CLOSE_WRITE(self, event):
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
if not self.profile.locks:
self.profile.queue.put(event.pathname)
if not self.locks:
self.perform_actions(event.pathname)
def process_IN_CREATE(self, event):
log.debug("EventHandler: IN_CREATE: %s" % event.pathname)
def process_IN_DELETE(self, event):
log.debug("EventHandler: IN_DELETE: %s" % event.pathname)
if self.profile.locks and event.pathname.endswith('.lock'):
self.profile.queue.put(event.pathname[:-5])
if self.locks and event.pathname.endswith('.lock'):
self.perform_actions(event.pathname[:-5])
def process_IN_MODIFY(self, event):
log.debug("EventHandler: IN_MODIFY: %s" % event.pathname)
def process_IN_MOVED_TO(self, event):
log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname)
if not self.profile.locks:
self.profile.queue.put(event.pathname)
if not self.locks:
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
def start_daemon(appname, daemonize=True):
"""
Starts the file monitor daemon.
"""
pid_path = get_pid_path()
if os.path.exists(pid_path):
print "File monitor is already running"
return
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
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)
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 profile in monitored.itervalues():
for path in profile.dirs:
wm.add_watch(path, mask, proc_fun=EventHandler(
actions=profile.actions, locks=profile.locks))
# 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()
if not daemonize:
sys.stderr.write("Starting file monitor. (Press Ctrl+C to quit.)\n")
notifier.loop(daemonize=daemonize, pid_file=pid_path)
def get_daemon(appname=None):
if appname is None:
appname = os.path.basename(sys.argv[0])
pid_path = edbob.config.get('%s.filemon' % appname, 'pid_path')
if not pid_path:
pid_path = '/tmp/%s_filemon.pid' % appname
def stop_daemon():
"""
Stops the file monitor daemon.
"""
monitor = FileMonitorDaemon(pid_path)
monitor.appname = appname
return monitor
pid_path = get_pid_path()
if not os.path.exists(pid_path):
print "File monitor is not running"
return
f = open(pid_path)
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
def start_daemon(appname):
get_daemon(appname).start()
def stop_daemon(appname):
get_daemon(appname).stop()
os.kill(int(pid), signal.SIGKILL)
os.remove(pid_path)

View file

@ -33,8 +33,8 @@ import logging
import threading
import edbob
from edbob import filemon
from edbob.errors import email_exception
from edbob.filemon import get_monitor_profiles
from edbob.win32 import Service, file_is_free
if sys.platform == 'win32': # docs should build for everyone
@ -69,7 +69,7 @@ class FileMonitorService(Service):
return False
# 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.
if not self.monitored:
@ -79,36 +79,34 @@ class FileMonitorService(Service):
for key, profile in self.monitored.iteritems():
# 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):
# 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)
log.debug("Initialize: Starting '%s' thread for folder: %s" %
(name, path))
thread = threading.Thread(target=monitor_files,
name=name, args=(profile, path))
thread = threading.Thread(
target=monitor_files,
name=name,
args=(queue, path, profile))
thread.daemon = True
thread.start()
# Create an action thread for the profile.
name = 'actions-%s' % key
log.debug("Initialize: Starting '%s' thread" % name)
thread = threading.Thread(target=filemon.perform_actions,
name=name, args=(profile,))
thread = threading.Thread(
target=perform_actions,
name=name,
args=(queue, profile))
thread.daemon = True
thread.start()
return True
def monitor_files(profile, path):
def monitor_files(queue, path, profile):
"""
Callable target for file monitor threads.
"""
@ -140,7 +138,40 @@ def monitor_files(profile, path):
winnt.FILE_ACTION_RENAMED_NEW_NAME):
log.debug("monitor_files: Queueing '%s' file: %s" %
(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__':

View file

@ -33,8 +33,6 @@ import shutil
import tempfile
import lockfile
import pkg_resources
__all__ = ['temp_path']
@ -100,31 +98,6 @@ def count_lines(path):
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.'):
"""
Convenience function to return a temporary file path. The arguments'

View file

@ -89,7 +89,6 @@ def init(appname='edbob', *args, **kwargs):
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'

View file

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

View file

@ -50,8 +50,7 @@ from edbob.pyramid.forms.formalchemy.renderers import *
__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
'AssociationProxyField', 'StrippingFieldRenderer',
'YesNoFieldRenderer']
'AssociationProxyField', 'YesNoFieldRenderer']
class TemplateEngine(formalchemy.templates.TemplateEngine):

View file

@ -32,7 +32,7 @@ from pyramid.renderers import render
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
'StrippingFieldRenderer', 'YesNoFieldRenderer']
'YesNoFieldRenderer']
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
@ -83,17 +83,6 @@ def EnumFieldRenderer(enum):
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):
def render_readonly(self, **kwargs):

View file

@ -26,29 +26,15 @@
``edbob.pyramid.forms.simpleform`` -- pyramid_simpleform Forms
"""
from pyramid.renderers import render
import formencode
import pyramid_simpleform
from pyramid.renderers import render
from pyramid_simpleform.renderers import FormRenderer
from edbob.pyramid import helpers
from edbob.pyramid.forms import Form
__all__ = ['Schema', '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
__all__ = ['SimpleForm']
class SimpleForm(Form):

View file

@ -54,9 +54,6 @@ class AlchemyGrid(Grid):
self._formalchemy_grid.prettify = prettify
self.noclick_fields = []
def __delattr__(self, attr):
delattr(self._formalchemy_grid, attr)
def __getattr__(self, 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

@ -87,12 +87,81 @@ div.error {
margin-bottom: 10px;
}
ul.error {
color: #dd6666;
font-weight: bold;
padding: 0px;
/* td.right { */
/* float: none; */
/* } */
/* table.wrapper td.right { */
/* vertical-align: bottom; */
/* } */
/******************************
* Main Layout
******************************/
html, body, #container {
height: 100%;
}
ul.error li {
list-style-type: none;
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 {
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 {
clear: both;
min-height: 30px;
overflow: auto;
padding: 5px;
}
div.field-wrapper.error {
background-color: #ddcccc;
border: 2px solid #dd6666;
min-height: 30px;
}
div.field-wrapper label {
color: #000000;
display: block;
float: left;
width: 140px;
@ -78,15 +71,6 @@ div.field-wrapper div.field textarea {
width: 320px;
}
label input[type=checkbox] {
margin-right: 8px;
}
/******************************
* Buttons
******************************/
div.buttons {
clear: both;
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 {
margin-bottom: 10px;
}
div.field-wrapper.permissions div.field div.group p {
div.field-couple.permissions div.field p.group {
font-weight: bold;
}
div.field-wrapper.permissions div.field label {
div.field-couple.permissions div.field label {
float: none;
font-weight: normal;
}
div.field-wrapper.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; */
div.field-couple.permissions div.field label input {
margin-right: 10px;
}

View file

@ -79,13 +79,6 @@ def context_found(event):
return has_permission(request.user, perm, session=Session())
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):
if request.params.get('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" />
<%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>
<%inherit file="/edbob/crud.mako" />
${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/edbob.js'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.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/forms.css'))}
@ -38,7 +37,7 @@
<h1 class="left">${self.title()}</h1>
<div id="login" class="right">
% 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'))})
% else:
${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+' : '+h.literal(str(form.fieldset.model))}</%def>
${parent.body()}

View file

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

View file

@ -5,8 +5,7 @@
<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/edbob.js'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}
<style type="text/css">
#container {

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="context_menu_items()">
% if request.has_perm('roles.create'):
<li>${h.link_to("Create a new Role", url('role.create'))}</li>
% endif
<li>${h.link_to("Create a new Role", url('role.new'))}</li>
</%def>
${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" />
<%def name="crud_name()">User</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Users", url('users'))}</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
<li>${h.link_to("Back to Users", url('users.list'))}</li>
</%def>
${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="context_menu_items()">
% if request.has_perm('users.create'):
<li>${h.link_to("Create a new User", url('user.create'))}</li>
% endif
<li>${h.link_to("Create a new User", url('user.new'))}</li>
</%def>
${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,48 +26,15 @@
``edbob.pyramid.views.auth`` -- Auth Views
"""
import formencode
from pyramid.httpexceptions import HTTPFound
from pyramid.security import remember, forget
import formencode
from pyramid_simpleform import Form
import pyramid_simpleform.renderers
from webhelpers.html import tags
from webhelpers.html.builder import HTML
from pyramid_simpleform.renderers import FormRenderer
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.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):
@ -125,47 +92,6 @@ def logout(request):
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):
config.add_route('login', '/login')
@ -173,6 +99,3 @@ def includeme(config):
config.add_route('logout', '/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

@ -33,38 +33,37 @@ from sqlalchemy import and_
# from formalchemy import Field
import edbob
# from edbob.pyramid import filters
# from edbob.pyramid import forms
# from edbob.pyramid import grids
# 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):
mapped_class = Person
mapped_class = edbob.Person
config_prefix = 'people'
sort = 'first_name'
def join_map(self):
return {
'email':
lambda q: q.outerjoin(PersonEmailAddress, and_(
PersonEmailAddress.parent_uuid == Person.uuid,
PersonEmailAddress.preference == 1)),
lambda q: q.outerjoin(edbob.PersonEmailAddress, and_(
edbob.PersonEmailAddress.parent_uuid == edbob.Person.uuid,
edbob.PersonEmailAddress.preference == 1)),
'phone':
lambda q: q.outerjoin(PersonPhoneNumber, and_(
PersonPhoneNumber.parent_uuid == Person.uuid,
PersonPhoneNumber.preference == 1)),
lambda q: q.outerjoin(edbob.PersonPhoneNumber, and_(
edbob.PersonPhoneNumber.parent_uuid == edbob.Person.uuid,
edbob.PersonPhoneNumber.preference == 1)),
}
def filter_map(self):
return self.make_filter_map(
ilike=['first_name', 'last_name'],
email=self.filter_ilike(PersonEmailAddress.address),
phone=self.filter_ilike(PersonPhoneNumber.number))
email=self.filter_ilike(edbob.PersonEmailAddress.address),
phone=self.filter_ilike(edbob.PersonPhoneNumber.number))
def filter_config(self):
return self.make_filter_config(
@ -78,8 +77,8 @@ class PeopleGrid(SearchableAlchemyGridView):
def sort_map(self):
return self.make_sort_map(
'first_name', 'last_name',
email=self.sorter(PersonEmailAddress.address),
phone=self.sorter(PersonPhoneNumber.number))
email=self.sorter(edbob.PersonEmailAddress.address),
phone=self.sorter(edbob.PersonPhoneNumber.number))
def grid(self):
g = self.make_grid()
@ -98,7 +97,7 @@ class PeopleGrid(SearchableAlchemyGridView):
class PersonCrud(CrudView):
mapped_class = Person
mapped_class = edbob.Person
home_route = 'people'
def fieldset(self, model):

View file

@ -26,83 +26,75 @@
``edbob.pyramid.views.roles`` -- Role Views
"""
import transaction
from pyramid.httpexceptions import HTTPFound
import formalchemy
from webhelpers.html import tags
from webhelpers.html.builder import HTML
from formalchemy import Field, FieldRenderer
from webhelpers.html import literal
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.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", [
('people.list', "List People"),
('people.read', "View Person"),
('people.create', "Create Person"),
('people.update', "Edit Person"),
('people.delete', "Delete Person"),
]),
("Roles", [
('roles.list', "List Roles"),
('roles.read', "View Role"),
('roles.create', "Create Role"),
('roles.update', "Edit Role"),
('roles.delete', "Delete Role"),
]),
("Users", [
('users.list', "List Users"),
('users.read', "View User"),
('users.create', "Create User"),
('users.update', "Edit User"),
('users.delete', "Delete User"),
]),
]
class RolesGrid(SearchableAlchemyGridView):
mapped_class = Role
config_prefix = 'roles'
sort = 'name'
def filter_map(self):
return self.make_filter_map(ilike=['name'])
def filter_config(self):
return self.make_filter_config(
def search_config(request, fmap):
return filters.get_search_config(
'roles.list', request, fmap,
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('name')
def search_form(config):
return filters.get_search_form(config)
def grid_config(request, search, fmap):
return grids.get_grid_config(
'roles.list', request, search,
filter_map=fmap, sort='name')
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
def roles(request):
fmap = filter_map()
config = search_config(request, fmap)
search = search_form(config)
config = grid_config(request, search, fmap)
roles = grids.get_pager(query, config)
g = forms.AlchemyGrid(
edbob.Role, roles, config,
gridurl=request.route_url('roles.list'),
objurl='role.edit')
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
grid = g.render(class_='clickable roles')
return grids.render_grid(request, grid, search)
class PermissionsField(formalchemy.Field):
class PermissionsField(Field):
def sync(self):
if not self.is_readonly():
@ -110,13 +102,22 @@ class PermissionsField(formalchemy.Field):
role.permissions = self.renderer.deserialize()
def PermissionsFieldRenderer(permissions, *args, **kwargs):
class PermissionsFieldRenderer(FieldRenderer):
perms = permissions
available_permissions = [
class PermissionsFieldRenderer(formalchemy.FieldRenderer):
("Batches", [
('batches.list', "List Batches"),
('batches.edit', "Edit Batch"),
('batches.create', "Create Batch"),
]),
permissions = perms
("Roles", [
('roles.list', "List Roles"),
('roles.edit', "Edit Role"),
('roles.create', "Create Role"),
]),
]
def deserialize(self):
perms = []
@ -127,28 +128,47 @@ def PermissionsFieldRenderer(permissions, *args, **kwargs):
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
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 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:
html += tags.hidden(self.name, value='') # ugly hack..or good idea?
res += hidden(self.name, value='') # ugly hack..or good idea?
else:
html = ''
for group, perms in self.permissions:
inner = HTML.tag('p', c=group)
res = ''
for group, perms in self.available_permissions:
res += literal('<p class="group">%s</p>' % 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)
res += literal('<p>%s</p>' % title)
else:
inner += tags.checkbox(self.name + '-' + perm,
checked = has_permission(role, perm)
res += checkbox(self.name + '-' + perm,
checked=checked, label=title)
html += HTML.tag('div', class_='group', c=inner)
return html
return res
def render(self, **kwargs):
return self._render(**kwargs)
@ -156,62 +176,80 @@ def PermissionsFieldRenderer(permissions, *args, **kwargs):
def render_readonly(self, **kwargs):
return self._render(readonly=True, **kwargs)
return PermissionsFieldRenderer
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 RoleCrud(CrudView):
fs.append(PermissionsField('permissions',
renderer=PermissionsFieldRenderer))
mapped_class = Role
home_route = 'roles'
permissions = default_permissions
def fieldset(self, role):
fs = self.make_fieldset(role)
fs.append(PermissionsField(
'permissions',
renderer=PermissionsFieldRenderer(self.permissions)))
fs.configure(
include=[
fs.name,
fs.permissions,
])
if not fs.edit:
del fs.permissions
return fs
def pre_delete(self, model):
admin = auth.administrator_role(Session())
guest = auth.guest_role(Session())
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 new_role(request):
fs = role_fieldset(edbob.Role, 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('roles.list')
return HTTPFound(location=home)
return {'fieldset': fs, 'crud': True}
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):
config.add_route('roles', '/roles')
config.add_view(RolesGrid, route_name='roles',
renderer='/roles/index.mako',
permission='roles.list')
config.add_route('roles.list', '/roles')
config.add_view(roles, route_name='roles.list', renderer='/roles/index.mako',
permission='roles.list', http_cache=0)
settings = config.get_settings()
perms = settings.get('edbob.permissions')
if perms:
RoleCrud.permissions = perms
config.add_route('role.new', '/roles/new')
config.add_view(new_role, route_name='role.new', renderer='/roles/role.mako',
permission='roles.create', http_cache=0)
config.add_route('role.create', '/roles/new')
config.add_view(RoleCrud, attr='create', route_name='role.create',
renderer='/roles/crud.mako',
permission='roles.create')
config.add_route('role.read', '/roles/{uuid}')
config.add_view(RoleCrud, attr='read', route_name='role.read',
renderer='/roles/crud.mako',
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')
config.add_route('role.edit', '/roles/{uuid}/edit')
config.add_view(edit_role, route_name='role.edit', renderer='/roles/role.mako',
permission='roles.edit', http_cache=0)

View file

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

View file

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

View file

@ -7,8 +7,10 @@
import os.path
from pyramid.config import Configurator
from pyramid.authentication import SessionAuthenticationPolicy
import edbob
from edbob.pyramid.auth import EdbobAuthorizationPolicy
def main(global_config, **settings):
@ -25,18 +27,24 @@ def main(global_config, **settings):
# * Raise an exception if a setting is missing or invalid.
# * Convert values from strings to their intended type.
settings.setdefault('mako.directories', [
settings['mako.directories'] = [
'{{package}}.pyramid:templates',
'edbob.pyramid:templates',
])
# Make two attempts when "retryable" errors happen during transactions.
settings.setdefault('tm.attempts', 2)
]
config = Configurator(settings=settings)
# Configure edbob
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')
# 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.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()

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)
if tz:
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)
if 'local' not in timezones:

View file

@ -38,16 +38,8 @@ if sys.platform == 'win32': # docs should build for everyone
import win32file
import win32print
import win32service
import winerror
try:
import win32serviceutil
except ImportError:
# Mock out for testing on Linux.
class Object(object):
pass
win32serviceutil = Object()
win32serviceutil.ServiceFramework = Object
import winerror
import edbob
@ -171,20 +163,6 @@ def RegDeleteTree(key, subkey):
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):
"""
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]
nocapture = 1
cover-package = edbob
cover-erase = 1
cover-inclusive = 1
cover-html = 1
cover-html-dir = htmlcov
[egg_info]
tag_build = .dev

View file

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