Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0b35dfadb | ||
![]() |
8fabdf8b72 | ||
![]() |
73978ffeb7 | ||
![]() |
183530afc9 | ||
![]() |
8a0f300fc0 | ||
![]() |
ee9c6622ff | ||
![]() |
4fe5ad9bf0 | ||
![]() |
25611f1e68 | ||
![]() |
4ed6b09a24 | ||
![]() |
3766dbfff1 | ||
![]() |
3bfda431fa | ||
![]() |
4dc4f3f1ec | ||
![]() |
2e57936ed1 | ||
![]() |
33c951e7fe | ||
![]() |
9262b598ac | ||
![]() |
6c517a9126 | ||
![]() |
ea7473f18d | ||
![]() |
896a320da5 | ||
![]() |
7ae344403a | ||
![]() |
884e48970e | ||
![]() |
4bb4143daa | ||
![]() |
092e325adc | ||
![]() |
a6488e1089 | ||
![]() |
88dcb1889c | ||
![]() |
f6bb5b2b77 | ||
![]() |
64ae8e9136 | ||
![]() |
d8744f3958 | ||
![]() |
a1d22df20c | ||
![]() |
7928461e08 | ||
![]() |
8217b91aa4 | ||
![]() |
c3f58d1b8c | ||
![]() |
a249fceed5 | ||
![]() |
99866edac8 | ||
![]() |
c2339ba124 | ||
![]() |
c79d6de56d | ||
![]() |
ba2c99d503 | ||
![]() |
9eb33258b4 | ||
![]() |
9b2589ca12 | ||
![]() |
e95a23ead8 | ||
![]() |
ed04caf68c | ||
![]() |
fb4a49b570 | ||
![]() |
8d3e474436 |
47 changed files with 818 additions and 797 deletions
90
CHANGES.txt
90
CHANGES.txt
|
@ -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
|
0.1a22
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
|
|
||||||
from edbob._version import __version__
|
from edbob._version import __version__
|
||||||
|
from edbob.enum import *
|
||||||
from edbob.core import *
|
from edbob.core import *
|
||||||
from edbob.time import *
|
from edbob.time import *
|
||||||
from edbob.files import *
|
from edbob.files import *
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '0.1a22'
|
__version__ = '0.1.2'
|
||||||
|
|
|
@ -105,6 +105,7 @@ Options:
|
||||||
Config path (may be specified more than once)
|
Config path (may be specified more than once)
|
||||||
-n, --no-init Don't load config before executing command
|
-n, --no-init Don't load config before executing command
|
||||||
-d, --debug Increase logging level to DEBUG
|
-d, --debug Increase logging level to DEBUG
|
||||||
|
-P, --progress Show progress indicators (where relevant)
|
||||||
-v, --verbose Increase logging level to INFO
|
-v, --verbose Increase logging level to INFO
|
||||||
-V, --version Display program version and exit
|
-V, --version Display program version and exit
|
||||||
|
|
||||||
|
@ -132,6 +133,7 @@ Try '%(name)s help <command>' for more help.""" % self
|
||||||
metavar='PATH')
|
metavar='PATH')
|
||||||
parser.add_argument('-d', '--debug', action='store_true', dest='debug')
|
parser.add_argument('-d', '--debug', action='store_true', dest='debug')
|
||||||
parser.add_argument('-n', '--no-init', action='store_true', default=False)
|
parser.add_argument('-n', '--no-init', action='store_true', default=False)
|
||||||
|
parser.add_argument('-P', '--progress', action='store_true', default=False)
|
||||||
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
|
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
|
||||||
parser.add_argument('-V', '--version', action='version',
|
parser.add_argument('-V', '--version', action='version',
|
||||||
version="%%(prog)s %s" % self.version)
|
version="%%(prog)s %s" % self.version)
|
||||||
|
@ -182,6 +184,7 @@ Try '%(name)s help <command>' for more help.""" % self
|
||||||
|
|
||||||
# And finally, do something of real value...
|
# And finally, do something of real value...
|
||||||
cmd = self.subcommands[cmd](parent=self)
|
cmd = self.subcommands[cmd](parent=self)
|
||||||
|
cmd.show_progress = args.progress
|
||||||
cmd._run(*(args.command + args.argv))
|
cmd._run(*(args.command + args.argv))
|
||||||
|
|
||||||
|
|
||||||
|
@ -432,11 +435,6 @@ class FileMonitorCommand(Subcommand):
|
||||||
uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service")
|
uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service")
|
||||||
uninstall.set_defaults(subcommand='remove')
|
uninstall.set_defaults(subcommand='remove')
|
||||||
|
|
||||||
else:
|
|
||||||
parser.add_argument('-D', '--dont-daemonize',
|
|
||||||
action='store_false', dest='daemonize',
|
|
||||||
help="Don't daemonize when starting")
|
|
||||||
|
|
||||||
def get_win32_module(self):
|
def get_win32_module(self):
|
||||||
from edbob.filemon import win32
|
from edbob.filemon import win32
|
||||||
return win32
|
return win32
|
||||||
|
@ -454,10 +452,10 @@ class FileMonitorCommand(Subcommand):
|
||||||
from edbob.filemon import linux as filemon
|
from edbob.filemon import linux as filemon
|
||||||
|
|
||||||
if args.subcommand == 'start':
|
if args.subcommand == 'start':
|
||||||
filemon.start_daemon(self.appname, daemonize=args.daemonize)
|
filemon.start_daemon(self.appname)
|
||||||
|
|
||||||
elif args.subcommand == 'stop':
|
elif args.subcommand == 'stop':
|
||||||
filemon.stop_daemon()
|
filemon.stop_daemon(self.appname)
|
||||||
|
|
||||||
elif sys.platform == 'win32':
|
elif sys.platform == 'win32':
|
||||||
from edbob import win32
|
from edbob import win32
|
||||||
|
|
|
@ -112,7 +112,7 @@ def graft(target, source, names=None):
|
||||||
if hasattr(source, '__all__'):
|
if hasattr(source, '__all__'):
|
||||||
names = source.__all__
|
names = source.__all__
|
||||||
else:
|
else:
|
||||||
names = dir(source)
|
names = [x for x in dir(source) if not x.startswith('_')]
|
||||||
elif isinstance(names, basestring):
|
elif isinstance(names, basestring):
|
||||||
names = [names]
|
names = [names]
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
|
|
||||||
from __future__ import absolute_import
|
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/
|
# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
|
||||||
|
|
||||||
import sys, os, time, atexit
|
import sys, os, time, atexit
|
||||||
|
import stat
|
||||||
from signal import SIGTERM
|
from signal import SIGTERM
|
||||||
|
|
||||||
class Daemon:
|
class Daemon:
|
||||||
|
@ -65,6 +66,7 @@ class Daemon:
|
||||||
atexit.register(self.delpid)
|
atexit.register(self.delpid)
|
||||||
pid = str(os.getpid())
|
pid = str(os.getpid())
|
||||||
file(self.pidfile,'w+').write("%s\n" % pid)
|
file(self.pidfile,'w+').write("%s\n" % pid)
|
||||||
|
os.chmod(self.pidfile, stat.S_IRUSR|stat.S_IWUSR)
|
||||||
|
|
||||||
def delpid(self):
|
def delpid(self):
|
||||||
os.remove(self.pidfile)
|
os.remove(self.pidfile)
|
||||||
|
|
|
@ -105,7 +105,7 @@ def grant_permission(role, permission, session=None):
|
||||||
role.permissions.append(permission)
|
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
|
Checks the given ``obj`` (which may be either a :class:`edbob.User`` or
|
||||||
:class:`edbob.Role` instance), and returns a boolean indicating whether or
|
:class:`edbob.Role` instance), and returns a boolean indicating whether or
|
||||||
|
@ -124,8 +124,9 @@ def has_permission(obj, perm, session=None):
|
||||||
if not session:
|
if not session:
|
||||||
session = object_session(obj)
|
session = object_session(obj)
|
||||||
assert session
|
assert session
|
||||||
|
if include_guest:
|
||||||
|
roles.append(guest_role(session))
|
||||||
admin = administrator_role(session)
|
admin = administrator_role(session)
|
||||||
roles.append(guest_role(session))
|
|
||||||
for role in roles:
|
for role in roles:
|
||||||
if role is admin:
|
if role is admin:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -366,7 +366,7 @@ def extend_framework():
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
for name in sorted(extensions, extension_sorter(extensions)):
|
for name in sorted(extensions, extension_sorter(extensions)):
|
||||||
log.info("Applying active extension: %s" % name)
|
log.debug("Applying active extension: %s" % name)
|
||||||
ext = extensions[name]
|
ext = extensions[name]
|
||||||
# merge_extension_metadata(ext)
|
# merge_extension_metadata(ext)
|
||||||
# ext.extend_classes()
|
# ext.extend_classes()
|
||||||
|
|
|
@ -51,10 +51,11 @@ class Permission(Base):
|
||||||
permission = Column(String(50), primary_key=True)
|
permission = Column(String(50), primary_key=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Permission: %s, %s>" % (self.role, self.permission)
|
return "Permission(role_uuid={0}, permission={1})".format(
|
||||||
|
repr(self.role_uuid), repr(self.permission))
|
||||||
|
|
||||||
def __str__(self):
|
def __unicode__(self):
|
||||||
return str(self.permission or '')
|
return unicode(self.permission or '')
|
||||||
|
|
||||||
|
|
||||||
class UserRole(Base):
|
class UserRole(Base):
|
||||||
|
@ -69,7 +70,7 @@ class UserRole(Base):
|
||||||
role_uuid = Column(String(32), ForeignKey('roles.uuid'))
|
role_uuid = Column(String(32), ForeignKey('roles.uuid'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<UserRole: %s : %s>" % (self.user, self.role)
|
return "UserRole(uuid={0})".format(repr(self.uuid))
|
||||||
|
|
||||||
|
|
||||||
class Role(Base):
|
class Role(Base):
|
||||||
|
@ -89,16 +90,18 @@ class Role(Base):
|
||||||
creator=lambda x: Permission(permission=x),
|
creator=lambda x: Permission(permission=x),
|
||||||
getset_factory=getset_factory)
|
getset_factory=getset_factory)
|
||||||
|
|
||||||
_users = relationship(UserRole, backref='role')
|
_users = relationship(
|
||||||
|
UserRole, backref='role',
|
||||||
|
cascade='save-update, merge, delete, delete-orphan')
|
||||||
users = association_proxy('_users', 'user',
|
users = association_proxy('_users', 'user',
|
||||||
creator=lambda x: UserRole(user=x),
|
creator=lambda x: UserRole(user=x),
|
||||||
getset_factory=getset_factory)
|
getset_factory=getset_factory)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Role: %s>" % self.name
|
return "Role(uuid={0})".format(repr(self.uuid))
|
||||||
|
|
||||||
def __str__(self):
|
def __unicode__(self):
|
||||||
return str(self.name or '')
|
return unicode(self.name or '')
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
|
@ -122,17 +125,18 @@ class User(Base):
|
||||||
getset_factory=getset_factory)
|
getset_factory=getset_factory)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<User: %s>" % self.username
|
return "User(uuid={0})".format(repr(self.uuid))
|
||||||
|
|
||||||
def __str__(self):
|
def __unicode__(self):
|
||||||
return str(self.username or '')
|
return unicode(self.username or '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
"""
|
"""
|
||||||
Returns the user's ``person.display_name``, if present, otherwise the
|
Returns :attr:`Person.display_name` if present; otherwise returns
|
||||||
``username``.
|
:attr:`username`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.person and self.person.display_name:
|
if self.person and self.person.display_name:
|
||||||
return self.person.display_name
|
return self.person.display_name
|
||||||
return self.username
|
return self.username
|
||||||
|
|
|
@ -72,7 +72,8 @@ class PhoneNumber(Base):
|
||||||
__mapper_args__ = {'polymorphic_on': parent_type}
|
__mapper_args__ = {'polymorphic_on': parent_type}
|
||||||
|
|
||||||
def __repr__(self):
|
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):
|
def __unicode__(self):
|
||||||
return unicode(self.number)
|
return unicode(self.number)
|
||||||
|
@ -103,7 +104,8 @@ class EmailAddress(Base):
|
||||||
__mapper_args__ = {'polymorphic_on': parent_type}
|
__mapper_args__ = {'polymorphic_on': parent_type}
|
||||||
|
|
||||||
def __repr__(self):
|
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):
|
def __unicode__(self):
|
||||||
return unicode(self.address)
|
return unicode(self.address)
|
||||||
|
@ -131,7 +133,7 @@ class Person(Base):
|
||||||
display_name = Column(String(100), default=get_person_display_name)
|
display_name = Column(String(100), default=get_person_display_name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Person: %s>" % self.display_name
|
return "Person(uuid={0})".format(repr(self.uuid))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode(self.display_name or '')
|
return unicode(self.display_name or '')
|
||||||
|
|
|
@ -54,7 +54,7 @@ class ActiveExtension(Base):
|
||||||
name = Column(String(50), primary_key=True)
|
name = Column(String(50), primary_key=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<ActiveExtension: %s>" % self.name
|
return "ActiveExtension(name={0})".format(repr(self.name))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name or '')
|
return str(self.name or '')
|
||||||
|
@ -71,4 +71,4 @@ class Setting(Base):
|
||||||
value = Column(Text)
|
value = Column(Text)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Setting: %s>" % self.name
|
return "Setting(name={0})".format(repr(self.name))
|
||||||
|
|
|
@ -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_HTML : "HTML",
|
||||||
EMAIL_PREFERENCE_MOBILE : "Mobile",
|
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",
|
||||||
|
}
|
|
@ -73,6 +73,12 @@ class LoadSpecError(Exception):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSpec(LoadSpecError):
|
||||||
|
|
||||||
|
def specifics(self):
|
||||||
|
return "invalid spec"
|
||||||
|
|
||||||
|
|
||||||
class ModuleMissingAttribute(LoadSpecError):
|
class ModuleMissingAttribute(LoadSpecError):
|
||||||
"""
|
"""
|
||||||
Raised during :func:`edbob.load_spec()` when the module imported okay but
|
Raised during :func:`edbob.load_spec()` when the module imported okay but
|
||||||
|
|
|
@ -26,10 +26,18 @@
|
||||||
``edbob.filemon`` -- File Monitoring Service
|
``edbob.filemon`` -- File Monitoring Service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import sys
|
||||||
|
import Queue
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
|
from edbob.errors import email_exception
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
import win32api
|
||||||
|
from edbob.win32 import file_is_free
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -65,6 +73,12 @@ class MonitorProfile(object):
|
||||||
self.locks = edbob.config.getboolean(
|
self.locks = edbob.config.getboolean(
|
||||||
'%s.filemon' % appname, '%s.locks' % key, default=False)
|
'%s.filemon' % appname, '%s.locks' % key, default=False)
|
||||||
|
|
||||||
|
self.process_existing = edbob.config.getboolean(
|
||||||
|
'%s.filemon' % appname, '%s.process_existing' % key, default=True)
|
||||||
|
|
||||||
|
self.stop_on_error = edbob.config.getboolean(
|
||||||
|
'%s.filemon' % appname, '%s.stop_on_error' % key, default=False)
|
||||||
|
|
||||||
|
|
||||||
def get_monitor_profiles(appname):
|
def get_monitor_profiles(appname):
|
||||||
"""
|
"""
|
||||||
|
@ -110,3 +124,105 @@ def get_monitor_profiles(appname):
|
||||||
del monitored[key]
|
del monitored[key]
|
||||||
|
|
||||||
return monitored
|
return monitored
|
||||||
|
|
||||||
|
|
||||||
|
def queue_existing(profile, path):
|
||||||
|
"""
|
||||||
|
Adds files found in a watched folder to a processing queue. This is called
|
||||||
|
when the monitor first starts, to handle the case of files which exist
|
||||||
|
prior to startup.
|
||||||
|
|
||||||
|
If files are found, they are first sorted by modification timestamp, using
|
||||||
|
a lexical sort on the filename as a tie-breaker, and then added to the
|
||||||
|
queue in that order.
|
||||||
|
|
||||||
|
:param profile: Monitor profile for which the folder is to be watched. The
|
||||||
|
profile is expected to already have a queue attached; any existing files
|
||||||
|
will be added to this queue.
|
||||||
|
:type profile: :class:`edbob.filemon.MonitorProfile` instance
|
||||||
|
|
||||||
|
:param path: Folder path which is to be checked for files.
|
||||||
|
:type path: string
|
||||||
|
|
||||||
|
:returns: ``None``
|
||||||
|
"""
|
||||||
|
|
||||||
|
def sorter(x, y):
|
||||||
|
mtime_x = os.path.getmtime(x)
|
||||||
|
mtime_y = os.path.getmtime(y)
|
||||||
|
if mtime_x < mtime_y:
|
||||||
|
return -1
|
||||||
|
if mtime_x > mtime_y:
|
||||||
|
return 1
|
||||||
|
return cmp(x, y)
|
||||||
|
|
||||||
|
paths = [os.path.join(path, x) for x in os.listdir(path)]
|
||||||
|
for path in sorted(paths, cmp=sorter):
|
||||||
|
|
||||||
|
# Only process normal files.
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If using locks, don't process "in transit" files.
|
||||||
|
if profile.locks and path.endswith('.lock'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.debug("queue_existing: queuing existing file for "
|
||||||
|
"profile '%s': %s" % (profile.key, path))
|
||||||
|
profile.queue.put(path)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_actions(profile):
|
||||||
|
"""
|
||||||
|
Callable target for action threads.
|
||||||
|
"""
|
||||||
|
|
||||||
|
keep_going = True
|
||||||
|
while keep_going:
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = profile.queue.get_nowait()
|
||||||
|
except Queue.Empty:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
|
||||||
|
# In some cases, processing one file may cause other related files
|
||||||
|
# to also be processed. When this happens, a path on the queue may
|
||||||
|
# point to a file which no longer exists.
|
||||||
|
if not os.path.exists(path):
|
||||||
|
log.info("perform_actions: path does not exist: %s" % path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.debug("perform_actions: processing file: %s" % path)
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
while not file_is_free(path):
|
||||||
|
win32api.Sleep(0)
|
||||||
|
|
||||||
|
for spec, func, args in profile.actions:
|
||||||
|
|
||||||
|
log.info("perform_actions: calling function '%s' on file: %s" %
|
||||||
|
(spec, path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
func(path, *args)
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.exception("perform_actions: exception occurred "
|
||||||
|
"while processing file: %s" % path)
|
||||||
|
email_exception()
|
||||||
|
|
||||||
|
# Don't process any more files if the profile is so
|
||||||
|
# configured.
|
||||||
|
if profile.stop_on_error:
|
||||||
|
keep_going = False
|
||||||
|
|
||||||
|
# Either way this particular file probably shouldn't be
|
||||||
|
# processed any further.
|
||||||
|
log.warning("perform_actions: no further processing "
|
||||||
|
"will be done for file: %s" % path)
|
||||||
|
break
|
||||||
|
|
||||||
|
log.warning("perform_actions: error encountered, and configuration "
|
||||||
|
"dictates that no more actions will be processed for "
|
||||||
|
"profile: %s" % profile.key)
|
||||||
|
|
|
@ -27,14 +27,24 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
import os.path
|
import os.path
|
||||||
import signal
|
import threading
|
||||||
|
import Queue
|
||||||
import logging
|
import logging
|
||||||
import pyinotify
|
|
||||||
|
try:
|
||||||
|
import pyinotify
|
||||||
|
except ImportError:
|
||||||
|
# Mock out for testing on Windows.
|
||||||
|
class Dummy(object):
|
||||||
|
pass
|
||||||
|
pyinotify = Dummy()
|
||||||
|
pyinotify.ProcessEvent = Dummy
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
from edbob.filemon import get_monitor_profiles
|
from edbob import filemon
|
||||||
|
from edbob.daemon import Daemon
|
||||||
|
from edbob.errors import email_exception
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -45,9 +55,8 @@ class EventHandler(pyinotify.ProcessEvent):
|
||||||
Event processor for file monitor daemon.
|
Event processor for file monitor daemon.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def my_init(self, actions=[], locks=False, **kwargs):
|
def my_init(self, profile=None, **kwargs):
|
||||||
self.actions = actions
|
self.profile = profile
|
||||||
self.locks = locks
|
|
||||||
|
|
||||||
def process_IN_ACCESS(self, event):
|
def process_IN_ACCESS(self, event):
|
||||||
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
|
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
|
||||||
|
@ -57,87 +66,85 @@ class EventHandler(pyinotify.ProcessEvent):
|
||||||
|
|
||||||
def process_IN_CLOSE_WRITE(self, event):
|
def process_IN_CLOSE_WRITE(self, event):
|
||||||
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
|
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
|
||||||
if not self.locks:
|
if not self.profile.locks:
|
||||||
self.perform_actions(event.pathname)
|
self.profile.queue.put(event.pathname)
|
||||||
|
|
||||||
def process_IN_CREATE(self, event):
|
def process_IN_CREATE(self, event):
|
||||||
log.debug("EventHandler: IN_CREATE: %s" % event.pathname)
|
log.debug("EventHandler: IN_CREATE: %s" % event.pathname)
|
||||||
|
|
||||||
def process_IN_DELETE(self, event):
|
def process_IN_DELETE(self, event):
|
||||||
log.debug("EventHandler: IN_DELETE: %s" % event.pathname)
|
log.debug("EventHandler: IN_DELETE: %s" % event.pathname)
|
||||||
if self.locks and event.pathname.endswith('.lock'):
|
if self.profile.locks and event.pathname.endswith('.lock'):
|
||||||
self.perform_actions(event.pathname[:-5])
|
self.profile.queue.put(event.pathname[:-5])
|
||||||
|
|
||||||
def process_IN_MODIFY(self, event):
|
def process_IN_MODIFY(self, event):
|
||||||
log.debug("EventHandler: IN_MODIFY: %s" % event.pathname)
|
log.debug("EventHandler: IN_MODIFY: %s" % event.pathname)
|
||||||
|
|
||||||
def process_IN_MOVED_TO(self, event):
|
def process_IN_MOVED_TO(self, event):
|
||||||
log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname)
|
log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname)
|
||||||
if not self.locks:
|
if not self.profile.locks:
|
||||||
self.perform_actions(event.pathname)
|
self.profile.queue.put(event.pathname)
|
||||||
|
|
||||||
def perform_actions(self, path):
|
|
||||||
for spec, func, args in self.actions:
|
|
||||||
func(path, *args)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pid_path():
|
class FileMonitorDaemon(Daemon):
|
||||||
"""
|
|
||||||
Returns the path to the PID file for the file monitor daemon.
|
|
||||||
"""
|
|
||||||
|
|
||||||
basename = os.path.basename(sys.argv[0])
|
def run(self):
|
||||||
pid_path = edbob.config.get('%s.filemon' % basename, 'pid_path')
|
|
||||||
|
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:
|
if not pid_path:
|
||||||
pid_path = '/tmp/%s_filemon.pid' % basename
|
pid_path = '/tmp/%s_filemon.pid' % appname
|
||||||
return pid_path
|
|
||||||
|
monitor = FileMonitorDaemon(pid_path)
|
||||||
|
monitor.appname = appname
|
||||||
|
return monitor
|
||||||
|
|
||||||
|
|
||||||
def start_daemon(appname, daemonize=True):
|
def start_daemon(appname):
|
||||||
"""
|
get_daemon(appname).start()
|
||||||
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 stop_daemon():
|
def stop_daemon(appname):
|
||||||
"""
|
get_daemon(appname).stop()
|
||||||
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)
|
|
||||||
|
|
|
@ -33,8 +33,8 @@ import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
|
from edbob import filemon
|
||||||
from edbob.errors import email_exception
|
from edbob.errors import email_exception
|
||||||
from edbob.filemon import get_monitor_profiles
|
|
||||||
from edbob.win32 import Service, file_is_free
|
from edbob.win32 import Service, file_is_free
|
||||||
|
|
||||||
if sys.platform == 'win32': # docs should build for everyone
|
if sys.platform == 'win32': # docs should build for everyone
|
||||||
|
@ -69,7 +69,7 @@ class FileMonitorService(Service):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Read monitor profile(s) from config.
|
# 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.
|
# Make sure we have something to do.
|
||||||
if not self.monitored:
|
if not self.monitored:
|
||||||
|
@ -79,34 +79,36 @@ class FileMonitorService(Service):
|
||||||
for key, profile in self.monitored.iteritems():
|
for key, profile in self.monitored.iteritems():
|
||||||
|
|
||||||
# Create a file queue for the profile.
|
# Create a file queue for the profile.
|
||||||
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):
|
for i, path in enumerate(profile.dirs, 1):
|
||||||
|
|
||||||
|
# Maybe put all pre-existing files in the queue.
|
||||||
|
if profile.process_existing:
|
||||||
|
filemon.queue_existing(profile, path)
|
||||||
|
|
||||||
|
# Create a monitor thread for the folder.
|
||||||
name = 'monitor-%s-%u' % (key, i)
|
name = 'monitor-%s-%u' % (key, i)
|
||||||
log.debug("Initialize: Starting '%s' thread for folder: %s" %
|
log.debug("Initialize: Starting '%s' thread for folder: %s" %
|
||||||
(name, path))
|
(name, path))
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(target=monitor_files,
|
||||||
target=monitor_files,
|
name=name, args=(profile, path))
|
||||||
name=name,
|
|
||||||
args=(queue, path, profile))
|
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
# Create an action thread for the profile.
|
# Create an action thread for the profile.
|
||||||
name = 'actions-%s' % key
|
name = 'actions-%s' % key
|
||||||
log.debug("Initialize: Starting '%s' thread" % name)
|
log.debug("Initialize: Starting '%s' thread" % name)
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(target=filemon.perform_actions,
|
||||||
target=perform_actions,
|
name=name, args=(profile,))
|
||||||
name=name,
|
|
||||||
args=(queue, profile))
|
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def monitor_files(queue, path, profile):
|
def monitor_files(profile, path):
|
||||||
"""
|
"""
|
||||||
Callable target for file monitor threads.
|
Callable target for file monitor threads.
|
||||||
"""
|
"""
|
||||||
|
@ -138,40 +140,7 @@ def monitor_files(queue, path, profile):
|
||||||
winnt.FILE_ACTION_RENAMED_NEW_NAME):
|
winnt.FILE_ACTION_RENAMED_NEW_NAME):
|
||||||
log.debug("monitor_files: Queueing '%s' file: %s" %
|
log.debug("monitor_files: Queueing '%s' file: %s" %
|
||||||
(profile.key, fpath))
|
(profile.key, fpath))
|
||||||
queue.put(fpath)
|
profile.queue.put(fpath)
|
||||||
|
|
||||||
|
|
||||||
def perform_actions(queue, profile):
|
|
||||||
"""
|
|
||||||
Callable target for action threads.
|
|
||||||
"""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
|
|
||||||
try:
|
|
||||||
path = queue.get_nowait()
|
|
||||||
except Queue.Empty:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
|
|
||||||
while not file_is_free(path):
|
|
||||||
win32api.Sleep(0)
|
|
||||||
|
|
||||||
for spec, func, args in profile.actions:
|
|
||||||
|
|
||||||
log.info("perform_actions: Calling function '%s' on file: %s" %
|
|
||||||
(spec, path))
|
|
||||||
|
|
||||||
try:
|
|
||||||
func(path, *args)
|
|
||||||
|
|
||||||
except:
|
|
||||||
log.exception("perform_actions: An exception occurred "
|
|
||||||
"while processing file: %s" % path)
|
|
||||||
email_exception()
|
|
||||||
|
|
||||||
# This file probably shouldn't be processed any further.
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -89,7 +89,8 @@ def init(appname='edbob', *args, **kwargs):
|
||||||
shell = kwargs.get('shell', False)
|
shell = kwargs.get('shell', False)
|
||||||
for paths in config_paths:
|
for paths in config_paths:
|
||||||
config.read(paths, recurse=not shell)
|
config.read(paths, recurse=not shell)
|
||||||
config.configure_logging()
|
if config.getboolean('edbob', 'configure_logging', default=True):
|
||||||
|
config.configure_logging()
|
||||||
|
|
||||||
default_modules = 'edbob.time'
|
default_modules = 'edbob.time'
|
||||||
modules = config.get('edbob', 'init', default=default_modules)
|
modules = config.get('edbob', 'init', default=default_modules)
|
||||||
|
|
|
@ -82,6 +82,9 @@ def load_spec(spec):
|
||||||
necessary.
|
necessary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if spec.count(':') != 1:
|
||||||
|
raise exceptions.InvalidSpec(spec)
|
||||||
|
|
||||||
module_path, obj = spec.split(':')
|
module_path, obj = spec.split(':')
|
||||||
module = import_module_path(module_path)
|
module = import_module_path(module_path)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -50,7 +50,8 @@ from edbob.pyramid.forms.formalchemy.renderers import *
|
||||||
__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
|
__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
|
||||||
'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
|
'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
|
||||||
'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
|
'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
|
||||||
'AssociationProxyField', 'YesNoFieldRenderer']
|
'AssociationProxyField', 'StrippingFieldRenderer',
|
||||||
|
'YesNoFieldRenderer']
|
||||||
|
|
||||||
|
|
||||||
class TemplateEngine(formalchemy.templates.TemplateEngine):
|
class TemplateEngine(formalchemy.templates.TemplateEngine):
|
||||||
|
|
|
@ -32,7 +32,7 @@ from pyramid.renderers import render
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
|
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
|
||||||
'YesNoFieldRenderer']
|
'StrippingFieldRenderer', 'YesNoFieldRenderer']
|
||||||
|
|
||||||
|
|
||||||
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
|
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
|
||||||
|
@ -83,6 +83,17 @@ def EnumFieldRenderer(enum):
|
||||||
return Renderer
|
return Renderer
|
||||||
|
|
||||||
|
|
||||||
|
class StrippingFieldRenderer(formalchemy.TextFieldRenderer):
|
||||||
|
"""
|
||||||
|
Standard text field renderer, which strips whitespace from either end of
|
||||||
|
the input value on deserialization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def deserialize(self):
|
||||||
|
value = super(StrippingFieldRenderer, self).deserialize()
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
|
class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
|
||||||
|
|
||||||
def render_readonly(self, **kwargs):
|
def render_readonly(self, **kwargs):
|
||||||
|
|
|
@ -54,6 +54,9 @@ class AlchemyGrid(Grid):
|
||||||
self._formalchemy_grid.prettify = prettify
|
self._formalchemy_grid.prettify = prettify
|
||||||
self.noclick_fields = []
|
self.noclick_fields = []
|
||||||
|
|
||||||
|
def __delattr__(self, attr):
|
||||||
|
delattr(self._formalchemy_grid, attr)
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self._formalchemy_grid, attr)
|
return getattr(self._formalchemy_grid, attr)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
|
@ -78,6 +78,15 @@ div.field-wrapper div.field textarea {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label input[type=checkbox] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Buttons
|
||||||
|
******************************/
|
||||||
|
|
||||||
div.buttons {
|
div.buttons {
|
||||||
clear: both;
|
clear: both;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
|
@ -28,6 +28,7 @@ body > #container {
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
|
clear: both;
|
||||||
margin-top: -4em;
|
margin-top: -4em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-couple.permissions div.field label {
|
div.field-wrapper.permissions div.field label {
|
||||||
float: none;
|
float: none;
|
||||||
font-weight: normal;
|
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;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,13 @@ def context_found(event):
|
||||||
return has_permission(request.user, perm, session=Session())
|
return has_permission(request.user, perm, session=Session())
|
||||||
request.has_perm = has_perm
|
request.has_perm = has_perm
|
||||||
|
|
||||||
|
def has_any_perm(perms):
|
||||||
|
for perm in perms:
|
||||||
|
if has_permission(request.user, perm, session=Session()):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
request.has_any_perm = has_any_perm
|
||||||
|
|
||||||
def get_referrer(default=None):
|
def get_referrer(default=None):
|
||||||
if request.params.get('referrer'):
|
if request.params.get('referrer'):
|
||||||
return request.params['referrer']
|
return request.params['referrer']
|
||||||
|
|
|
@ -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()}
|
${parent.body()}
|
||||||
|
|
|
@ -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()}
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="fieldset">
|
<div class="fieldset">
|
||||||
% for field in fieldset.render_fields.itervalues():
|
% for field in fieldset.render_fields.itervalues():
|
||||||
% if field.requires_label:
|
% if field.requires_label:
|
||||||
<div class="field-wrapper">
|
<div class="field-wrapper ${field.name}">
|
||||||
${field.label_tag()|n}
|
${field.label_tag()|n}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
${field.render_readonly()}
|
${field.render_readonly()}
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
<title>Working...</title>
|
<title>Working...</title>
|
||||||
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
|
||||||
${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
|
||||||
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/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">
|
<style type="text/css">
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
<%inherit file="/base.mako" />
|
|
||||||
${parent.body()}
|
|
18
edbob/pyramid/templates/roles/crud.mako
Normal file
18
edbob/pyramid/templates/roles/crud.mako
Normal 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()}
|
|
@ -1,10 +1,11 @@
|
||||||
<%inherit file="/roles/base.mako" />
|
<%inherit file="/grid.mako" />
|
||||||
<%inherit file="/index.mako" />
|
|
||||||
|
|
||||||
<%def name="title()">Roles</%def>
|
<%def name="title()">Roles</%def>
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
<%def name="context_menu_items()">
|
||||||
<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>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
${parent.body()}
|
||||||
|
|
|
@ -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
59
edbob/pyramid/tweens.py
Normal 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
|
|
@ -33,37 +33,38 @@ from sqlalchemy import and_
|
||||||
|
|
||||||
# from formalchemy import Field
|
# from formalchemy import Field
|
||||||
|
|
||||||
import edbob
|
|
||||||
# from edbob.pyramid import filters
|
# from edbob.pyramid import filters
|
||||||
# from edbob.pyramid import forms
|
# from edbob.pyramid import forms
|
||||||
# from edbob.pyramid import grids
|
# from edbob.pyramid import grids
|
||||||
# from edbob.pyramid import Session
|
# from edbob.pyramid import Session
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from edbob.db.extensions.contact.model import (
|
||||||
|
Person, PersonEmailAddress, PersonPhoneNumber)
|
||||||
|
|
||||||
|
|
||||||
class PeopleGrid(SearchableAlchemyGridView):
|
class PeopleGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = edbob.Person
|
mapped_class = Person
|
||||||
config_prefix = 'people'
|
config_prefix = 'people'
|
||||||
sort = 'first_name'
|
sort = 'first_name'
|
||||||
|
|
||||||
def join_map(self):
|
def join_map(self):
|
||||||
return {
|
return {
|
||||||
'email':
|
'email':
|
||||||
lambda q: q.outerjoin(edbob.PersonEmailAddress, and_(
|
lambda q: q.outerjoin(PersonEmailAddress, and_(
|
||||||
edbob.PersonEmailAddress.parent_uuid == edbob.Person.uuid,
|
PersonEmailAddress.parent_uuid == Person.uuid,
|
||||||
edbob.PersonEmailAddress.preference == 1)),
|
PersonEmailAddress.preference == 1)),
|
||||||
'phone':
|
'phone':
|
||||||
lambda q: q.outerjoin(edbob.PersonPhoneNumber, and_(
|
lambda q: q.outerjoin(PersonPhoneNumber, and_(
|
||||||
edbob.PersonPhoneNumber.parent_uuid == edbob.Person.uuid,
|
PersonPhoneNumber.parent_uuid == Person.uuid,
|
||||||
edbob.PersonPhoneNumber.preference == 1)),
|
PersonPhoneNumber.preference == 1)),
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_map(self):
|
def filter_map(self):
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
ilike=['first_name', 'last_name'],
|
ilike=['first_name', 'last_name'],
|
||||||
email=self.filter_ilike(edbob.PersonEmailAddress.address),
|
email=self.filter_ilike(PersonEmailAddress.address),
|
||||||
phone=self.filter_ilike(edbob.PersonPhoneNumber.number))
|
phone=self.filter_ilike(PersonPhoneNumber.number))
|
||||||
|
|
||||||
def filter_config(self):
|
def filter_config(self):
|
||||||
return self.make_filter_config(
|
return self.make_filter_config(
|
||||||
|
@ -77,8 +78,8 @@ class PeopleGrid(SearchableAlchemyGridView):
|
||||||
def sort_map(self):
|
def sort_map(self):
|
||||||
return self.make_sort_map(
|
return self.make_sort_map(
|
||||||
'first_name', 'last_name',
|
'first_name', 'last_name',
|
||||||
email=self.sorter(edbob.PersonEmailAddress.address),
|
email=self.sorter(PersonEmailAddress.address),
|
||||||
phone=self.sorter(edbob.PersonPhoneNumber.number))
|
phone=self.sorter(PersonPhoneNumber.number))
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
g = self.make_grid()
|
g = self.make_grid()
|
||||||
|
@ -97,7 +98,7 @@ class PeopleGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
class PersonCrud(CrudView):
|
class PersonCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = edbob.Person
|
mapped_class = Person
|
||||||
home_route = 'people'
|
home_route = 'people'
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
|
|
@ -26,75 +26,83 @@
|
||||||
``edbob.pyramid.views.roles`` -- Role Views
|
``edbob.pyramid.views.roles`` -- Role Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import transaction
|
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
from formalchemy import Field, FieldRenderer
|
import formalchemy
|
||||||
from webhelpers.html import literal
|
from webhelpers.html import tags
|
||||||
from webhelpers.html.tags import checkbox, hidden
|
from webhelpers.html.builder import HTML
|
||||||
|
|
||||||
import edbob
|
from edbob.db import auth
|
||||||
from edbob.db.auth import administrator_role, has_permission
|
|
||||||
from edbob.pyramid import filters
|
|
||||||
from edbob.pyramid import forms
|
|
||||||
from edbob.pyramid import grids
|
|
||||||
from edbob.pyramid import Session
|
from edbob.pyramid import Session
|
||||||
|
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from edbob.db.extensions.auth.model import Role
|
||||||
|
|
||||||
|
|
||||||
def filter_map():
|
default_permissions = [
|
||||||
return filters.get_filter_map(
|
|
||||||
edbob.Role,
|
|
||||||
ilike=['name'])
|
|
||||||
|
|
||||||
def search_config(request, fmap):
|
("People", [
|
||||||
return filters.get_search_config(
|
('people.list', "List People"),
|
||||||
'roles.list', request, fmap,
|
('people.read', "View Person"),
|
||||||
include_filter_name=True,
|
('people.create', "Create Person"),
|
||||||
filter_type_name='lk')
|
('people.update', "Edit Person"),
|
||||||
|
('people.delete', "Delete Person"),
|
||||||
|
]),
|
||||||
|
|
||||||
def search_form(config):
|
("Roles", [
|
||||||
return filters.get_search_form(config)
|
('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):
|
("Users", [
|
||||||
return grids.get_grid_config(
|
('users.list', "List Users"),
|
||||||
'roles.list', request, search,
|
('users.read', "View User"),
|
||||||
filter_map=fmap, sort='name')
|
('users.create', "Create User"),
|
||||||
|
('users.update', "Edit User"),
|
||||||
def sort_map():
|
('users.delete', "Delete User"),
|
||||||
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):
|
class RolesGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
fmap = filter_map()
|
mapped_class = Role
|
||||||
config = search_config(request, fmap)
|
config_prefix = 'roles'
|
||||||
search = search_form(config)
|
sort = 'name'
|
||||||
config = grid_config(request, search, fmap)
|
|
||||||
roles = grids.get_pager(query, config)
|
|
||||||
|
|
||||||
g = forms.AlchemyGrid(
|
def filter_map(self):
|
||||||
edbob.Role, roles, config,
|
return self.make_filter_map(ilike=['name'])
|
||||||
gridurl=request.route_url('roles.list'),
|
|
||||||
objurl='role.edit')
|
|
||||||
|
|
||||||
g.configure(
|
def filter_config(self):
|
||||||
include=[
|
return self.make_filter_config(
|
||||||
g.name,
|
include_filter_name=True,
|
||||||
],
|
filter_type_name='lk')
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
grid = g.render(class_='clickable roles')
|
def sort_map(self):
|
||||||
return grids.render_grid(request, grid, search)
|
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):
|
def sync(self):
|
||||||
if not self.is_readonly():
|
if not self.is_readonly():
|
||||||
|
@ -102,154 +110,108 @@ class PermissionsField(Field):
|
||||||
role.permissions = self.renderer.deserialize()
|
role.permissions = self.renderer.deserialize()
|
||||||
|
|
||||||
|
|
||||||
class PermissionsFieldRenderer(FieldRenderer):
|
def PermissionsFieldRenderer(permissions, *args, **kwargs):
|
||||||
|
|
||||||
available_permissions = [
|
perms = 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 '[ ]') + '</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')
|
|
||||||
|
|
||||||
fs.append(PermissionsField('permissions',
|
class PermissionsFieldRenderer(formalchemy.FieldRenderer):
|
||||||
renderer=PermissionsFieldRenderer))
|
|
||||||
|
|
||||||
fs.configure(
|
permissions = perms
|
||||||
include=[
|
|
||||||
fs.name,
|
|
||||||
fs.permissions,
|
|
||||||
])
|
|
||||||
|
|
||||||
if not fs.edit:
|
def deserialize(self):
|
||||||
del fs.permissions
|
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)
|
mapped_class = Role
|
||||||
if request.POST:
|
home_route = 'roles'
|
||||||
fs.rebind(data=request.params)
|
permissions = default_permissions
|
||||||
if fs.validate():
|
|
||||||
|
|
||||||
with transaction.manager:
|
def fieldset(self, role):
|
||||||
fs.sync()
|
fs = self.make_fieldset(role)
|
||||||
fs.model = Session.merge(fs.model)
|
fs.append(PermissionsField(
|
||||||
request.session.flash("%s \"%s\" has been %s." % (
|
'permissions',
|
||||||
fs.crud_title, fs.get_display_text(),
|
renderer=PermissionsFieldRenderer(self.permissions)))
|
||||||
'updated' if fs.edit else 'created'))
|
fs.configure(
|
||||||
home = request.route_url('roles.list')
|
include=[
|
||||||
|
fs.name,
|
||||||
|
fs.permissions,
|
||||||
|
])
|
||||||
|
return fs
|
||||||
|
|
||||||
return HTTPFound(location=home)
|
def pre_delete(self, model):
|
||||||
|
admin = auth.administrator_role(Session())
|
||||||
return {'fieldset': fs, 'crud': True}
|
guest = auth.guest_role(Session())
|
||||||
|
if model in (admin, guest):
|
||||||
|
self.request.session.flash("You may not delete the %s role." % str(model), 'error')
|
||||||
def edit_role(request):
|
return HTTPFound(location=self.request.get_referrer())
|
||||||
uuid = request.matchdict['uuid']
|
|
||||||
role = Session.query(edbob.Role).get(uuid) if uuid else None
|
|
||||||
assert role
|
|
||||||
|
|
||||||
fs = role_fieldset(role, request)
|
|
||||||
if request.POST:
|
|
||||||
fs.rebind(data=request.params)
|
|
||||||
if fs.validate():
|
|
||||||
|
|
||||||
with transaction.manager:
|
|
||||||
Session.add(fs.model)
|
|
||||||
fs.sync()
|
|
||||||
request.session.flash("%s \"%s\" has been %s." % (
|
|
||||||
fs.crud_title, fs.get_display_text(),
|
|
||||||
'updated' if fs.edit else 'created'))
|
|
||||||
home = request.route_url('roles.list')
|
|
||||||
|
|
||||||
return HTTPFound(location=home)
|
|
||||||
|
|
||||||
return {'fieldset': fs, 'crud': True}
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
||||||
|
config.add_route('roles', '/roles')
|
||||||
|
config.add_view(RolesGrid, route_name='roles',
|
||||||
|
renderer='/roles/index.mako',
|
||||||
|
permission='roles.list')
|
||||||
|
|
||||||
config.add_route('roles.list', '/roles')
|
settings = config.get_settings()
|
||||||
config.add_view(roles, route_name='roles.list', renderer='/roles/index.mako',
|
perms = settings.get('edbob.permissions')
|
||||||
permission='roles.list', http_cache=0)
|
if perms:
|
||||||
|
RoleCrud.permissions = perms
|
||||||
|
|
||||||
config.add_route('role.new', '/roles/new')
|
config.add_route('role.create', '/roles/new')
|
||||||
config.add_view(new_role, route_name='role.new', renderer='/roles/role.mako',
|
config.add_view(RoleCrud, attr='create', route_name='role.create',
|
||||||
permission='roles.create', http_cache=0)
|
renderer='/roles/crud.mako',
|
||||||
|
permission='roles.create')
|
||||||
|
|
||||||
config.add_route('role.edit', '/roles/{uuid}/edit')
|
config.add_route('role.read', '/roles/{uuid}')
|
||||||
config.add_view(edit_role, route_name='role.edit', renderer='/roles/role.mako',
|
config.add_view(RoleCrud, attr='read', route_name='role.read',
|
||||||
permission='roles.edit', http_cache=0)
|
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')
|
||||||
|
|
|
@ -26,33 +26,35 @@
|
||||||
``edbob.pyramid.views.users`` -- User Views
|
``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
|
import formalchemy
|
||||||
from formalchemy.fields import SelectFieldRenderer
|
from formalchemy.fields import SelectFieldRenderer
|
||||||
|
|
||||||
import edbob
|
from edbob.db import auth
|
||||||
from edbob.db.auth import set_user_password
|
|
||||||
from edbob.pyramid import Session
|
from edbob.pyramid import Session
|
||||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||||
|
from edbob.db.extensions.auth.model import User, Role
|
||||||
|
from edbob.db.extensions.contact.model import Person
|
||||||
|
|
||||||
|
|
||||||
class UsersGrid(SearchableAlchemyGridView):
|
class UsersGrid(SearchableAlchemyGridView):
|
||||||
|
|
||||||
mapped_class = edbob.User
|
mapped_class = User
|
||||||
config_prefix = 'users'
|
config_prefix = 'users'
|
||||||
sort = 'username'
|
sort = 'username'
|
||||||
|
|
||||||
def join_map(self):
|
def join_map(self):
|
||||||
return {
|
return {
|
||||||
'person':
|
'person':
|
||||||
lambda q: q.outerjoin(edbob.Person),
|
lambda q: q.outerjoin(Person),
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_map(self):
|
def filter_map(self):
|
||||||
return self.make_filter_map(
|
return self.make_filter_map(
|
||||||
ilike=['username'],
|
ilike=['username'],
|
||||||
person=self.filter_ilike(edbob.Person.display_name))
|
person=self.filter_ilike(Person.display_name))
|
||||||
|
|
||||||
def filter_config(self):
|
def filter_config(self):
|
||||||
return self.make_filter_config(
|
return self.make_filter_config(
|
||||||
|
@ -64,7 +66,7 @@ class UsersGrid(SearchableAlchemyGridView):
|
||||||
def sort_map(self):
|
def sort_map(self):
|
||||||
return self.make_sort_map(
|
return self.make_sort_map(
|
||||||
'username',
|
'username',
|
||||||
person=self.sorter(edbob.Person.display_name))
|
person=self.sorter(Person.display_name))
|
||||||
|
|
||||||
def grid(self):
|
def grid(self):
|
||||||
g = self.make_grid()
|
g = self.make_grid()
|
||||||
|
@ -86,22 +88,22 @@ class UsersGrid(SearchableAlchemyGridView):
|
||||||
return g
|
return g
|
||||||
|
|
||||||
|
|
||||||
class _RolesFieldRenderer(SelectFieldRenderer):
|
|
||||||
|
|
||||||
def render_readonly(self, **kwargs):
|
|
||||||
roles = Session.query(edbob.Role)
|
|
||||||
res = literal('<ul>')
|
|
||||||
for uuid in self.value:
|
|
||||||
role = roles.get(uuid)
|
|
||||||
res += literal('<li>%s</li>' % (
|
|
||||||
tags.link_to(role.name,
|
|
||||||
self.request.route_url('role.edit', uuid=role.uuid))))
|
|
||||||
res += literal('</ul>')
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def RolesFieldRenderer(request):
|
def RolesFieldRenderer(request):
|
||||||
return type('RolesFieldRenderer', (_RolesFieldRenderer,), {'request': request})
|
|
||||||
|
class RolesFieldRenderer(SelectFieldRenderer):
|
||||||
|
|
||||||
|
def render_readonly(self, **kwargs):
|
||||||
|
roles = Session.query(Role)
|
||||||
|
html = ''
|
||||||
|
for uuid in self.value:
|
||||||
|
role = roles.get(uuid)
|
||||||
|
link = tags.link_to(
|
||||||
|
role.name, request.route_url('role.read', uuid=role.uuid))
|
||||||
|
html += HTML.tag('li', c=link)
|
||||||
|
html = HTML.tag('ul', c=html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
return RolesFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
class RolesField(formalchemy.Field):
|
class RolesField(formalchemy.Field):
|
||||||
|
@ -116,14 +118,15 @@ class RolesField(formalchemy.Field):
|
||||||
return [x.uuid for x in user.roles]
|
return [x.uuid for x in user.roles]
|
||||||
|
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
q = Session.query(edbob.Role.name, edbob.Role.uuid)
|
q = Session.query(Role.name, Role.uuid)
|
||||||
q = q.order_by(edbob.Role.name)
|
q = q.filter(Role.uuid != auth.guest_role(Session()).uuid)
|
||||||
|
q = q.order_by(Role.name)
|
||||||
return q.all()
|
return q.all()
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
if not self.is_readonly():
|
if not self.is_readonly():
|
||||||
user = self.model
|
user = self.model
|
||||||
roles = Session.query(edbob.Role)
|
roles = Session.query(Role)
|
||||||
data = self.renderer.deserialize()
|
data = self.renderer.deserialize()
|
||||||
user.roles = [roles.get(x) for x in data]
|
user.roles = [roles.get(x) for x in data]
|
||||||
|
|
||||||
|
@ -138,7 +141,7 @@ class _ProtectedPersonRenderer(formalchemy.FieldRenderer):
|
||||||
|
|
||||||
|
|
||||||
def ProtectedPersonRenderer(uuid):
|
def ProtectedPersonRenderer(uuid):
|
||||||
person = Session.query(edbob.Person).get(uuid)
|
person = Session.query(Person).get(uuid)
|
||||||
assert person
|
assert person
|
||||||
return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,),
|
return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,),
|
||||||
{'person': person})
|
{'person': person})
|
||||||
|
@ -180,12 +183,12 @@ class PasswordField(formalchemy.Field):
|
||||||
if not self.is_readonly():
|
if not self.is_readonly():
|
||||||
password = self.renderer.deserialize()
|
password = self.renderer.deserialize()
|
||||||
if password:
|
if password:
|
||||||
set_user_password(self.model, password)
|
auth.set_user_password(self.model, password)
|
||||||
|
|
||||||
|
|
||||||
class UserCrud(CrudView):
|
class UserCrud(CrudView):
|
||||||
|
|
||||||
mapped_class = edbob.User
|
mapped_class = User
|
||||||
home_route = 'users'
|
home_route = 'users'
|
||||||
|
|
||||||
def fieldset(self, user):
|
def fieldset(self, user):
|
||||||
|
@ -206,8 +209,12 @@ class UserCrud(CrudView):
|
||||||
fs.roles,
|
fs.roles,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if self.readonly:
|
||||||
|
del fs.password
|
||||||
|
del fs.confirm_password
|
||||||
|
|
||||||
# if fs.edit and user.person:
|
# if fs.edit and user.person:
|
||||||
if isinstance(user, edbob.User) and user.person:
|
if isinstance(user, User) and user.person:
|
||||||
fs.person.set(readonly=True,
|
fs.person.set(readonly=True,
|
||||||
renderer=LinkedPersonRenderer(self.request))
|
renderer=LinkedPersonRenderer(self.request))
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,8 @@
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
from pyramid.config import Configurator
|
||||||
from pyramid.authentication import SessionAuthenticationPolicy
|
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
from edbob.pyramid.auth import EdbobAuthorizationPolicy
|
|
||||||
|
|
||||||
|
|
||||||
def main(global_config, **settings):
|
def main(global_config, **settings):
|
||||||
|
@ -27,24 +25,18 @@ def main(global_config, **settings):
|
||||||
# * Raise an exception if a setting is missing or invalid.
|
# * Raise an exception if a setting is missing or invalid.
|
||||||
# * Convert values from strings to their intended type.
|
# * Convert values from strings to their intended type.
|
||||||
|
|
||||||
settings['mako.directories'] = [
|
settings.setdefault('mako.directories', [
|
||||||
'{{package}}.pyramid:templates',
|
'{{package}}.pyramid:templates',
|
||||||
'edbob.pyramid:templates',
|
'edbob.pyramid:templates',
|
||||||
]
|
])
|
||||||
|
|
||||||
|
# Make two attempts when "retryable" errors happen during transactions.
|
||||||
|
settings.setdefault('tm.attempts', 2)
|
||||||
|
|
||||||
config = Configurator(settings=settings)
|
config = Configurator(settings=settings)
|
||||||
|
|
||||||
# Configure edbob
|
# Configure edbob
|
||||||
edbob.init('{{package}}', os.path.abspath(settings['edbob.config']))
|
edbob.init('{{package}}', os.path.abspath(settings['edbob.config']))
|
||||||
|
|
||||||
# Configure session
|
|
||||||
config.include('pyramid_beaker')
|
|
||||||
|
|
||||||
# Configure auth
|
|
||||||
config.set_authentication_policy(SessionAuthenticationPolicy())
|
|
||||||
config.set_authorization_policy(EdbobAuthorizationPolicy())
|
|
||||||
|
|
||||||
# Include "core" stuff provided by edbob.
|
|
||||||
config.include('edbob.pyramid')
|
config.include('edbob.pyramid')
|
||||||
|
|
||||||
# Additional config is defined elsewhere within {{project}}. This includes
|
# Additional config is defined elsewhere within {{project}}. This includes
|
||||||
|
@ -53,4 +45,8 @@ def main(global_config, **settings):
|
||||||
config.include('{{package}}.pyramid.subscribers')
|
config.include('{{package}}.pyramid.subscribers')
|
||||||
config.include('{{package}}.pyramid.views')
|
config.include('{{package}}.pyramid.views')
|
||||||
|
|
||||||
|
# Consider PostgreSQL server restart errors to be "retryable."
|
||||||
|
config.add_tween('edbob.pyramid.tweens.sqlerror_tween_factory',
|
||||||
|
under='pyramid_tm.tm_tween_factory')
|
||||||
|
|
||||||
return config.make_wsgi_app()
|
return config.make_wsgi_app()
|
||||||
|
|
19
edbob/tests/__init__.py
Normal file
19
edbob/tests/__init__.py
Normal 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)
|
|
@ -75,7 +75,7 @@ def init(config):
|
||||||
tz = config.get('edbob.time', key)
|
tz = config.get('edbob.time', key)
|
||||||
if tz:
|
if tz:
|
||||||
key = key[5:]
|
key = key[5:]
|
||||||
log.info("'%s' timezone set to '%s'" % (key, tz))
|
log.debug("'%s' timezone set to '%s'" % (key, tz))
|
||||||
set_timezone(tz, key)
|
set_timezone(tz, key)
|
||||||
|
|
||||||
if 'local' not in timezones:
|
if 'local' not in timezones:
|
||||||
|
|
|
@ -38,9 +38,17 @@ if sys.platform == 'win32': # docs should build for everyone
|
||||||
import win32file
|
import win32file
|
||||||
import win32print
|
import win32print
|
||||||
import win32service
|
import win32service
|
||||||
import win32serviceutil
|
|
||||||
import winerror
|
import winerror
|
||||||
|
|
||||||
|
try:
|
||||||
|
import win32serviceutil
|
||||||
|
except ImportError:
|
||||||
|
# Mock out for testing on Linux.
|
||||||
|
class Object(object):
|
||||||
|
pass
|
||||||
|
win32serviceutil = Object()
|
||||||
|
win32serviceutil.ServiceFramework = Object
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,6 +171,20 @@ def RegDeleteTree(key, subkey):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def capture_output(command):
|
||||||
|
"""
|
||||||
|
Runs ``command`` and returns any output it produces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We *need* to pipe ``stdout`` because that's how we capture the output of
|
||||||
|
# the ``hg`` command. However, we must pipe *all* handles in order to
|
||||||
|
# prevent issues when running as a GUI but *from* the Windows console.
|
||||||
|
# See also: http://bugs.python.org/issue3905
|
||||||
|
kwargs = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
output = subprocess.Popen(command, **kwargs).communicate()[0]
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
def delayed_auto_start_service(name):
|
def delayed_auto_start_service(name):
|
||||||
"""
|
"""
|
||||||
Configures the Windows service named ``name`` such that its startup type is
|
Configures the Windows service named ``name`` such that its startup type is
|
||||||
|
|
36
fabfile.py
vendored
Normal file
36
fabfile.py
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# edbob -- Pythonic Software Framework
|
||||||
|
# Copyright © 2010-2012 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of edbob.
|
||||||
|
#
|
||||||
|
# edbob is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
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')
|
|
@ -1,2 +1,7 @@
|
||||||
[egg_info]
|
[nosetests]
|
||||||
tag_build = .dev
|
nocapture = 1
|
||||||
|
cover-package = edbob
|
||||||
|
cover-erase = 1
|
||||||
|
cover-inclusive = 1
|
||||||
|
cover-html = 1
|
||||||
|
cover-html-dir = htmlcov
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -73,7 +73,10 @@ requires = [
|
||||||
'decorator', # 3.3.2
|
'decorator', # 3.3.2
|
||||||
'lockfile', # 0.9.1
|
'lockfile', # 0.9.1
|
||||||
'progressbar', # 2.3
|
'progressbar', # 2.3
|
||||||
'pytz', # 2012b
|
|
||||||
|
# Hardcode ``pytz`` minimum since apparently it isn't (any longer?) enough
|
||||||
|
# to simply require the library.
|
||||||
|
'pytz>=2013b', # 2013b
|
||||||
]
|
]
|
||||||
|
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
|
@ -209,6 +212,8 @@ setup(
|
||||||
|
|
||||||
install_requires = requires,
|
install_requires = requires,
|
||||||
extras_require = extras,
|
extras_require = extras,
|
||||||
|
tests_require = requires + ['nose'],
|
||||||
|
test_suite = 'nose.collector',
|
||||||
|
|
||||||
packages = find_packages(),
|
packages = find_packages(),
|
||||||
include_package_data = True,
|
include_package_data = True,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue