Commits

Gael Pasgrimaud  committed 21fb883

initial commit

  • Participants

Comments (0)

Files changed (73)

+syntax: glob
+
+bin
+dist
+eggs
+parts
+data
+develop-eggs
+afpy.mail
+afpy.core
+afpy.ldap
+afpy.wsgi
+.svn
+.installed.cfg
+*.egg-info
+*.swp
+*.pyc
+*.pyo
+*.log
+
+include membersafpyorg/config/deployment.ini_tmpl
+recursive-include membersafpyorg/public *
+recursive-include membersafpyorg/templates *
+This file is for you to describe the members.afpy.org application. Typically
+you would include information such as the information below:
+
+Installation and Setup
+======================
+
+Install ``members.afpy.org`` using easy_install::
+
+    easy_install members.afpy.org
+
+Make a config file as follows::
+
+    paster make-config members.afpy.org config.ini
+
+Tweak the config file as appropriate and then setup the application::
+
+    paster setup-app config.ini
+
+Then you are ready to go.

File bootstrap.py

+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                     ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+    cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+    os.P_WAIT, sys.executable, sys.executable,
+    '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout>=1.0.6',
+    dict(os.environ,
+         PYTHONPATH=
+         ws.find(pkg_resources.Requirement.parse('setuptools')).location
+         ),
+    ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
+

File buildout.cfg

+[buildout]
+newest=false
+extensions=gp.vcsdevelop
+develop=.
+vcs-extend-develop=
+    hg+https://hg.afpy.org/afpy.ldap/@tip#egg=afpy.ldap
+    svn+https://svn.afpy.org/misc/afpy.wsgi/trunk#egg=afpy.wsgi
+    svn+https://svn.afpy.org/misc/afpy.core#egg=afpy.core
+    svn+https://svn.afpy.org/misc/afpy.mail#egg=afpy.mail
+
+parts=app
+versions=versions
+
+[versions]
+Pylons=0.9.7
+
+[app]
+recipe = zc.recipe.egg
+eggs=
+    AfpyMembers
+    afpy.wsgi
+    afpy.core
+    afpy.mail
+    afpy.ldap
+    FormAlchemy
+    repoze.who-friendlyform
+    repoze.what.plugins.ini
+    repoze.what-pylons
+    AuthKit
+    PasteScript
+    ipython
+    zope.app.file
+    SQLAlchemy
+    Babel
+    nose
+interpreter = python
+scripts =
+    paster=paster
+    ipython=ipython
+    nosetests=nosetests

File deployement.ini

+#
+# members.afpy.org - Pylons development environment configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+[DEFAULT]
+debug = true
+email_to = gawel@afpy.org
+smtp_server = localhost
+error_email_from = postmaster@afpy.org
+
+[server:main]
+use = egg:PasteScript#cherrypy
+host = 127.0.0.1
+port = 5000
+
+[app:main]
+use = egg:AfpyMembers
+full_stack = false
+cache_dir = %(here)s/data
+beaker.session.key = OPI5P9CC39zecqemlqjmqzrccrcct
+beaker.session.secret = zemljk,qsclsjhfghgbssqfbrhgjctzf
+
+# If you'd like to fine-tune the individual locations of the cache data dirs
+# for the Cache data, or the Session saves, un-comment the desired settings
+# here:
+#beaker.cache.data_dir = %(here)s/data/cache
+#beaker.session.data_dir = %(here)s/data/sessions
+
+# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
+# Debug mode will enable the interactive debugging tool, allowing ANYONE to
+# execute malicious code after an exception is raised.
+set debug = false
+
+# Logging configuration
+[loggers]
+keys = root, members
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_members]
+level = DEBUG
+handlers =
+qualname = members
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = DEBUG
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

File development.ini

+#
+# members.afpy.org - Pylons development environment configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+[DEFAULT]
+debug = false
+email_to = gawel@afpy.org
+smtp_server = localhost
+error_email_from = postmaster@afpy.org
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5001
+
+[app:main]
+use = egg:AfpyMembers
+full_stack = false
+cache_dir = %(here)s/data
+beaker.session.key = members
+beaker.session.secret = somesecret
+
+# If you'd like to fine-tune the individual locations of the cache data dirs
+# for the Cache data, or the Session saves, un-comment the desired settings
+# here:
+#beaker.cache.data_dir = %(here)s/data/cache
+#beaker.session.data_dir = %(here)s/data/sessions
+
+# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
+# Debug mode will enable the interactive debugging tool, allowing ANYONE to
+# execute malicious code after an exception is raised.
+#set debug = false
+
+lang=fr
+auth.permissions = %(here)s/permissions.ini
+
+# Logging configuration
+[loggers]
+keys = root, members
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_members]
+level = DEBUG
+handlers =
+qualname = members
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = DEBUG
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

File docs/index.txt

+membersafpyorg
+++++++++++++++
+
+This is the main index page of your documentation. It should be written in
+`reStructuredText format <http://docutils.sourceforge.net/rst.html>`_.
+
+You can generate your documentation in HTML format by running this command::
+
+    setup.py pudge
+
+For this to work you will need to download and install ``buildutils`` and
+``pudge``.

File docs/testing.txt

