Compare commits

..

42 commits

Author SHA1 Message Date
Lance Edgar
a0b35dfadb Remove custom PyPI stuff from Fabric release task. 2014-04-28 14:28:54 -07:00
Lance Edgar
8fabdf8b72 update changelog 2014-04-28 14:27:32 -07:00
Lance Edgar
73978ffeb7 Allow config file to prevent logging configuration from happening. 2014-04-28 14:26:21 -07:00
Lance Edgar
183530afc9 update changelog 2014-01-28 09:13:45 -08:00
Lance Edgar
8a0f300fc0 Add minimum version requirement for pytz. 2013-07-31 22:36:54 -07:00
Lance Edgar
ee9c6622ff Add mock import for testing on Linux. 2013-06-18 22:46:58 -07:00
Lance Edgar
4fe5ad9bf0 Prepare for tests.
This doesn't add any actual tests but it should pave the way for that.  Tests
may be run like so:

.. code-block:: sh

   python setup.py nosetests --with-coverage
2013-06-18 22:19:52 -07:00
Lance Edgar
25611f1e68 Tweaked Fabric `release` command. 2013-05-22 18:09:51 -07:00
Lance Edgar
4ed6b09a24 update changelog 2013-05-21 22:30:03 -07:00
Lance Edgar
3766dbfff1 Updated `repr()` output for model classes. 2013-05-17 12:21:39 -07:00
Lance Edgar
3bfda431fa Changed some logging instances from INFO to DEBUG.
I was just getting tired of the noise.
2013-05-16 07:00:40 -07:00
Lance Edgar
4dc4f3f1ec Removed setup.cfg file.
The `tag_build` setting was not doing us any favors.
2013-05-09 21:47:07 -07:00
Lance Edgar
2e57936ed1 update changelog 2013-04-30 16:16:02 -07:00
Lance Edgar
33c951e7fe Added initial Fabric script. 2013-04-30 16:13:47 -07:00
Lance Edgar
9262b598ac Added --progress argument to command system. 2013-04-30 16:12:20 -07:00
Lance Edgar
6c517a9126 Secured daemon PID files.
They are no longer readable or writeable (whoops) by anyone other than the user
who owns the process.
2013-04-22 08:15:46 -07:00
Lance Edgar
ea7473f18d update changelog 2013-02-17 08:59:47 -08:00
Lance Edgar
896a320da5 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.
2013-02-17 08:46:19 -08:00
Lance Edgar
7ae344403a update changelog 2013-02-12 22:15:18 -08:00
Lance Edgar
884e48970e fix graft() when using dir(module) 2012-11-29 07:19:06 -08:00
Lance Edgar
4bb4143daa tweak checkbox labels in forms.css 2012-11-28 15:48:29 -08:00
Lance Edgar
092e325adc add StrippingFieldRenderer 2012-11-28 09:31:31 -08:00
Lance Edgar
a6488e1089 bump version 2012-11-27 21:38:44 -08:00
Lance Edgar
88dcb1889c update changelog 2012-11-27 21:36:47 -08:00
Lance Edgar
f6bb5b2b77 raise InvalidSpec if no colon 2012-11-27 14:33:23 -08:00
Lance Edgar
64ae8e9136 fix css in progress template 2012-11-27 11:27:31 -08:00
Lance Edgar
d8744f3958 move contact enum to core 2012-11-26 11:15:59 -08:00
Lance Edgar
a1d22df20c add some __unicode__() methods 2012-11-21 10:05:24 -08:00
Lance Edgar
7928461e08 add PHONE_TYPE enum 2012-11-21 10:05:01 -08:00
Lance Edgar
8217b91aa4 add sqlerror_tween 2012-11-14 05:06:31 -08:00
Lance Edgar
c3f58d1b8c bump version 2012-11-13 07:56:54 -08:00
Lance Edgar
a249fceed5 update changelog 2012-11-13 07:55:38 -08:00
Lance Edgar
99866edac8 Merge branch 'master' of ssh://edbob.org/srv/git/edbob 2012-11-12 22:59:41 -08:00
Lance Edgar
c2339ba124 exclude guest when editing user roles 2012-11-12 22:53:27 -08:00
Lance Edgar
c79d6de56d fix guest bug in role perms editing 2012-11-12 22:52:37 -08:00
Lance Edgar
ba2c99d503 bump version 2012-11-12 15:17:22 -08:00
Lance Edgar
9eb33258b4 update changelog 2012-11-12 15:14:46 -08:00
Lance Edgar
9b2589ca12 update user/role management (now it works) 2012-11-12 07:41:03 -08:00
Lance Edgar
e95a23ead8 tweak footer css 2012-11-09 09:42:02 -08:00
Lance Edgar
ed04caf68c remove pyramid/handlers (what was that still doing there?) 2012-11-09 09:00:33 -08:00
Lance Edgar
fb4a49b570 add win32.capture_output() 2012-11-09 08:59:58 -08:00
Lance Edgar
8d3e474436 bump version 2012-11-08 19:11:22 -08:00
47 changed files with 818 additions and 797 deletions

