diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2333beb..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -edbob.egg-info \ No newline at end of file diff --git a/CHANGES.txt b/CHANGES.txt index 8bfbfbc..952c981 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,194 +1,4 @@ -0.1.2 ------ - -* Allow config file to prevent logging configuration from happening. - - -0.1.1 ------ - -* Some random things required in production at MaMa Jean's... - - Specifically this is known to replace occurrences of e.g. ``edbob.User`` with - a more standard (properly imported) reference to ``User``. - - -0.1a29 ------- - -* Removed ``setup.cfg`` file. - -* Changed some logging instances from ``INFO`` to ``DEBUG``. - -* Updated ``repr()`` output for model classes. - - -0.1a28 ------- - -- [bug] Secured daemon PID files. They are no longer readable or writeable - (whoops) by anyone other than the user who owns the process. - -- [feature] Added ``--progress`` argument to command system. - -- [general] Added initial Fabric script. - - -0.1a27 ------- - -- [feature] Overhauled file monitors. Added the "process existing" (defaults - to true) and "stop on error" (defaults to false) features to both Linux and - Win32 file monitors. Both features may be overridden in config. The Linux - file monitor was rewritten as an ``edbob.daemon.Daemon`` class. - -0.1a26 ------- - -- [feature] Added ``StrippingFieldRenderer``. - -- [general] Some style tweaks. - -- [bug] Fixed ``graft()`` function when called with module reference. - -0.1a25 ------- - -- [feature] Added "sqlerror" tween which, in combination with ``pyramid_tm``, - can allow Pyramid apps to gracefully handle database server restarts. Any - transaction which fails due to a disconnection from the server may be - attempted more than once. The assumption is that the second attempt will - succeed, since the underlying connection pool will have been invalidated upon - the first failure. - -- [feature] Added ``PHONE_TYPE`` enumeration. - -- [general] Added some ``__unicode__()`` methods to database models. - -- [bug] Fixed CSS in the ``progress.mako`` template. - -- [feature] Improved the ``load_spec()`` function so that it raises an - ``InvalidSpec`` exception if the spec doesn't contain exactly one colon. - -0.1a24 ------- - -- [bug] Fixed bug where creating a new ``Role`` with the UI would use default - permissions of the Guest role. Now the default permissions are empty. - -- [bug] Fixed ``User.roles`` UI so that the Guest role is never shown. - -0.1a23 ------- - -- [feature] Added ``capture_output()`` function to ``win32`` module. This is a - convenience function which works around an issue when attempting to capture - output from a command when the calling application is a Windows GUI app which - was launched via the Windows console (DOS terminal). - -- [feature] Updated ``User`` and ``Role`` management views for Pyramid apps. - -0.1a22 ------- - -- [feature] Added a view which allows a user to change his/her password from - within Pyramid web apps. - -- [general] Tweaked styles for form validation errors. - -- [general] Added convenience subclass of ``formencode.Schema``. - -- [general] Split CSS styles into ``base.css`` and ``layout.css``, since really - they serve different purposes. - -- [feature] Added ``overwriting_move()`` convenience function. - -0.1a21 ------- - -- [feature] Added the ability to specify a Mako template for use when - generating error emails. Templates may be referenced in the config file - using either a resource path or an absolute file path. If no template is - specified, the plain text fallback will be used. The ``content_type`` of the - email is also configurable (defaults to ``'text/html'`` if a Mako template is - used; ``'text/plain'`` otherwise). - -0.1a20 ------- - -- [feature] Added ability to configure path to the PID file used by file - monitor daemons running on Linux. This was necessary to allow multiple - daemons to run on the same machine. - -0.1a19 ------- - -- [general] Added ``active_extensions`` stubs to alembic migration script - template. - -- [general] Updated pyramid user views and templates. These were sorely out of - date. - -0.1a18 ------- - -- [feature] Added ``engine_from_config()`` function to ``edbob.sqlalchemy`` - module. This was done to provide awareness of the ``poolclass`` parameter to - engine configuration. - -0.1a17 ------- - -- [feature] Added ``display()`` method to ``AutocompleteView``. This allows - overriding the display text for autocomplete results, in the event that - ``str(result)`` isn't good enough. - -- [bug] Fixed ``request.has_perm()`` function so that it can check for - permissions on the "guest" role, in the event a user is not logged in. - -0.1a16 ------- - -- Add ``alembic/*.py`` to ``MANIFEST.in``. - -- Various progress tweaks. - -- Add ``OrderedDict`` import to ``util`` module. - -0.1a15 ------- - -- Add ``alembic`` to ``db`` dependencies. - -- Fix alembic ``env.py`` template. - -0.1a14 ------- - -- Slight overhaul of init() system; added ``edbob.init_modules()`` function. - -- Added ``read_service()`` method to ``AppConfigParser`` class, for use with - Windows services. - -- Added generic ``Service`` class to ``edbob.win32`` module. (File monitor now - inherits from it.) - -- Tweaked ``edbob.db`` initialization somewhat. (``Base.metadata`` no longer - binds to ``edbob.db.engine``.) - -- Fixed guest role bug in ``edbob.db.auth.has_permission()`` function. - -- Added "automagical" enumeration support for database extensions. - -- Added ``EMAIL_PREFERENCE`` enum to ``contact`` database extension. - -- Tweaked ``edbob.pyramid.includeme()``. - -- Tweaked ``people`` Pyramid views. - -- Added ``edbob.daemon`` module. - 0.1a13 ------ diff --git a/MANIFEST.in b/MANIFEST.in index acc928b..4943929 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,5 +19,3 @@ recursive-include edbob/pyramid/templates *.mako recursive-include edbob/scaffolds/edbob *.py recursive-include edbob/scaffolds/edbob *_tmpl recursive-include edbob/scaffolds/edbob/+package+/pyramid/templates *.mako - -recursive-include edbob/templates *.mako diff --git a/edbob/__init__.py b/edbob/__init__.py index e1574df..aa2b830 100644 --- a/edbob/__init__.py +++ b/edbob/__init__.py @@ -28,10 +28,12 @@ from edbob._version import __version__ -from edbob.enum import * from edbob.core import * from edbob.time import * from edbob.files import * from edbob.modules import * from edbob.configuration import * from edbob.initialization import * + + +inited = False diff --git a/edbob/_version.py b/edbob/_version.py index 10939f0..71fe714 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1.2' +__version__ = '0.1a13' diff --git a/edbob/commands.py b/edbob/commands.py index c6e5b0f..6c47598 100644 --- a/edbob/commands.py +++ b/edbob/commands.py @@ -105,7 +105,6 @@ Options: Config path (may be specified more than once) -n, --no-init Don't load config before executing command -d, --debug Increase logging level to DEBUG - -P, --progress Show progress indicators (where relevant) -v, --verbose Increase logging level to INFO -V, --version Display program version and exit @@ -133,7 +132,6 @@ Try '%(name)s help ' for more help.""" % self metavar='PATH') parser.add_argument('-d', '--debug', action='store_true', dest='debug') parser.add_argument('-n', '--no-init', action='store_true', default=False) - parser.add_argument('-P', '--progress', action='store_true', default=False) parser.add_argument('-v', '--verbose', action='store_true', dest='verbose') parser.add_argument('-V', '--version', action='version', version="%%(prog)s %s" % self.version) @@ -184,7 +182,6 @@ Try '%(name)s help ' for more help.""" % self # And finally, do something of real value... cmd = self.subcommands[cmd](parent=self) - cmd.show_progress = args.progress cmd._run(*(args.command + args.argv)) @@ -435,6 +432,11 @@ class FileMonitorCommand(Subcommand): uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service") uninstall.set_defaults(subcommand='remove') + else: + parser.add_argument('-D', '--dont-daemonize', + action='store_false', dest='daemonize', + help="Don't daemonize when starting") + def get_win32_module(self): from edbob.filemon import win32 return win32 @@ -452,10 +454,10 @@ class FileMonitorCommand(Subcommand): from edbob.filemon import linux as filemon if args.subcommand == 'start': - filemon.start_daemon(self.appname) + filemon.start_daemon(self.appname, daemonize=args.daemonize) elif args.subcommand == 'stop': - filemon.stop_daemon(self.appname) + filemon.stop_daemon() elif sys.platform == 'win32': from edbob import win32 diff --git a/edbob/configuration.py b/edbob/configuration.py index 5991f7d..4d8edbe 100644 --- a/edbob/configuration.py +++ b/edbob/configuration.py @@ -289,25 +289,6 @@ class AppConfigParser(ConfigParser.SafeConfigParser): self.paths_loaded.append(path) log.info("Read config file: %s" % path) - def read_service(self, service, paths): - """ - "Special" version of :meth:`read()` which will first inspect the - file(s) for a service-specific directive, the presence of which - indicates the *true* config file to be used for the service. - - This method is pretty much a hack to get around certain limitations of - Windows service implementations; it is not used otherwise. - """ - - config = ConfigParser.SafeConfigParser() - config.read(paths) - - if (config.has_section('edbob.service_config') - and config.has_option('edbob.service_config', service)): - paths = eval(config.get('edbob.service_config', service)) - - self.read(paths, recurse=True) - def require(self, section, option, msg=None): """ Convenience method which will raise an exception if the given option diff --git a/edbob/console.py b/edbob/console.py index 292afd0..ed6896e 100644 --- a/edbob/console.py +++ b/edbob/console.py @@ -38,7 +38,7 @@ class Progress(edbob.Object): """ def __init__(self, message, maximum): - sys.stderr.write("\n%s...(%u total)\n" % (message, maximum)) + print >> sys.stderr, "\n%s...(%u total)" % (message, maximum) widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', progressbar.ETA()] self.progress = progressbar.ProgressBar(maxval=maximum, widgets=widgets).start() @@ -47,4 +47,4 @@ class Progress(edbob.Object): return True def destroy(self): - sys.stderr.write("\n") + print >> sys.stderr, '' diff --git a/edbob/core.py b/edbob/core.py index 70ebc79..553ddab 100644 --- a/edbob/core.py +++ b/edbob/core.py @@ -112,7 +112,7 @@ def graft(target, source, names=None): if hasattr(source, '__all__'): names = source.__all__ else: - names = [x for x in dir(source) if not x.startswith('_')] + names = dir(source) elif isinstance(names, basestring): names = [names] diff --git a/edbob/daemon.py b/edbob/daemon.py deleted file mode 100644 index 05c90cb..0000000 --- a/edbob/daemon.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import absolute_import - -# This code was (mostly, with some tweaks) stolen from: -# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ - -import sys, os, time, atexit -import stat -from signal import SIGTERM - -class Daemon: - """ - A generic daemon class. - - Usage: subclass the Daemon class and override the run() method - """ - def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): - self.stdin = stdin - self.stdout = stdout - self.stderr = stderr - self.pidfile = pidfile - - def daemonize(self): - """ - do the UNIX double-fork magic, see Stevens' "Advanced - Programming in the UNIX Environment" for details (ISBN 0201563177) - http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 - """ - try: - pid = os.fork() - if pid > 0: - # exit first parent - sys.exit(0) - except OSError, e: - sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) - sys.exit(1) - - # decouple from parent environment - os.chdir("/") - os.setsid() - os.umask(0) - - # do second fork - try: - pid = os.fork() - if pid > 0: - # exit from second parent - sys.exit(0) - except OSError, e: - sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) - sys.exit(1) - - # redirect standard file descriptors - sys.stdout.flush() - sys.stderr.flush() - si = file(self.stdin, 'r') - so = file(self.stdout, 'a+') - se = file(self.stderr, 'a+', 0) - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) - - # write pidfile - atexit.register(self.delpid) - pid = str(os.getpid()) - file(self.pidfile,'w+').write("%s\n" % pid) - os.chmod(self.pidfile, stat.S_IRUSR|stat.S_IWUSR) - - def delpid(self): - os.remove(self.pidfile) - - def start(self): - """ - Start the daemon - """ - # Check for a pidfile to see if the daemon already runs - try: - pf = file(self.pidfile,'r') - pid = int(pf.read().strip()) - pf.close() - except IOError: - pid = None - - if pid: - message = "pidfile %s already exist. Daemon already running?\n" - sys.stderr.write(message % self.pidfile) - sys.exit(1) - - # Start the daemon - self.daemonize() - self.run() - - def stop(self): - """ - Stop the daemon - """ - # Get the pid from the pidfile - try: - pf = file(self.pidfile,'r') - pid = int(pf.read().strip()) - pf.close() - except IOError: - pid = None - - if not pid: - message = "pidfile %s does not exist. Daemon not running?\n" - sys.stderr.write(message % self.pidfile) - return # not an error in a restart - - # Try killing the daemon process - try: - while 1: - os.kill(pid, SIGTERM) - time.sleep(0.1) - except OSError, err: - err = str(err) - if err.find("No such process") > 0: - if os.path.exists(self.pidfile): - os.remove(self.pidfile) - else: - print str(err) - sys.exit(1) - - def restart(self): - """ - Restart the daemon - """ - self.stop() - self.start() - - def run(self): - """ - You should override this method when you subclass Daemon. It will be called after the process has been - daemonized by start() or restart(). - """ diff --git a/edbob/db/__init__.py b/edbob/db/__init__.py index 74b01d2..84f0d42 100644 --- a/edbob/db/__init__.py +++ b/edbob/db/__init__.py @@ -28,16 +28,16 @@ from __future__ import absolute_import -from sqlalchemy import MetaData +from sqlalchemy import engine_from_config, MetaData from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base import edbob -from edbob.sqlalchemy import engine_from_config __all__ = ['engines', 'engine', 'Session', 'get_setting', 'save_setting'] +inited = False engines = None engine = None Session = sessionmaker() @@ -71,36 +71,36 @@ def init(config): from edbob.db import enum from edbob.db.extensions import extend_framework - global engines, engine + global inited, engines, engine - keys = config.get('edbob.db', 'keys') + keys = config.get('edbob.db', 'sqlalchemy.keys') if keys: - keys = keys.split(',') + keys = keys.split() else: keys = ['default'] engines = {} cfg = config.get_dict('edbob.db') for key in keys: - key = key.strip() try: - engines[key] = engine_from_config(cfg, '%s.' % key) + engines[key] = engine_from_config(cfg, 'sqlalchemy.%s.' % key) except KeyError: if key == 'default': try: - engines[key] = engine_from_config(cfg, 'sqlalchemy.') + engines[key] = engine_from_config(cfg) except KeyError: pass engine = engines.get('default') if engine: - Session.configure(bind=engine) + Base.metadata.bind = engine extend_framework() edbob.graft(edbob, edbob.db) edbob.graft(edbob, model) edbob.graft(edbob, enum) + inited = True def get_setting(name, session=None): diff --git a/edbob/db/auth.py b/edbob/db/auth.py index 31fbb5b..cf1dec8 100644 --- a/edbob/db/auth.py +++ b/edbob/db/auth.py @@ -85,12 +85,12 @@ def guest_role(session): """ uuid = 'f8a27c98965a11dfaff7001143047286' - guest = session.query(edbob.Role).get(uuid) - if guest: - return guest - guest = edbob.Role(uuid=uuid, name='Guest') - session.add(guest) - return guest + admin = session.query(edbob.Role).get(uuid) + if admin: + return admin + admin = edbob.Role(uuid=uuid, name='Guest') + session.add(admin) + return admin def grant_permission(role, permission, session=None): @@ -105,7 +105,7 @@ def grant_permission(role, permission, session=None): role.permissions.append(permission) -def has_permission(obj, perm, include_guest=True, session=None): +def has_permission(obj, perm, session=None): """ Checks the given ``obj`` (which may be either a :class:`edbob.User`` or :class:`edbob.Role` instance), and returns a boolean indicating whether or @@ -114,7 +114,7 @@ def has_permission(obj, perm, include_guest=True, session=None): """ if isinstance(obj, edbob.User): - roles = list(obj.roles) + roles = obj.roles elif isinstance(obj, edbob.Role): roles = [obj] elif obj is None: @@ -124,9 +124,8 @@ def has_permission(obj, perm, include_guest=True, session=None): if not session: session = object_session(obj) assert session - if include_guest: - roles.append(guest_role(session)) admin = administrator_role(session) + roles.append(guest_role(session)) for role in roles: if role is admin: return True diff --git a/edbob/db/extensions/__init__.py b/edbob/db/extensions/__init__.py index a235b3e..c99c7c7 100644 --- a/edbob/db/extensions/__init__.py +++ b/edbob/db/extensions/__init__.py @@ -76,11 +76,6 @@ class Extension(edbob.Object): # :meth:`Extension.get_models_module()` for more info). model_module = '' - # You can set this to any dotted module path you like. If unset a default - # will be assumed, of the form ``.enum`` (see - # :meth:`Extension.get_enum_module()` for more info). - enum_module = '' - # @property # @requires_impl(is_property=True) # def name(self): @@ -124,9 +119,6 @@ class Extension(edbob.Object): """ edbob.graft(edbob, self.get_model_module()) - enum = self.get_enum_module() - if enum: - edbob.graft(edbob, enum) # def extend_mappers(self, metadata): # """ @@ -152,26 +144,6 @@ class Extension(edbob.Object): self.populate_metadata(meta, recurse) return meta - def get_enum_module(self): - """ - Imports and returns a reference to the Python module providing - enumeration data for the extension (if one exists). - - :attr:`Extension.enum_module` is first consulted to determine the - dotted module path. If nothing is found there, a default path is - constructed by appending ``'.enum'`` to the extension module's own - dotted path. - """ - - if self.enum_module: - module = self.enum_module - else: - module = str(self.__class__.__module__) + '.enum' - try: - return import_module_path(module) - except ImportError: - return None - def get_model_module(self): """ Imports and returns a reference to the Python module providing schema @@ -366,7 +338,7 @@ def extend_framework(): session.close() for name in sorted(extensions, extension_sorter(extensions)): - log.debug("Applying active extension: %s" % name) + log.info("Applying active extension: %s" % name) ext = extensions[name] # merge_extension_metadata(ext) # ext.extend_classes() diff --git a/edbob/db/extensions/auth/model.py b/edbob/db/extensions/auth/model.py index f167739..44f486b 100644 --- a/edbob/db/extensions/auth/model.py +++ b/edbob/db/extensions/auth/model.py @@ -51,11 +51,10 @@ class Permission(Base): permission = Column(String(50), primary_key=True) def __repr__(self): - return "Permission(role_uuid={0}, permission={1})".format( - repr(self.role_uuid), repr(self.permission)) + return "" % (self.role, self.permission) - def __unicode__(self): - return unicode(self.permission or '') + def __str__(self): + return str(self.permission or '') class UserRole(Base): @@ -70,7 +69,7 @@ class UserRole(Base): role_uuid = Column(String(32), ForeignKey('roles.uuid')) def __repr__(self): - return "UserRole(uuid={0})".format(repr(self.uuid)) + return "" % (self.user, self.role) class Role(Base): @@ -90,18 +89,16 @@ class Role(Base): creator=lambda x: Permission(permission=x), getset_factory=getset_factory) - _users = relationship( - UserRole, backref='role', - cascade='save-update, merge, delete, delete-orphan') + _users = relationship(UserRole, backref='role') users = association_proxy('_users', 'user', creator=lambda x: UserRole(user=x), getset_factory=getset_factory) def __repr__(self): - return "Role(uuid={0})".format(repr(self.uuid)) + return "" % self.name - def __unicode__(self): - return unicode(self.name or '') + def __str__(self): + return str(self.name or '') class User(Base): @@ -125,18 +122,17 @@ class User(Base): getset_factory=getset_factory) def __repr__(self): - return "User(uuid={0})".format(repr(self.uuid)) + return "" % self.username - def __unicode__(self): - return unicode(self.username or '') + def __str__(self): + return str(self.username or '') @property def display_name(self): """ - Returns :attr:`Person.display_name` if present; otherwise returns - :attr:`username`. + Returns the user's ``person.display_name``, if present, otherwise the + ``username``. """ - if self.person and self.person.display_name: return self.person.display_name return self.username diff --git a/edbob/db/extensions/contact/model.py b/edbob/db/extensions/contact/model.py index 6576b5c..8cd05de 100644 --- a/edbob/db/extensions/contact/model.py +++ b/edbob/db/extensions/contact/model.py @@ -72,8 +72,7 @@ class PhoneNumber(Base): __mapper_args__ = {'polymorphic_on': parent_type} def __repr__(self): - return "{0}(uuid={1})".format( - self.__class__.__name__, repr(self.uuid)) + return "<%s: %s>" % (self.__class__.__name__, self.number) def __unicode__(self): return unicode(self.number) @@ -104,8 +103,7 @@ class EmailAddress(Base): __mapper_args__ = {'polymorphic_on': parent_type} def __repr__(self): - return "{0}(uuid={1})".format( - self.__class__.__name__, repr(self.uuid)) + return "<%s: %s>" % (self.__class__.__name__, self.address) def __unicode__(self): return unicode(self.address) @@ -133,7 +131,7 @@ class Person(Base): display_name = Column(String(100), default=get_person_display_name) def __repr__(self): - return "Person(uuid={0})".format(repr(self.uuid)) + return "" % self.display_name def __unicode__(self): return unicode(self.display_name or '') diff --git a/edbob/db/model.py b/edbob/db/model.py index b5222c3..a077ba0 100644 --- a/edbob/db/model.py +++ b/edbob/db/model.py @@ -54,7 +54,7 @@ class ActiveExtension(Base): name = Column(String(50), primary_key=True) def __repr__(self): - return "ActiveExtension(name={0})".format(repr(self.name)) + return "" % self.name def __str__(self): return str(self.name or '') @@ -71,4 +71,4 @@ class Setting(Base): value = Column(Text) def __repr__(self): - return "Setting(name={0})".format(repr(self.name)) + return "" % self.name diff --git a/edbob/enum.py b/edbob/enum.py deleted file mode 100644 index 958dd7e..0000000 --- a/edbob/enum.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# edbob -- Pythonic Software Framework -# Copyright © 2010-2012 Lance Edgar -# -# This file is part of edbob. -# -# edbob is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# edbob is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with edbob. If not, see . -# -################################################################################ - -""" -``edbob.enum`` -- Enumerations -""" - - -EMAIL_PREFERENCE_NONE = 0 -EMAIL_PREFERENCE_TEXT = 1 -EMAIL_PREFERENCE_HTML = 2 -EMAIL_PREFERENCE_MOBILE = 3 - -EMAIL_PREFERENCE = { - EMAIL_PREFERENCE_NONE : "No Emails", - EMAIL_PREFERENCE_TEXT : "Text", - EMAIL_PREFERENCE_HTML : "HTML", - EMAIL_PREFERENCE_MOBILE : "Mobile", - } - - -PHONE_TYPE_HOME = 'home' -PHONE_TYPE_MOBILE = 'mobile' -PHONE_TYPE_OTHER = 'other' - -PHONE_TYPE = { - PHONE_TYPE_HOME : "Home", - PHONE_TYPE_MOBILE : "Mobile", - PHONE_TYPE_OTHER : "Other", - } diff --git a/edbob/errors.py b/edbob/errors.py index 8f15c4e..4151350 100644 --- a/edbob/errors.py +++ b/edbob/errors.py @@ -26,7 +26,6 @@ ``edbob.errors`` -- Error Alert Emails """ -import os.path import sys import socket import logging @@ -34,7 +33,6 @@ from traceback import format_exception from cStringIO import StringIO import edbob -from edbob.files import resource_path from edbob.mail import sendmail_with_config @@ -62,52 +60,15 @@ def email_exception(type=None, value=None, traceback=None): if not (type and value and traceback): type, value, traceback = sys.exc_info() - hostname = socket.gethostname() - traceback = ''.join(format_exception(type, value, traceback)) - traceback = traceback.strip() - data = { - 'host_name': hostname, - 'host_ip': socket.gethostbyname(hostname), - 'host_time': edbob.local_time(), - 'traceback': traceback, - } - - body, ctype = render_exception(data) - ctype = edbob.config.get('edbob.errors', 'content_type', default=ctype) - sendmail_with_config('errors', body, content_type=ctype) - - -def render_exception(data): - """ - Renders the exception data using a Mako template if one is configured; - otherwise as a simple string. - """ - - template = edbob.config.get('edbob.errors', 'template') - if template: - template = resource_path(template) - if os.path.exists(template): - - # Assume Mako template; render and return. - from mako.template import Template - template = Template(filename=template) - return template.render(**data), 'text/html' - - # If not a Mako template, return regular text with substitutions. body = StringIO() - data['host_time'] = data['host_time'].strftime('%Y-%m-%d %H:%M:%S %Z%z') - body.write("""\ -An unhandled exception occurred. + hostname = socket.gethostname() + body.write("An exception occurred.\n") + body.write("\n") + body.write("Machine Name: %s (%s)\n" % (hostname, socket.gethostbyname(hostname))) + body.write("Local Time: %s\n" % (edbob.local_time().strftime('%Y-%m-%d %H:%M:%S %Z%z'))) + body.write("\n") + body.write("%s\n" % ''.join(format_exception(type, value, traceback))) -Machine Name: %(host_name)s (%(host_ip)s) - -Machine Time: %(host_time)s - -%(traceback)s -""" % data) - - b = body.getvalue() + sendmail_with_config('errors', body.getvalue()) body.close() - - return b, 'text/plain' diff --git a/edbob/exceptions.py b/edbob/exceptions.py index 58da82f..a5a802e 100644 --- a/edbob/exceptions.py +++ b/edbob/exceptions.py @@ -73,12 +73,6 @@ class LoadSpecError(Exception): return None -class InvalidSpec(LoadSpecError): - - def specifics(self): - return "invalid spec" - - class ModuleMissingAttribute(LoadSpecError): """ Raised during :func:`edbob.load_spec()` when the module imported okay but diff --git a/edbob/filemon/__init__.py b/edbob/filemon/__init__.py index 205ac31..07662e5 100644 --- a/edbob/filemon/__init__.py +++ b/edbob/filemon/__init__.py @@ -26,18 +26,10 @@ ``edbob.filemon`` -- File Monitoring Service """ -import os import os.path -import sys -import Queue import logging import edbob -from edbob.errors import email_exception - -if sys.platform == 'win32': - import win32api - from edbob.win32 import file_is_free log = logging.getLogger(__name__) @@ -73,12 +65,6 @@ class MonitorProfile(object): self.locks = edbob.config.getboolean( '%s.filemon' % appname, '%s.locks' % key, default=False) - self.process_existing = edbob.config.getboolean( - '%s.filemon' % appname, '%s.process_existing' % key, default=True) - - self.stop_on_error = edbob.config.getboolean( - '%s.filemon' % appname, '%s.stop_on_error' % key, default=False) - def get_monitor_profiles(appname): """ @@ -124,105 +110,3 @@ def get_monitor_profiles(appname): del monitored[key] return monitored - - -def queue_existing(profile, path): - """ - Adds files found in a watched folder to a processing queue. This is called - when the monitor first starts, to handle the case of files which exist - prior to startup. - - If files are found, they are first sorted by modification timestamp, using - a lexical sort on the filename as a tie-breaker, and then added to the - queue in that order. - - :param profile: Monitor profile for which the folder is to be watched. The - profile is expected to already have a queue attached; any existing files - will be added to this queue. - :type profile: :class:`edbob.filemon.MonitorProfile` instance - - :param path: Folder path which is to be checked for files. - :type path: string - - :returns: ``None`` - """ - - def sorter(x, y): - mtime_x = os.path.getmtime(x) - mtime_y = os.path.getmtime(y) - if mtime_x < mtime_y: - return -1 - if mtime_x > mtime_y: - return 1 - return cmp(x, y) - - paths = [os.path.join(path, x) for x in os.listdir(path)] - for path in sorted(paths, cmp=sorter): - - # Only process normal files. - if not os.path.isfile(path): - continue - - # If using locks, don't process "in transit" files. - if profile.locks and path.endswith('.lock'): - continue - - log.debug("queue_existing: queuing existing file for " - "profile '%s': %s" % (profile.key, path)) - profile.queue.put(path) - - -def perform_actions(profile): - """ - Callable target for action threads. - """ - - keep_going = True - while keep_going: - - try: - path = profile.queue.get_nowait() - except Queue.Empty: - pass - else: - - # In some cases, processing one file may cause other related files - # to also be processed. When this happens, a path on the queue may - # point to a file which no longer exists. - if not os.path.exists(path): - log.info("perform_actions: path does not exist: %s" % path) - continue - - log.debug("perform_actions: processing file: %s" % path) - - if sys.platform == 'win32': - while not file_is_free(path): - win32api.Sleep(0) - - for spec, func, args in profile.actions: - - log.info("perform_actions: calling function '%s' on file: %s" % - (spec, path)) - - try: - func(path, *args) - - except: - log.exception("perform_actions: exception occurred " - "while processing file: %s" % path) - email_exception() - - # Don't process any more files if the profile is so - # configured. - if profile.stop_on_error: - keep_going = False - - # Either way this particular file probably shouldn't be - # processed any further. - log.warning("perform_actions: no further processing " - "will be done for file: %s" % path) - break - - log.warning("perform_actions: error encountered, and configuration " - "dictates that no more actions will be processed for " - "profile: %s" % profile.key) diff --git a/edbob/filemon/linux.py b/edbob/filemon/linux.py index ae1c4a0..23bbad1 100644 --- a/edbob/filemon/linux.py +++ b/edbob/filemon/linux.py @@ -27,24 +27,14 @@ """ import sys +import os import os.path -import threading -import Queue +import signal import logging - -try: - import pyinotify -except ImportError: - # Mock out for testing on Windows. - class Dummy(object): - pass - pyinotify = Dummy() - pyinotify.ProcessEvent = Dummy +import pyinotify import edbob -from edbob import filemon -from edbob.daemon import Daemon -from edbob.errors import email_exception +from edbob.filemon import get_monitor_profiles log = logging.getLogger(__name__) @@ -55,8 +45,9 @@ class EventHandler(pyinotify.ProcessEvent): Event processor for file monitor daemon. """ - def my_init(self, profile=None, **kwargs): - self.profile = profile + def my_init(self, actions=[], locks=False, **kwargs): + self.actions = actions + self.locks = locks def process_IN_ACCESS(self, event): log.debug("EventHandler: IN_ACCESS: %s" % event.pathname) @@ -66,85 +57,84 @@ class EventHandler(pyinotify.ProcessEvent): def process_IN_CLOSE_WRITE(self, event): log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname) - if not self.profile.locks: - self.profile.queue.put(event.pathname) + if not self.locks: + self.perform_actions(event.pathname) def process_IN_CREATE(self, event): log.debug("EventHandler: IN_CREATE: %s" % event.pathname) def process_IN_DELETE(self, event): log.debug("EventHandler: IN_DELETE: %s" % event.pathname) - if self.profile.locks and event.pathname.endswith('.lock'): - self.profile.queue.put(event.pathname[:-5]) + if self.locks and event.pathname.endswith('.lock'): + self.perform_actions(event.pathname[:-5]) def process_IN_MODIFY(self, event): log.debug("EventHandler: IN_MODIFY: %s" % event.pathname) def process_IN_MOVED_TO(self, event): log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname) - if not self.profile.locks: - self.profile.queue.put(event.pathname) + if not self.locks: + self.perform_actions(event.pathname) + + def perform_actions(self, path): + for spec, func, args in self.actions: + func(path, *args) -class FileMonitorDaemon(Daemon): +def get_pid_path(): + """ + Returns the path to the PID file for the file monitor daemon. + """ - def run(self): - - 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() + basename = os.path.basename(sys.argv[0]) + return '/tmp/%s_filemon.pid' % basename -def get_daemon(appname=None): - if appname is None: - appname = os.path.basename(sys.argv[0]) - pid_path = edbob.config.get('%s.filemon' % appname, 'pid_path') - if not pid_path: - pid_path = '/tmp/%s_filemon.pid' % appname +def start_daemon(appname, daemonize=True): + """ + Starts the file monitor daemon. + """ - monitor = FileMonitorDaemon(pid_path) - monitor.appname = appname - return monitor + pid_path = get_pid_path() + if os.path.exists(pid_path): + print "File monitor is already running" + return + + wm = pyinotify.WatchManager() + notifier = pyinotify.Notifier(wm) + + monitored = get_monitor_profiles(appname) + + mask = (pyinotify.IN_ACCESS | pyinotify.IN_ATTRIB + | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CREATE + | pyinotify.IN_DELETE | pyinotify.IN_MODIFY + | pyinotify.IN_MOVED_TO) + for profile in monitored.itervalues(): + for path in profile.dirs: + wm.add_watch(path, mask, proc_fun=EventHandler( + actions=profile.actions, locks=profile.locks)) + + if not daemonize: + sys.stderr.write("Starting file monitor. (Press Ctrl+C to quit.)\n") + notifier.loop(daemonize=daemonize, pid_file=pid_path) -def start_daemon(appname): - get_daemon(appname).start() +def stop_daemon(): + """ + Stops the file monitor daemon. + """ + pid_path = get_pid_path() + if not os.path.exists(pid_path): + print "File monitor is not running" + return -def stop_daemon(appname): - get_daemon(appname).stop() + 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) diff --git a/edbob/filemon/win32.py b/edbob/filemon/win32.py index 504406c..a2a85e3 100644 --- a/edbob/filemon/win32.py +++ b/edbob/filemon/win32.py @@ -33,9 +33,9 @@ import logging import threading import edbob -from edbob import filemon from edbob.errors import email_exception -from edbob.win32 import Service, file_is_free +from edbob.filemon import get_monitor_profiles +from edbob.win32 import file_is_free if sys.platform == 'win32': # docs should build for everyone import win32api @@ -50,26 +50,39 @@ if sys.platform == 'win32': # docs should build for everyone log = logging.getLogger(__name__) -class FileMonitorService(Service): +class FileMonitorService(win32serviceutil.ServiceFramework): """ Implements edbob's file monitor Windows service. """ - _svc_name_ = 'EdbobFileMonitor' + _svc_name_ = "Edbob File Monitor" _svc_display_name_ = "Edbob : File Monitoring Service" _svc_description_ = ("Monitors one or more folders for incoming files, " "and performs configured actions as new files arrive.") + appname = 'edbob' + + def __init__(self, args): + """ + Constructor. + """ + + # super(FileMonitorService, self).__init__(args) + win32serviceutil.ServiceFramework.__init__(self, args) + + # Create "wait stop" event, for main worker loop. + self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) + def Initialize(self): """ Service initialization. """ - if not Service.Initialize(self): - return False + # Read configuration file(s). + edbob.init(self.appname) # Read monitor profile(s) from config. - self.monitored = filemon.get_monitor_profiles(self.appname) + self.monitored = get_monitor_profiles(self.appname) # Make sure we have something to do. if not self.monitored: @@ -79,36 +92,78 @@ class FileMonitorService(Service): for key, profile in self.monitored.iteritems(): # Create a file queue for the profile. - profile.queue = Queue.Queue() + queue = Queue.Queue() - # Perform setup for each of the watched folders. + # Create a monitor thread for each folder in profile. for i, path in enumerate(profile.dirs, 1): - - # Maybe put all pre-existing files in the queue. - if profile.process_existing: - filemon.queue_existing(profile, path) - - # Create a monitor thread for the folder. name = 'monitor-%s-%u' % (key, i) log.debug("Initialize: Starting '%s' thread for folder: %s" % (name, path)) - thread = threading.Thread(target=monitor_files, - name=name, args=(profile, path)) + thread = threading.Thread( + target=monitor_files, + name=name, + args=(queue, path, profile)) thread.daemon = True thread.start() # Create an action thread for the profile. name = 'actions-%s' % key log.debug("Initialize: Starting '%s' thread" % name) - thread = threading.Thread(target=filemon.perform_actions, - name=name, args=(profile,)) + thread = threading.Thread( + target=perform_actions, + name=name, + args=(queue, profile)) thread.daemon = True thread.start() return True + def SvcDoRun(self): + """ + This method is invoked when the service starts. + """ + + import servicemanager + + # Write start occurrence to Windows Event Log. + servicemanager.LogMsg( + servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STARTED, + (self._svc_name_, '')) + + # Figure out what we're supposed to be doing. + if self.Initialize(): + + # Wait infinitely for stop request, while threads do their thing. + log.info("SvcDoRun: All threads started; waiting for stop request.") + win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE) + log.info("SvcDoRun: Stop request received.") + + else: # Nothing to be done... + msg = "Nothing to do! No valid monitor profiles found in config." + servicemanager.LogWarningMsg(msg) + log.warning("SvcDoRun: %s" % msg) + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + + # Write stop occurrence to Windows Event Log. + servicemanager.LogMsg( + servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STOPPED, + (self._svc_name_, '')) + + def SvcStop(self): + """ + This method is invoked when the service is requested to stop itself. + """ + + # Let the SCM know we're trying to stop. + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + + # Let worker loop know its job is done. + win32event.SetEvent(self.hWaitStop) + -def monitor_files(profile, path): +def monitor_files(queue, path, profile): """ Callable target for file monitor threads. """ @@ -140,7 +195,40 @@ def monitor_files(profile, path): winnt.FILE_ACTION_RENAMED_NEW_NAME): log.debug("monitor_files: Queueing '%s' file: %s" % (profile.key, fpath)) - profile.queue.put(fpath) + queue.put(fpath) + + +def perform_actions(queue, profile): + """ + Callable target for action threads. + """ + + while True: + + try: + path = queue.get_nowait() + except Queue.Empty: + pass + else: + + while not file_is_free(path): + win32api.Sleep(0) + + for spec, func, args in profile.actions: + + log.info("perform_actions: Calling function '%s' on file: %s" % + (spec, path)) + + try: + func(path, *args) + + except: + log.exception("perform_actions: An exception occurred " + "while processing file: %s" % path) + email_exception() + + # This file probably shouldn't be processed any further. + break if __name__ == '__main__': diff --git a/edbob/files.py b/edbob/files.py index f60f542..e50a8af 100644 --- a/edbob/files.py +++ b/edbob/files.py @@ -33,8 +33,6 @@ import shutil import tempfile import lockfile -import pkg_resources - __all__ = ['temp_path'] @@ -100,31 +98,6 @@ def count_lines(path): return lines -def overwriting_move(src, dst): - """ - Convenience function which is equivalent to ``shutil.move()``, except it - will cause the destination file to be overwritten if it exists. - """ - - if os.path.isdir(dst): - dst = os.path.join(dst, os.path.basename(src)) - if os.path.exists(dst): - os.remove(dst) - shutil.move(src, dst) - - -def resource_path(path): - """ - Returns a resource file path. ``path`` is assumed either to be a package - resource, or a regular file path. In the latter case it is returned - unchanged. - """ - - if not os.path.isabs(path) and ':' in path: - return pkg_resources.resource_filename(*path.split(':')) - return path - - def temp_path(suffix='.tmp', prefix='edbob.'): """ Convenience function to return a temporary file path. The arguments' diff --git a/edbob/initialization.py b/edbob/initialization.py index af2aeff..4a93344 100644 --- a/edbob/initialization.py +++ b/edbob/initialization.py @@ -38,9 +38,7 @@ from edbob.configuration import ( from edbob.exceptions import InitError -__all__ = ['init', 'init_modules', 'inited'] - -inited = [] +__all__ = ['init'] log = logging.getLogger(__name__) @@ -82,41 +80,22 @@ def init(appname='edbob', *args, **kwargs): else: config_paths = default_system_paths(appname) + default_user_paths(appname) - service = kwargs.get('service') - if service: - config.read_service(service, config_paths) - else: - shell = kwargs.get('shell', False) - for paths in config_paths: - config.read(paths, recurse=not shell) - if config.getboolean('edbob', 'configure_logging', default=True): - config.configure_logging() + shell = bool(kwargs.get('shell')) + for paths in config_paths: + config.read(paths, recurse=not shell) + config.configure_logging() default_modules = 'edbob.time' modules = config.get('edbob', 'init', default=default_modules) if modules: - modules = modules.split(',') - init_modules(modules, config) - - edbob.graft(edbob, locals(), 'config') - inited.append('edbob') - - -def init_modules(names, config=None): - """ - Initialize the given modules. ``names`` should be a sequence of strings, - each of which should be a dotted module name. If ``config`` is not - specified, :attr:`edbob.config` is assumed. - """ - - if config is None: - config = edbob.config - - for name in names: - name = name.strip() - if name not in inited: + for name in modules.split(','): + name = name.strip() module = __import__(name, globals(), locals(), fromlist=['init']) if not hasattr(module, 'init'): raise InitError(module) getattr(module, 'init')(config) - inited.append(name) + # config.inited.append(name) + + # config.inited.append('edbob') + edbob.graft(edbob, locals(), 'config') + edbob.inited = True diff --git a/edbob/modules.py b/edbob/modules.py index 2cdf59b..e3d4287 100644 --- a/edbob/modules.py +++ b/edbob/modules.py @@ -82,9 +82,6 @@ def load_spec(spec): necessary. """ - if spec.count(':') != 1: - raise exceptions.InvalidSpec(spec) - module_path, obj = spec.split(':') module = import_module_path(module_path) try: diff --git a/edbob/pyramid/__init__.py b/edbob/pyramid/__init__.py index a3abb88..64defc6 100644 --- a/edbob/pyramid/__init__.py +++ b/edbob/pyramid/__init__.py @@ -29,8 +29,6 @@ from sqlalchemy.orm import sessionmaker, scoped_session from zope.sqlalchemy import ZopeTransactionExtension -import edbob - __all__ = ['Session'] @@ -58,7 +56,6 @@ def includeme(config): config.include('pyramid_tm') # Configure SQLAlchemy session. - Session.configure(bind=edbob.engine) Session.configure(extension=ZopeTransactionExtension()) # Configure user authentication / authorization. @@ -72,6 +69,3 @@ def includeme(config): # Add static views. config.include('edbob.pyramid.static') - - # Add subscriber hooks. - config.include('edbob.pyramid.subscribers') diff --git a/edbob/pyramid/forms/formalchemy/__init__.py b/edbob/pyramid/forms/formalchemy/__init__.py index d6aec5f..9789988 100644 --- a/edbob/pyramid/forms/formalchemy/__init__.py +++ b/edbob/pyramid/forms/formalchemy/__init__.py @@ -50,8 +50,7 @@ from edbob.pyramid.forms.formalchemy.renderers import * __all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer', 'FieldSet', 'make_fieldset', 'required', 'pretty_datetime', - 'AssociationProxyField', 'StrippingFieldRenderer', - 'YesNoFieldRenderer'] + 'AssociationProxyField', 'YesNoFieldRenderer'] class TemplateEngine(formalchemy.templates.TemplateEngine): @@ -185,6 +184,7 @@ class DateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer): if isinstance(value, datetime.datetime): value = edbob.local_time(value) return value.strftime(self.format) + print type(value) return '' FieldSet.default_renderers[formalchemy.types.DateTime] = DateTimeFieldRenderer diff --git a/edbob/pyramid/forms/formalchemy/renderers.py b/edbob/pyramid/forms/formalchemy/renderers.py index 7b5f7e3..263f728 100644 --- a/edbob/pyramid/forms/formalchemy/renderers.py +++ b/edbob/pyramid/forms/formalchemy/renderers.py @@ -32,7 +32,7 @@ from pyramid.renderers import render __all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', - 'StrippingFieldRenderer', 'YesNoFieldRenderer'] + 'YesNoFieldRenderer'] def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'): @@ -74,26 +74,17 @@ def EnumFieldRenderer(enum): return '' if value in enum: return enum[value] - return str(value) + return value def render(self, **kwargs): - opts = [(enum[x], x) for x in sorted(enum)] + opts = [] + for value in sorted(enum): + opts.append((enum[value], value)) return formalchemy.fields.SelectFieldRenderer.render(self, opts, **kwargs) return Renderer -class StrippingFieldRenderer(formalchemy.TextFieldRenderer): - """ - Standard text field renderer, which strips whitespace from either end of - the input value on deserialization. - """ - - def deserialize(self): - value = super(StrippingFieldRenderer, self).deserialize() - return value.strip() - - class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer): def render_readonly(self, **kwargs): diff --git a/edbob/pyramid/forms/simpleform.py b/edbob/pyramid/forms/simpleform.py index 0abcec4..c1d9c51 100644 --- a/edbob/pyramid/forms/simpleform.py +++ b/edbob/pyramid/forms/simpleform.py @@ -26,29 +26,15 @@ ``edbob.pyramid.forms.simpleform`` -- pyramid_simpleform Forms """ -from pyramid.renderers import render - -import formencode import pyramid_simpleform +from pyramid.renderers import render from pyramid_simpleform.renderers import FormRenderer from edbob.pyramid import helpers from edbob.pyramid.forms import Form -__all__ = ['Schema', 'SimpleForm'] - - -class Schema(formencode.Schema): - """ - Subclass of ``formencode.Schema``, which exists only to ignore extra - fields. These normally would cause a schema instance to be deemed invalid, - and pretty much *every* form has a submit button which would be considered - an extra field. - """ - - allow_extra_fields = True - filter_extra_fields = True +__all__ = ['SimpleForm'] class SimpleForm(Form): diff --git a/edbob/pyramid/grids/alchemy.py b/edbob/pyramid/grids/alchemy.py index ca9e787..4402ac4 100644 --- a/edbob/pyramid/grids/alchemy.py +++ b/edbob/pyramid/grids/alchemy.py @@ -54,9 +54,6 @@ class AlchemyGrid(Grid): self._formalchemy_grid.prettify = prettify self.noclick_fields = [] - def __delattr__(self, attr): - delattr(self._formalchemy_grid, attr) - def __getattr__(self, attr): return getattr(self._formalchemy_grid, attr) diff --git a/edbob/pyramid/handlers/__init__.py b/edbob/pyramid/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/edbob/pyramid/handlers/base.py b/edbob/pyramid/handlers/base.py new file mode 100644 index 0000000..bf2eac2 --- /dev/null +++ b/edbob/pyramid/handlers/base.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``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) diff --git a/edbob/pyramid/handlers/util.py b/edbob/pyramid/handlers/util.py new file mode 100644 index 0000000..0530947 --- /dev/null +++ b/edbob/pyramid/handlers/util.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``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 diff --git a/edbob/pyramid/progress.py b/edbob/pyramid/progress.py index ec6750d..65c79e7 100644 --- a/edbob/pyramid/progress.py +++ b/edbob/pyramid/progress.py @@ -47,32 +47,50 @@ class SessionProgress(object): def __init__(self, session, key): self.session = get_progress_session(session, key) - self.canceled = False - self.clear() + self.cancelled = False def __call__(self, message, maximum): - self.clear() + self.session['complete'] = False self.session['message'] = message self.session['maximum'] = maximum + self.session['cancelled'] = False self.session['value'] = 0 self.session.save() return self - def clear(self): - self.session.clear() - self.session['complete'] = False - self.session['error'] = False - self.session['canceled'] = False - self.session.save() - def update(self, value): self.session.load() - if self.session.get('canceled'): - self.canceled = True + if self.session.get('cancelled'): + self.cancelled = True else: self.session['value'] = value self.session.save() - return not self.canceled + return not self.cancelled + + def destroy(self): + if not self.cancelled: + self.session['complete'] = True + self.session.save() + + def secondary_progress(self): + return SecondarySessionProgress(self) + + +class SecondarySessionProgress(object): + + def __init__(self, parent): + self.parent = parent + self.session = parent.session + + def __call__(self, message, maximum): + self.session['message'] = message + self.session['value'] = 0 + self.session['maximum'] = maximum + self.session.save() + return self + + def update(self, value): + return self.parent.update(value) def destroy(self): pass diff --git a/edbob/pyramid/static/css/base.css b/edbob/pyramid/static/css/edbob.css similarity index 52% rename from edbob/pyramid/static/css/base.css rename to edbob/pyramid/static/css/edbob.css index b3b3408..5417a07 100644 --- a/edbob/pyramid/static/css/base.css +++ b/edbob/pyramid/static/css/edbob.css @@ -87,12 +87,81 @@ div.error { margin-bottom: 10px; } -ul.error { - color: #dd6666; - font-weight: bold; - padding: 0px; +/* td.right { */ +/* float: none; */ +/* } */ + +/* table.wrapper td.right { */ +/* vertical-align: bottom; */ +/* } */ + + +/****************************** + * Main Layout + ******************************/ + +html, body, #container { + height: 100%; } -ul.error li { - list-style-type: none; +body > #container { + height: auto; + min-height: 100%; +} + +#container { + margin: 0 auto; + width: 1000px; +} + +#header { + border-bottom: 1px solid #000000; + overflow: auto; +} + +#body { + padding-top: 15px; + padding-bottom: 5em; +} + +#footer { + margin-top: -4em; + text-align: center; +} + + +/****************************** + * Header + ******************************/ + +#header h1 { + margin: 0px 5px 10px 5px; +} + +#login { + margin: 8px 20px auto auto; +} + +#user-menu { + float: left; +} + +#home-link { + font-weight: bold; +} + +#header-links { + float: right; + text-align: right; +} + +#main-menu { + border-top: 1px solid black; + clear: both; + font-weight: bold; +} + +#main-menu li { + display: inline; + margin-right: 15px; } diff --git a/edbob/pyramid/static/css/forms.css b/edbob/pyramid/static/css/forms.css index ff17b12..3047756 100644 --- a/edbob/pyramid/static/css/forms.css +++ b/edbob/pyramid/static/css/forms.css @@ -38,18 +38,11 @@ div.fieldset { div.field-wrapper { clear: both; - min-height: 30px; overflow: auto; - padding: 5px; -} - -div.field-wrapper.error { - background-color: #ddcccc; - border: 2px solid #dd6666; + min-height: 30px; } div.field-wrapper label { - color: #000000; display: block; float: left; width: 140px; @@ -78,15 +71,6 @@ div.field-wrapper div.field textarea { width: 320px; } -label input[type=checkbox] { - margin-right: 8px; -} - - -/****************************** - * Buttons - ******************************/ - div.buttons { clear: both; margin-top: 10px; diff --git a/edbob/pyramid/static/css/layout.css b/edbob/pyramid/static/css/layout.css deleted file mode 100644 index d389c0d..0000000 --- a/edbob/pyramid/static/css/layout.css +++ /dev/null @@ -1,75 +0,0 @@ - -/****************************** - * Main Layout - ******************************/ - -html, body, #container { - height: 100%; -} - -body > #container { - height: auto; - min-height: 100%; -} - -#container { - margin: 0 auto; - width: 1000px; -} - -#header { - border-bottom: 1px solid #000000; - overflow: auto; -} - -#body { - padding-top: 15px; - padding-bottom: 5em; -} - -#footer { - clear: both; - margin-top: -4em; - text-align: center; -} - - -/****************************** - * Header - ******************************/ - -#header h1 { - margin: 0px 5px 10px 5px; -} - -#login { - margin: 8px 20px auto auto; -} - -#login a.username { - font-weight: bold; -} - -#user-menu { - float: left; -} - -#home-link { - font-weight: bold; -} - -#header-links { - float: right; - text-align: right; -} - -#main-menu { - border-top: 1px solid black; - clear: both; - font-weight: bold; -} - -#main-menu li { - display: inline; - margin-right: 15px; -} diff --git a/edbob/pyramid/static/css/perms.css b/edbob/pyramid/static/css/perms.css index 86fcfce..764f8eb 100644 --- a/edbob/pyramid/static/css/perms.css +++ b/edbob/pyramid/static/css/perms.css @@ -1,33 +1,17 @@ /****************************** - * Permission Lists + * perms.css ******************************/ -div.field-wrapper.permissions div.field div.group { - margin-bottom: 10px; -} - -div.field-wrapper.permissions div.field div.group p { +div.field-couple.permissions div.field p.group { font-weight: bold; } -div.field-wrapper.permissions div.field label { +div.field-couple.permissions div.field label { float: none; font-weight: normal; } -div.field-wrapper.permissions div.field label input { - margin-left: 15px; - margin-right: 10px; -} - -div.field-wrapper.permissions div.field div.group p.perm { - font-weight: normal; - margin-left: 15px; -} - -div.field-wrapper.permissions div.field div.group p.perm span { - font-family: monospace; - /* font-weight: bold; */ +div.field-couple.permissions div.field label input { margin-right: 10px; } diff --git a/edbob/pyramid/subscribers.py b/edbob/pyramid/subscribers.py index 0340d97..2f30feb 100644 --- a/edbob/pyramid/subscribers.py +++ b/edbob/pyramid/subscribers.py @@ -76,16 +76,11 @@ def context_found(event): request.user = Session.query(edbob.User).get(uuid) def has_perm(perm): - return has_permission(request.user, perm, session=Session()) + if not request.user: + return False + return has_permission(request.user, perm) request.has_perm = has_perm - def has_any_perm(perms): - for perm in perms: - if has_permission(request.user, perm, session=Session()): - return True - return False - request.has_any_perm = has_any_perm - def get_referrer(default=None): if request.params.get('referrer'): return request.params['referrer'] diff --git a/edbob/pyramid/templates/change_password.mako b/edbob/pyramid/templates/change_password.mako deleted file mode 100644 index e00d2a5..0000000 --- a/edbob/pyramid/templates/change_password.mako +++ /dev/null @@ -1,15 +0,0 @@ -<%inherit file="/base.mako" /> - -<%def name="title()">Change Password - -
- ${h.form(url('change_password'))} - ${form.referrer_field()} - ${form.field_div('current_password', form.password('current_password'))} - ${form.field_div('new_password', form.password('new_password'))} - ${form.field_div('confirm_password', form.password('confirm_password'))} -
- ${h.submit('submit', "Change Password")} -
- ${h.end_form()} -
diff --git a/edbob/pyramid/templates/crud.mako b/edbob/pyramid/templates/crud.mako index 8844cd1..0ca6e40 100644 --- a/edbob/pyramid/templates/crud.mako +++ b/edbob/pyramid/templates/crud.mako @@ -1,18 +1,3 @@ -<%inherit file="/form.mako" /> - -<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+h.literal(str(form.fieldset.model))} - -<%def name="head_tags()"> - ${parent.head_tags()} - - +<%inherit file="/edbob/crud.mako" /> ${parent.body()} diff --git a/edbob/pyramid/templates/base.mako b/edbob/pyramid/templates/edbob/base.mako similarity index 93% rename from edbob/pyramid/templates/base.mako rename to edbob/pyramid/templates/edbob/base.mako index dab999b..f3af0bf 100644 --- a/edbob/pyramid/templates/base.mako +++ b/edbob/pyramid/templates/edbob/base.mako @@ -18,8 +18,7 @@ ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.autocomplete.js'))} ${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))} - ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))} - ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/grids.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/filters.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/forms.css'))} @@ -38,7 +37,7 @@