+About tests
+==============
+
+Test are run with the real afpy's ldap server since it's hard to mock a ldap server.
+
+To run tests create a tunnel::
+
+  $ ssh -L 1389:localhost:389 py.afpy.org
+  $ ./bin/nosetest
+
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL     = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+    'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+    'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+    'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+    'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+    'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+    'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+    'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+    'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+    'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+    'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+    'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+    'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+    'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+    'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+    'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+    'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+    'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+    'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+    'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+    'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+    'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+    'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+    'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+    'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+    'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+    def do_download():
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+    try:
+        import pkg_resources
+    except ImportError:
+        return do_download()       
+    try:
+        pkg_resources.require("setuptools>="+version); return
+    except pkg_resources.VersionConflict, e:
+        if was_imported:
+            print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first, using 'easy_install -U setuptools'."
+            "\n\n(Currently using %r)"
+            ) % (version, e.args[0])
+            sys.exit(2)
+        else:
+            del pkg_resources, sys.modules['pkg_resources']    # reload ok
+            return do_download()
+    except pkg_resources.DistributionNotFound:
+        return do_download()
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    try:
+        import setuptools
+    except ImportError:
+        egg = None
+        try:
+            egg = download_setuptools(version, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            if egg and os.path.exists(egg):
+                os.unlink(egg)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+
+

File members/__init__.py

Empty file added.

File members/config/__init__.py

Empty file added.

File members/config/deployment.ini_tmpl

+#
+# members.afpy.org - Pylons configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+[DEFAULT]
+debug = true
+email_to = you@yourdomain.com
+smtp_server = localhost
+error_email_from = paste@localhost
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5000
+
+[app:main]
+use = egg:AfpyMembers
+full_stack = true
+cache_dir = %(here)s/data
+beaker.session.key = MKKKLLKmsfkq616513525
+beaker.session.secret = ${app_instance_secret}
+app_instance_uuid = ${app_instance_uuid}
+
+# If you'd like to fine-tune the individual locations of the cache data dirs
+# for the Cache data, or the Session saves, un-comment the desired settings
+# here:
+#beaker.cache.data_dir = %(here)s/data/cache
+#beaker.session.data_dir = %(here)s/data/sessions
+
+# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
+# Debug mode will enable the interactive debugging tool, allowing ANYONE to
+# execute malicious code after an exception is raised.
+set debug = false
+
+
+# Logging configuration
+[loggers]
+keys = root
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s

File members/config/environment.py

+"""Pylons environment configuration"""
+import os
+
+from mako.lookup import TemplateLookup
+from pylons.error import handle_mako_error
+from pylons import config
+
+import members.lib.app_globals as app_globals
+import members.lib.helpers
+from members.config.routing import make_map
+
+def load_environment(global_conf, app_conf):
+    """Configure the Pylons environment via the ``pylons.config``
+    object
+    """
+    # Pylons paths
+    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    paths = dict(root=root,
+                 controllers=os.path.join(root, 'controllers'),
+                 static_files=os.path.join(root, 'public'),
+                 templates=[os.path.join(root, 'templates')])
+
+    # Initialize config with the basic options
+    config.init_app(global_conf, app_conf, package='members', paths=paths)
+
+    config['routes.map'] = make_map()
+    config['pylons.app_globals'] = app_globals.Globals()
+    config['pylons.h'] = members.lib.helpers
+
+    # Create the Mako TemplateLookup, with the default auto-escaping
+    config['pylons.app_globals'].mako_lookup = TemplateLookup(
+        directories=paths['templates'],
+        error_handler=handle_mako_error,
+        module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
+        input_encoding='utf-8', output_encoding='utf-8',
+        imports=['from webhelpers.html import escape'],
+        default_filters=['escape'])
+        
+    # CONFIGURATION OPTIONS HERE (note: all config options will override
+    # any Pylons config options)

File members/config/middleware.py

+"""Pylons middleware initialization"""
+from beaker.middleware import CacheMiddleware, SessionMiddleware
+from paste.cascade import Cascade
+from paste.registry import RegistryManager
+from paste.urlparser import StaticURLParser
+from paste.deploy.converters import asbool
+from pylons import config
+from pylons.middleware import ErrorHandler, StatusCodeRedirect
+from pylons.wsgiapp import PylonsApp
+from routes.middleware import RoutesMiddleware
+
+from members.config.environment import load_environment
+from afpy.wsgi.who import AuthenticationMiddleware
+
+def make_app(global_conf, full_stack=True, **app_conf):
+    """Create a Pylons WSGI application and return it
+
+    ``global_conf``
+        The inherited configuration for this application. Normally from
+        the [DEFAULT] section of the Paste ini file.
+
+    ``full_stack``
+        Whether or not this application provides a full WSGI stack (by
+        default, meaning it handles its own exceptions and errors).
+        Disable full_stack when this application is "managed" by
+        another WSGI middleware.
+
+    ``app_conf``
+        The application's local configuration. Normally specified in
+        the [app:<name>] section of the Paste ini file (where <name>
+        defaults to main).
+
+    """
+    # Configure the Pylons environment
+    load_environment(global_conf, app_conf)
+
+    # The Pylons WSGI app
+    app = PylonsApp()
+    
+    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
+    app = AuthenticationMiddleware(app, config)
+    # Routing/Session/Cache Middleware
+    app = RoutesMiddleware(app, config['routes.map'])
+    app = SessionMiddleware(app, config)
+    app = CacheMiddleware(app, config)
+    
+    if asbool(full_stack):
+        # Handle Python exceptions
+        app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
+
+        # Display error documents for 401, 403, 404 status codes (and
+        # 500 when debug is disabled)
+        if asbool(config['debug']):
+            app = StatusCodeRedirect(app)
+        else:
+            app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
+
+    # Establish the Registry for this application
+    app = RegistryManager(app)
+
+    # Static files (If running in production, and Apache or another web 
+    # server is handling this static content, remove the following 2 lines)
+    static_app = StaticURLParser(config['pylons.paths']['static_files'])
+    app = Cascade([static_app, app])
+    return app

File members/config/routing.py

+"""Routes configuration
+
+The more specific and detailed routes should be defined first so they
+may take precedent over the more generic routes. For more information
+refer to the routes manual at http://routes.groovie.org/docs/
+"""
+from pylons import config
+from routes import Mapper
+
+def make_map():
+    """Create, configure and return the routes Mapper"""
+    map = Mapper(directory=config['pylons.paths']['controllers'],
+                 always_scan=config['debug'])
+    map.minimization = False
+
+    # The ErrorController route (handles 404/500 error pages); it should
+    # likely stay at the top, ensuring it can always be resolved
+    map.connect('/error/{action}', controller='error')
+    map.connect('/error/{action}/{id}', controller='error')
+
+    # CUSTOM ROUTES HERE
+    map.connect('/', controller='my', action='index')
+    map.connect('/login', controller='utils', action='login')
+    map.connect('/register/confirm/{uid}/{key}', controller='register', action='confirm')
+
+    # alias for communication
+    map.connect('/carte', controller='maps', action='index')
+    map.connect('/courrier', controller='my', action='courrier')
+    map.connect('/adhesion', controller='my',
+                            action='subscribe_form')
+    # ajax stuff
+    map.connect('/my/subscribers/{stype}/{letter}', controller='my', action='subscribers')
+    map.connect('/my/save_payment/{act}/{uid}/{paymentDate}', controller='my',
+                                        action='save_payment', paymentDate=None)
+
+    map.connect('/{controller}')
+    map.connect('/{controller}/')
+    map.connect('/{controller}/{action}')
+    map.connect('/{controller}/{action}/')
+    map.connect('/{controller}/{action}/{id}')
+
+    return map

File members/controllers/__init__.py

Empty file added.

File members/controllers/admin.py

+import logging
+
+from members.lib.base import *
+from members.forms.users import NewUserForm
+from members.forms.payments import NewPaymentForm, PaymentForm
+from afpy.mail import LDAPMailTemplate
+from afpy.core import mailman
+import random, string
+import datetime
+
+log = logging.getLogger(__name__)
+
+class AdminController(BaseController):
+
+    @authorize(AdminUser)
+    def index(self):
+        return ''
+
+    @authorize(AdminUser)
+    def new(self):
+        message = ''
+        user = ldap.AfpyUser()
+        user.st = 'FR'
+        fs = NewUserForm.bind(user, data=request.POST or None)
+
+        payment = ldap.Payment()
+        payment.paymentDate = h.to_python(h.to_string(datetime.datetime.now()), datetime.date)
+        payment.paymentObject = ldap.PERSONNAL_MEMBERSHIP
+        fs2 = NewPaymentForm.bind(payment, data=request.POST or None)
+
+        if request.POST:
+            if fs.validate() and fs2.validate():
+                fs.sync()
+                fs2.sync()
+
+                uid = fs.uid.value
+                conn = ldap.get_conn()
+
+                user._dn = conn.uid2dn(uid)
+                user.cn = user.uid
+                conn.add(user)
+                user.append(payment)
+
+
+                # add user
+                passwd = ''.join(random.sample(string.ascii_letters,6))
+                user.change_password(passwd)
+                manage_ZopeUser('add', uid, passwd)
+
+                # ml
+                mailman.subscribeTo('afpy-membres', user)
+
+                # confirmation
+                mail = LDAPMailTemplate(
+                        name='new_members',
+                        subject='Votre inscription sur afpy.org',
+                        signature='tresorier',
+                        passwd=passwd,
+                        member=user,
+                        mfrom='noreply@afpy.org', **data)
+
+                if '/test_' in request.environ['SCRIPT_NAME']:
+                    mail.send(user, cc='www@afpy.org')
+                else:
+                    mail.send(user, cc='tresorerie@afpy.org')
+
+                # result in readonly
+                fs = NewUserForm.bind(user)
+                fs2 = NewPaymentForm.bind(payment)
+                fs.readonly = True
+                fs2.readonly = True
+
+                message = 'Utilisateur ajout&eacute; et son mot de passe envoy&eacute; par courriel'
+
+        c.title = 'Inscription de membre'
+        html = h.form(h.url_for())
+        html += fs.render(message=message)
+        if fs2.readonly:
+            html += '<table width="100%" class="payments_listing listing">'
+            html += fs2.header()
+            html += fs2.render()
+            html += '</table>'
+        else:
+            html += '<table width="100%" class="payments_listing listing">'
+            html += fs2.header()
+            html += '</table>'
+            html += fs2.render()
+        html += h.end_form()
+        c.body = html
+        return render('/generic.mako')

File members/controllers/error.py

+import cgi
+import os.path
+
+from paste.urlparser import PkgResourcesParser
+from pylons import request
+from pylons.controllers.util import forward
+from pylons.middleware import error_document_template
+from webhelpers.html.builder import literal
+
+from members.lib.base import BaseController
+
+class ErrorController(BaseController):
+    """Generates error documents as and when they are required.
+
+    The ErrorDocuments middleware forwards to ErrorController when error
+    related status codes are returned from the application.
+
+    This behaviour can be altered by changing the parameters to the
+    ErrorDocuments middleware in your config/middleware.py file.
+    
+    """
+    def document(self):
+        """Render the error document"""
+        resp = request.environ.get('pylons.original_response')
+        content = literal(resp.body) or cgi.escape(request.GET.get('message'))
+        page = error_document_template % \
+            dict(prefix=request.environ.get('SCRIPT_NAME', ''),
+                 code=cgi.escape(request.GET.get('code', str(resp.status_int))),
+                 message=content)
+        return page
+
+    def img(self, id):
+        """Serve Pylons' stock images"""
+        return self._serve_file('/'.join(['media/img', id]))
+
+    def style(self, id):
+        """Serve Pylons' stock stylesheets"""
+        return self._serve_file('/'.join(['media/style', id]))
+
+    def _serve_file(self, path):
+        """Call Paste's FileApp (a WSGI application) to serve the file
+        at the specified path
+        """
+        static = PkgResourcesParser('pylons', 'pylons')
+        request.environ['PATH_INFO'] = '/%s' % path
+        return static(request.environ, self.start_response)

File members/controllers/maps.py

+import re
+import urllib
+import logging
+from datetime import datetime
+from cPickle import load
+from members.lib.base import *
+from afpy.core import config
+from afpy.core.countries import COUNTRIES
+
+log = logging.getLogger(__name__)
+
+filename = '/tmp/google.maps.coords.dump'
+api_key = config.get('api_keys', 'maps.google.com')
+
+
+class MapsController(BaseController):
+
+    def index(self):
+        c.api_key = api_key
+        return render('/maps.mako')
+
+    @jsonify
+    def datas(self):
+        conn = ldap.get_conn()
+        users = conn.search_nodes(filter='(&(postalCode=*)(street=*))',
+                    attrs=['uid', 'street', 'postalCode', 'l', 'st'])
+        datas = {}
+        if os.path.isfile(filename):
+            coords = load(open(filename))
+        else:
+            coords = {}
+        for user in users:
+            try:
+                short_address = u'%s, %s' % (
+                        user.postalCode.strip(),
+                        user.st.strip())
+            except UnicodeDecodeError, e:
+                log.error('%r %s - %s', e, e, user)
+                coord = None
+            else:
+                coord = coords.get(short_address)
+            if coord:
+                datas[short_address] = datas.get(short_address, []) + [user['uid']]
+        return {'result':[dict(point=[coords[k].get('lng'), coords[k].get('lat')], address=k,
+                     users=''.join(['<div>%s</div>' % u for u in v])
+                     ) for k, v in datas.items()]}
+

File members/controllers/my.py

+# -*- coding: utf-8 -*-
+import datetime
+import logging
+from members.lib.base import *
+from members.forms.users import UserForm, AdminUserForm
+from members.forms import ValidationError
+from webhelpers.rails.tags import content_tag
+from afpy.core import mailman
+from afpy.mail import LDAPMailTemplate
+from afpy.mail.ldap import getAddress
+import string, datetime
+
+log = logging.getLogger(__name__)
+
+tag = content_tag
+
+MEMBER_TYPES = (
+        (u'Liste des inscrits','all'),
+        (u'Liste des adhérents','subscribers'),
+        (u'Liste des non adhérents','membres'),
+        (u'Recherche','search'),)
+
+def form_message(message):
+    return u'%s - %s' % (message, datetime.datetime.now().strftime('%H:%M:%S'))
+
+def url(*args):
+    action = h.url_for(controller='my', action=None, id=None)
+    if '?' in action:
+        action = action.split('?')[0]
+    query = u'/'.join(args)
+    if query:
+        return u'%s/%s' % (action, query)
+    return action
+
+def get_menu(menus, tag='dl', stag='dd', element='contents', **kwargs):
+    html = u'<%s>' % tag
+    for label, action in menus:
+        html += u' <%s>' % stag
+        u = action.startswith('/') and action or url(action)
+        if 'no_remote' in action:
+            html += h.link_to(label, u)
+        else:
+            kwargs.update(dict(update=element, url=u))
+            html += h.link_to_remote(label, kwargs)
+        html += u'</%s>' % stag
+    html += u'</%s>' % tag
+    return html.encode('utf-8')
+
+class MyController(BaseController):
+
+    @authorize(AfpyUser)
+    def index(self):
+        c.user_id = self.user_id
+        return render('/site.mako')
+
+    @authorize(AfpyUser)
+    def courrier(self):
+        c.u = self.user
+        c.t = ldap.getUser('gawel')
+        c.now = datetime.datetime.now()
+        request.headers['x-deliverance-no-theme'] = 'true'
+        request.environ['afpy.skin.none'] = True
+        return render('/courrier.mako')
+
+    @authorize(AfpyUser)
+    def bulletin(self):
+        c.t = ldap.getUser('gawel')
+        c.now = datetime.datetime.now()
+        request.headers['x-deliverance-no-theme'] = 'true'
+        request.environ['afpy.skin.none'] = True
+        return render('/bulletin.mako')
+
+    @authorize(AfpyUser)
+    def menu(self):
+        """ left menu """
+        user = self.user_id
+        menus = ((u'Mes informations',h.url_for(action='info')),
+                 ('Mes Listes', h.url_for(action='listes')),
+                 ('Mes paiements', h.url_for(controller='payments',
+                                             action=None)),
+                 ('Mon mot de passe', h.url_for(action='password_form')))
+
+        if self.user.expired:
+            menus = ((u"<span style='color:red'>Adhérer à l'afpy</span>",
+                      h.url_for(action='subscribe_form',
+                                no_remote='true')),) + menus
+        html = get_menu(menus,
+                    complete=h.update_element_function('letters',
+                                                        action='empty'))
+        if self.admin:
+            user += ' (Admin)'
+            menus = [(l, h.url_for(action='letters', id=v)) for l,v in MEMBER_TYPES]
+            html += get_menu(menus, element='letters',
+                        complete=h.update_element_function('contents',
+                                                           action='empty'))
+            menus = (
+                     (u"Inscription manuelle",
+                      h.url_for(controller='admin', action='new',
+                                no_remote='true')),
+                     (u"Impression bulletin",
+                      h.url_for(controller='my', action='bulletin',
+                                no_remote='true', notheme='')),
+                    )
+            html += get_menu(menus,
+                        complete=h.update_element_function('letters',
+                                                            action='empty'))
+
+        # plone presentation...
+        html = html.replace('dl>','div>')
+        html = html.replace('<dd>','<div class="nav1">')
+        html = html.replace('</dd>','</div>')
+        return """
+        <div class="portlet">
+            <h5>Espace de %s</h5>
+            <div class="portletBody">
+            %s
+            </div>
+        </div><div>&nbsp;</div>
+        """ % (user, html)
+
+    @authorize(AdminUser)
+    def letters(self, id='all'):
+        """ letters menu """
+        stype = id
+        for label,v in MEMBER_TYPES:
+            if stype == v:
+                break
+        if v == 'search':
+            form = h.form_remote_tag(
+                   url=h.url_for(action='subscribers',
+                                 stype='search', letter='all'),
+                   update=dict(success='contents', failure='contents'))
+            contents = form + h.text_field('letter') + \
+                       h.submit('Rechercher') + h.end_form()
+        else:
+            menus = [(l.upper(), h.url_for(action='subscribers',
+                                       stype=stype,letter=l)) \
+                    for l in string.ascii_lowercase]
+            contents = get_menu(menus, tag='div', stag='span')
+        return tag('h1', label) + tag('fieldset',
+                                      tag('legend', 'Navigation') + \
+                   contents)
+
+    @authorize(AfpyUser)
+    def info(self, id='', message=None):
+        """ user form """
+        user = id and ldap.getUser(id) or self.user
+        admin = self.admin
+
+        html = title = ''
+        if user != self.user and not admin:
+            raise NotAuthorizedError()
+
+        if admin:
+            fs = AdminUserForm.bind(user)
+        else:
+            fs = UserForm.bind(user)
+        if message:
+            e = ValidationError(message)
+            fs._errors = [e]
+        html += fs.render()
+
+        if admin and self.user != user:
+            element = 'infos_%s' % user.uid
+            hidden = h.hidden_field('uid',user.uid)
+        else:
+            element = 'contents'
+            hidden = ''
+        form = h.form_remote_tag(url=h.url_for(action='save_info',id=None),
+                                 update=dict(success=element, failure=element))
+
+        html = title + tag('fieldset', tag('legend', u'Mes informations') + \
+                form + hidden + tag('table', html) + \
+                h.submit("Sauver", name='save',
+                         **{'class':'context'}) + '</form>')
+        return html
+
+    @authorize(AfpyUser)
+    def save_info(self):
+        """ save user form """
+        admin = self.admin
+        uid = request.POST.get('uid')
+        if admin and uid:
+            user = ldap.getUser(uid)
+        else:
+            user = self.user
+        fs = admin and AdminUserForm or UserForm
+        fs = fs.bind(user, data=request.POST)
+        if fs.validate():
+            fs.sync()
+            user.save()
+            message = form_message(u'Modifie').encode('utf-8')
+        else:
+            message = None
+        return self.info(id=user.uid, message=message)
+
+    @authorize(AfpyUser)
+    def listes(self, id='', errors=''):
+        user = id and ldap.getUser(id) or self.user
+        admin = self.admin
+
+        html = title = ''
+        if user != self.user and not admin:
+            raise NotAuthorizedError()
+
+        if admin and self.user != user:
+            element = 'listes_%s' % user
+            hidden = h.hidden_field('uid',user.uid)
+        else:
+            element = 'contents'
+            hidden = ''
+        html += content_tag('legend', 'Listes de diffusion')
+        html += display_errors(errors)
+        html += h.form_remote_tag(url=h.url_for(action='save_listes',id=None),
+                                 update=dict(success=element, failure=element))
+        html += hidden
+
+        email = user.email
+        selected = mailman.lists.getListsFor(email)
+        for ml in mailman.lists.values():
+            # hide private list if not subbcribed
+            if not admin and not ml.public and ml.name not in selected:
+                continue
+            html += '<div>'
+            html += h.check_box('selected',
+                                id=ml.name,
+                                checked=ml.name in selected,
+                                value=ml.name)
+            html += ' '
+            html += content_tag('label', ml.title, **{'for':ml.name})
+            html += ' ('
+            html += h.link_to('infos',
+                         'http://lists.afpy.org/mailman/listinfo/%s' % ml.name)
+            html += ')'
+            html += '</div>'
+
+        html += content_tag('div', '&nbsp;')
+        html += h.submit('Sauver', **{'class':'context'})
+        html += h.end_form()
+
+        return content_tag('fieldset', html)
+
+    @authorize(AfpyUser)
+    def save_listes(self):
+        """ save mailing lists
+        """
+        user = self.user_id
+        admin = self.admin
+        uid = request.POST.get('uid')
+        if admin and uid:
+            user = ldap.getUser(uid)
+        else:
+            user = self.user
+        email = user.email
+        new_selected = request.POST.getall('selected')
+        selected = mailman.lists.getListsFor(email)
+        for ml in mailman.lists.values():
+            if ml.name in new_selected:
+                if ml.name not in selected:
+                    if ml.name == 'afpy-membres' and user.expired:
+                        continue
+                    elif not self.admin and not ml.public:
+                        continue
+                    else:
+                        ml.append(email)
+            elif ml.name in selected:
+                del ml[email]
+        return self.listes(id=self.user_id, errors=[form_message(u'Modification sauvegardées')])
+
+    @authorize(AdminUser)
+    def subscribers(self, stype='', letter=''):
+        """ get subscribers listing """
+        if stype == 'members':
+            f = '(&(!(membershipExpirationDate=*))(uid=%s*))' % letter
+        elif stype == 'subscribers':
+            f = '(&(membershipExpirationDate=*)(uid=%s*))' % letter
+        elif stype == 'search':
+            letter = request.POST['letter']
+            f = '(&(objectClass=person)(sn=*%s*))' % letter
+        else:
+            f = '(&(objectClass=person)(uid=%s*))' % letter
+        conn = ldap.get_conn()
+        res = conn.search_nodes(filter=f, attrs=['uid'])
+        members = [m.uid for m in res]
+        members.sort()
+        c.members = members
+        return render('/listing.mako')
+
+    @authorize(AfpyUser)
+    def subscribe_form(self):
+        c.uid = self.user_id
+
+        for k in ['telephoneNumber', 'cn', 'l', 'birthDate',
+                  'st', 'street', 'sn',
+                  'postalCode', 'mail']:
+            if not getattr(user, k):
+                c.user_id = self.user.uid
+                c.need_infos = True
+                return render('/subscribe_form.mako')
+
+        c.memberships = u''.join([u"<li>%s - %i &euro;/an</li>" % (l, p) for l,p in (
+                 ('Membre classique', 20),
+                 (u'Tarif étudiant', 10),
+                 #(ldap.CORPORATE_MEMBERSHIP, u'Tarif entreprise', 100),
+                 )])
+
+        field = lambda l, f:tag('div',
+                tag('label', l)+tag('div','')+f, **{'class':'field'})
+
+        paymentObject = h.options_for_select((
+                         ('Membre classique', ldap.PERSONNAL_MEMBERSHIP),
+                         (u'Tarif étudiant', ldap.STUDENT_MEMBERSHIP),
+                            ))
+        html = field('Type', h.select('paymentObject', paymentObject))
+        html += field('Mode de paiement',
+                h.select('paymentMode',
+                    h.options_for_select(
+                                ((u'Chèque','cheque'),
+                                 ('Paypal', 'paypal'),
+                                 (u'Pre-payé', 'payed')), 'cheque')))
+        html += field(u'Pr&eacute;cision (si d&eacute;j&agrave; pay&eacute;)',
+                h.text_field('paymentComment', size='50'))
+        html += h.submit(u'Adh&eacute;rer', **{'class':'context'})
+        c.form = html
+
+        # error message handling
+        error = request.GET.get('error', None)
+        if error == 'payed':
+            errors = [u"""Vous devez pr&eacute;ciser un commentaire en cas
+                      d'adh&eacute;sion pr&eacute; pay&eacute;e"""]
+            c.errors = display_errors(errors)
+        elif error == 'unknow':
+            errors = [u"""Impossible de vous inscrire.
+                      Veullez contacter l'administrateur"""]
+        elif error == 'unknow':
+            errors = [u"""Impossible de vous inscrire à la liste des
+                      membres.  Veullez contacter l'administrateur"""]
+            c.errors = display_errors(errors)
+        c.user_id = self.user_id
+        return render('/subscribe_form.mako')
+
+    @authorize(AfpyUser)
+    def subscribe(self):
+        user = self.user
+        paymentObject = request.POST.get('paymentObject')
+        paymentMode = request.POST.get('paymentMode')
+        paymentComment = request.POST.get('paymentComment')
+
+        if not paymentMode:
+            raise RuntimeError('No paymentMode')
+
+        if paymentMode == 'payed' and not paymentComment.strip():
+            return redirect_to('http://www.afpy.org'+ \
+                    h.url_for(controller='my',
+                              action='subscribe_form',
+                              error='payed'))
+
+        member=user.getMemberData()
+        address=getAddress('tresorier')
+
+        c.user = user
+        c.address = address.replace('\n', '<br />')
+        c.amount = paymentObject == ldap.PERSONNAL_MEMBERSHIP and 20 or 10
+        c.paymentMode = paymentMode
+
+        paymentDate = None
+        now = datetime.datetime.now()
+        last_payment = user.lastRealPayment()
+        if last_payment:
+            d = last_payment.get('paymentDate')
+            if d + datetime.timedelta(365) < now - datetime.timedelta(90):
+                paymentDate = d + datetime.timedelta(365)
+        if not paymentDate:
+            paymentDate = now
+
+        if mailman.subscribeTo('afpy-membres', user) > 0:
+            return redirect_to('http://www.afpy.org'+ \
+                    h.url_for(controller='my',
+                              action='subscribe_form',
+                              error='mailman'))
+
+        user.addPayment(paymentDate=paymentDate,
+                        paymentAmount=0,
+                        paymentObject=paymentObject,
+                        invoiceReference='awaiting')
+
+        mail = LDAPMailTemplate(name='new_subscription',
+                            subject=u"[AFPy-adhésion] Accusé de réception",
+                            signature='tresorier',
+                            member=member,
+                            address=address,
+                            paymentObject=paymentObject,
+                            paymentMode=paymentMode,
+                            paymentComment=paymentComment,
+                            )
+
+        if '/test_' in request.environ['SCRIPT_NAME']:
+            mail.send(user, cc='www@afpy.org')
+        else:
+            mail.send(user, cc='tresorerie@afpy.org')
+
+        return render('/subscribe.mako')
+
+    @authorize(AfpyUser)
+    def password_form(self,errors=''):
+        element = 'contents'
+        errors = display_errors(errors)
+        form = ''
+        for name, label in (('passwd', 'Mot de passe'),
+                            ('new_passwd', 'Nouveau mot de passe'),
+                            ('confirm_passwd', 'Confirmation')):
+            value = ''
+            form += ldap_field(name, value, label=label)
+        form = tag('table', form)
+        form += h.submit('Valider', name='validate', **{'class':'context'})
+        return tag('h1', 'Changer mon mot de passe') + \
+               h.form_remote_tag(url=h.url_for(action='change_password'),
+                   update=dict(success=element, failure=element)) + \
+               errors + form + h.end_form()
+
+    @authorize(AfpyUser)
+    def change_password(self):
+        user = self.user
+        passwd = request.POST.get('passwd')
+        try:
+            new = str(request.POST.get('new_passwd'))
+            confirm = str(request.POST.get('confirm_passwd'))
+        except:
+            new = confirm = ''
+
+        try:
+            user.check(passwd)
+        except:
+            errors = ['Mot de passe incorect']
+        else:
+            if len(new) >= 6 and new == confirm:
+                ldap.changePassword(user.uid, new)
+                manage_ZopeUser('edit', str(user.uid), new)
+
+                mail = LDAPMailTemplate(name='send_password',
+                                        subject='Votre password sur afpy.org',
+                                        passwd=new,
+                                        mfrom='www@afpy.org')
+                mail.send(user.uid)
+                msg = 'Mot de passe modifié. Identifiez vous'
+                return redirect_to(
+                        'http://www.afpy.org/?portal_status_message=' + msg)
+            else:
+                errors = [u"""Votre nouveau mot de passe doit faire au moins 6
+                        caractères et être le même que la confirmation.
+                        il ne doit pas contenir de caractère accentué"""]
+        return self.password_form(errors=errors)
+
+

File members/controllers/password.py

+# -*- coding: utf-8 -*-
+import logging
+
+from members.lib.base import *
+from webhelpers.rails.tags import content_tag
+from afpy.mail import LDAPMailTemplate
+import md5, random, string
+
+log = logging.getLogger(__name__)
+
+tag = content_tag
+
+
+class PasswordController(BaseController):
+
+    def index(self):
+        return render('/password.mako')
+
+    def password_form(self,uid='',mail='',errors=''):
+        element = 'password_form'
+        errors = display_errors(errors)
+        description = h.content_tag('p', u"""Saisissez votre login ou votre
+                                         courriel puis validez pour réinitialiser
+                                         votre mot de passe""",
+                                         **{'class':'documentDescription'})
+        form = ''
+        for name, value in (('uid',uid),('mail',mail)):
+            form += ldap_field(name, value)
+        form += h.content_tag('td',
+                h.submit('Valider', name='validate', **{'class':'context'}),
+                colspan="2", align='center')
+        form = tag('table', form)
+        return h.form_remote_tag(
+                    url=h.url_for(action='change_password', id=None),
+                    update=dict(success=element, failure=element)
+                    ) + errors + description + form + h.end_form()
+
+    def change_password(self):
+        uid = request.POST.get('uid')
+        mail = request.POST.get('mail')
+
+        user = None
+        errors = []
+        passwd = ''.join(random.sample(string.ascii_letters,6))
+
+        if uid:
+            if ldap.isUser(uid):
+                user = ldap.User(uid)
+            else:
+                errors.append('Impossible de trouver votre login')
+
+        if not user and mail:
+            members = ldap.conn.search(ldap.conn.members_dn,filter='(mail=%s)' % mail)
+            if len(members) == 1:
+                dn = members[0][0]
+                dn = ldap.getDN(dn)
+                user = ldap.User(dn)
+            else:
+                errors.append('Impossible de trouver votre courriel')
+
+        if user:
+            ldap.changePassword(user.uid, passwd)
+            manage_ZopeUser('edit', str(user.uid), passwd)
+
+            mail = LDAPMailTemplate(name='send_password',
+                                    subject='Votre mot de passe sur afpy.org',
+                                    passwd=passwd,
+                                    mfrom='postmaster@afpy.org')
+            mail.send(user.uid)
+            return u"""Votre mot de passe à été modifié. Vous allez recevoir un
+                    courriel de confirmation."""
+
+        errors.insert(0,'Impossible de vous identifier')
+        return self.password_form(uid=uid, mail=mail, errors=errors)
+
+

File members/controllers/payments.py

+import logging
+
+from members.lib.base import *
+from members.forms.payments import NewPaymentForm, PaymentForm
+from datetime import datetime
+from datetime import timedelta
+
+log = logging.getLogger(__name__)
+
+def payment_dn(fs, id):
+    dn = ldap.getDN(id)
+    dn = 'paymentDate=%s,%s' % (fs.uid.value, dn)
+    return dn
+
+class PaymentsController(BaseController):
+
+    @authorize(AfpyUser)
+    def index(self):
+        payments = self.user.payments
+        c.forms = []
+        for payment in payments:
+            fs = PaymentForm.bind(payment)
+            fs.readonly = True
+            c.forms.append(fs)
+        c.user = self.user
+        return render('/payments.mako')
+
+    @authorize(AdminUser)
+    def edit(self, id, message=None):
+        id = str(id)
+        c.user = ldap.getUser(id)
+        dn = request.POST.get('dn')
+
+        # forms
+        payments = c.user.payments
+        c.forms = []
+        for payment in payments:
+            if request.POST and payment._dn == dn:
+                fs = PaymentForm.bind(payment, data=request.POST or None)
+                if fs.validate():
+                    fs.sync()
+                    payment.save()
+                    ldap.updateExpirationDate(user)
+            fs = PaymentForm.bind(payment)
+            c.forms.append(fs)
+
+        # add form
+        now = datetime.now()
+        new_date = now
+        payments = [p for p in payments if p.paymentAmount]
+        if payments:
+            last_payment = payments[-1]
+            d = h.to_python(h.to_string(last_payment.paymentDate), datetime)
+            if d + timedelta(365) < now - timedelta(90):
+                new_date = d + timedelta(365)
+        payment = ldap.Payment()
+        payment.paymentDate = new_date
+        c.new = NewPaymentForm.bind(payment, request.POST or None)
+        c.message = message
+        return render('/edit_payments.mako')
+
+    @authorize(AdminUser)
+    def delete(self, id):
+        user = ldap.getUser(id)
+        dn = request.POST['dn']
+        for p in user.payments:
+            if p._dn == dn:
+                user._conn.delete(p)
+                ldap.updateExpirationDate(user)
+                break
+        ldap.updateExpirationDate(user)
+        message = 'Supprimer a %s' % datetime.now().strftime('%H:%M:%S')
+        return self.edit(id, message=message)
+
+    @authorize(AdminUser)
+    def add(self, id):
+        id = str(id)
+        payment = ldap.Payment()
+        fs = NewPaymentForm.bind(payment, data=request.POST)
+        if fs.validate():
+            fs.sync()
+            user = ldap.getUser(id)
+            user.append(payment)
+            payment.save()
+            message = 'Ajouter a %s' % datetime.now().strftime('%H:%M:%S')
+        else:
+            message = 'Erreur a %s' % datetime.now().strftime('%H:%M:%S')
+        return self.edit(id, message=message)
+
+

File members/controllers/register.py

+# -*- coding: utf-8 -*-
+import logging
+
+from members.lib.base import *
+from members.forms.users import RegisterForm
+from afpy.mail import LDAPMailTemplate
+import md5, random, string
+
+log = logging.getLogger(__name__)
+
+
+class RegisterController(BaseController):
+
+    def index(self):
+        return render('/register.mako')
+
+    def register_form(self, fs = None):
+        element = 'register_form'
+        if not fs:
+            fs = RegisterForm.bind(ldap.AfpyUser())
+        form = fs.render()
+        form += h.submit('Valider', name='validate', **{'class':'context'})
+        return '\n'.join([
+                    '<div>',
+                    h.form_remote_tag(
+                        url=h.url_for(action='register',id=None),
+                        update=dict(success=element, failure=element)
+                        ),
+                    form,
+                    h.end_form(),
+                    '</div>'])
+
+    def register(self):
+        form = RegisterForm.bind(ldap.AfpyUser(), data=request.POST)
+
+        if form.validate():
+            uid = str(form.uid.value)
+            sn = form.sn.value
+            mail = str(form.mail.value)
+            passwd = str(form.password.value)
+            key = md5.new(''.join(
+                    random.sample(string.ascii_letters,6))).hexdigest()
+            conn = ldap.get_conn()
+            user = ldap.AfpyUser(uid=uid, conn=conn,
+                                 attrs=dict(sn=sn, mail=mail,
+                                 street=key, st='UNCONFIRMED'))
+            conn.save(user)
+            user.change_password(passwd)
+            manage_ZopeUser('add', uid, passwd)
+
+            confirm_url = 'http://www.afpy.org' + h.url_for(
+                                    action='confirm',
+                                    uid=uid,
+                                    key=key)
+            mail = LDAPMailTemplate(
+                        name='send_key',
+                        subject='Votre inscription sur afpy.org',
+                        confirm_url=confirm_url, passwd=passwd,
+                        mfrom='noreply@afpy.org')
+            mail.send(uid)
+            return u"""Votre inscription a été prise en compte. Vous allez
+                    recevoir un courriel de confirmation."""
+        return self.register_form(fs=form)
+
+    def confirm(self, uid, key):
+        user = ldap.getUser(uid)
+        if user:
+            url = 'http://www.afpy.org/membres/login?portal_status_message='
+            url += 'Votre inscription est maintenant confirmée'
+            if str(user.street) == str(key):
+                user.street=' '
+                user.st='FR'
+                user.save()
+                redirect_to(url)
+        return 'You lose'
+
+

File members/controllers/template.py

+from members.lib.base import *
+
+class TemplateController(BaseController):
+
+    def view(self, url):
+        """By default, the final controller tried to fulfill the request
+        when no other routes match. It may be used to display a template
+        when all else fails, e.g.::
+
+            def view(self, url):
+                return render('/%s' % url)
+
+        Or if you're using Mako and want to explicitly send a 404 (Not
+        Found) response code when the requested template doesn't exist::
+
+            import mako.exceptions
+
+            def view(self, url):
+                try:
+                    return render('/%s' % url)
+                except mako.exceptions.TopLevelLookupException:
+                    abort(404)
+
+        By default this controller aborts the request with a 404 (Not
+        Found)
+        """
+        abort(404)

File members/controllers/utils.py

+import logging
+
+from members.lib.base import *
+
+log = logging.getLogger(__name__)
+
+class UtilsController(BaseController):
+
+    def personnal_bar(self):
+        return render('/personnal_bar.mako')
+
+    def login(self):
+        return render('/login.mako')
+
+    def error(self):
+        raise RuntimeError('test error is raised and sent by email')

File members/forms/__init__.py

+# -*- coding: utf-8 -*-
+from mako.template import Template
+from formalchemy import SimpleMultiDict
+from formalchemy import validators
+from formalchemy import ValidationError
+from formalchemy import types
+from afpy.ldap import custom as ldap
+from afpy.ldap.forms import FieldSet as BaseFieldSet, Grid as BaseGrid, Field
+from datetime import timedelta
+from datetime import datetime
+from datetime import date
+import string
+
+class TestRequest(object):
+    POST = None
+
+template_render = r"""
+<%
+_ = F_
+_focus_rendered = False
+%>\
+
+% for error in fieldset.errors.get(None, []):
+<div class="portalMessage">
+  ${_(error)}
+</div>
+% endfor
+
+% for field in fieldset.render_fields.itervalues():
+  % if field.requires_label:
+<div class="field">
+  <label for="${field.renderer.name}">${field.label_text or fieldset.prettify(field.key)}</label>
+  %if field.is_required():
+      <span class="field_req">*</span>
+  %endif
+  <div></div>
+  ${field.render()}
+  <div class="formHelp">
+  % for error in field.errors:
+  <span class="field_error">${_(error)}</span>
+  % endfor
+  </div>
+</div>
+
+% if (fieldset.focus == field or fieldset.focus is True) and not _focus_rendered:
+<script type="text/javascript">
+//<![CDATA[
+document.getElementById("${field.renderer.name}").focus();
+//]]>
+</script>
+<% _focus_rendered = True %>\
+% endif
+  % else:
+${field.render()}
+  % endif
+% endfor
+""".strip()
+
+template_render_readonly = r"""
+%if message:
+    <div class="portalMessage">
+    ${message}
+    </div>
+%endif
+% for field in fieldset.render_fields.itervalues():
+<div class="field">
+  <label>${field.label_text or fieldset.prettify(field.key)}</label>
+  : ${field.render_readonly()}
+<div>
+%endfor
+"""
+class FieldSet(BaseFieldSet):
+
+    _render = staticmethod(Template(template_render).render_unicode)
+    _render_readonly = staticmethod(Template(template_render_readonly).render_unicode)
+
+def validate_uid(value):
+    try:
+        value = str(value)
+    except UnicodeEncodeError:
+        raise validators.ValidationError(
+                "Le login ne doit contenir que de l'ASCII")
+    for v in value:
+        if v not in string.ascii_letters:
+            raise validators.ValidationError(
+                "Le login ne doit contenir que de l'ASCII")
+    if ' ' in value:
+        raise validators.ValidationError(
+                "Le login ne doit pas contenir d'espace")
+    if len(value) < 4:
+        raise validators.ValidationError(
+                "Le login doit contenir au moins 4 characteres")
+    if ldap.getUser(value):
+        raise validators.ValidationError('Cet identifiant est pris')
+    return value
+
+def validate_email(value):
+    validators.email(value)
+    conn = ldap.get_conn()
+    if conn.search(filter='(mail=%s)' % value):
+        raise validators.ValidationError('Cet email est pris')
+    return value
+

File members/forms/payments.py

+# -*- coding: utf-8 -*-
+from members.forms import *
+from formalchemy.fields import SelectFieldRenderer, IntegerFieldRenderer
+from members.forms import FieldSet as BaseFieldSet
+__doc__ = '''
+>>> from afpy.ldap import custom as ldap
+>>> payments = ldap.getUser('gawel').payments
+>>> form = PaymentForm.bind(payments[0])
+>>> form.paymentDate.render()
+'2005-01-01'
+>>> form.paymentAmount.value
+20
+>>> print form.paymentAmount.render()
+<input id="Payment-20050101000000Z-paymentAmount" name="Payment-20050101000000Z-paymentAmount" size="5" type="text" value="20" />
+
+>>> form = NewPaymentForm.bind(ldap.Payment())
+>>> print form.paymentDate.render() #doctest: +ELLIPSIS
+<span id="Payment--paymentDate">...
+
+'''
+
+class ObjectRenderer(SelectFieldRenderer):
+    def render(self, **kwargs):
+        options = [(v.decode('utf-8'), k) \
+                        for k, v in sorted(ldap.PAYMENTS_OPTIONS.items())]
+        return super(ObjectRenderer, self).render(options=options, **kwargs)
+    def render_readonly(self):
+        return ldap.PAYMENTS_OPTIONS.get(self._value)
+
+class IntRenderer(IntegerFieldRenderer):
+    def render(self, **kwargs):
+        kwargs['size'] = '5'
+        return IntegerFieldRenderer.render(self, **kwargs)
+
+template_header = r"""
+<tr>
+% for field in fieldset.render_fields.itervalues():
+<th ${'paymentAmount' not in field.name and 'width="150"' or ''}>
+${field.label_text}
+</th>
+% endfor
+%if not fieldset.readonly:
+<th>Actions</th>
+%endif
+</tr>
+""".strip()
+
+template_render = r"""
+<table width="100%" class="payments_listing listing">
+<tr>
+% for field in fieldset.render_fields.itervalues():
+<td ${'paymentAmount' not in field.name and 'width="150"' or ''}
+    align="center">
+    %if field.is_readonly():
+        ${field.render_readonly()}
+        <div style="display:none">
+        ${field.render()}
+        </div>
+    %else:
+        ${field.render()}
+    %endif
+</td>
+% endfor
+<td>
+%if fieldset.model._pk:
+<div>
+<input type="hidden" name="dn" value="${fieldset.model._dn}" />
+${h.link_to_function('Sauver', h.remote_function(
+        url=h.url_for(action='edit'),
+        submit=fieldset.model._pk,
+        update='payments_%s' % user.uid))}
+</div>
+<div>
+${h.link_to_function('Supprimer', h.remote_function(
+        url=h.url_for(action='delete'),
+        submit=fieldset.model._pk,
+        update='payments_%s' % user.uid))}
+<div>
+%else:
+<input type="submit" class="context" value="Ajouter" />
+%endif
+</td>
+</tr>
+</table>
+""".strip()
+
+template_render_readonly = r"""
+<tr>
+% for field in fieldset.render_fields.itervalues():
+%if 'uid' not in field.name:
+<td width="20%">${field.render_readonly()}</td>
+%endif
+% endfor
+</tr>
+""".strip()
+
+class FieldSet(BaseFieldSet):
+    _render_header = staticmethod(Template(template_header).render_unicode)
+    _render = staticmethod(Template(template_render).render)
+    _render_readonly = staticmethod(Template(template_render_readonly).render_unicode)
+
+    def header(self):
+        return self._render_header(fieldset=self)
+
+PaymentForm = FieldSet(ldap.Payment)
+PaymentForm.configure(include=[PaymentForm.paymentDate.readonly(), PaymentForm.paymentObject.with_renderer(ObjectRenderer),
+                      PaymentForm.paymentAmount.with_renderer(IntRenderer), PaymentForm.invoiceReference])
+
+NewPaymentForm = FieldSet(ldap.Payment)
+NewPaymentForm.configure(include=[NewPaymentForm.paymentDate, NewPaymentForm.paymentObject.with_renderer(ObjectRenderer),
+                      NewPaymentForm.paymentAmount.with_renderer(IntRenderer), NewPaymentForm.invoiceReference])
+

File members/forms/users.py

+# -*- coding: utf-8 -*-
+from members.forms import *
+from formalchemy.fields import SelectFieldRenderer, PasswordFieldRenderer
+from afpy.core.countries import COUNTRIES
+
+class CountriesRenderer(SelectFieldRenderer):
+    def render(self, **kwargs):
+        options = [(v.decode('utf-8'), k) for k, v in sorted(COUNTRIES.items())]
+        return super(CountriesRenderer, self).render(options=options, **kwargs)
+
+class RoleRenderer(SelectFieldRenderer):
+    def render(self, **kwargs):
+        options = ['', 'president', 'vice-president', 'tresorier', 'vice-tresorier', 'secretaire', 'vice-secretaire']
+        options = [(v, v) for v in options]
+        return super(RoleRenderer, self).render(options=options, **kwargs)
+
+
+__doc__ = '''
+>>> from afpy.ldap import custom as ldap
+>>> user = ldap.getUser('gawel')
+>>> user.uid
+'gawel'
+>>> form = UserForm.bind(user)
+>>> form.uid.value
+'gawel'
+>>> form.uid.render()
+'<input id="AfpyUser-gawel-uid" name="AfpyUser-gawel-uid" type="hidden" value="gawel" />'
+
+'''
+
+class UserInfos(object):
+    uid = Field().label('Login')