View file

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

View file

@ -28,6 +28,7 @@
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.1a22'
__version__ = '0.1.2'

View file

@ -105,6 +105,7 @@ 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
@ -132,6 +133,7 @@ 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)
@ -182,6 +184,7 @@ 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))
@ -432,11 +435,6 @@ 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
@ -454,10 +452,10 @@ class FileMonitorCommand(Subcommand):
from edbob.filemon import linux as filemon
if args.subcommand == 'start':
filemon.start_daemon(self.appname, daemonize=args.daemonize)
filemon.start_daemon(self.appname)
elif args.subcommand == 'stop':
filemon.stop_daemon()
filemon.stop_daemon(self.appname)
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 = dir(source)
names = [x for x in dir(source) if not x.startswith('_')]
elif isinstance(names, basestring):
names = [names]

View file

@ -3,10 +3,11 @@
from __future__ import absolute_import
# This code was stolen from:
# 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:
@ -65,6 +66,7 @@ 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, session=None):
def has_permission(obj, perm, include_guest=True, 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,8 +124,9 @@ def has_permission(obj, perm, 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.info("Applying active extension: %s" % name)
log.debug("Applying active extension: %s" % name)
ext = extensions[name]
# merge_extension_metadata(ext)
# ext.extend_classes()

View file

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

View file

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

View file

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

@ -73,6 +73,12 @@ 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,10 +26,18 @@
``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__)
@ -65,6 +73,12 @@ 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):
"""
@ -110,3 +124,105 @@ 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,14 +27,24 @@
"""
import sys
import os
import os.path
import signal
import threading
import Queue
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
from edbob.filemon import get_monitor_profiles
from edbob import filemon
from edbob.daemon import Daemon
from edbob.errors import email_exception
log = logging.getLogger(__name__)
@ -45,9 +55,8 @@ class EventHandler(pyinotify.ProcessEvent):
Event processor for file monitor daemon.
"""
def my_init(self, actions=[], locks=False, **kwargs):
self.actions = actions
self.locks = locks
def my_init(self, profile=None, **kwargs):
self.profile = profile
def process_IN_ACCESS(self, event):
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
@ -57,87 +66,85 @@ class EventHandler(pyinotify.ProcessEvent):
def process_IN_CLOSE_WRITE(self, event):
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
if not self.locks:
self.perform_actions(event.pathname)
if not self.profile.locks:
self.profile.queue.put(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.locks and event.pathname.endswith('.lock'):
self.perform_actions(event.pathname[:-5])
if self.profile.locks and event.pathname.endswith('.lock'):
self.profile.queue.put(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.locks:
self.perform_actions(event.pathname)
def perform_actions(self, path):
for spec, func, args in self.actions:
func(path, *args)
if not self.profile.locks:
self.profile.queue.put(event.pathname)
def get_pid_path():
"""
Returns the path to the PID file for the file monitor daemon.
"""
class FileMonitorDaemon(Daemon):
basename = os.path.basename(sys.argv[0])
pid_path = edbob.config.get('%s.filemon' % basename, 'pid_path')
def run(self):
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):
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' % basename
return pid_path
pid_path = '/tmp/%s_filemon.pid' % appname
monitor = FileMonitorDaemon(pid_path)
monitor.appname = appname
return monitor
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)
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):
get_daemon(appname).start()
def stop_daemon():
"""
Stops the file monitor daemon.
"""
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
os.kill(int(pid), signal.SIGKILL)
os.remove(pid_path)
def stop_daemon(appname):
get_daemon(appname).stop()

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 = get_monitor_profiles(self.appname)
self.monitored = filemon.get_monitor_profiles(self.appname)
# Make sure we have something to do.
if not self.monitored:
@ -79,34 +79,36 @@ class FileMonitorService(Service):
for key, profile in self.monitored.iteritems():
# Create a file queue for the profile.
queue = Queue.Queue()
profile.queue = Queue.Queue()
# Create a monitor thread for each folder in profile.
# Perform setup for each of the watched folders.
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=(queue, path, profile))
thread = threading.Thread(target=monitor_files,
name=name, args=(profile, path))
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=perform_actions,
name=name,
args=(queue, profile))
thread = threading.Thread(target=filemon.perform_actions,
name=name, args=(profile,))
thread.daemon = True
thread.start()
return True
def monitor_files(queue, path, profile):
def monitor_files(profile, path):
"""
Callable target for file monitor threads.
"""
@ -138,40 +140,7 @@ def monitor_files(queue, path, profile):
winnt.FILE_ACTION_RENAMED_NEW_NAME):
log.debug("monitor_files: Queueing '%s' file: %s" %
(profile.key, 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
profile.queue.put(fpath)
if __name__ == '__main__':

View file

@ -89,7 +89,8 @@ def init(appname='edbob', *args, **kwargs):
shell = kwargs.get('shell', False)
for paths in config_paths:
config.read(paths, recurse=not shell)
config.configure_logging()
if config.getboolean('edbob', 'configure_logging', default=True):
config.configure_logging()
default_modules = 'edbob.time'
modules = config.get('edbob', 'init', default=default_modules)

View file

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

View file

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

@ -54,6 +54,9 @@ 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

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

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

@ -78,6 +78,15 @@ 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

@ -28,6 +28,7 @@ body > #container {
}
#footer {
clear: both;
margin-top: -4em;
text-align: center;
}

View file

@ -1,17 +1,33 @@
/******************************
* perms.css
* Permission Lists
******************************/
div.field-couple.permissions div.field p.group {
div.field-wrapper.permissions div.field div.group {
margin-bottom: 10px;
}
div.field-wrapper.permissions div.field div.group p {
font-weight: bold;
}
div.field-couple.permissions div.field label {
div.field-wrapper.permissions div.field label {
float: none;
font-weight: normal;
}
div.field-couple.permissions div.field label input {
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; */
margin-right: 10px;
}

View file

@ -79,6 +79,13 @@ 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,3 +1,18 @@
<%inherit file="/edbob/crud.mako" />
<%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>
${parent.body()}

View file

@ -1,5 +0,0 @@
<%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">
<div class="field-wrapper ${field.name}">
${field.label_tag()|n}
<div class="field">
${field.render_readonly()}

View file

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

View file

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

View file

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

View file

@ -1,15 +0,0 @@
<%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()}

59
edbob/pyramid/tweens.py Normal file
View file

@ -0,0 +1,59 @@
#!/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

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

View file

@ -26,75 +26,83 @@
``edbob.pyramid.views.roles`` -- Role Views
"""
import transaction
from pyramid.httpexceptions import HTTPFound
from formalchemy import Field, FieldRenderer
from webhelpers.html import literal
from webhelpers.html.tags import checkbox, hidden
import formalchemy
from webhelpers.html import tags
from webhelpers.html.builder import HTML
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.db import auth
from edbob.pyramid import Session
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
from edbob.db.extensions.auth.model import Role
def filter_map():
return filters.get_filter_map(
edbob.Role,
ilike=['name'])
default_permissions = [
def search_config(request, fmap):
return filters.get_search_config(
'roles.list', request, fmap,
include_filter_name=True,
filter_type_name='lk')
("People", [
('people.list', "List People"),
('people.read', "View Person"),
('people.create', "Create Person"),
('people.update', "Edit Person"),
('people.delete', "Delete Person"),
]),
def search_form(config):
return filters.get_search_form(config)
("Roles", [
('roles.list', "List Roles"),
('roles.read', "View Role"),
('roles.create', "Create Role"),
('roles.update', "Edit Role"),
('roles.delete', "Delete Role"),
]),
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
("Users", [
('users.list', "List Users"),
('users.read', "View User"),
('users.create', "Create User"),
('users.update', "Edit User"),
('users.delete', "Delete User"),
]),
]
def roles(request):
class RolesGrid(SearchableAlchemyGridView):
fmap = filter_map()
config = search_config(request, fmap)
search = search_form(config)
config = grid_config(request, search, fmap)
roles = grids.get_pager(query, config)
mapped_class = Role
config_prefix = 'roles'
sort = 'name'
g = forms.AlchemyGrid(
edbob.Role, roles, config,
gridurl=request.route_url('roles.list'),
objurl='role.edit')
def filter_map(self):
return self.make_filter_map(ilike=['name'])
g.configure(
include=[
g.name,
],
readonly=True)
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
grid = g.render(class_='clickable roles')
return grids.render_grid(request, grid, search)
def sort_map(self):
return self.make_sort_map('name')
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(Field):
class PermissionsField(formalchemy.Field):
def sync(self):
if not self.is_readonly():
@ -102,154 +110,108 @@ class PermissionsField(Field):
role.permissions = self.renderer.deserialize()
class PermissionsFieldRenderer(FieldRenderer):
def PermissionsFieldRenderer(permissions, *args, **kwargs):
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')
perms = permissions
fs.append(PermissionsField('permissions',
renderer=PermissionsFieldRenderer))
class PermissionsFieldRenderer(formalchemy.FieldRenderer):
fs.configure(
include=[
fs.name,
fs.permissions,
])
permissions = perms
if not fs.edit:
del fs.permissions
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
return fs
def _render(self, readonly=False, **kwargs):
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
def new_role(request):
class RoleCrud(CrudView):
fs = role_fieldset(edbob.Role, request)
if request.POST:
fs.rebind(data=request.params)
if fs.validate():
mapped_class = Role
home_route = 'roles'
permissions = default_permissions
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')
def fieldset(self, role):
fs = self.make_fieldset(role)
fs.append(PermissionsField(
'permissions',
renderer=PermissionsFieldRenderer(self.permissions)))
fs.configure(
include=[
fs.name,
fs.permissions,
])
return fs
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 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 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.edit', '/roles/{uuid}/edit')
config.add_view(edit_role, route_name='role.edit', renderer='/roles/role.mako',
permission='roles.edit', http_cache=0)
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')