${self.title()}

% if request.user: - ${h.link_to(request.user.display_name, url('change_password'), class_='username')} + logged in as ${request.user.display_name} (${h.link_to("logout", url('logout'))}) % else: ${h.link_to("login", url('login'))} diff --git a/edbob/pyramid/templates/edbob/crud.mako b/edbob/pyramid/templates/edbob/crud.mako new file mode 100644 index 0000000..885dba3 --- /dev/null +++ b/edbob/pyramid/templates/edbob/crud.mako @@ -0,0 +1,5 @@ +<%inherit file="/form.mako" /> + +<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+str(form.fieldset.model)} + +${parent.body()} diff --git a/edbob/pyramid/templates/forms/fieldset_readonly.mako b/edbob/pyramid/templates/forms/fieldset_readonly.mako index 350a315..0eea814 100644 --- a/edbob/pyramid/templates/forms/fieldset_readonly.mako +++ b/edbob/pyramid/templates/forms/fieldset_readonly.mako @@ -1,7 +1,7 @@
% for field in fieldset.render_fields.itervalues(): % if field.requires_label: -
+
${field.label_tag()|n}
${field.render_readonly()} diff --git a/edbob/pyramid/templates/people/base.mako b/edbob/pyramid/templates/people/base.mako new file mode 100644 index 0000000..27f7dd9 --- /dev/null +++ b/edbob/pyramid/templates/people/base.mako @@ -0,0 +1,2 @@ +<%inherit file="/base.mako" /> +${parent.body()} diff --git a/edbob/pyramid/templates/people/crud.mako b/edbob/pyramid/templates/people/crud.mako deleted file mode 100644 index 439dcb7..0000000 --- a/edbob/pyramid/templates/people/crud.mako +++ /dev/null @@ -1,28 +0,0 @@ -<%inherit file="/crud.mako" /> - -<%def name="crud_name()">Person - -<%def name="context_menu_items()"> -
  • ${h.link_to("Back to People", url('people'))}
  • - - -${parent.body()} - -## % if fieldset.edit: -##

    User Info

    -## % if user: -## ${user.render()|n} -##
    -## -##
    -## % else: -##

    This person does not have a user account.

    -## ${h.form(url('user.new'))} -## ${h.hidden('User--person_uuid', value=fieldset.model.uuid)} -## ${h.hidden('User--username')} -##
    -## ${h.submit('submit', "Create User")} -##
    -## ${h.end_form()} -## % endif -## % endif diff --git a/edbob/pyramid/templates/people/index.mako b/edbob/pyramid/templates/people/index.mako index 77b7bad..577fedf 100644 --- a/edbob/pyramid/templates/people/index.mako +++ b/edbob/pyramid/templates/people/index.mako @@ -1,11 +1,12 @@ -<%inherit file="/grid.mako" /> +<%inherit file="/people/base.mako" /> +<%inherit file="/index.mako" /> <%def name="title()">People <%def name="context_menu_items()"> -## % if request.has_perm('people.create'): -##
  • ${h.link_to("Create a new Person", url('person.new'))}
  • -## % endif + % if request.has_perm('people.create'): +
  • ${h.link_to("Create a new Person", url('person.new'))}
  • + % endif ${parent.body()} diff --git a/edbob/pyramid/templates/people/person.mako b/edbob/pyramid/templates/people/person.mako new file mode 100644 index 0000000..c9f0cf5 --- /dev/null +++ b/edbob/pyramid/templates/people/person.mako @@ -0,0 +1,29 @@ +<%inherit file="/people/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="crud_name()">Person + +<%def name="menu()"> +

    ${h.link_to("Back to People", url('people.list'))}

    + + +${parent.body()} + +% if fieldset.edit: +

    User Info

    + % if user: + ${user.render()|n} +
    + +
    + % else: +

    This person does not have a user account.

    + ${h.form(url('user.new'))} + ${h.hidden('User--person_uuid', value=fieldset.model.uuid)} + ${h.hidden('User--username')} +
    + ${h.submit('submit', "Create User")} +
    + ${h.end_form()} + % endif +% endif diff --git a/edbob/pyramid/templates/progress.mako b/edbob/pyramid/templates/progress.mako index ea83598..d33beb9 100644 --- a/edbob/pyramid/templates/progress.mako +++ b/edbob/pyramid/templates/progress.mako @@ -5,8 +5,7 @@ Working... ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))} ${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))} - ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))} - ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}