View file

@ -26,33 +26,35 @@
``edbob.pyramid.views.users`` -- User Views
"""
from webhelpers.html import literal, tags
from webhelpers.html import tags
from webhelpers.html.builder import HTML
import formalchemy
from formalchemy.fields import SelectFieldRenderer
import edbob
from edbob.db.auth import set_user_password
from edbob.db import auth
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
class UsersGrid(SearchableAlchemyGridView):
mapped_class = edbob.User
mapped_class = User
config_prefix = 'users'
sort = 'username'
def join_map(self):
return {
'person':
lambda q: q.outerjoin(edbob.Person),
lambda q: q.outerjoin(Person),
}
def filter_map(self):
return self.make_filter_map(
ilike=['username'],
person=self.filter_ilike(edbob.Person.display_name))
person=self.filter_ilike(Person.display_name))
def filter_config(self):
return self.make_filter_config(
@ -64,7 +66,7 @@ class UsersGrid(SearchableAlchemyGridView):
def sort_map(self):
return self.make_sort_map(
'username',
person=self.sorter(edbob.Person.display_name))
person=self.sorter(Person.display_name))
def grid(self):
g = self.make_grid()
@ -86,22 +88,22 @@ class UsersGrid(SearchableAlchemyGridView):
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):
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):
@ -116,14 +118,15 @@ class RolesField(formalchemy.Field):
return [x.uuid for x in user.roles]
def get_options(self):
q = Session.query(edbob.Role.name, edbob.Role.uuid)
q = q.order_by(edbob.Role.name)
q = Session.query(Role.name, Role.uuid)
q = q.filter(Role.uuid != auth.guest_role(Session()).uuid)
q = q.order_by(Role.name)
return q.all()
def sync(self):
if not self.is_readonly():
user = self.model
roles = Session.query(edbob.Role)
roles = Session.query(Role)
data = self.renderer.deserialize()
user.roles = [roles.get(x) for x in data]
@ -138,7 +141,7 @@ class _ProtectedPersonRenderer(formalchemy.FieldRenderer):
def ProtectedPersonRenderer(uuid):
person = Session.query(edbob.Person).get(uuid)
person = Session.query(Person).get(uuid)
assert person
return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,),
{'person': person})
@ -180,12 +183,12 @@ class PasswordField(formalchemy.Field):
if not self.is_readonly():
password = self.renderer.deserialize()
if password:
set_user_password(self.model, password)
auth.set_user_password(self.model, password)
class UserCrud(CrudView):
mapped_class = edbob.User
mapped_class = User
home_route = 'users'
def fieldset(self, user):
@ -206,8 +209,12 @@ class UserCrud(CrudView):
fs.roles,
])
if self.readonly:
del fs.password
del fs.confirm_password
# if fs.edit and user.person:
if isinstance(user, edbob.User) and user.person:
if isinstance(user, User) and user.person:
fs.person.set(readonly=True,
renderer=LinkedPersonRenderer(self.request))

View file

@ -7,10 +7,8 @@
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):
@ -27,24 +25,18 @@ def main(global_config, **settings):
# * Raise an exception if a setting is missing or invalid.
# * Convert values from strings to their intended type.
settings['mako.directories'] = [
'{{package}}.pyramid:templates',
'edbob.pyramid:templates',
]
settings.setdefault('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
@ -53,4 +45,8 @@ 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()

19
edbob/tests/__init__.py Normal file
View file

@ -0,0 +1,19 @@
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.info("'%s' timezone set to '%s'" % (key, tz))
log.debug("'%s' timezone set to '%s'" % (key, tz))
set_timezone(tz, key)
if 'local' not in timezones:

View file

@ -38,9 +38,17 @@ if sys.platform == 'win32': # docs should build for everyone
import win32file
import win32print
import win32service
import win32serviceutil
import winerror
try:
import win32serviceutil
except ImportError:
# Mock out for testing on Linux.
class Object(object):
pass
win32serviceutil = Object()
win32serviceutil.ServiceFramework = Object
import edbob
@ -163,6 +171,20 @@ 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 Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
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,2 +1,7 @@
[egg_info]
tag_build = .dev
[nosetests]
nocapture = 1
cover-package = edbob
cover-erase = 1
cover-inclusive = 1
cover-html = 1
cover-html-dir = htmlcov

View file

@ -73,7 +73,10 @@ requires = [
'decorator', # 3.3.2
'lockfile', # 0.9.1
'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):
@ -209,6 +212,8 @@ setup(
install_requires = requires,
extras_require = extras,
tests_require = requires + ['nose'],
test_suite = 'nose.collector',
packages = find_packages(),
include_package_data = True,