commit 8b02623d5dfd1e801c82ffea5406284d1df7e8d7
Author: Frédéric Péters <fpeters@entrouvert.com>
Date:   Thu Aug 10 13:35:15 2017 +0200

    misc: change module name and installation directory (#17959)

diff --git a/MANIFEST.in b/MANIFEST.in
index d9763d2..14a2b97 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -7,7 +7,7 @@ include po/*.po
 include po/*.pot
 recursive-include apache-errors/ *.html
 recursive-include data/themes/ *.css *.png *.jpeg '*.jpg *.xml *.html *.js *.ezt *.gif *.otf
-recursive-include extra/ *.py
+recursive-include auquotidien/ *.py
 recursive-include static/ *.png *.jpeg '*.jpg *.js *.gif
 recursive-include texts/ *.html
 recursive-include theme/ *.css *.png *.jpeg '*.jpg *.xml *.html *.js *.ezt *.gif
diff --git a/auquotidien/__init__.py b/auquotidien/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auquotidien/auquotidien.py b/auquotidien/auquotidien.py
new file mode 100644
index 0000000..bf12784
--- /dev/null
+++ b/auquotidien/auquotidien.py
@@ -0,0 +1,56 @@
+from quixote import get_publisher
+
+from qommon import _
+from qommon.publisher import get_publisher_class, get_request
+from qommon.misc import get_cfg
+
+import modules.admin
+import modules.backoffice
+import modules.links_ui
+import modules.announces_ui
+import modules.categories_admin
+import modules.events_ui
+import modules.payments_ui
+import modules.strongbox_ui
+import modules.formpage
+import modules.template
+import modules.root
+import modules.payments
+import modules.connectors
+import modules.abelium_domino_ui
+import modules.abelium_domino_vars
+import modules.abelium_domino_synchro
+
+get_publisher_class().register_translation_domain('auquotidien')
+get_publisher_class().default_configuration_path = 'au-quotidien-wcs-settings.xml'
+
+rdb = get_publisher_class().backoffice_directory_class
+
+rdb.items = []
+
+rdb.register_directory('announces', modules.announces_ui.AnnouncesDirectory())
+rdb.register_menu_item('announces/', _('Announces'))
+
+rdb.register_directory('links', modules.links_ui.LinksDirectory())
+rdb.register_menu_item('links/', _('Links'))
+
+rdb.register_directory('events', modules.events_ui.EventsDirectory())
+rdb.register_menu_item('events/', _('Events'))
+
+rdb.register_directory('payments', modules.payments_ui.PaymentsDirectory())
+rdb.register_menu_item('payments/', _('Payments'))
+
+rdb.register_directory('strongbox', modules.strongbox_ui.StrongboxDirectory())
+rdb.register_menu_item('strongbox/', _('Strongbox'))
+
+rdb.register_directory('settings', modules.admin.SettingsDirectory())
+rdb.register_directory('categories', modules.categories_admin.CategoriesDirectory())
+
+import wcs.admin.settings
+wcs.admin.settings.SettingsDirectory.domino = modules.abelium_domino_ui.AbeliumDominoDirectory()
+wcs.admin.settings.SettingsDirectory._q_exports.append('domino')
+
+import wcs.categories
+wcs.categories.Category.XML_NODES = [('name', 'str'), ('url_name', 'str'),
+        ('description', 'str'), ('position', 'int'),
+        ('homepage_position', 'str'), ('redirect_url', 'str'), ('limit', 'int')]
diff --git a/auquotidien/modules/__init__.py b/auquotidien/modules/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auquotidien/modules/abelium_domino_synchro.py b/auquotidien/modules/abelium_domino_synchro.py
new file mode 100644
index 0000000..b821857
--- /dev/null
+++ b/auquotidien/modules/abelium_domino_synchro.py
@@ -0,0 +1,164 @@
+import sys
+from datetime import datetime
+import collections
+from decimal import Decimal
+
+from qommon import _
+from qommon.cron import CronJob
+from qommon.publisher import get_publisher_class
+from qommon import get_logger
+
+from wcs.users import User
+
+from abelium_domino_ui import (get_client, is_activated, get_invoice_regie,
+        abelium_domino_ws)
+from payments import Invoice, Transaction
+
+DOMINO_ID_PREFIX = 'DOMINO-'
+
+def synchronize_domino(publisher):
+    regie = get_invoice_regie(publisher)
+    logger = get_logger()
+    if not is_activated(publisher) or not regie:
+        return
+    client = get_client(publisher)
+    if client is None:
+        logger.warning('Unable to create a DominoWS object')
+        return
+    client.clear_cache()
+    users = User.values()
+    users_by_mail = dict(((user.email, user) for user in users))
+    users_by_code_interne = {}
+    for user in users:
+        if hasattr(user, 'abelium_domino_code_famille'):
+            users_by_code_interne[user.abelium_domino_code_famille] = user
+    try:
+        invoices = client.invoices
+    except abelium_domino_ws.DominoException, e:
+        publisher.notify_of_exception(sys.exc_info(), context='[DOMINO]')
+        logger.error('domino cron: failure to retrieve invoice list from domino '
+                'for synchronization [error:%s]', e)
+        return
+    # import new invoices
+    logger.info('domino cron: retrieved %i invoices', len(invoices))
+    for invoice_id, invoice in invoices.iteritems():
+        user = None
+        if invoice.family.code_interne in users_by_code_interne:
+            user = users_by_code_interne[invoice.family.code_interne]
+        if user is None:
+            for email in (invoice.family.email_pere, invoice.family.email_mere,
+                    invoice.family.adresse_internet):
+                user = users_by_mail.get(email)
+                if user:
+                    break
+            else:
+                continue
+        external_id = '%s%s' % (DOMINO_ID_PREFIX, invoice.id)
+        payment_invoice = Invoice.get_on_index(external_id, 'external_id', ignore_errors=True)
+        if payment_invoice:
+            continue
+        if invoice.reste_du == Decimal(0) or invoice.reste_du < Decimal(0):
+            continue
+        payment_invoice = Invoice()
+        payment_invoice.user_id = user.id
+        payment_invoice.external_id = external_id
+        payment_invoice.regie_id = regie.id
+        payment_invoice.formdef_id = None
+        payment_invoice.formdata_id = None
+        payment_invoice.amount = invoice.reste_du
+        payment_invoice.date = invoice.creation
+        payment_invoice.domino_synchro_date = datetime.now()
+        if 'etablissement' in invoice._detail:
+            etablissement = invoice._detail['etablissement'].encode('utf-8')
+            payment_invoice.subject = _('%s - Childcare services') % etablissement
+        else:
+            payment_invoice.subject = _('Childcare services')
+        if invoice._detail.get('lignes'):
+            details = []
+            details.append('<table class="invoice-details"><thead>')
+            tpl = '''<tr>
+    <td>%(designation)s</td>
+    <td>%(quantite)s</td>
+    <td>%(prix)s</td>
+    <td>%(montant)s</td>
+</tr>'''
+            captions = {
+                    'designation': _('Caption'),
+                    'quantite': _('Quantity'),
+                    'prix': _('Price'),
+                    'amount': _('Amount')
+            }
+            details.append(tpl % captions)
+            details.append('</thead>')
+            details.append('<tbody>')
+            for ligne in invoice._detail['lignes']:
+                def encode(x):
+                    a, b = x
+                    b = b.encode('utf-8')
+                    return (a,b)
+                ligne = map(encode, ligne)
+                ligne = dict(ligne)
+                base = collections.defaultdict(lambda:'')
+                base.update(ligne)
+                details.append(tpl % base)
+            details.append('</tbody></table>')
+            payment_invoice.details = '\n'.join(details)
+        payment_invoice.store()
+        logger.info('domino cron: remote invoice %s for family %s added to user %s invoices with id %s',
+                invoice.id, invoice.family.id, user.id, payment_invoice.id)
+
+    # update invoices
+    invoices_ids = dict(invoices.iteritems())
+    for payment_invoice in Invoice.values():
+        if payment_invoice.external_id is None or not payment_invoice.external_id.startswith(DOMINO_ID_PREFIX):
+            continue # not a payment related to domino we skip
+        i = payment_invoice.external_id[len(DOMINO_ID_PREFIX):]
+        i = int(i)
+        invoice = invoices_ids.get(i)
+        if payment_invoice.paid:
+            if not invoice: 
+                # invoice has been paid (locally or not) but remote invoice has
+                # been deleted do, we do nothing.
+                continue
+            if getattr(payment_invoice, 'domino_knows_its_paid', None) or getattr(payment_invoice, 'paid_by_domino', None):
+                # synchronization of payment already done, skip
+                continue
+            transactions = Transaction.get_with_indexed_value('invoice_ids', payment_invoice.id)
+            if not transactions:
+                logger.warning("domino cron: invoice %s is marked paid but does "
+                        "not have any linked transaction.", payment_invoice.id)
+                details = '' # no details about the payment, problem
+            else:
+                details = repr(transactions[0].__dict__)
+            if invoice.montant != payment_invoice.amount:
+                pass # add warning logs
+            try:
+                client.pay_invoice([invoice], invoice.montant, details,
+                        payment_invoice.paid_date)
+            except abelium_domino_ws.DominoException, e:
+                logger.error('domino cron: invoice %s has been paid, but the remote system '
+                        'is unreachable, notification will be done again '
+                        'later [error: %s]', invoice.id, e)
+            else:
+                # memorize the date of synchronization
+                payment_invoice.domino_knows_its_paid = datetime.now()
+                payment_invoice.store()
+                logger.info('domino cron: domino: invoice %s has been paid; remote system has been '
+                        'notified', payment_invoice.id)
+        else: # unpaid
+            if not invoice:
+                logger.info('domino cron: remote invoice %s disapearred, so its '
+                        'still-unpaid local counterpart invoice %s was deleted.',
+                        i, payment_invoice.id)
+                payment_invoice.remove_self()
+            elif invoice.paid():
+                payment_invoice.paid_by_domino = True
+                payment_invoice.pay()
+                logger.info('domino cron: remote invoice %s has beend paid, '
+                        'local invoice %s of user %s is now marked as paid.',
+                        invoice.id, payment_invoice.id, payment_invoice.user_id)
+            else: # not invoice.paid()
+                pass # still waiting for the payment
+
+get_publisher_class().register_cronjob(CronJob(function=synchronize_domino,
+    hours=range(0, 24), minutes=range(0, 60, 30)))
diff --git a/auquotidien/modules/abelium_domino_ui.py b/auquotidien/modules/abelium_domino_ui.py
new file mode 100644
index 0000000..e297e8e
--- /dev/null
+++ b/auquotidien/modules/abelium_domino_ui.py
@@ -0,0 +1,170 @@
+from quixote import get_publisher, redirect, get_request
+from quixote.directory import Directory, AccessControlled
+from quixote.html import TemplateIO, htmltext
+
+from qommon import _
+from qommon import get_cfg, get_logger
+from qommon.form import Form, StringWidget, CheckboxWidget, SingleSelectWidget
+from qommon.backoffice.menu import html_top
+from quixote.html import htmltext
+
+from payments import Regie
+
+
+# constants
+ABELIUM_DOMINO = 'abelium_domino'
+ACTIVATED = 'activated'
+WSDL_URL = 'wsdl_url'
+SERVICE_URL = 'service_url'
+DOMAIN = 'domain'
+LOGIN = 'login'
+PASSWORD = 'password'
+INVOICE_REGIE = 'invoice_regie'
+
+try:
+    import abelium_domino_ws
+except ImportError, e:
+    abelium_domino_ws = None
+    import_error = e
+
+def get_abelium_cfg(publisher):
+    if not publisher:
+        publisher = get_publisher()
+    return publisher.cfg.get(ABELIUM_DOMINO, {})
+
+def is_activated(publisher=None):
+    cfg = get_abelium_cfg(publisher)
+    return cfg.get(ACTIVATED, False) and abelium_domino_ws is not None
+
+def get_client(publisher=None):
+    publisher = publisher or get_publisher()
+
+    cfg = get_abelium_cfg(publisher)
+    try:
+        publisher._ws_cache = abelium_domino_ws.DominoWs(
+            url=cfg.get(WSDL_URL, ''),
+            domain=cfg.get(DOMAIN,''),
+            login=cfg.get(LOGIN, ''),
+            password=cfg.get(PASSWORD, ''),
+            location=cfg.get(SERVICE_URL),
+            logger=get_logger())
+    except IOError:
+        return None
+    return publisher._ws_cache
+
+def get_family(user, publisher=None):
+    family = None
+    if user is None:
+        return None
+    client = get_client(publisher)
+    if not client:
+        return None
+    if hasattr(user, 'abelium_domino_code_famille'):
+        family = client.get_family_by_code_interne(
+            user.abelium_domino_code_famille)
+    if family is None and user.email:
+        family = client.get_family_by_mail(user.email)
+    return family
+
+def get_invoice_regie(publisher=None):
+    cfg = get_abelium_cfg(publisher)
+    regie_id = cfg.get(INVOICE_REGIE)
+    if not regie_id:
+        return None
+    return Regie.get(regie_id, ignore_errors=True)
+
+class AbeliumDominoDirectory(Directory):
+    _q_exports = [ '' , 'debug' ]
+    label = N_('Domino')
+
+    def debug(self):
+        from abelium_domino_vars import SESSION_CACHE
+        html_top(ABELIUM_DOMINO)
+        r = TemplateIO(html=True)
+        r += htmltext('<form method="post"><button>Lancer le cron</button></form>')
+        if get_request().get_method() == 'POST':
+            try:
+                from abelium_domino_synchro import synchronize_domino
+                synchronize_domino(get_publisher())
+            except Exception, e:
+                r += htmltext('<pre>%s</pre>') % repr(e)
+        r += htmltext('<p>code interne: %s</p>') % getattr(get_request().user, str('abelium_domino_code_famille'), None)
+        r += htmltext('<dl>')
+        context = get_publisher().substitutions.get_context_variables()
+        for var in sorted(context.keys()):
+            value = context[var]
+            if value:
+                r += htmltext('<dt>%s</dt>') % var
+                r += htmltext('<dd>%s</dt>') % value
+        r += htmltext('</dl>')
+        delattr(get_request().session, SESSION_CACHE)
+
+    def _q_index(self):
+        publisher = get_publisher()
+        cfg = get_cfg(ABELIUM_DOMINO, {})
+        form = self.form(cfg)
+
+        title = _('Abelium Domino')
+        html_top(ABELIUM_DOMINO, title = title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % title
+
+        if form.is_submitted() and not form.has_errors():
+            if form.get_widget('cancel').parse():
+                return redirect('..')
+            if form.get_widget('submit').parse():
+                for name in [f[0] for f in self.form_desc] + [INVOICE_REGIE]:
+                    widget = form.get_widget(name)
+                    if widget:
+                        cfg[name] = widget.parse()
+                publisher.cfg[ABELIUM_DOMINO] = cfg
+                publisher.write_cfg()
+                return redirect('.')
+
+        if abelium_domino_ws:
+            r += form.render()
+        else:
+            message = _('The Abelium Domino module is not '
+                    'activated because of this error when '
+                    'loading it: %r') % import_error
+            r += htmltext('<p class="errornotice">%s</p>') % message
+        r += htmltext('<dl style="display: none">')
+        context = get_publisher().substitutions.get_context_variables()
+        for var in sorted(context.keys()):
+            value = context[var]
+            if value:
+                r += htmltext('<dt>%s</dt>') % var
+                r += htmltext('<dd>%s</dt>') % value
+        r += htmltext('</dl>')
+        return r.getvalue()
+
+    form_desc = (
+            # name, required, title, kind
+            (ACTIVATED, False, _('Activated'), bool),
+            (WSDL_URL, True, _('WSDL URL'), str),
+            (SERVICE_URL, False, _('Service URL'), str),
+            (DOMAIN, True, _('Domain'), str),
+            (LOGIN, True, _('Login'), str),
+            (PASSWORD, True, _('Password'), str),
+    )
+
+
+    def form(self, initial_value={}):
+        form = Form(enctype='multipart/form-data')
+        kinds = { str: StringWidget, bool: CheckboxWidget }
+        for name, required, title, kind in self.form_desc:
+            widget = kinds[kind]
+            form.add(widget, name, required=required, title=title,
+                    value=initial_value.get(name, ''))
+        options = [(regie.id, regie.label) \
+                        for regie in Regie.values()]
+        options.insert(0, (None, _('None')))
+        form.add(SingleSelectWidget, INVOICE_REGIE,
+                title=_('Regie which will receive payments'),
+                value=initial_value.get(INVOICE_REGIE),
+                options=options)
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        return form
diff --git a/auquotidien/modules/abelium_domino_vars.py b/auquotidien/modules/abelium_domino_vars.py
new file mode 100644
index 0000000..6f32bf3
--- /dev/null
+++ b/auquotidien/modules/abelium_domino_vars.py
@@ -0,0 +1,81 @@
+from decimal import Decimal
+import logging
+
+from quixote.publish import get_publisher
+
+from qommon import _
+from qommon.substitution import Substitutions
+from wcs.publisher import WcsPublisher
+
+from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
+
+SESSION_CACHE = 'abelium_domino_variable_cache'
+
+class DominoVariables(object):
+    VARIABLE_TEMPLATE = 'domino_var_%s'
+    CHILD_VARIABLE_TEMPLATE = 'domino_var_%s_enfant%s'
+
+    CHILD_COLUMNS = abelium_domino_ws.Child.COLUMNS
+    FAMILY_COLUMNS = abelium_domino_ws.Family.COLUMNS \
+        + abelium_domino_ws.Family.MORE_COLUMNS
+    def __init__(self, publisher=None, request=None):
+        self.publisher = publisher
+        self.request = request
+
+    def get_substitution_variables(self):
+        vars = {}
+        if not is_activated() or not self.request or not self.request.user \
+                or not getattr(self.request.user, 'email'):
+            return vars
+
+        # test cache
+        cache = getattr(self.request.session, SESSION_CACHE, None)
+        if cache is not None:
+            return cache
+        # call the web service
+        try:
+            charset = get_publisher().site_charset
+            family = get_family(self.request.user)
+            if family:
+                family.complete()
+                for i, child in enumerate(family.children):
+                    for remote_name, name, converter, desc in self.CHILD_COLUMNS:
+                        v = getattr(child, name, None)
+                        if v is None:
+                            continue
+                        if hasattr(v, 'encode'):
+                            v = v.encode(charset)
+                        vars[self.CHILD_VARIABLE_TEMPLATE % (name, i+1)] = v
+                vars[self.VARIABLE_TEMPLATE % 'nombre_enfants'] = len(family.children)
+                for remote_name, name, converted, desc in self.FAMILY_COLUMNS:
+                    if hasattr(family, name):
+                        v = getattr(family, name)
+                        if v is None:
+                            continue
+                        if hasattr(v, 'encode'):
+                            v = v.encode(charset)
+                        vars[self.VARIABLE_TEMPLATE % name] = v
+                amount = Decimal(0)
+                for invoice in family.invoices:
+                    amount += invoice.reste_du
+                if amount:
+                    vars['user_famille_reste_du'] = str(amount)
+        except abelium_domino_ws.DominoException:
+            logging.exception('unable to call the domino ws for user %s', self.request.user.id)
+        setattr(self.request.session, SESSION_CACHE, vars)
+        self.request.session.store()
+        return vars
+
+    def get_substitution_variables_list(cls):
+        if not is_activated():
+            return ()
+        vars = []
+        for remote_name, name, converted, desc in cls.FAMILY_COLUMNS:
+            vars.append((_('Domino'), cls.VARIABLE_TEMPLATE % name, desc))
+        for remote_name, name, converted, desc in cls.CHILD_COLUMNS:
+            vars.append((_('Domino'), cls.CHILD_VARIABLE_TEMPLATE % (name, '{0,1,2,..}'), desc))
+        return vars
+    get_substitution_variables_list = classmethod(get_substitution_variables_list)
+
+Substitutions.register_dynamic_source(DominoVariables)
+WcsPublisher.register_extra_source(DominoVariables)
diff --git a/auquotidien/modules/abelium_domino_workflow.py b/auquotidien/modules/abelium_domino_workflow.py
new file mode 100644
index 0000000..f1be0a7
--- /dev/null
+++ b/auquotidien/modules/abelium_domino_workflow.py
@@ -0,0 +1,151 @@
+import re
+import time
+
+from quixote import get_request, get_publisher, get_session
+from quixote.directory import Directory
+
+from qommon import _
+from qommon.substitution import Substitutions
+from qommon.form import Form, ValidatedStringWidget
+import qommon.misc
+from qommon import get_logger
+
+from wcs.workflows import Workflow, WorkflowStatusJumpItem, register_item_class
+from wcs.forms.common import FormStatusPage
+
+from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
+import abelium_domino_ws
+
+class InternalCodeStringWidget(ValidatedStringWidget):
+    regex = '\d*'
+
+class AbeliumDominoRegisterFamilyWorkflowStatusItem(WorkflowStatusJumpItem):
+    status = None
+    description = N_('Abelium Domino: Register a Family')
+    key = 'abelium-domino-register-family'
+    category = ('aq-abelium', N_('Abelium'))
+    label = None
+
+    def render_as_line(self):
+        return _('Register a Family into Abelium Domino')
+
+    def get_family(self, formdata):
+        try:
+            user = formdata.get_user()
+            if user:
+                family = get_family(user)
+                if family:
+                    family.complete()
+                return family
+        except abelium_domino_ws.DominoException:
+            pass
+        return None
+
+    def fill_form(self, form, formdata, user):
+        family = self.get_family(formdata)
+        if 'family_id' not in form._names:
+            form.add(InternalCodeStringWidget, 'family_id',
+                     title=_('Family internal code'),
+                     value=family and family.code_interne.encode('utf8'),
+                     hint=_('If a family internal code is present, the '
+                            'family is updated, if not it is created'))
+            form.add_submit('create_update_button%s' % self.id,
+                    _('Create or update the family'))
+
+    def update(self, form, formdata, user, evo):
+        fid_widget = form.get_widget('family_id')
+        code_interne = fid_widget.parse()
+        try:
+            code_interne = int(code_interne)
+        except ValueError:
+            raise ValueError('Le code interne est invalide')
+        code_interne = '%05d' % code_interne
+        family = get_client().get_family_by_code_interne(code_interne)
+        if not family:
+            raise ValueError('Le code interne est invalide')
+        family.complete()
+        self.extract_family(form, formdata, user, evo, family)
+        family.save()
+        return family
+
+    def create(self, form, formdata, user, evo):
+        family = abelium_domino_ws.Family(client=get_client())
+        self.extract_family(form, formdata, user, evo, family)
+        return family
+
+    def extract_family(self, form, formdata, user, evo, family):
+        formdef = formdata.formdef
+        children = [abelium_domino_ws.Child() for i in range(5)]
+        max_i = 0
+        for field in formdef.fields:
+            value = formdata.data.get(field.id)
+            if value in (None, ''):
+                continue
+            if hasattr(field, 'date_in_the_past'):
+                value = time.strftime('%Y%m%d', value)
+            value = unicode(value, 'utf8')
+            if field.prefill and \
+               field.prefill.get('type') == 'formula':
+                v = field.prefill.get('value', '').strip()
+                i = None
+                name = None
+                m = re.search('domino_var_([^ ]*)_enfant([0-9]*)', v)
+                m2 = re.search('domino_var_([^ ]*)', v)
+                if m:
+                    name, i = m.groups()
+                    try:
+                        i = int(i)
+                    except ValueError:
+                        continue
+                    max_i = max(i, max_i)
+                    print 'enfant', name, i-1, value
+                    setattr(children[i-1], name, value)
+                elif m2:
+                    name = m2.group(1)
+                    print 'family', name, value
+                    setattr(family, name, value)
+        for child1, child2 in zip(family.children, children[:max_i]):
+            child1.__dict__.update(child2.__dict__)
+        family.save()
+        if max_i > len(family.children): # add new children
+            for child in children[len(family.children):max_i]:
+                family.add_child(child)
+
+    def submit_form(self, form, formdata, user, evo):
+        logger = get_logger()
+        if form.get_submit() != 'create_update_button%s' % self.id:
+            return
+        try:
+            if form.get_widget('family_id').parse():
+                family = self.update(form, formdata, user, evo)
+                msg = _('Sucessfully updated the family %s')
+                log_msg = _('Sucessfully updated the family %(code)s of %(user)s')
+            else:
+                family = self.create(form, formdata, user, evo)
+                msg = _('Sucessfully created the family %s')
+                log_msg = _('Sucessfully created the family %(code)s of %(user)s')
+            code_interne = family.code_interne.encode('utf8')
+            msg = msg % code_interne
+            logger.info(log_msg, {'code': code_interne, 'user': formdata.get_user()})
+            form_user = formdata.get_user()
+            form_user.abelium_domino_code_famille = code_interne
+            form_user.store()
+        except Exception, e:
+            if form.get_widget('family_id').parse():
+                msg = _('Unable to update family: %s') % str(e)
+            else:
+                msg = _('Unable to create family: %s') % str(e)
+            evo.comment = msg
+            logger.exception(msg  % formdata.get_user())
+        else:
+            evo.comment = msg
+            wf_status = self.get_target_status()
+            if wf_status:
+                evo.status = 'wf-%s' % wf_status[0].id
+        return False
+
+    def is_available(self, workflow=None):
+        return get_publisher().has_site_option('domino')
+    is_available = classmethod(is_available)
+
+register_item_class(AbeliumDominoRegisterFamilyWorkflowStatusItem)
diff --git a/auquotidien/modules/abelium_domino_ws.py b/auquotidien/modules/abelium_domino_ws.py
new file mode 100644
index 0000000..fc006bf
--- /dev/null
+++ b/auquotidien/modules/abelium_domino_ws.py
@@ -0,0 +1,648 @@
+# -*- coding: utf-8 -*-
+from decimal import Decimal
+import time
+import datetime
+from xml.etree import ElementTree as etree
+import logging
+
+try:
+    from suds.client import Client
+    from suds.bindings.binding import Binding
+    # Webdev is bugged and using an HTML generator to produce XML content, 
+    Binding.replyfilter = lambda self, x: x.replace('&nbsp;', '&#160;')
+except ImportError:
+    Client = None
+    Binding = None
+
+logger = logging.getLogger(__name__)
+
+# cleaning and parsing functions
+
+LINE_SEPARATOR = '\n'
+COLUMN_SEPARATOR = '\t'
+
+def unicode_and_strip(x):
+    return unicode(x).strip()
+
+def strip_and_int(x):
+    try:
+        return int(x.strip())
+    except ValueError:
+        return None
+
+def strip_and_date(x):
+    try:
+        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
+    except ValueError:
+        return None
+
+def parse_date(date_string):
+    if date_string:
+        return datetime.datetime.strptime(date_string, "%Y%m%d")
+    else:
+        None
+
+class DominoException(Exception):
+    pass
+
+def object_cached(function):
+    '''Decorate an object method so that its results is cached on the object
+       instance after the first call.
+    '''
+    def decorated_function(self, *args, **kwargs):
+        cache_name = '__%s_cache' % function.__name__
+        if not hasattr(self, cache_name):
+            setattr(self, cache_name, (time.time(), {}))
+        t, d = getattr(self, cache_name)
+        if time.time() - t > 80:
+            setattr(self, cache_name, (time.time(), {}))
+            t, d = getattr(self, cache_name)
+        k = tuple(*args) + tuple(sorted(kwargs.items()))
+        if not k in d:
+            d[k] = function(self, *args, **kwargs)
+        return d[k]
+    return decorated_function
+
+# Data model
+class SimpleObject(object):
+    '''Base class for object returned by the web service'''
+
+    '''Describe basic columns'''
+    COLUMNS = ()
+    '''Describe extended object columns'''
+    MORE_COLUMNS = ()
+
+    def __init__(self, **kwargs):
+        self.__dict__.update(kwargs)
+
+    def __repr__(self):
+        c = {}
+        for remote_name, name, converter, desc in self.COLUMNS:
+            if hasattr(self, name):
+                c[name] = getattr(self, name)
+        return str(c)
+
+    def serialize(self):
+        l = []
+        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
+            if local_name == 'id':
+                continue
+            v = getattr(self, local_name, None)
+            if v is None:
+                continue
+            if isinstance(v, (datetime.date, datetime.datetime)):
+                v = v.strftime('%Y%m%d')
+            if remote_name.endswith('_DA') and '-' in v:
+                v = v.replace('-', '')
+            l.append(u'{0}: "{1}"'.format(remote_name, v))
+        return u','.join(l)
+
+    def debug(self):
+        '''Output a debugging view of this object'''
+        res = ''
+        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
+            if hasattr(self, name):
+                res += name + ':' + repr(getattr(self, name)) + '\n'
+        return res
+
+    def __int__(self):
+        '''Return the object id'''
+        return self.id
+
+class UrgentContact(SimpleObject):
+    COLUMNS = (
+            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
+            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
+            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
+            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
+            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
+            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
+            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
+            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
+            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
+            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
+            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
+            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
+            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
+            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
+            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
+            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
+    )
+
+class Child(SimpleObject):
+    COLUMNS = (
+            ('IDENFANTS', 'id', strip_and_int, 'Identifiant de ENFANTS'),
+            ('NOM_CH', 'nom', unicode_and_strip, 'Nom'),
+            ('PRENOM_CH', 'prenom', unicode_and_strip, 'Prénom'),
+            ('NAISSANCE_DA', 'date_naissance', strip_and_date, 'Date de Naissance'),
+            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'Commentaires / Notes'),
+            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
+            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'Code Postal'),
+            ('VILLE_CH', 'ville', unicode_and_strip, 'Ville'),
+            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'Code Interne'),
+            ('LIEUNAISSANCE_CH', 'lieu_naissance', unicode_and_strip, 'Lieu de Naissance'),
+            ('DEPNAISSANCE_CH', 'departement_naissance', unicode_and_strip, 'Département Naissance'),
+            ('NUMSECU_CH', 'num_securite_sociale', unicode_and_strip, 'N° de SECU'),
+            ('NATIONALITE_CH', 'nationalite', unicode_and_strip, 'Nationalité'),
+            ('PRENOM2_CH', 'prenom2', unicode_and_strip, 'Prénom 2'),
+            ('SEXE_CH', 'sexe', unicode_and_strip, 'Sexe'),
+            ('IDTABLELIBRE1', 'IDTABLELIBRE1', unicode_and_strip, 'IDTABLELIBRE1'),
+            ('IDTABLELIBRE2', 'IDTABLELIBRE2', unicode_and_strip, 'IDTABLELIBRE2'),
+            ('IDTABLELIBRE3', 'IDTABLELIBRE3', unicode_and_strip, 'IDTABLELIBRE3'),
+            ('IDTABLELIBRE4', 'IDTABLELIBRE4', unicode_and_strip, 'IDTABLELIBRE4'),
+            ('CHAMPLIBRE1_CH', 'CHAMPLIBRE1_CH', unicode_and_strip, 'Valeur Champ Libre 1'),
+            ('CHAMPLIBRE2_CH', 'CHAMPLIBRE2_CH', unicode_and_strip, 'Valeur Champ Libre 2'),
+            ('CHAMPCALCULE1_CH', 'CHAMPCALCULE1_CH', unicode_and_strip, 'Valeur Champ Calculé 1'),
+            ('CHAMPCALCULE2_CH', 'CHAMPCALCULE2_CH', unicode_and_strip, 'Valeur Champ Calculé 2'),
+            ('SOMMEIL_ME', 'sommeil', unicode_and_strip, 'Sommeil'),
+            ('ACTIVITE_ME', 'activite', unicode_and_strip, 'Activités'),
+            ('HABITUDE_ME', 'habitude', unicode_and_strip, 'Habitudes'),
+            ('PHOTO_CH', 'photographie', unicode_and_strip, 'Photographie'),
+            ('NUMCOMPTE_CH', 'numcompte', unicode_and_strip, 'N° Compte Comptable'),
+            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'Téléphone'),
+            ('IDFAMILLES2', 'id_famille2', unicode_and_strip, 'Identifiant famille 2'),
+            ('PERE_CH', 'pere', unicode_and_strip, 'Nom du père'),
+            ('MERE_CH', 'mere', unicode_and_strip, 'Nom de la mère'),
+            ('AUTOPARENTALEMERE_IN', 'autorisation_parentale_mere', unicode_and_strip, 'Autorisation Parentale Mère'),
+            ('AUTOPARENTALEPERE_IN', 'autorisation_parentale_pere', unicode_and_strip, 'Autorisation Parentale de Père'),
+            ('IDPORTAIL_ENFANTS', 'id_portail_enfants', unicode_and_strip, 'Identifiant de PORTAIL_ENFANTS'),
+            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'Adresse Internet'),
+    )
+
+    def save(self):
+        if hasattr(self, 'id'):
+            self.client.update_child(self)
+        else:
+            self.id = self.client.add_child(self)
+        self.client.clear_cache()
+
+class Family(SimpleObject):
+    COLUMNS = (
+            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
+            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
+            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
+            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
+            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
+            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
+    )
+
+    MORE_COLUMNS = (
+            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
+            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
+            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
+            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
+            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
+            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
+            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
+            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
+            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
+            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
+            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
+            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
+            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
+            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
+            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
+            ('REVENUMENSUEL_MO', 'revenu_mensuel', unicode_and_strip, 'revenu mensuel de la famille'),
+            ('REVENUANNUEL_MO', 'revenu_annuel', unicode_and_strip, 'revenu annuel de la famille'),
+            ('QUOTIENTFAMILIAL_MO', 'quotient_familial', unicode_and_strip, 'quotient familial'),
+            ('NBTOTALENFANTS_EN', 'nb_total_enfants', unicode_and_strip, 'nombre total d\'enfants'),
+            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge', unicode_and_strip, 'nombre d\'enfants à charge'),
+            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
+            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
+            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere', unicode_and_strip, 'autorisation parentale de père'),
+            ('DATENAISPERE_DA', 'date_naissance_pere', strip_and_date, 'date de naisance du père'),
+            ('DEPNAISPERE_EN', 'departement_naissance_pere', unicode_and_strip, 'département de naissance du père'),
+            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
+            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
+            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
+            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
+            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
+            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
+            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
+            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
+            ('TELPERE_LR_IN', 'tel_pere_liste_rouge', unicode_and_strip, 'téléphone liste rouge père'),
+            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge père'),
+            ('TEL_LR_IN', 'tel_liste_rourge', unicode_and_strip, 'téléphone liste rouge'),
+            ('TEL2_LR_IN', 'tel2_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge'),
+            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
+            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
+            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere', unicode_and_strip, 'autorisation parentale mère'),
+            ('DATENAISMERE_DA', 'date_naissance_mere', strip_and_date, 'date de naissance de la mère'),
+            ('DEPNAISMERE_EN', 'departement_naissance_mere', unicode_and_strip, 'département de naissance de la mère'),
+            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
+            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
+            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere', unicode_and_strip, 'revenu mensuel du père'),
+            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
+            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
+            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
+            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
+            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere', unicode_and_strip, 'revenu mensuel mère'),
+            ('REVANNUELPERE_MO', 'revenu_annuel_pere', unicode_and_strip, 'revenu annuel père'),
+            ('REVANNUELMERE_MO', 'revenu_annuel_mere', unicode_and_strip, 'revenu annuel mère'),
+            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
+            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
+            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge', unicode_and_strip, 'téléphone liste rouge mère'),
+            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge mère'),
+            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
+            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
+            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
+            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
+            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
+            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
+            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
+            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
+            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
+            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
+            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
+            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
+            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
+            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
+            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
+            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
+            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
+            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
+            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
+            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
+            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
+            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
+            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
+            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
+            ('TOTALDU_MO', 'total_du', unicode_and_strip, 'total dû'),
+            ('TOTALREGLE_MO', 'total_regle', unicode_and_strip, 'total réglé'),
+            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
+            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
+            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
+            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
+            ('RIVOLI_EN', 'code_rivoli', unicode_and_strip, 'identifiant code rivoli'),
+            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
+            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
+            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
+            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
+            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'commentaires / notes'),
+            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
+            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
+            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
+            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
+            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
+            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
+            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
+            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
+            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
+            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
+            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
+            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
+            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
+            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
+            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
+            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
+            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
+            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
+            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
+            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
+            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
+            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
+            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
+            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
+            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
+            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
+            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
+#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
+#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
+            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
+            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
+            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
+            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
+            ('ECHEANCEASSURANCE_DA', 'echeance_assurance', unicode_and_strip, 'date echéance assurance'),
+            ('RM_MIKADO_MO', 'rm_mikado', unicode_and_strip, 'revenus mensuels mikado'),
+            ('RA_MIKADO_MO', 'ra_mikado', unicode_and_strip, 'revenus annuels mikado'),
+            ('QF_MIKADO_MO', 'qf_mikado', unicode_and_strip, 'quotient familial mikado'),
+            ('RM_DIABOLO_MO', 'rm_diabolo', unicode_and_strip, 'revenus mensuels diabolo'),
+            ('RA_DIABOLO_MO', 'ra_diabolo', unicode_and_strip, 'revenus annuels diabolo'),
+            ('QF_DIABOLO_MO', 'qf_diabolo', unicode_and_strip, 'quotient familial diabolo'),
+            ('RM_OLIGO_MO', 'rm_oligo', unicode_and_strip, 'revenus mensuels oligo'),
+            ('RA_OLIGO_MO', 'ra_oligo', unicode_and_strip, 'revenus annuels oligo'),
+            ('QF_OLIGO_MO', 'qf_oligo', unicode_and_strip, 'quotient familial oligo'),
+            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado', unicode_and_strip, 'date d\'application des revenus de mikado'),
+            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo', unicode_and_strip, 'date d\'application des revenus de diabolo'),
+            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo', unicode_and_strip, 'date d\'application des revenus de oligo'),
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.children = []
+        super(Family, self).__init__(*args, **kwargs)
+
+    def complete(self):
+        k = [a for a,b,c,d in self.MORE_COLUMNS]
+        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
+                columns=self.MORE_COLUMNS, instances=(self,)))
+        l = self.client.get_children(self.id).values()
+        self.children = sorted(l, key=lambda c: c.id)
+        return self
+
+    @property
+    def invoices(self):
+        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
+
+    def add_child(self, child):
+        if hasattr(self, 'id'):
+            child.id_famille = self.id
+            child.client = self.client
+        self.children.append(child)
+
+    def save(self):
+        if hasattr(self, 'id'):
+            self.client.update_family(self)
+        else:
+            self.code_interne = self.client.new_code_interne()
+            self.id = self.client.add_family(self)
+        for child in self.children:
+            child.id_famille = self.id
+            child.save()
+        self.client.clear_cache()
+
+class Invoice(SimpleObject):
+    COLUMNS = (
+        ('', 'id_famille', int, ''),
+        ('', 'id', int, ''),
+        ('', 'numero', str, ''),
+        ('', 'debut_periode', parse_date, ''),
+        ('', 'fin_periode', parse_date, ''),
+        ('', 'creation', parse_date, ''),
+        ('', 'echeance', parse_date, ''),
+        ('', 'montant', Decimal, ''),
+        ('', 'reste_du', Decimal, ''),
+    )
+    _detail = {}
+
+    def detail(self):
+        if not self._detail:
+            self.client.factures_detail([self])
+        return self._detail
+
+    @property
+    def family(self):
+        return self.client.families[self.id_famille]
+
+    def paid(self):
+        return self.reste_du == Decimal(0)
+
+class DominoWs(object):
+    '''Interface to the WebService exposed by Abelium Domino.
+
+       It allows to retrieve family and invoices.
+
+       Beware that it does a lot of caching to limit call to the webservice, so
+       if you need fresh data, call clear_cache()
+
+       All family are in the families dictionnary and all invoices in the
+       invoices dictionnary.
+    '''
+
+    def __init__(self, url, domain, login, password, location=None,
+            logger=logger):
+        if not Client:
+            raise ValueError('You need python suds')
+        self.logger = logger
+        self.logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)',
+                url, domain, login, password, location)
+        self.url = url
+        self.domain = domain
+        self.login = login
+        self.password = password
+        self.client = Client(url, location=location, timeout=60)
+        self.client.options.cache.setduration(seconds=60)
+
+    def clear_cache(self):
+        '''Remove cached attributes from the instance.'''
+
+        for key, value in self.__dict__.items():
+            if key.startswith('__') and key.endswith('_cache'):
+                del self.__dict__[key]
+
+    def call(self, function_name, *args):
+        '''Call SOAP method named function_name passing args list as parameters.
+
+           Any error is converted into the DominoException class.'''
+        print 'call', function_name, args
+
+        try:
+            self.logger.debug(('soap call to %s(%s)' % (function_name, args)).encode('utf-8'))
+            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
+            self.logger.debug((u'result: %s' % data).encode('utf-8'))
+            self.data = data
+        except IOError, e:
+            raise DominoException('Erreur IO', e)
+        if data is None:
+           data = ''
+        if data.startswith('ERREUR'):
+            raise DominoException(data[9:].encode('utf8'))
+        return data
+
+    def parse_tabular_data(self, data):
+        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
+           Empty lines (ignoring spaces) are ignored.
+        '''
+
+        rows = data.split(LINE_SEPARATOR)
+        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
+        return rows
+
+    def __call__(self, function_name, cls=None, args=[], instances=None, columns=None):
+        '''Call SOAP method named function_name, splitlines, map tab separated
+        values to _map keys in a dictionnary, and use this dictionnary to
+        initialize an object of class cls.
+
+        - If instances is present, the given instances are updated with the
+          returned content, in order, row by row.
+        - If cls is not None and instances is None, a new instance of the class
+          cls is instancied for every row and initialized with the content of
+          the row.
+        - If cls and instances are None, the raw data returned by the SOAP call
+          is returned.
+        '''
+
+        data = self.call(function_name, *args)
+        if cls or instances:
+            rows = self.parse_tabular_data(data)
+            kwargs = {}
+            if instances:
+                rows = zip(rows, instances)
+            for row in rows:
+                if instances:
+                    row, instance = row
+                if not row[0]:
+                    continue
+                for a, b in zip(columns or cls.COLUMNS, row):
+                    x, name, converter, desc = a
+                    kwargs[name] = converter(b.strip())
+                if instances:
+                    instance.__dict__.update(kwargs)
+                    yield instance
+                else:
+                    yield cls(client=self, **kwargs)
+        else:
+            yield data
+
+    def add_family(self, family):
+        result = self.call('AJOUTER_FAMILLE', family.serialize())
+        return int(result.strip())
+
+    def update_family(self, family):
+        if not hasattr(family, 'id'):
+            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
+        result = self.call('MODIFIER_FAMILLE', unicode(family.id), family.serialize())
+        return result.strip() == 'OK'
+
+    def add_child(self, child):
+        result = self.call('AJOUTER_ENFANT', child.serialize())
+        return int(result.strip())
+
+    def update_child(self, child):
+        if not hasattr(child, 'id'):
+            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
+        result = self.call('MODIFIER_ENFANT', unicode(child.id), child.serialize())
+        return result.strip() == 'OK'
+
+    @property
+    @object_cached
+    def families(self):
+        '''Dictionary of all families indexed by their id.
+
+           After the first use, the value is cached. Use clear_cache() to reset
+           it.
+        '''
+
+        return self.get_families()
+
+    def get_families(self, id_famille=0, full=False):
+        '''Get families informations.
+           There is no caching.
+
+           id_famille - if not 0, the family with this id is retrieved. If 0
+           all families are retrieved. Default to 0.
+           full - If True return all the columns of the family table. Default
+           to False.
+        '''
+        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
+        families = self('LISTER_FAMILLES',
+                Family,
+                args=(','.join([x[0] for x in columns]), id_famille))
+        return dict([(int(x), x) for x in families])
+
+    def get_children(self, id_famille=0):
+        columns = Child.COLUMNS
+        if id_famille == 0:
+            children = self('LISTER_ENFANTS',
+                Child,
+                args=((','.join([x[0] for x in columns])),))
+        else:
+            children = self('LISTER_ENFANTS_FAMILLE',
+                Child,
+                args=(id_famille, (','.join([x[0] for x in columns]))))
+        return dict([(int(x), x) for x in children])
+
+    def get_urgent_contacts(self, id_enfant):
+        columns = UrgentContact.COLUMNS
+        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
+                UrgentContact,
+                args=((id_enfant, ','.join([x[0] for x in columns]))))
+        return dict([(int(x), x) for x in urgent_contacts])
+
+    @property
+    @object_cached
+    def invoices(self):
+        '''Dictionnary of all invoices indexed by their id.
+
+           After the first use, the value is cached. Use clear_cache() to reset
+           it.
+        '''
+        invoices = self.get_invoices()
+        for invoice in invoices.values():
+            invoice.famille = self.families[invoice.id_famille]
+        return invoices
+
+    def new_code_interne(self):
+        max_ci = 0
+        for family in self.families.values():
+            try:
+                max_ci = max(max_ci, int(family.code_interne))
+            except:
+                pass
+        return '%05d' % (max_ci+1)
+
+    def get_invoices(self, id_famille=0, state='TOUTES'):
+        '''Get invoices informations.
+
+           id_famille - If value is not 0, only invoice for the family with
+           this id are retrieved. If value is 0, invoices for all families are
+           retrieved. Default to 0.
+           etat - state of the invoices to return, possible values are
+           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
+        '''
+        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
+            args=(id_famille, state))
+        invoices = list(invoices)
+        for invoice in invoices:
+            invoice.famille = self.families[invoice.id_famille]
+        return dict(((int(x), x) for x in invoices))
+
+    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
+    def factures_detail(self, invoices):
+        '''Retrieve details of some invoice'''
+        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
+        try:
+            tree = etree.fromstring(data.encode('utf8'))
+            for invoice, facture_node in zip(invoices, tree.findall('facture')):
+                rows = []
+                for ligne in facture_node.findall('detail_facture/ligne'):
+                    row = []
+                    rows.append(row)
+                    for header in self.FACTURE_DETAIL_HEADERS:
+                        if header in ligne.attrib:
+                            row.append((header, ligne.attrib[header]))
+                etablissement = facture_node.find('detail_etablissements/etablissement')
+                if etablissement is not None:
+                    nom = etablissement.get('nom').strip()
+                else:
+                    nom = ''
+                d = { 'etablissement': nom, 'lignes': rows }
+                invoice._detail = d
+        except Exception, e:
+            raise DominoException('Exception when retrieving invoice details', e)
+
+    def get_family_by_mail(self, email):
+        '''Return the first whose one email attribute matches the given email'''
+        for famille in self.families.values():
+            if email in (famille.email_pere, famille.email_mere,
+                    famille.adresse_internet):
+                return famille
+        return None
+
+    def get_family_by_code_interne(self, code_interne):
+        '''Return the first whose one email attribute matches the given email'''
+        for famille in self.families.values():
+            if getattr(famille, 'code_interne', None) == code_interne:
+                return famille
+        return None
+
+    def pay_invoice(self, id_invoices, amount, other_information, date=None):
+        '''Notify Domino of the payment of some invoices.
+
+           id_invoices - integer if of the invoice or Invoice instances
+           amount - amount as a Decimal object
+           other_information - free content to attach to the payment, like a
+           bank transaction number for correlation.
+           date - date of the payment, must be a datetime object. If None,
+           now() is used. Default to None.
+        '''
+
+        if not date:
+            date = datetime.datetime.now()
+        due = sum([self.invoices[int(id_invoice)].reste_du
+                 for id_invoice in id_invoices])
+        if Decimal(amount) == Decimal(due):
+            return self('SOLDER_FACTURE', None, args=(str(amount), 
+                    ''.join([ '%s;' % int(x) for x in id_invoices]),
+                    date.strftime('%Y-%m-%d'), other_information))
+        else:
+            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
diff --git a/auquotidien/modules/admin.py b/auquotidien/modules/admin.py
new file mode 100644
index 0000000..b238469
--- /dev/null
+++ b/auquotidien/modules/admin.py
@@ -0,0 +1,285 @@
+import os
+
+from quixote import get_publisher, redirect
+from quixote.directory import Directory
+from quixote.html import htmltext, TemplateIO
+
+import wcs.admin.root
+import wcs.root
+from wcs.roles import get_user_roles
+
+from qommon import _
+from qommon import errors, get_cfg
+from qommon.form import *
+
+import wcs.admin.settings
+from wcs.formdef import FormDef
+from wcs.categories import Category
+from qommon.backoffice.menu import html_top
+
+from events import get_default_event_tags
+import re
+from abelium_domino_ui import AbeliumDominoDirectory
+
+class AdminRootDirectory(wcs.admin.root.RootDirectory):
+    def __init__(self):
+        self._q_exports = wcs.admin.root.RootDirectory._q_exports
+        self.menu_items[-1] = ('/', N_('Publik'))
+
+    def get_intro_text(self):
+        return _('Welcome on Publik administration interface')
+
+
+class PanelDirectory(Directory):
+    _q_exports = ['', 'update', 'announces', 'permissions', 'event_keywords',
+            'announce_themes', 'strongbox', 'clicrdv', 'domino']
+    label = N_('Control Panel')
+
+    domino = AbeliumDominoDirectory()
+
+    def _verify_mask(self, form):
+        if form.is_submitted():
+            if not re.match("[0-9Xx]*$", form.get('mobile_mask') or ''):
+                form.set_error('mobile_mask', _('Invalid value'))
+            else:
+                string_value = form.get_widget('mobile_mask').value
+                if string_value:
+                    form.get_widget('mobile_mask').value = string_value.upper()
+
+    def announces(self):
+        announces_cfg = get_cfg('announces', {})
+        sms_cfg = get_cfg('sms', {})
+        form = Form(enctype='multipart/form-data')
+        hint = ""
+        if sms_cfg.get('mode','') in ("none",""):
+            hint = htmltext(_('You must also <a href="%s">configure your SMS provider</a>') % "../settings/sms")
+
+        form.add(CheckboxWidget, 'sms_support', title = _('SMS support'),
+                hint = hint, value = announces_cfg.get('sms_support', 0))
+        form.add(StringWidget, 'mobile_mask', title = _('Mask for mobile numbers'),
+                hint = _('example: 06XXXXXXXX'),
+                value = announces_cfg.get('mobile_mask',''))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        self._verify_mask(form)
+
+        if form.get_widget('cancel').parse():
+            return redirect('..')
+
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('aq/announces', _('Announces Options')))
+            html_top('settings', _('Announces Options'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Announces Options')
+            r += form.render()
+            return r.getvalue()
+        else:
+            from wcs.admin.settings import cfg_submit
+            cfg_submit(form, 'announces', ('sms_support','mobile_mask'))
+            return redirect('..')
+
+    def permissions(self):
+        permissions_cfg = get_cfg('aq-permissions', {})
+        form = Form(enctype='multipart/form-data')
+        form.add(SingleSelectWidget, 'forms', title = _('Admin role for forms'),
+                value = permissions_cfg.get('forms', None),
+                options = [(None, _('Nobody'), None)] + get_user_roles())
+        form.add(SingleSelectWidget, 'events', title = _('Admin role for events'),
+                value = permissions_cfg.get('events', None),
+                options = [(None, _('Nobody'), None)] + get_user_roles())
+        form.add(SingleSelectWidget, 'links', title = _('Admin role for links'),
+                value = permissions_cfg.get('links', None),
+                options = [(None, _('Nobody'), None)] + get_user_roles())
+        form.add(SingleSelectWidget, 'announces', title = _('Admin role for announces'),
+                value = permissions_cfg.get('announces', None),
+                options = [(None, _('Nobody'), None)] + get_user_roles())
+        form.add(SingleSelectWidget, 'payments', title = _('Admin role for payments'),
+                value = permissions_cfg.get('payments', None),
+                options = [(None, _('Nobody'), None)] + get_user_roles())
+        form.add(SingleSelectWidget, 'strongbox', title = _('Admin role for strongbox'),
+                value = permissions_cfg.get('strongbox', None),
+                options = [(None, _('Nobody'), None)] + get_user_roles())
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_widget('cancel').parse():
+            return redirect('..')
+
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('aq/permissions', _('Permissions')))
+            html_top('settings', _('Permissions'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Permissions')
+            r += form.render()
+            return r.getvalue()
+        else:
+            from wcs.admin.settings import cfg_submit
+            cfg_submit(form, 'aq-permissions',
+                        ('forms', 'events', 'links', 'announces', 'payments', 'strongbox'))
+            return redirect('..')
+
+    def event_keywords(self):
+        misc_cfg = get_cfg('misc', {})
+        form = Form(enctype='multipart/form-data')
+        form.add(WidgetList, 'event_tags', title = _('Event Keywords'),
+                value = misc_cfg.get('event_tags', get_default_event_tags()),
+                elemnt_type = StringWidget,
+                add_element_label = _('Add Keyword'),
+                element_kwargs = {str('render_br'): False, str('size'): 30})
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_widget('cancel').parse():
+            return redirect('..')
+
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('aq/event_keywords', _('Event Keywords')))
+            html_top('settings', _('Event Keywords'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Event Keywords')
+            r += form.render()
+            return r.getvalue()
+        else:
+            from wcs.admin.settings import cfg_submit
+            cfg_submit(form, 'misc', ('event_tags',))
+            return redirect('..')
+
+    def announce_themes(self):
+        misc_cfg = get_cfg('misc', {})
+        form = Form(enctype='multipart/form-data')
+        form.add(WidgetList, 'announce_themes', title = _('Announce Themes'),
+                value = misc_cfg.get('announce_themes', []),
+                elemnt_type = StringWidget,
+                add_element_label = _('Add Theme'),
+                element_kwargs = {str('render_br'): False, str('size'): 30})
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_widget('cancel').parse():
+            return redirect('..')
+
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('aq/announce_themes', _('Announce Themes')))
+            html_top('settings', _('Announce Themes'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Announce Themes')
+            r += form.render()
+            return r.getvalue()
+        else:
+            from wcs.admin.settings import cfg_submit
+            cfg_submit(form, 'misc', ('announce_themes',))
+            return redirect('..')
+
+    def strongbox(self):
+        if not get_publisher().has_site_option('strongbox'):
+            raise errors.TraversalError()
+        misc_cfg = get_cfg('misc', {})
+        form = Form(enctype='multipart/form-data')
+        form.add(CheckboxWidget, 'aq-strongbox', title=_('Strongbox Support'),
+                value=misc_cfg.get('aq-strongbox'), required=False)
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_widget('cancel').parse():
+            return redirect('..')
+
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('aq/strongbox', _('Strongbox Support')))
+            html_top('settings', _('Strongbox Support'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Strongbox Support')
+            r += form.render()
+            return r.getvalue()
+        else:
+            from wcs.admin.settings import cfg_submit
+            cfg_submit(form, 'misc', ('aq-strongbox',))
+            return redirect('..')
+
+    def clicrdv(self):
+        if not get_publisher().has_site_option('clicrdv'):
+            raise errors.TraversalError()
+        misc_cfg = get_cfg('misc', {})
+        form = Form(enctype='multipart/form-data')
+        form.add(SingleSelectWidget, 'aq-clicrdv-server', title=_('ClicRDV Server'),
+                value=misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), required=True,
+                options=[(str('www.clicrdv.com'), _('Production Server')),
+                         (str('sandbox.clicrdv.com'), _('Sandbox Server'))])
+        form.add(StringWidget, 'aq-clicrdv-api-key', title=_('API Key'),
+                value=misc_cfg.get('aq-clicrdv-api-key'), required=False,
+                size=40, hint=_('Empty to disable ClicRDV support'))
+        form.add(StringWidget, 'aq-clicrdv-api-username', title=_('Username'),
+                value=misc_cfg.get('aq-clicrdv-api-username'), required=False)
+        form.add(StringWidget, 'aq-clicrdv-api-password', title=_('Password'),
+                value=misc_cfg.get('aq-clicrdv-api-password'), required=False)
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_widget('cancel').parse():
+            return redirect('..')
+
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('aq/clicrdv', _('ClicRDV Integration')))
+            html_top('settings', _('ClicRDV Integration'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('ClicRDV Integration')
+            r += form.render()
+            r += htmltext('<p>%s</p>') % _('Available Interventions: ')
+            try:
+                from clicrdv import get_all_intervention_sets
+                intervention_sets = get_all_intervention_sets()
+                r += htmltext('<ul>')
+                for s in intervention_sets:
+                    r += htmltext('<li><strong>clicrdv_get_interventions_in_set(%s)</strong> - %s') % (
+                            s['id'], s['name'])
+                    r += htmltext('<ul>')
+                    for n, intervention in s['interventions']:
+                        r += htmltext('<li>%s (id: %s)</li>') % (intervention, n)
+                    r += htmltext('</ul></li>')
+                r += htmltext('</ul>')
+            except Exception, e:
+                r += htmltext('<p>%s (%s)</p>') % (
+                        _('Cannot access to ClicRDV service'), str(e))
+            return r.getvalue()
+        else:
+            from wcs.admin.settings import cfg_submit
+            cfg_submit(form, 'misc', ('aq-clicrdv-server',
+                                      'aq-clicrdv-api-key',
+                                      'aq-clicrdv-api-username',
+                                      'aq-clicrdv-api-password'))
+            return redirect('..')
+
+
+class SettingsDirectory(wcs.admin.settings.SettingsDirectory):
+    def _q_index(self):
+        r = TemplateIO(html=True)
+        r += htmltext(super(SettingsDirectory, self)._q_index())
+        r += htmltext('<div class="splitcontent-right">')
+        r += htmltext('<div class="bo-block">')
+        r += htmltext('<h2>%s</h2>') % _('Extra Options')
+        r += htmltext('<ul>')
+        r += htmltext('<li><a href="aq/announces">%s</a></li>') % _('Announces Options')
+        r += htmltext('<li><a href="aq/permissions">%s</a></li>') % _('Permissions')
+        r += htmltext('<li><a href="aq/event_keywords">%s</a></li>') % _('Event Keywords')
+        r += htmltext('<li><a href="aq/announce_themes">%s</a></li>') % _('Announce Themes')
+        if get_publisher().has_site_option('strongbox'):
+            r += htmltext('<li><a href="aq/strongbox">%s</a></li>') % _('Strongbox Support')
+        if get_publisher().has_site_option('clicrdv'):
+            r += htmltext('<li><a href="aq/clicrdv">%s</a></li>') % _('ClicRDV Integration')
+        if get_publisher().has_site_option('domino'):
+            r += htmltext('<li><a href="aq/domino">%s</a></li>') % _('Abelium Domino Integration')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        if component == 'aq':
+            return PanelDirectory()
+        return super(SettingsDirectory, self)._q_lookup(component)
+
+import categories_admin
diff --git a/auquotidien/modules/agenda.py b/auquotidien/modules/agenda.py
new file mode 100644
index 0000000..6f82c9b
--- /dev/null
+++ b/auquotidien/modules/agenda.py
@@ -0,0 +1,307 @@
+import time
+import datetime
+from sets import Set
+
+from quixote.directory import Directory
+from quixote import get_publisher, get_request, redirect, get_session, get_response
+from quixote.html import htmltext, TemplateIO
+
+from qommon import _
+from qommon import misc, template, errors, get_cfg
+from qommon.form import *
+
+from events import Event, RemoteCalendar, get_default_event_tags
+
+
+class TagDirectory(Directory):
+    def _q_lookup(self, component):
+        events = Event.select()
+        for remote_calendar in RemoteCalendar.select():
+            if remote_calendar.events:
+                events.extend(remote_calendar.events)
+        self.events = [x for x in events if component in (x.keywords or [])]
+        self.events.sort(lambda x,y: cmp(x.date_start, y.date_start))
+        self.tag = component
+        return self.display_events()
+
+    def display_events(self):
+        template.html_top(_('Agenda'))
+        r = TemplateIO(html=True)
+        if len(self.events) > 1:
+            r += htmltext('<p id="nb-events">')
+            r += _('%(nb)d events with %(keyword)s keyword') % {
+                    'nb': len(self.events),
+                    'keyword': self.tag
+            }
+            r += htmltext('</p>')
+
+        if self.events:
+            r += htmltext('<dl id="events">')
+            for ev in self.events:
+                r += htmltext(ev.as_html_dt_dd())
+            r += htmltext('</dl>')
+        else:
+            r += htmltext('<p id="nb-events">')
+            r += _('No event registered with the %s keyword.') % self.tag
+            r += htmltext('</p>')
+        return r.getvalue()
+
+
+class AgendaDirectory(Directory):
+    _q_exports = ['', 'icalendar', 'tag', 'atom', 'filter']
+
+    year = None
+    month = None
+
+    tag = TagDirectory()
+
+    def _q_traverse(self, path):
+        get_response().breadcrumb.append(('agenda/', _('Agenda')))
+        self.year, self.month = time.localtime()[:2]
+        if len(path) >= 1 and path[0].isdigit():
+            self.year, self.month = (None, None)
+            self.year = int(path[0])
+            get_response().breadcrumb.append(('%s/' % self.year, self.year))
+            path = path[1:]
+            if len(path) >= 1 and path[0] in [str(x) for x in range(1, 13)]:
+                self.month = int(path[0])
+                get_response().breadcrumb.append(('%s/' % self.month,
+                            misc.get_month_name(self.month)))
+                path = path[1:]
+            if len(path) == 0:
+                return redirect(get_request().get_path() + '/')
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        if self.month:
+            r = TemplateIO(html=True)
+            r += htmltext(self.display_month_links())
+            r += htmltext(self.display_month())
+            return r.getvalue()
+        else:
+            return redirect('..')
+
+    def display_month(self):
+        template.html_top(_('Agenda'))
+        events = Event.select()
+        remote_cal = get_request().form.get('cal')
+        if remote_cal != 'local':
+            if remote_cal:
+                try:
+                    events = RemoteCalendar.get(remote_cal).events
+                except KeyError:
+                    raise errors.TraversalError()
+                if not events:
+                    events = []
+            else:
+                for remote_calendar in RemoteCalendar.select():
+                    if remote_calendar.events:
+                        events.extend(remote_calendar.events)
+        events = [x for x in events if x.in_month(self.year, self.month)]
+        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
+
+        r = TemplateIO(html=True)
+        if events:
+            if len(events) > 1:
+                r += htmltext('<p id="nb-events">')
+                r += _('%(nb)d events for %(month_name)s %(year)s') % {
+                        'nb': len(events),
+                        'month_name': misc.get_month_name(self.month),
+                        'year': self.year}
+                r += htmltext('</p>')
+
+            r += htmltext('<dl id="events">')
+            for ev in events:
+                r += htmltext(ev.as_html_dt_dd())
+            r += htmltext('</dl>')
+        else:
+            r += htmltext('<p id="nb-events">')
+            r += _('No event registered for the month of %s.') % '%s %s' % (
+                    misc.get_month_name(self.month), self.year)
+            r += htmltext('</p>')
+
+        root_url = get_publisher().get_root_url()
+        r += htmltext('<div id="agenda-subs">')
+        r += htmltext('<p>')
+        r += _('You can subscribe to this calendar:')
+        r += htmltext('</p>')
+        r += htmltext('<ul>')
+        r += htmltext('  <li><a href="%sagenda/icalendar" id="par_ical">%s</a></li>') % (
+                root_url, _('iCalendar'))
+        r += htmltext('  <li><a href="%sagenda/atom" id="par_rss">%s</a></li>') % (
+                root_url, _('Feed'))
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def display_month_links(self):
+        today = datetime.date(*(time.localtime()[:2] + (1,)))
+        r = TemplateIO(html=True)
+        r += htmltext('<ul id="month-links">')
+        for i in range(12):
+            r += htmltext('<li>')
+            if (today.year, today.month) == (self.year, self.month):
+                r += htmltext('<strong>')
+                r += '%s %s' % (misc.get_month_name(today.month), today.year)
+                r += htmltext('</strong>')
+            else:
+                root_url = get_publisher().get_root_url()
+                r += htmltext('<a href="%sagenda/%s/%s/">') % (root_url, today.year, today.month)
+                r += '%s %s' % (misc.get_month_name(today.month), today.year)
+                r += htmltext('</a>')
+            r += htmltext('</li>')
+            today += datetime.timedelta(31)
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def display_remote_calendars(self):
+        r = TemplateIO(html=True)
+        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
+        if not remote_calendars:
+            return
+        remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
+        r += htmltext('<p class="tags">')
+        remote_cal = get_request().form.get('cal')
+        agenda_root_url = get_publisher().get_root_url() + 'agenda/'
+        if remote_cal:
+            r += htmltext('<a href="%s">%s</a> ') % (agenda_root_url, _('All'))
+        else:
+            r += htmltext('<strong><a href="%s">%s</a></strong> ') % (agenda_root_url, _('All'))
+        if remote_cal != 'local':
+            r += htmltext('<a href="%s?cal=local">%s</a> ') % (agenda_root_url, _('Local'))
+        else:
+            r += htmltext('<strong><a href="%s?cal=local">%s</a></strong> ') % (agenda_root_url, _('Local'))
+        for cal in remote_calendars:
+            if remote_cal == str(cal.id):
+                r += htmltext('<strong><a href="%s?cal=%s">%s</a></strong> ') % (
+                        agenda_root_url, cal.id, cal.label)
+            else:
+                r += htmltext('<a href="%s?cal=%s">%s</a> ') % (agenda_root_url, cal.id, cal.label)
+        r += htmltext('</p>')
+        return r.getvalue()
+
+    def icalendar(self):
+        if not Event.keys():
+            raise errors.TraversalError()
+        response = get_response()
+        response.set_content_type('text/calendar', 'utf-8')
+        vcal = Event.as_vcalendar()
+        if type(vcal) is unicode:
+            return vcal.encode('utf-8')
+        else:
+            return vcal
+
+    def atom(self):
+        response = get_response()
+        response.set_content_type('application/atom+xml')
+
+        from pyatom import pyatom
+        xmldoc = pyatom.XMLDoc()
+        feed = pyatom.Feed()
+        xmldoc.root_element = feed
+        feed.title = get_cfg('misc', {}).get('sitename', 'Publik') + ' - ' + _('Agenda')
+        feed.id = get_request().get_url()
+
+        author_email = get_cfg('emails', {}).get('reply_to')
+        if not author_email:
+            author_email = get_cfg('emails', {}).get('from')
+        if author_email:
+            feed.authors.append(pyatom.Author(author_email))
+
+        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
+
+        year, month = time.localtime()[:2]
+        nyear, nmonth = year, month+1
+        if nmonth > 12:
+            nyear, nmonth = nyear+1, 1
+
+        events = [x for x in Event.select() if x.in_month(year, month) or x.in_month(nyear, nmonth)]
+        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
+        events.reverse()
+
+        for item in events:
+            entry = item.get_atom_entry()
+            if entry is not None:
+                feed.entries.append(entry)
+
+        return str(feed)
+
+    def filter(self, no_event=False):
+        template.html_top(_('Agenda'))
+        tags = get_cfg('misc', {}).get('event_tags')
+        if not tags:
+            tags = get_default_event_tags()
+        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
+
+        form = Form(enctype='multipart/form-data')
+        if tags and remote_calendars:
+            form.widgets.append(HtmlWidget('<table id="agenda-filter"><tr><td>'))
+        if tags:
+            form.add(CheckboxesWidget, 'tags', title=_('Tags'),
+                    options=[(x,x) for x in tags],
+                    inline=False)
+        if tags and remote_calendars:
+            form.widgets.append(HtmlWidget('</td><td>'))
+        if remote_calendars:
+            remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
+            form.add(CheckboxesWidget, 'calendars', title=_('Calendars'),
+                    options=[('local', _('Local'))] + [(x.id, x.label) for x in remote_calendars],
+                    inline=False)
+        if tags and remote_calendars:
+            form.widgets.append(HtmlWidget('</td></tr></table>'))
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_widget('cancel').parse():
+            return redirect('.')
+
+        if no_event or not form.is_submitted():
+            r = TemplateIO(html=True)
+            if no_event:
+                r += htmltext('<p id="nb-events">')
+                r += _('No events matching the filter.')
+                r += htmltext('</p>')
+            r += form.render()
+            return r.getvalue()
+        else:
+            return self.filter_submitted(form, tags, remote_calendars)
+
+    def filter_submitted(self, form, tags, remote_calendars):
+        if remote_calendars:
+            selected_remote_calendars = form.get_widget('calendars').parse()
+            events = []
+            for remote_calendar in selected_remote_calendars:
+                if remote_calendar == 'local':
+                    events.extend(Event.select())
+                else:
+                    try:
+                        events.extend(RemoteCalendar.get(remote_calendar).events)
+                    except KeyError:
+                        pass
+        else:
+            events = Event.select()
+
+        events = [x for x in events if x.after_today()]
+
+        if tags:
+            selected_tags = Set(form.get_widget('tags').parse())
+            if selected_tags and len(selected_tags) != len(tags):
+                events = [x for x in events if Set(x.keywords).intersection(selected_tags)]
+
+        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
+
+        r = TemplateIO(html=True)
+
+        if len(events) > 1:
+            r += htmltext('<p id="nb-events">')
+            r += htmltext(_('%(nb)d events')) % {'nb': len(events)}
+            r += htmltext('</p>')
+
+        if events:
+            r += htmltext('<dl id="events">')
+            for ev in events:
+                r += htmltext(ev.as_html_dt_dd())
+            r += htmltext('</dl>')
+            return r.getvalue()
+        else:
+            return self.filter(no_event=True)
diff --git a/auquotidien/modules/announces.py b/auquotidien/modules/announces.py
new file mode 100644
index 0000000..4a1306d
--- /dev/null
+++ b/auquotidien/modules/announces.py
@@ -0,0 +1,182 @@
+import time
+
+from quixote import get_publisher
+
+from quixote.html import htmlescape
+
+from qommon import _
+from qommon.storage import StorableObject
+from qommon import get_cfg, get_logger
+from qommon import errors
+from qommon import misc
+
+from qommon import emails
+from qommon.sms import SMS
+from qommon.admin.emails import EmailsDirectory
+
+class AnnounceSubscription(StorableObject):
+    _names = 'announce-subscriptions'
+    _indexes = ['user_id']
+
+    user_id = None
+    email = None
+    sms = None
+    enabled = True
+    enabled_sms = False
+    enabled_themes = None
+
+    def remove(self, type=None):
+        """ type (string) : email or sms """
+        if type == "email":
+            self.email = None
+        elif type == "sms":
+            self.sms = None
+            self.enabled_sms = False
+        if not type or (not self.sms and not self.email):
+            self.remove_self()
+        else:
+            self.store()
+        
+    def get_user(self):
+        if self.user_id:
+            try:
+                return get_publisher().user_class.get(self.user_id)
+            except KeyError:
+                return None
+        return None
+    user = property(get_user)
+
+
+class Announce(StorableObject):
+    _names = 'announces'
+
+    title = None
+    text = None
+
+    hidden = False
+
+    publication_time = None
+    modification_time = None
+    expiration_time = None
+    sent_by_email_time = None
+    sent_by_sms_time = None
+    theme = None
+
+    position = None
+
+    def sort_by_position(cls, links):
+        def cmp_position(x, y):
+            if x.position == y.position:
+                return 0
+            if x.position is None:
+                return 1
+            if y.position is None:
+                return -1
+            return cmp(x.position, y.position)
+        links.sort(cmp_position)
+    sort_by_position = classmethod(sort_by_position)
+
+    def get_atom_entry(self):
+        from pyatom import pyatom
+        entry = pyatom.Entry()
+        entry.id = self.get_url()
+        entry.title = self.title
+
+        entry.content.attrs['type'] = 'html'
+        entry.content.text = str('<p>' + htmlescape(
+                    unicode(self.text, get_publisher().site_charset).encode('utf-8')) + '</p>')
+
+        link = pyatom.Link(self.get_url())
+        entry.links.append(link)
+
+        if self.publication_time:
+            entry.published = misc.format_time(self.publication_time,
+                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
+                    gmtime = True)
+
+        if self.modification_time:
+            entry.updated = misc.format_time(self.modification_time,
+                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
+                    gmtime = True)
+
+        return entry
+
+    def get_url(self):
+        return '%s/announces/%s/' % (get_publisher().get_frontoffice_url(), self.id)
+
+    def store(self):
+        self.modification_time = time.gmtime()
+        StorableObject.store(self)
+
+    def email(self, job=None):
+        self.sent_by_email_time = time.gmtime()
+        StorableObject.store(self)
+
+        data = {
+            'title': self.title,
+            'text': self.text
+        }
+
+        subscribers = AnnounceSubscription.select(lambda x: x.enabled)
+
+        rcpts = []
+        for l in subscribers:
+            if self.theme:
+                if l.enabled_themes is not None:
+                    if self.theme not in l.enabled_themes:
+                        continue
+            if l.user and l.user.email:
+                rcpts.append(l.user.email)
+            elif l.email:
+                rcpts.append(l.email)
+
+        emails.custom_ezt_email('aq-announce', data, email_rcpt = rcpts, hide_recipients = True)
+
+    def sms(self, job=None):
+        self.sent_by_sms_time = time.gmtime()
+        StorableObject.store(self)
+
+        subscribers = AnnounceSubscription.select(lambda x: x.enabled_sms)
+
+        rcpts = []
+        for sub in subscribers:
+            if self.theme:
+                if sub.enabled_themes is not None:
+                    if self.theme not in sub.enabled_themes:
+                        continue
+            if sub.sms:
+                rcpts.append(sub.sms)
+
+        sms_cfg = get_cfg('sms', {})
+        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
+        message = "%s: %s" % (self.title, self.text)
+        mode = sms_cfg.get('mode', 'none')
+        sms = SMS.get_sms_class(mode)
+        try:
+            sms.send(sender, rcpts, message[:160])
+        except errors.SMSError, e:
+            get_logger().error(e)
+
+    def get_published_announces(cls):
+        announces = cls.select(lambda x: not x.hidden)
+        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
+                    y.publication_time or y.modification_time))
+        announces = [x for x in announces if x.publication_time < time.gmtime()
+                     and (x.expiration_time is None or x.expiration_time > time.gmtime())]
+        announces.reverse()
+        return announces
+    get_published_announces = classmethod(get_published_announces)
+
+
+EmailsDirectory.register('aq-announce',
+        N_('Publication of announce to subscriber'),
+        N_('Available variables: title, text'),
+        default_subject = N_('Announce: [title]'),
+        default_body = N_("""\
+[text]
+
+-- 
+This is an announce sent to you by your city, you can opt to not receive
+those messages anymore on the city website.
+"""))
+
diff --git a/auquotidien/modules/announces_ui.py b/auquotidien/modules/announces_ui.py
new file mode 100644
index 0000000..d916a3b
--- /dev/null
+++ b/auquotidien/modules/announces_ui.py
@@ -0,0 +1,459 @@
+from quixote import get_request, get_response, get_session, redirect
+from quixote.directory import Directory, AccessControlled
+from quixote.html import htmltext, TemplateIO
+
+import wcs
+
+from qommon import _
+from qommon.backoffice.menu import html_top
+from qommon.admin.menu import command_icon
+from qommon import get_cfg
+from qommon import errors
+from qommon.form import *
+from qommon.afterjobs import AfterJob
+
+from announces import Announce, AnnounceSubscription
+
+
+class SubscriptionDirectory(Directory):
+    _q_exports = ['delete_email', "delete_sms"]
+
+    def __init__(self, subscription):
+        self.subscription = subscription
+
+    def delete_email(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to delete this subscription.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('announces', title = _('Delete Subscription'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.subscription.remove("email")
+            return redirect('..')
+
+    def delete_sms(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to delete this subscription.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('announces', title = _('Delete Subscription'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.subscription.remove("sms")
+            return redirect('..')
+
+
+class SubscriptionsDirectory(Directory):
+    _q_exports = ['']
+
+    def _q_traverse(self, path):
+        get_response().breadcrumb.append(('subscriptions', _('Subscriptions')))
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        html_top('announces', _('Announces Subscribers'))
+        r = TemplateIO(html=True)
+
+        r += htmltext('<h2>%s</h2>') % _('Announces Subscribers')
+
+        subscribers = AnnounceSubscription.select()
+        r += htmltext('<ul class="biglist" id="subscribers-list">')
+        for l in subscribers:
+            if l.email:
+                if l.enabled is False:
+                    r += htmltext('<li class="disabled">')
+                else:
+                    r += htmltext('<li>')
+                r += htmltext('<strong class="label">')
+                if l.user:
+                    r += l.user.display_name
+                elif l.email:
+                    r += l.email
+                r += htmltext('</strong>')
+                r += htmltext('<p class="details">')
+                if l.user:
+                    r += l.user.email
+                r += htmltext('</p>')
+                r += htmltext('<p class="commands">')
+                r += command_icon('%s/delete_email' % l.id, 'remove', popup = True)
+                r += htmltext('</p></li>')
+                r += htmltext('</li>')
+            if l.sms:
+                if l.enabled_sms is False:
+                    r += htmltext('<li class="disabled">')
+                else:
+                    r += htmltext('<li>')
+                r += htmltext('<strong class="label">')
+                if l.user:
+                    r += l.user.display_name
+                elif l.email:
+                    r += l.email
+                r += htmltext('</strong>')
+                r += htmltext('<p class="details">')
+                r += l.sms
+                r += htmltext('</p>')
+                r += htmltext('<p class="commands">')
+                r += command_icon('%s/delete_sms' % l.id, 'remove', popup = True)
+                r += htmltext('</p></li>')
+                r += htmltext('</li>')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            sub = AnnounceSubscription.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(sub.id), str(sub.id)))
+        return SubscriptionDirectory(sub)
+
+    def listing(self):
+        return redirect('.')
+
+class AnnounceDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete', 'email', 'sms']
+
+    def __init__(self, announce):
+        self.announce = announce
+
+    def _q_index(self):
+        form = Form(enctype='multipart/form-data')
+        get_response().filter['sidebar'] = self.get_sidebar()
+
+        if self.announce.sent_by_email_time is None:
+            form.add_submit('email', _('Send email'))
+
+        announces_cfg = get_cfg('announces', {})
+        if announces_cfg.get('sms_support', 0) and self.announce.sent_by_sms_time is None:
+            form.add_submit('sms', _('Send SMS'))
+
+        if form.get_submit() == 'edit':
+            return redirect('edit')
+        if form.get_submit() == 'delete':
+            return redirect('delete')
+        if form.get_submit() == 'email':
+            return redirect('email')
+        if form.get_submit() == 'sms':
+            return redirect('sms')
+
+        html_top('announces', title = _('Announce: %s') % self.announce.title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Announce: %s') % self.announce.title
+        r += htmltext('<div class="bo-block">')
+        r += htmltext('<p>')
+        r += self.announce.text
+        r += htmltext('</p>')
+        r += htmltext('</div>')
+
+        if form.get_submit_widgets():
+            r += form.render()
+
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul>')
+        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
+        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def email(self):
+        if get_request().form.get('job'):
+            try:
+                job = AfterJob.get(get_request().form.get('job'))
+            except KeyError:
+                return redirect('..')
+            html_top('announces', title = _('Announce: %s') % self.announce.title)
+            r = TemplateIO(html=True)
+            get_response().add_javascript(['jquery.js', 'afterjob.js'])
+            r += htmltext('<dl class="job-status">')
+            r += htmltext('<dt>')
+            r += _(job.label)
+            r += htmltext('</dt>')
+            r += htmltext('<dd>')
+            r += htmltext('<span class="afterjob" id="%s">') % job.id
+            r += _(job.status)
+            r += htmltext('</span>')
+            r += htmltext('</dd>')
+            r += htmltext('</dl>')
+
+            r += htmltext('<div class="done">')
+            r += htmltext('<a href="../">%s</a>') % _('Back')
+            r += htmltext('</div>')
+
+            return r.getvalue()
+        else:
+            job = get_response().add_after_job(
+                    str(N_('Sending emails for announce')),
+                    self.announce.email)
+            return redirect('email?job=%s' % job.id)
+
+    def sms(self):
+        if get_request().form.get('job'):
+            try:
+                job = AfterJob.get(get_request().form.get('job'))
+            except KeyError:
+                return redirect('..')
+            html_top('announces', title = _('Announce: %s') % self.announce.title)
+            get_response().add_javascript(['jquery.js', 'afterjob.js'])
+            r = TemplateIO(html=True)
+            r += htmltext('<dl class="job-status">')
+            r += htmltext('<dt>')
+            r += _(job.label)
+            r += htmltext('</dt>')
+            r += htmltext('<dd>')
+            r += htmltext('<span class="afterjob" id="%s">') % job.id
+            r += _(job.status)
+            r += htmltext('</span>')
+            r += htmltext('</dd>')
+            r += htmltext('</dl>')
+
+            r += htmltext('<div class="done">')
+            r += htmltext('<a href="../">%s</a>') % _('Back')
+            r += htmltext('</div>')
+
+            return r.getvalue()
+        else:
+            job = get_response().add_after_job(
+                    str(N_('Sending sms for announce')),
+                    self.announce.sms)
+            return redirect('sms?job=%s' % job.id)
+
+    def edit(self):
+        form = self.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit(form)
+            return redirect('..')
+
+        html_top('announces', title = _('Edit Announce: %s') % self.announce.title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Edit Announce: %s') % self.announce.title
+        r += form.render()
+        return r.getvalue()
+
+    def form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'title', title = _('Title'), required = True,
+                value = self.announce.title)
+        if self.announce.publication_time:
+            pub_time = time.strftime(misc.date_format(), self.announce.publication_time)
+        else:
+            pub_time = None
+        form.add(DateWidget, 'publication_time', title = _('Publication Time'),
+                value = pub_time)
+        if self.announce.expiration_time:
+            exp_time = time.strftime(misc.date_format(), self.announce.expiration_time)
+        else:
+            exp_time = None
+        form.add(DateWidget, 'expiration_time', title = _('Expiration Time'),
+                value = exp_time)
+        form.add(TextWidget, 'text', title = _('Text'), required = True,
+                value = self.announce.text, rows = 10, cols = 70)
+        if get_cfg('misc', {}).get('announce_themes'):
+            form.add(SingleSelectWidget, 'theme', title = _('Announce Theme'),
+                    value = self.announce.theme,
+                    options = get_cfg('misc', {}).get('announce_themes'))
+        form.add(CheckboxWidget, 'hidden', title = _('Hidden'),
+                value = self.announce.hidden)
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit(self, form):
+        for k in ('title', 'text', 'hidden', 'theme'):
+            widget = form.get_widget(k)
+            if widget:
+                setattr(self.announce, k, widget.parse())
+        for k in ('publication_time', 'expiration_time'):
+            widget = form.get_widget(k)
+            if widget:
+                wid_time = widget.parse()
+                if wid_time:
+                    setattr(self.announce, k, time.strptime(wid_time, misc.date_format()))
+                else:
+                    setattr(self.announce, k, None)
+        self.announce.store()
+
+    def delete(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this announce.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('announces', title = _('Delete Announce'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Announce: %s') % self.announce.title
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.announce.remove_self()
+            return redirect('..')
+
+
+class AnnouncesDirectory(AccessControlled, Directory):
+    _q_exports = ['', 'new', 'listing', 'subscriptions', 'update_order', 'log']
+    label = N_('Announces')
+
+    subscriptions = SubscriptionsDirectory()
+
+    def is_accessible(self, user):
+        from .backoffice import check_visibility
+        return check_visibility('announces', user)
+
+    def _q_access(self):
+        user = get_request().user
+        if not user:
+            raise errors.AccessUnauthorizedError()
+
+        if not self.is_accessible(user):
+            raise errors.AccessForbiddenError(
+                    public_msg = _('You are not allowed to access Announces Management'),
+                    location_hint = 'backoffice')
+
+        get_response().breadcrumb.append(('announces/', _('Announces')))
+
+    def _q_index(self):
+        html_top('announces', _('Announces'))
+        r = TemplateIO(html=True)
+
+        get_response().filter['sidebar'] = self.get_sidebar()
+
+        announces = Announce.select()
+        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
+                    y.publication_time or y.modification_time))
+        announces.reverse()
+
+        r += htmltext('<ul class="biglist" id="announces-list">')
+        for l in announces:
+            announce_id = l.id
+            if l.hidden:
+                r += htmltext('<li class="disabled" class="biglistitem" id="itemId_%s">') % announce_id
+            else:
+                r += htmltext('<li class="biglistitem" id="itemId_%s">') % announce_id
+            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (l.id, l.title)
+            if l.publication_time:
+                r += htmltext('<p class="details">')
+                r += time.strftime(misc.date_format(), l.publication_time)
+                r += htmltext('</p>')
+            r += htmltext('</li>')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul id="sidebar-actions">')
+        r += htmltext(' <li><a class="new-item" href="new">%s</a></li>') % _('New')
+        r += htmltext(' <li><a href="subscriptions/">%s</a></li>') % _('Subscriptions')
+        r += htmltext(' <li><a href="log">%s</a></li>') % _('Log')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def log(self):
+        announces = Announce.select()
+        log = []
+        for l in announces:
+            if l.publication_time:
+                log.append((l.publication_time, _('Publication'), l))
+            if l.sent_by_email_time:
+                log.append((l.sent_by_email_time, _('Email'), l))
+            if l.sent_by_sms_time:
+                log.append((l.sent_by_sms_time, _('SMS'), l))
+        log.sort()
+
+        get_response().breadcrumb.append(('log', _('Log')))
+        html_top('announces', title = _('Log'))
+        r = TemplateIO(html=True)
+
+        r += htmltext('<table>')
+        r += htmltext('<thead>')
+        r += htmltext('<tr>')
+        r += htmltext('<th>%s</th>') % _('Time')
+        r += htmltext('<th>%s</th>') % _('Type')
+        r += htmltext('<td></td>')
+        r += htmltext('</tr>')
+        r += htmltext('</thead>')
+        r += htmltext('<tbody>')
+        for log_time, log_type, log_announce in log:
+            r += htmltext('<tr>')
+            r += htmltext('<td>')
+            r += misc.localstrftime(log_time)
+            r += htmltext('</td>')
+            r += htmltext('<td>')
+            r += log_type
+            r += htmltext('</td>')
+            r += htmltext('<td>')
+            r += htmltext('<a href="%s">%s</a>') % (log_announce.id, log_announce.title)
+            r += htmltext('</td>')
+            r += htmltext('</tr>')
+        r += htmltext('</tbody>')
+        r += htmltext('</table>')
+        return r.getvalue()
+
+    def update_order(self):
+        request = get_request()
+        new_order = request.form['order'].strip(';').split(';')
+        announces = Announce.select()
+        dict = {}
+        for l in announces:
+            dict[str(l.id)] = l
+        for i, o in enumerate(new_order):
+            dict[o].position = i + 1
+            dict[o].store()
+        return 'ok'
+
+    def new(self):
+        announce = Announce()
+        announce.publication_time = time.gmtime()
+        announce_ui = AnnounceDirectory(announce)
+
+        form = announce_ui.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            announce_ui.submit(form)
+            return redirect('%s/' % announce_ui.announce.id)
+
+        get_response().breadcrumb.append(('new', _('New Announce')))
+        html_top('announces', title = _('New Announce'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Announce')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            announce = Announce.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(announce.id), announce.title))
+        return AnnounceDirectory(announce)
+
+    def listing(self):
+        return redirect('.')
+
diff --git a/auquotidien/modules/backoffice.py b/auquotidien/modules/backoffice.py
new file mode 100644
index 0000000..0b41830
--- /dev/null
+++ b/auquotidien/modules/backoffice.py
@@ -0,0 +1,55 @@
+import os
+
+from quixote import get_publisher, redirect
+from quixote.directory import Directory
+from quixote.html import TemplateIO, htmltext
+
+from qommon import _
+from qommon.publisher import get_publisher_class
+
+import wcs.backoffice.management
+import wcs.root
+from wcs.categories import Category
+from wcs.formdef import FormDef
+
+from qommon import get_cfg, errors
+from qommon.form import *
+
+CURRENT_USER = object()
+
+def check_visibility(target, user=CURRENT_USER):
+    if user is CURRENT_USER:
+        user = get_request().user
+    if not user:
+        return False
+    target = target.strip('/')
+    if target == 'management':
+        target = 'forms'
+    if target == 'strongbox':
+        if not get_publisher().has_site_option(target):
+            # strongbox disabled in site-options.cfg
+            return False
+        if not get_cfg('misc', {}).get('aq-strongbox'):
+            # strongbox disabled in settings panel
+            return False
+    admin_role = get_cfg('aq-permissions', {}).get(target, None)
+    if not admin_role:
+        return False
+    if not (user.is_admin or admin_role in (user.roles or [])):
+        return False
+    return True
+
+
+class BackofficeRootDirectory(wcs.backoffice.root.RootDirectory):
+    def get_intro_text(self):
+        return _('Welcome on Publik back office interface')
+
+    def _q_index(self):
+        if len(self.get_menu_items()) == 1:
+            return redirect(self.get_menu_items()[0]['url'])
+        return wcs.backoffice.root.RootDirectory._q_index(self)
+
+    def home(self):
+        return redirect('management/')
+
+get_publisher_class().backoffice_directory_class = BackofficeRootDirectory
diff --git a/auquotidien/modules/categories_admin.py b/auquotidien/modules/categories_admin.py
new file mode 100644
index 0000000..8fc3aa5
--- /dev/null
+++ b/auquotidien/modules/categories_admin.py
@@ -0,0 +1,123 @@
+# w.c.s. - web application for online forms
+# Copyright (C) 2005-2010  Entr'ouvert
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+from quixote import redirect
+from quixote.directory import Directory
+from quixote.html import TemplateIO, htmltext
+
+from qommon import _
+from qommon import misc
+from wcs.categories import Category
+from qommon.form import *
+from qommon.backoffice.menu import html_top
+from qommon.admin.menu import command_icon, error_page
+import wcs.admin.categories
+
+class CategoryUI:
+    def __init__(self, category):
+        self.category = category
+        if self.category is None:
+            self.category = Category()
+
+    def get_form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'name', title = _('Category Name'), required = True, size=30,
+                value = self.category.name)
+        form.add(TextWidget, 'description', title = _('Description'), cols = 80, rows = 10,
+                value = self.category.description)
+
+        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
+        if not homepage_redirect_url:
+            form.add(SingleSelectWidget, 'homepage_position',
+                        title=_('Position on homepage'),
+                        value=self.category.get_homepage_position(),
+                        options = [('1st', _('First Column')),
+                                   ('2nd', _('Second Column')),
+                                   ('side', _('Sidebar')),
+                                   ('none', _('None'))])
+            form.add(IntWidget, 'limit',
+                        title=_('Limit number of items displayed on homepage'),
+                        value=self.category.get_limit())
+
+        form.add(StringWidget, 'redirect_url', size=32,
+                title=_('URL Redirection'),
+                hint=_('If set, redirect the site category page to the given URL.'),
+                value=self.category.redirect_url)
+
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit_form(self, form):
+        if self.category.id:
+            self.category.name = form.get_widget('name').parse()
+            category = self.category
+        else:
+            category = Category(name = form.get_widget('name').parse())
+
+        name = form.get_widget('name').parse()
+        category_names = [x.name for x in Category.select() if x.id != category.id]
+        if name in category_names:
+            form.get_widget('name').set_error(_('This name is already used'))
+            raise ValueError()
+
+        category.description = form.get_widget('description').parse()
+        category.redirect_url = form.get_widget('redirect_url').parse()
+
+        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
+        if not homepage_redirect_url:
+            category.homepage_position = form.get_widget('homepage_position').parse()
+            category.limit = form.get_widget('limit').parse()
+
+        category.store()
+
+
+
+class CategoryPage(wcs.admin.categories.CategoryPage):
+    def __init__(self, component):
+        self.category = Category.get(component)
+        self.category_ui = CategoryUI(self.category)
+        get_response().breadcrumb.append((component + '/', self.category.name))
+
+
+class CategoriesDirectory(wcs.admin.categories.CategoriesDirectory):
+    label = N_('Categories')
+
+    def new(self):
+        get_response().breadcrumb.append( ('categories/', _('Categories')) )
+        get_response().breadcrumb.append( ('new', _('New')) )
+        category_ui = CategoryUI(None)
+        form = category_ui.get_form()
+        if form.get_widget('cancel').parse():
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            try:
+                category_ui.submit_form(form)
+            except ValueError:
+                pass
+            else:
+                return redirect('.')
+
+        html_top('categories', title = _('New Category'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Category')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        get_response().breadcrumb.append( ('categories/', _('Categories')) )
+        return CategoryPage(component)
diff --git a/auquotidien/modules/clicrdv.py b/auquotidien/modules/clicrdv.py
new file mode 100644
index 0000000..3beae71
--- /dev/null
+++ b/auquotidien/modules/clicrdv.py
@@ -0,0 +1,365 @@
+import base64
+import datetime
+import urllib2
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+import time
+import vobject
+
+from qommon import _
+from qommon import get_cfg
+from qommon.misc import format_time
+from qommon.form import *
+
+from wcs.data_sources import register_data_source_function
+from wcs.formdata import Evolution
+from wcs.forms.common import FormStatusPage
+from wcs.workflows import Workflow, WorkflowStatusItem, register_item_class
+
+def get_clicrdv_req(url):
+    misc_cfg = get_cfg('misc', {})
+
+    url = 'https://%s/api/v1/%s' % (
+                misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), url)
+    if '?' in url:
+       url = url + '&apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
+    else:
+       url = url + '?apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
+
+    req = urllib2.Request(url)
+    username = misc_cfg.get('aq-clicrdv-api-username')
+    password = misc_cfg.get('aq-clicrdv-api-password')
+    authheader = 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
+    req.add_header('Authorization', authheader)
+    return req
+
+def get_json(url):
+    return json.load(urllib2.urlopen(get_clicrdv_req(url)))
+
+def as_str(s):
+    if type(s) is unicode:
+        return s.encode(get_publisher().site_charset)
+    return s
+
+def get_all_intervention_sets():
+    interventions_set = []
+    for interventionset in sorted(get_json('interventionsets').get('records'), 
+            lambda x,y: cmp(x['sort'],y['sort'])):
+        interventions = []
+        for intervention in sorted(get_json('interventionsets/%s/interventions' % interventionset.get('id')).get('records'),
+                lambda x,y: cmp(x['sort'], y['sort'])):
+            if intervention.get('deleted') == True:
+                continue
+            name = '%s' % as_str(intervention.get('publicname'))
+            if not name:
+                name = '%s' % as_str(intervention.get('name'))
+            interventions.append((intervention.get('id'), as_str(name)))
+        interventions_set.append({
+                'id': interventionset.get('id'),
+                'group_id': interventionset.get('group_id'),
+                'name': as_str(interventionset.get('name')),
+                'publicname': as_str(interventionset.get('publicname')) or '',
+                'description': as_str(interventionset.get('description')) or '',
+                'interventions': interventions
+                })
+    return interventions_set
+
+def get_all_interventions():
+    interventions = []
+    for s in get_all_intervention_sets():
+        for i, publicname in s['interventions']:
+            intervention_label = '%s - %s' % (s['publicname'], publicname)
+            interventions.append((i, as_str(intervention_label)))
+    return interventions
+
+def get_interventions_in_set(interventionset_id):
+    interventions = []
+    interventions_json = get_json('interventionsets/%s/interventions' % interventionset_id)
+    for intervention in interventions_json.get('records'):
+        if intervention.get('deleted') != True:
+            name = '%s' % as_str(intervention.get('publicname'))
+            if not name:
+                name = '%s' % as_str(intervention.get('name'))
+            interventions.append((intervention.get('id'), name))
+    return interventions
+
+def get_available_timeslots(intervention, date_start=None, date_end=None):
+    timeslots = []
+    iid = intervention
+    gid = get_json('interventions/%s' % iid).get('group_id')
+    request_url = 'availabletimeslots?intervention_ids[]=%s&group_id=%s' % (iid, gid)
+    if date_start is None:
+        date_start = datetime.datetime.today().strftime('%Y-%m-%d')
+    if date_end is None:
+        date_end = (datetime.datetime.today() + datetime.timedelta(366)).strftime('%Y-%m-%d')
+    if date_start:
+        request_url = request_url + '&start=%s' % urllib2.quote(date_start)
+    if date_end:
+        request_url = request_url + '&end=%s' % urllib2.quote(date_end)
+    for timeslot in get_json(request_url).get('availabletimeslots'):
+        timeslots.append(timeslot.get('start'))
+    timeslots.sort()
+    return timeslots
+
+def get_available_dates(intervention):
+    dates = []
+    for timeslot in get_available_timeslots(intervention):
+        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
+        date_tuple = (time.strftime('%Y-%m-%d', parsed),
+                      format_time(parsed, '%(weekday_name)s %(day)0.2d/%(month)0.2d/%(year)s'))
+        if date_tuple in dates:
+            continue
+        dates.append(date_tuple)
+    return dates
+
+def get_available_times(intervention, date):
+    times = []
+    timeslots = get_available_timeslots(intervention,
+                    date_start='%s 00:00:00' % date,
+                    date_end='%s 23:59:59' % date)
+    for timeslot in timeslots:
+        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
+        time_tuple = (time.strftime('%H:%M:%S', parsed),
+                      time.strftime('%Hh%M', parsed))
+        times.append(time_tuple)
+    times.sort()
+    return times
+
+register_data_source_function(get_all_interventions, 'clicrdv_get_all_interventions')
+register_data_source_function(get_interventions_in_set, 'clicrdv_get_interventions_in_set')
+register_data_source_function(get_available_dates, 'clicrdv_get_available_dates')
+register_data_source_function(get_available_times, 'clicrdv_get_available_times')
+
+def form_download_event(self):
+    self.check_receiver()
+
+    found = False
+    for evo in self.filled.evolution:
+        if evo.parts:
+            for p in evo.parts:
+                if not isinstance(p, AppointmentPart):
+                    continue
+                cal = vobject.iCalendar()
+                cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
+                vevent = vobject.newFromBehavior('vevent')
+                vevent.add('uid').value = 'clicrdv-%s' % p.id
+                vevent.add('summary').value = p.json_dict.get('group_name')
+                vevent.add('dtstart').value = datetime.datetime.strptime(
+                                p.json_dict.get('start'), '%Y-%m-%d %H:%M:%S')
+                vevent.add('dtend').value = datetime.datetime.strptime(
+                                p.json_dict.get('end'), '%Y-%m-%d %H:%M:%S')
+                vevent.add('location').value = p.json_dict.get('location')
+                cal.add(vevent)
+
+                response = get_response()
+                response.set_content_type('text/calendar')
+                return cal.serialize()
+
+    raise TraversalError()
+
+
+class AppointmentPart(object):
+    def __init__(self, json_dict):
+        self.id = json_dict.get('id')
+        self.json_dict = json_dict
+
+    def view(self):
+        return htmltext('<p class="appointment"><a href="clicrdvevent">%s</a></p>' % (
+                                _('Download Appointment')))
+
+
+class AppointmentErrorPart(object):
+    def __init__(self, msg):
+        self.msg = msg
+
+    def view(self):
+        return htmltext('<p class="appointment-error">%s</p>' % str(self.msg))
+
+
+class ClicRdvCreateAppointment(WorkflowStatusItem):
+    description = N_('Create a ClicRDV Appointment')
+    key = 'clicrdv-create'
+    category = ('aq-clicrdv', N_('ClicRDV'))
+
+    endpoint = False
+
+    var_firstname = None
+    var_lastname = None
+    var_email = None
+    var_firstphone = None
+    var_secondphone = None
+    var_datetime = None
+    var_intervention_id = None
+    status_on_success = None
+    status_on_failure = None
+
+    def init(cls):
+        FormStatusPage._q_extra_exports.append('clicrdvevent')
+        FormStatusPage.clicrdvevent = form_download_event
+    init = classmethod(init)
+
+    def is_available(self, workflow=None):
+        return get_publisher().has_site_option('clicrdv')
+    is_available = classmethod(is_available)
+
+    def render_as_line(self):
+        return _('Create an appointment in ClicRDV')
+
+    def get_parameters(self):
+        return ('var_firstname', 'var_lastname', 'var_email', 'var_firstphone',
+                'var_secondphone', 'var_datetime', 'var_intervention_id',
+                'status_on_success', 'status_on_failure')
+
+    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
+        parameter_labels = {
+            'var_firstname': N_('First Name'),
+            'var_lastname': N_('Last Name'),
+            'var_email': N_('Email'),
+            'var_firstphone': N_('Phone (1st)'),
+            'var_secondphone':  N_('Phone (2nd)'),
+            'var_datetime': N_('Date/time'),
+            'var_intervention_id': N_('Intervention Id'),
+        }
+        for parameter in self.get_parameters():
+            if not parameter in parameter_labels:
+                continue
+            if parameter in parameters:
+                form.add(StringWidget, '%s%s' % (prefix, parameter),
+                         title=_(parameter_labels.get(parameter)),
+                         value=getattr(self, parameter),
+                         required=False)
+        if 'status_on_success' in parameters:
+            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
+                    title=_('Status On Success'), value=self.status_on_success,
+                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
+        if 'status_on_failure' in parameters:
+            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
+                    title=_('Status On Failure'), value=self.status_on_failure,
+                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
+
+    def perform(self, formdata):
+        args = {}
+        for parameter in self.get_parameters():
+            args[parameter] = self.compute(getattr(self, parameter))
+            if not args.get(parameter):
+                del args[parameter]
+        message = {'appointment':
+                      {'fiche': {'firstname': args.get('var_firstname', '-'),
+                                 'lastname': args.get('var_lastname', '-'),
+                                 'email': args.get('var_email'),
+                                 'firstphone': args.get('var_firstphone'),
+                                 'secondphone': args.get('var_secondphone'),
+                                },
+                       'date': args.get('var_datetime'),
+                       'intervention_ids': [int(args.get('var_intervention_id'))],
+                       # 'comments': '-',
+                       'websource': 'Publik'}
+                  }
+
+        req = get_clicrdv_req('appointments')
+        req.add_data(json.dumps(message))
+        req.add_header('Content-Type', 'application/json')
+
+        try:
+            fd = urllib2.urlopen(req)
+        except urllib2.HTTPError, e:
+            success = False
+            try:
+                msg = json.load(e.fp)[0].get('error')
+            except:
+                msg = _('unknown error')
+
+            if formdata.evolution:
+                evo = formdata.evolution[-1]
+            else:
+                formdata.evolution = []
+                evo = Evolution()
+                evo.time = time.localtime()
+                evo.status = formdata.status
+                formdata.evolution.append(evo)
+            evo.add_part(AppointmentErrorPart(msg))
+        else:
+            success = True
+            response = json.load(fd)
+            appointment_id = response.get('records')[0].get('id')
+
+            # add a message in formdata.evolution
+            if formdata.evolution:
+                evo = formdata.evolution[-1]
+            else:
+                formdata.evolution = []
+                evo = Evolution()
+                evo.time = time.localtime()
+                evo.status = formdata.status
+                formdata.evolution.append(evo)
+            evo.add_part(AppointmentPart(response.get('records')[0]))
+
+        formdata.store()
+
+        if (success and self.status_on_success) or (success is False and self.status_on_failure):
+            if success:
+                formdata.status = 'wf-%s' % self.status_on_success
+            else:
+                formdata.status = 'wf-%s' % self.status_on_failure
+
+register_item_class(ClicRdvCreateAppointment)
+
+
+class ClicRdvCancelAppointment(WorkflowStatusItem):
+    description = N_('Cancel a ClicRDV Appointment')
+    key = 'clicrdv-cancel'
+    category = ('aq-clicrdv', N_('ClicRDV'))
+
+    endpoint = False
+
+    status_on_success = None
+    status_on_failure = None
+
+    def get_parameters(self):
+        return ('status_on_success', 'status_on_failure')
+
+    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
+        if 'status_on_success' in parameters:
+            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
+                    title=_('Status On Success'), value=self.status_on_success,
+                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
+        if 'status_on_failure' in parameters:
+            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
+                    title=_('Status On Failure'), value=self.status_on_failure,
+                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
+
+    def is_available(self, workflow=None):
+        return get_publisher().has_site_option('clicrdv')
+    is_available = classmethod(is_available)
+
+    def render_as_line(self):
+        return _('Cancel an appointment in ClicRDV')
+
+    def perform(self, formdata):
+        success = True
+
+        for evo in [evo for evo in formdata.evolution if evo.parts]:
+            for part in [part for part in evo.parts if isinstance(part, AppointmentPart)]:
+                appointment_id = part.id
+                try:
+                    req = get_clicrdv_req('appointments/%s' % appointment_id)
+                    req.get_method = (lambda: 'DELETE')
+                    fd = urllib2.urlopen(req)
+                    none = fd.read()
+                except urllib2.URLError:
+                    # clicrdv will return a "Bad Request" (HTTP 400) response
+                    # when it's not possible to remove an appointment
+                    # (for example because it's too late)
+                    success = False
+
+        if (success and self.status_on_success) or (success is False and self.status_on_failure):
+            if success:
+                formdata.status = 'wf-%s' % self.status_on_success
+            else:
+                formdata.status = 'wf-%s' % self.status_on_failure
+
+register_item_class(ClicRdvCancelAppointment)
diff --git a/auquotidien/modules/connectors.py b/auquotidien/modules/connectors.py
new file mode 100644
index 0000000..271312e
--- /dev/null
+++ b/auquotidien/modules/connectors.py
@@ -0,0 +1,2 @@
+import clicrdv
+import abelium_domino_workflow
diff --git a/auquotidien/modules/events.py b/auquotidien/modules/events.py
new file mode 100644
index 0000000..930707d
--- /dev/null
+++ b/auquotidien/modules/events.py
@@ -0,0 +1,238 @@
+import time
+import datetime
+import urllib2
+import vobject
+
+from quixote import get_request, get_publisher, get_response
+from quixote.html import htmltext, TemplateIO, htmlescape
+
+from qommon import _
+from qommon.publisher import get_publisher_class
+from qommon.storage import StorableObject
+from qommon.cron import CronJob
+from qommon import misc
+
+class Event(StorableObject):
+    _names = 'events'
+
+    title = None
+    description = None
+    url = None
+    date_start = None
+    date_end = None
+    location = None
+    organizer = None
+    more_infos = None
+    keywords = None
+
+    def in_month(self, year, month):
+        if not self.date_end: # a single date
+            return tuple(self.date_start[:2]) == (year, month)
+        else:
+            # an interval
+            if tuple(self.date_start[:2]) > (year, month): # start later
+                return False
+            if tuple(self.date_end[:2]) < (year, month): # ended before
+                return False
+        return True
+
+    def after_today(self):
+        today = time.localtime()[:3]
+        if not self.date_end:
+            return tuple(self.date_start[:3]) > today
+        return tuple(self.date_end[:3]) > today
+
+    def format_date(self):
+        d = {
+            'year_start': self.date_start[0],
+            'month_start': misc.get_month_name(self.date_start[1]),
+            'day_start': self.date_start[2]
+            }
+        if self.date_end and self.date_start[:3] != self.date_end[:3]:
+            d.update({
+                'year_end': self.date_end[0],
+                'month_end': misc.get_month_name(self.date_end[1]),
+                'day_end': self.date_end[2]
+                })
+            d2 = datetime.date(*self.date_start[:3]) + datetime.timedelta(days=1)
+            if tuple(self.date_end[:3]) == (d2.year, d2.month, d2.day):
+                # two consecutive days
+                if self.date_start[1] == self.date_end[1]:
+                    return _('On %(month_start)s %(day_start)s and %(day_end)s') % d
+                else:
+                    return _('On %(month_start)s %(day_start)s and %(month_end)s %(day_end)s') % d
+            else:
+                if self.date_start[0] == self.date_end[0]: # same year
+                    if self.date_start[1] == self.date_end[1]: # same month
+                        return _('From %(month_start)s %(day_start)s to %(day_end)s') % d
+                    else:
+                        return _('From %(month_start)s %(day_start)s '
+                                 'to %(month_end)s %(day_end)s') % d
+                else:
+                    return _('From %(month_start)s %(day_start)s %(year_start)s '
+                             'to %(month_end)s %(day_end)s %(year_end)s') % d
+        else:
+            return _('On %(month_start)s %(day_start)s') % d
+
+    def as_vevent(self):
+        vevent = vobject.newFromBehavior('vevent')
+        site_charset = get_publisher().site_charset
+        vevent.add('uid').value = '%04d%02d%02d-%s@%s' % (self.date_start[:3] + (self.id,
+            get_request().get_server().lower().split(':')[0].rstrip('.')))
+        vevent.add('summary').value = unicode(self.title, site_charset)
+        vevent.add('dtstart').value = datetime.date(*self.date_start[:3])
+        vevent.dtstart.value_param = 'DATE'
+        if self.date_end:
+            vevent.add('dtend').value = datetime.date(*self.date_end[:3])
+            vevent.dtend.value_param = 'DATE'
+        if self.description:
+            vevent.add('description').value = unicode(self.description.strip(), site_charset)
+        if self.url:
+            vevent.add('url').value = unicode(self.url, site_charset)
+        if self.location:
+            vevent.add('location').value = unicode(self.location, site_charset)
+        if self.organizer:
+            vevent.add('organizer').value = unicode(self.organizer, site_charset)
+        if self.keywords:
+            vevent.add('categories').value = [unicode(x, site_charset) for x in self.keywords]
+        vevent.add('class').value = 'PUBLIC'
+        return vevent
+
+    def as_vcalendar(cls):
+        cal = vobject.iCalendar()
+        cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
+        for x in cls.select():
+            cal.add(x.as_vevent())
+        return cal.serialize()
+    as_vcalendar = classmethod(as_vcalendar)
+    
+    def as_html_dt_dd(self):
+        root_url = get_publisher().get_root_url()
+        r = TemplateIO(html=True)
+        r += htmltext('<dt>')
+        r += self.format_date()
+        r += htmltext('</dt>')
+        r += htmltext('<p><dd><strong>%s</strong>') % self.title
+        if self.description:
+            r += ' - ' + self.description
+        r += htmltext('</p>')
+        if (self.location or self.organizer or self.more_infos or self.keywords):
+            r += htmltext('<ul>')
+            if self.location:
+                r += htmltext('<li>%s: %s</li>') % (_('Location'), self.location)
+            if self.organizer:
+                r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.organizer)
+            if self.more_infos:
+                r += htmltext('<li>%s</li>') % self.more_infos
+            if self.keywords:
+                r += htmltext('<li>')
+                for k in self.keywords:
+                    r += htmltext('<a class="tag" href="%sagenda/tag/%s">%s</a> ') % (root_url, k, k)
+                r += htmltext('</li>')
+            r += htmltext('</ul>')
+
+        if self.url:
+            if get_response().iframe_mode:
+                r += htmltext('<a class="external" href="%s" target="_top">%s</a>') % (
+                        self.url, _('More information'))
+            else:
+                r += htmltext('<a class="external" href="%s">%s</a>') % (
+                        self.url, _('More information'))
+        r += htmltext('</dd>')
+        return r.getvalue()
+
+    def get_url(self):
+        return '%s/agenda/events/%s/' % (get_publisher().get_frontoffice_url(), self.id)
+
+    def get_atom_entry(self):
+        from pyatom import pyatom
+        entry = pyatom.Entry()
+        entry.id = self.get_url()
+        entry.title = self.title
+
+        entry.content.attrs['type'] = 'html'
+        entry.content.text = str('<p>' + htmlescape(
+                    unicode(self.description, get_publisher().site_charset).encode('utf-8')) + '</p>')
+
+        return entry
+
+
+class RemoteCalendar(StorableObject):
+    _names = 'remote_calendars'
+
+    label = None
+    url = None
+    content = None
+    events = None
+    error = None  # (time, string, params)
+
+    def download_and_parse(self, job=None):
+        old_content = self.content
+
+        try:
+            self.content = urllib2.urlopen(self.url).read()
+        except urllib2.HTTPError, e:
+            self.error = (time.localtime(), N_('HTTP Error %s on download'), (e.code,))
+            self.store()
+            return
+        except urllib2.URLError, e:
+            self.error = (time.localtime(), N_('Error on download'), ())
+            self.store()
+            return
+
+        if self.error:
+            self.error = None
+            self.store()
+
+        if self.content == old_content:
+            return
+
+        self.events = []
+        try:
+            parsed_cal = vobject.readOne(self.content)
+        except vobject.base.ParseError:
+            self.error = (time.localtime(), N_('Failed to parse file'), ())
+            self.store()
+            return
+
+        site_charset = get_publisher().site_charset
+        for vevent in parsed_cal.vevent_list:
+            ev = Event()
+            ev.title = vevent.summary.value.encode(site_charset, 'replace')
+            try:
+                ev.url = vevent.url.value.encode(site_charset, 'replace')
+            except AttributeError:
+                pass
+            ev.date_start = vevent.dtstart.value.timetuple()
+            try:
+                ev.date_end = vevent.dtend.value.timetuple()
+            except AttributeError:
+                pass
+            try:
+                ev.description = vevent.description.value.encode(site_charset, 'replace')
+            except AttributeError:
+                pass
+            try:
+                ev.keywords = [x.encode(site_charset) for x in vevent.categories.value]
+            except AttributeError:
+                pass
+            self.events.append(ev)
+        self.store()
+
+        
+    def get_error_message(self):
+        if not self.error:
+            return None
+        return '(%s) %s' % (misc.localstrftime(self.error[0]),
+                _(self.error[1]) % self.error[2])
+
+
+def update_remote_calendars(publisher):
+    for source in RemoteCalendar.select():
+        source.download_and_parse()
+
+def get_default_event_tags():
+    return [_('All Public'), _('Adults'), _('Children'), _('Free')]
+
+get_publisher_class().register_cronjob(CronJob(update_remote_calendars, minutes = [0]))
+
diff --git a/auquotidien/modules/events_ui.py b/auquotidien/modules/events_ui.py
new file mode 100644
index 0000000..664e65c
--- /dev/null
+++ b/auquotidien/modules/events_ui.py
@@ -0,0 +1,418 @@
+import time
+
+from quixote import get_request, get_response, get_session, redirect
+from quixote.directory import Directory, AccessControlled
+from quixote.html import TemplateIO, htmltext
+
+import wcs
+import wcs.admin.root
+
+from qommon import _
+from qommon.backoffice.menu import html_top
+from qommon.admin.menu import command_icon
+from qommon import get_cfg
+from qommon import errors, misc
+from qommon.form import *
+from qommon.strftime import strftime
+
+from events import Event, RemoteCalendar, get_default_event_tags
+
+
+
+class RemoteCalendarDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete', 'update']
+
+    def __init__(self, calendar):
+        self.calendar = calendar
+
+    def _q_index(self):
+        form = Form(enctype='multipart/form-data')
+        form.add_submit('edit', _('Edit'))
+        form.add_submit('delete', _('Delete'))
+        form.add_submit('update', _('Update'))
+        form.add_submit('back', _('Back'))
+
+        if form.get_submit() == 'edit':
+            return redirect('edit')
+        if form.get_submit() == 'update':
+            return redirect('update')
+        if form.get_submit() == 'delete':
+            return redirect('delete')
+        if form.get_submit() == 'back':
+            return redirect('..')
+
+        html_top('events', title = _('Remote Calendar: %s') % self.calendar.label)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Remote Calendar: %s') % self.calendar.label
+
+        r += get_session().display_message()
+
+        r += htmltext('<p>')
+        self.calendar.url
+        if self.calendar.error:
+            r += htmltext(' - <span class="error-message">%s</span>') % self.calendar.get_error_message()
+        r += htmltext('</p>')
+
+        if not self.calendar.content:
+            r += htmltext('<p>')
+            r += _('No content has been retrieved yet.')
+            r += htmltext('</p>')
+        else:
+            r += htmltext('<ul>')
+            for ev in sorted(self.calendar.events, lambda x,y: cmp(x.date_start, y.date_start)):
+                r += htmltext('<li>')
+                if ev.date_start:
+                    r += strftime(misc.date_format(), ev.date_start)
+                if ev.date_end and ev.date_start[:3] != ev.date_end[:3]:
+                    r += ' - '
+                    r += strftime(misc.date_format(), ev.date_start)
+
+                r += ' : '
+                if ev.url:
+                    r += htmltext('<a href="%s">%s</a>') % (ev.url, ev.title)
+                else:
+                    r += ev.title
+                r += htmltext('</li>')
+            r += htmltext('</ul>')
+
+        r += form.render()
+        return r.getvalue()
+
+    def edit(self):
+        form = self.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit(form)
+            return redirect('..')
+
+        html_top('events', title = _('Edit Remote Calendar: %s') % self.calendar.label)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Edit Remote Calendar: %s') % self.calendar.label
+        r += form.render()
+        return r.getvalue()
+
+    def form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'label', title = _('Label'), required = True,
+                value = self.calendar.label)
+        form.add(StringWidget, 'url', title = _('URL'), required = True,
+                value = self.calendar.url, size = 40)
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit(self, form):
+        for k in ('label', 'url'):
+            widget = form.get_widget(k)
+            if widget:
+                setattr(self.calendar, k, widget.parse())
+        self.calendar.store()
+
+    def delete(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this remote calendar.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('events', title = _('Delete Remote Calendar'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Remote Calendar: %s') % self.calendar.label
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.calendar.remove_self()
+            return redirect('..')
+
+    def update(self):
+        get_session().message = ('info',
+                _('Calendar update has been requested, reload in a few moments'))
+        get_response().add_after_job('updating remote calendar',
+                self.calendar.download_and_parse,
+                fire_and_forget = True)
+        return redirect('.')
+
+
+
+class RemoteCalendarsDirectory(Directory):
+    _q_exports = ['', 'new']
+
+    def _q_traverse(self, path):
+        get_response().breadcrumb.append(('remote/', _('Remote Calendars')))
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        return redirect('..')
+
+    def new(self):
+        calendar_ui = RemoteCalendarDirectory(RemoteCalendar())
+
+        form = calendar_ui.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            calendar_ui.submit(form)
+            return redirect('%s/' % calendar_ui.calendar.id)
+
+        get_response().breadcrumb.append(('new', _('New Remote Calendar')))
+        html_top('events', title = _('New Remote Calendar'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Remote Calendar')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            event = RemoteCalendar.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(event.id), event.label))
+        return RemoteCalendarDirectory(event)
+
+
+class EventDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete']
+
+    def __init__(self, event):
+        self.event = event
+
+    def _q_index(self):
+        form = Form(enctype='multipart/form-data')
+        form.add_submit('edit', _('Edit'))
+        form.add_submit('delete', _('Delete'))
+        form.add_submit('back', _('Back'))
+
+        if form.get_submit() == 'edit':
+            return redirect('edit')
+        if form.get_submit() == 'delete':
+            return redirect('delete')
+        if form.get_submit() == 'back':
+            return redirect('..')
+
+        html_top('events', title = _('Event: %s') % self.event.title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Event: %s') % self.event.title
+        r += htmltext('<p>')
+        r += self.event.description
+        r += htmltext('</p>')
+        r += htmltext('<ul>')
+        if self.event.location:
+            r += htmltext('<li>%s: %s</li>') % (_('Location'), self.event.location)
+        if self.event.organizer:
+            r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.event.organizer)
+        if self.event.url:
+            r += htmltext('<li>%s: <a href="%s">%s</a></li>') % (_('URL'), self.event.url, self.event.url)
+        r += htmltext('</ul>')
+
+        if self.event.more_infos:
+            r += htmltext('<p>')
+            r += self.event.more_infos
+            r += htmltext('</p>')
+
+        r += form.render()
+        return r.getvalue()
+
+    def edit(self):
+        form = self.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit(form)
+            return redirect('..')
+
+        html_top('events', title = _('Edit Event: %s') % self.event.title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Edit Event: %s') % self.event.title
+        r += form.render()
+        return r.getvalue()
+
+    def form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'title', title = _('Title'), required = True,
+                value = self.event.title)
+        form.add(TextWidget, 'description', title = _('Description'),
+                cols = 70, rows = 10,
+                required = True, value = self.event.description)
+        form.add(StringWidget, 'url', title = _('URL'), required = False,
+                value = self.event.url, size = 40)
+        form.add(DateWidget, 'date_start', title = _('Start Date'), required = True,
+                value = strftime(misc.date_format(), self.event.date_start))
+        form.add(DateWidget, 'date_end', title = _('End Date'), required = False,
+                value = strftime(misc.date_format(), self.event.date_end))
+        form.add(TextWidget, 'location', title = _('Location'),
+                cols = 70, rows = 4,
+                required = False, value = self.event.location)
+        form.add(StringWidget, 'organizer', title = _('Organizer'), required = False,
+                value = self.event.organizer, size = 40)
+        form.add(TextWidget, 'more_infos', title = _('More informations'),
+                cols = 70, rows = 10,
+                required = False, value = self.event.more_infos)
+        form.add(TagsWidget, 'keywords', title = _('Keywords'),
+                value = self.event.keywords, size = 50,
+                known_tags = get_cfg('misc', {}).get('event_tags', get_default_event_tags()))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit(self, form):
+        for k in ('title', 'description', 'url', 'date_start', 'date_end',
+                    'organizer', 'location', 'more_infos', 'keywords'):
+            widget = form.get_widget(k)
+            if widget:
+                if k in ('date_start', 'date_end'):
+                    # convert dates to 9-item tuples
+                    v = widget.parse()
+                    if v:
+                        setattr(self.event, k, time.strptime(v, misc.date_format()))
+                    else:
+                        setattr(self.event, k, None)
+                else:
+                    setattr(self.event, k, widget.parse())
+        self.event.store()
+
+    def delete(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this event.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('events', title = _('Delete Event'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Event: %s') % self.event.title
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.event.remove_self()
+            return redirect('..')
+
+
+
+
+class EventsDirectory(AccessControlled, Directory):
+    _q_exports = ['', 'new', 'listing', 'remote']
+    label = N_('Events')
+
+    remote = RemoteCalendarsDirectory()
+
+    def is_accessible(self, user):
+        from .backoffice import check_visibility
+        return check_visibility('events', user)
+
+    def _q_access(self):
+        user = get_request().user
+        if not user:
+            raise errors.AccessUnauthorizedError()
+
+        if not self.is_accessible(user):
+            raise errors.AccessForbiddenError(
+                    public_msg = _('You are not allowed to access Events Management'),
+                    location_hint = 'backoffice')
+
+        get_response().breadcrumb.append(('events/', _('Events')))
+
+
+    def _q_index(self):
+        html_top('events', _('Events'))
+        r = TemplateIO(html=True)
+
+        get_response().filter['sidebar'] = self.get_sidebar()
+
+        r += htmltext('<div class="splitcontent-left">')
+
+        r += htmltext('<div class="bo-block">')
+        events = Event.select()
+        r += htmltext('<h2>%s</h2>') % _('Events')
+        if not events:
+            r += htmltext('<p>')
+            r += _('There is no event defined at the moment.')
+            r += htmltext('</p>')
+        r += htmltext('<ul class="biglist" id="events-list">')
+        for l in events:
+            event_id = l.id
+            r += htmltext('<li class="biglistitem" id="itemId_%s">') % event_id
+            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (event_id, l.title)
+            r += ' - '
+            r += l.format_date()
+            r += htmltext('<p class="commands">')
+            r += command_icon('%s/edit' % event_id, 'edit')
+            r += command_icon('%s/delete' % event_id, 'remove')
+            r += htmltext('</p></li>')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        r += htmltext('</div>')
+
+        r += htmltext('<div class="splitcontent-right">')
+        r += htmltext('<div class="bo-block">')
+        rcalendars = RemoteCalendar.select()
+        r += htmltext('<h2>%s</h2>') % _('Remote Calendars')
+        if not rcalendars:
+            r += htmltext('<p>')
+            r += _('There is no remote calendars defined at the moment.')
+            r += htmltext('</p>')
+
+        r += htmltext('<ul class="biglist" id="events-list">')
+        for l in rcalendars:
+            rcal_id = l.id
+            r += htmltext('<li class="biglistitem" id="itemId_%s">') % rcal_id
+            r += htmltext('<strong class="label"><a href="remote/%s/">%s</a></strong>') % (rcal_id, l.label)
+            r += htmltext('<p class="details">')
+            r += l.url
+            if l.error:
+                r += htmltext('<br /><span class="error-message">%s</span>') % l.get_error_message()
+            r += htmltext('</p>')
+            r += htmltext('<p class="commands">')
+            r += command_icon('remote/%s/edit' % rcal_id, 'edit')
+            r += command_icon('remote/%s/delete' % rcal_id, 'remove')
+            r += htmltext('</p></li>')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul id="sidebar-actions">')
+        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Event')
+        r += htmltext('  <li><a class="new-item" href="remote/new">%s</a></li>') % _('New Remote Calendar')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def new(self):
+        event_ui = EventDirectory(Event())
+
+        form = event_ui.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            event_ui.submit(form)
+            return redirect('%s/' % event_ui.event.id)
+
+        get_response().breadcrumb.append(('new', _('New Event')))
+        html_top('events', title = _('New Event'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Event')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            event = Event.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(event.id), event.title))
+        return EventDirectory(event)
+
+    def listing(self):
+        return redirect('.')
diff --git a/auquotidien/modules/formpage.py b/auquotidien/modules/formpage.py
new file mode 100644
index 0000000..7905e84
--- /dev/null
+++ b/auquotidien/modules/formpage.py
@@ -0,0 +1,41 @@
+from quixote import get_publisher, get_request, redirect
+from quixote.directory import Directory
+from quixote.html import htmltext
+
+import os
+
+import wcs
+import wcs.forms.root
+import wcs.forms.preview
+from qommon import _
+from qommon import template
+from qommon import errors
+from qommon.form import *
+from wcs.roles import logged_users_role
+
+from qommon import emails
+
+OldFormPage = wcs.forms.root.FormPage
+
+class AlternateFormPage(OldFormPage):
+    def form_side(self, *args, **kwargs):
+        form_side_html = OldFormPage.form_side(self, *args, **kwargs)
+        # add a 'Steps' title
+        form_side_html = str(form_side_html).replace('<ol>', '<h2>%s</h2>\n<ol>' % _('Steps'))
+        get_response().filter['gauche'] = form_side_html
+        get_response().filter['steps'] = form_side_html
+        return
+
+wcs.forms.root.FormPage = AlternateFormPage
+wcs.forms.root.PublicFormStatusPage.form_page_class = AlternateFormPage
+wcs.forms.preview.PreviewFormPage.__bases__ = (AlternateFormPage,)
+
+
+OldFormsRootDirectory = wcs.forms.root.RootDirectory
+
+class AlternateFormsRootDirectory(OldFormsRootDirectory):
+    def form_list(self, *args, **kwargs):
+        form_list = OldFormsRootDirectory.form_list(self, *args, **kwargs)
+        return htmltext(str(form_list).replace('h2>', 'h3>'))
+
+wcs.forms.root.RootDirectory = AlternateFormsRootDirectory
diff --git a/auquotidien/modules/links.py b/auquotidien/modules/links.py
new file mode 100644
index 0000000..90557d7
--- /dev/null
+++ b/auquotidien/modules/links.py
@@ -0,0 +1,21 @@
+from qommon.storage import StorableObject
+
+class Link(StorableObject):
+    _names = 'links'
+
+    title = None
+    url = None
+    position = None
+
+    def sort_by_position(cls, links):
+        def cmp_position(x, y):
+            if x.position == y.position:
+                return 0
+            if x.position is None:
+                return 1
+            if y.position is None:
+                return -1
+            return cmp(x.position, y.position)
+        links.sort(cmp_position)
+    sort_by_position = classmethod(sort_by_position)
+
diff --git a/auquotidien/modules/links_ui.py b/auquotidien/modules/links_ui.py
new file mode 100644
index 0000000..cccf59b
--- /dev/null
+++ b/auquotidien/modules/links_ui.py
@@ -0,0 +1,194 @@
+from quixote import get_request, get_response, get_session, redirect
+from quixote.directory import Directory, AccessControlled
+from quixote.html import TemplateIO, htmltext
+
+import wcs
+import wcs.admin.root
+
+from qommon import _
+from qommon import errors
+from qommon.form import *
+from qommon.backoffice.menu import html_top
+from qommon.admin.menu import command_icon
+from qommon import get_cfg
+
+from links import Link
+
+
+class LinkDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete']
+
+    def __init__(self, link):
+        self.link = link
+
+    def _q_index(self):
+        form = Form(enctype='multipart/form-data')
+        form.add_submit('edit', _('Edit'))
+        form.add_submit('delete', _('Delete'))
+        form.add_submit('back', _('Back'))
+
+        if form.get_submit() == 'edit':
+            return redirect('edit')
+        if form.get_submit() == 'delete':
+            return redirect('delete')
+        if form.get_submit() == 'back':
+            return redirect('..')
+
+        html_top('links', title = _('Link: %s') % self.link.title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Link: %s') % self.link.title
+        r += htmltext('<p>')
+        r += self.link.url
+        r += htmltext('</p>')
+
+        r += form.render()
+        return r.getvalue()
+
+    def edit(self):
+        form = self.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit(form)
+            return redirect('..')
+
+        html_top('links', title = _('Edit Link: %s') % self.link.title)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Edit Link: %s') % self.link.title
+        r += form.render()
+        return r.getvalue()
+
+
+    def form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'title', title = _('Title'), required = True,
+                value = self.link.title)
+        form.add(StringWidget, 'url', title = _('URL'), required=False,
+                value = self.link.url,
+                hint=_('Leave empty to create a title'))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit(self, form):
+        for k in ('title', 'url'):
+            widget = form.get_widget(k)
+            if widget:
+                setattr(self.link, k, widget.parse())
+        self.link.store()
+
+    def delete(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this link.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('links', title = _('Delete Link'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Link: %s') % self.link.title
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.link.remove_self()
+            return redirect('..')
+
+
+class LinksDirectory(AccessControlled, Directory):
+    _q_exports = ['', 'new', 'listing', 'update_order']
+    label = N_('Links')
+
+    def is_accessible(self, user):
+        from .backoffice import check_visibility
+        return check_visibility('links', user)
+
+    def _q_access(self):
+        user = get_request().user
+        if not user:
+            raise errors.AccessUnauthorizedError()
+
+        if not self.is_accessible(user):
+            raise errors.AccessForbiddenError(
+                    public_msg = _('You are not allowed to access Links Management'),
+                    location_hint = 'backoffice')
+
+        get_response().breadcrumb.append(('links/', _('Links')))
+
+
+    def _q_index(self):
+        html_top('links', _('Links'))
+        r = TemplateIO(html=True)
+        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js'])
+        get_response().filter['sidebar'] = self.get_sidebar()
+
+        links = Link.select()
+        Link.sort_by_position(links)
+
+        r += htmltext('<ul class="biglist sortable" id="links-list">')
+        for l in links:
+            link_id = l.id
+            r += htmltext('<li class="biglistitem" id="itemId_%s">') % link_id
+            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (link_id, l.title)
+            r += htmltext('<p class="details">')
+            r += l.url
+            r += htmltext('</p>')
+            r += htmltext('<p class="commands">')
+            r += command_icon('%s/edit' % link_id, 'edit')
+            r += command_icon('%s/delete' % link_id, 'remove')
+            r += htmltext('</p></li>')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul id="sidebar-actions">')
+        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Link')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def update_order(self):
+        request = get_request()
+        new_order = request.form['order'].strip(';').split(';')
+        links = Link.select()
+        dict = {}
+        for l in links:
+            dict[str(l.id)] = l
+        for i, o in enumerate(new_order):
+            dict[o].position = i + 1
+            dict[o].store()
+        return 'ok'
+
+
+    def new(self):
+        link_ui = LinkDirectory(Link())
+
+        form = link_ui.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            link_ui.submit(form)
+            return redirect('%s/' % link_ui.link.id)
+
+        get_response().breadcrumb.append(('new', _('New Link')))
+        html_top('links', title = _('New Link'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Link')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            link = Link.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(link.id), link.title))
+        return LinkDirectory(link)
+
+    def listing(self):
+        return redirect('.')
+
diff --git a/auquotidien/modules/myspace.py b/auquotidien/modules/myspace.py
new file mode 100644
index 0000000..3ec2e06
--- /dev/null
+++ b/auquotidien/modules/myspace.py
@@ -0,0 +1,721 @@
+try:
+    import lasso
+except ImportError:
+    pass
+
+import json
+
+from quixote import get_publisher, get_request, redirect, get_response, get_session_manager, get_session
+from quixote.directory import AccessControlled, Directory
+from quixote.html import TemplateIO, htmltext
+from quixote.util import StaticFile, FileStream
+
+from qommon import _
+from qommon import template
+from qommon.form import *
+from qommon import get_cfg, get_logger
+from qommon import errors
+from wcs.api import get_user_from_api_query_string
+
+import qommon.ident.password
+from qommon.ident.password_accounts import PasswordAccount
+
+from qommon.admin.texts import TextsDirectory
+
+from wcs.formdef import FormDef
+import wcs.myspace
+import root
+
+from announces import AnnounceSubscription
+from strongbox import StrongboxItem, StrongboxType
+from payments import Invoice, Regie, is_payment_supported
+
+class MyInvoicesDirectory(Directory):
+    _q_exports = ['']
+
+    def _q_traverse(self, path):
+        if not is_payment_supported():
+            raise errors.TraversalError()
+        get_response().breadcrumb.append(('invoices/', _('Invoices')))
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        user = get_request().user
+        if not user or user.anonymous:
+            raise errors.AccessUnauthorizedError()
+
+        template.html_top(_('Invoices'))
+        r = TemplateIO(html=True)
+        r += TextsDirectory.get_html_text('aq-myspace-invoice')
+
+        r += get_session().display_message()
+
+        invoices = []
+        invoices.extend(Invoice.get_with_indexed_value(
+            str('user_id'), str(user.id)))
+
+        def cmp_invoice(a, b):
+            t = cmp(a.regie_id, b.regie_id)
+            if t != 0:
+                return t
+            return -cmp(a.date, b.date)
+
+        invoices.sort(cmp_invoice)
+
+        last_regie_id = None
+        unpaid = False
+        for invoice in invoices:
+            if invoice.regie_id != last_regie_id:
+                if last_regie_id:
+                    r += htmltext('</ul>')
+                    if unpaid:
+                        r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
+                    r += htmltext('</form>')
+                last_regie_id = invoice.regie_id
+                r += htmltext('<h3>%s</h3>') % Regie.get(last_regie_id).label
+                unpaid = False
+                r += htmltext('<form action="%s/invoices/multiple">' % get_publisher().get_frontoffice_url())
+                r += htmltext('<ul>')
+
+            r += htmltext('<li>')
+            if not (invoice.paid or invoice.canceled):
+                r += htmltext('<input type="checkbox" name="invoice" value="%s"/>' % invoice.id)
+                unpaid = True
+            r += misc.localstrftime(invoice.date)
+            r += ' - '
+            r += '%s' % invoice.subject
+            r += ' - '
+            r += '%s' % invoice.amount
+            r += htmltext(' &euro;')
+            r += ' - '
+            button = '<span class="paybutton">%s</span>' % _('Pay')
+            if invoice.canceled:
+                r += _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
+                r += ' - '
+                button = _('Details')
+            if invoice.paid:
+                r += _('paid on %s') % misc.localstrftime(invoice.paid_date)
+                r += ' - '
+                button = _('Details')
+            r += htmltext('<a href="%s/invoices/%s">%s</a>' % (get_publisher().get_frontoffice_url(),
+                    invoice.id, button))
+            r += htmltext('</li>')
+
+        if last_regie_id:
+            r += htmltext('</ul>')
+            if unpaid:
+                r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
+            r += htmltext('</form>')
+
+        return r.getvalue()
+
+
+class StrongboxDirectory(Directory):
+    _q_exports = ['', 'add', 'download', 'remove', 'pick', 'validate']
+
+    def _q_traverse(self, path):
+        if not get_cfg('misc', {}).get('aq-strongbox'):
+            raise errors.TraversalError()
+        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
+        return Directory._q_traverse(self, path)
+
+    def get_form(self):
+        types = [(x.id, x.label) for x in StrongboxType.select()]
+        form = Form(action='add', enctype='multipart/form-data')
+        form.add(StringWidget, 'description', title=_('Description'), size=60)
+        form.add(FileWidget, 'file', title=_('File'), required=True)
+        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
+                 options = [(None, _('Not specified'))] + types)
+        form.add(DateWidget, 'date_time', title = _('Document Date'))
+        form.add_submit('submit', _('Upload'))
+        return form
+
+    def _q_index(self):
+        template.html_top(_('Strongbox'))
+        r = TemplateIO(html=True)
+
+        # TODO: a paragraph of explanations here could be useful
+
+        sffiles = StrongboxItem.get_with_indexed_value(
+                        str('user_id'), str(get_request().user.id))
+        if sffiles:
+            r += htmltext('<table id="strongbox-items">')
+            r += htmltext('<tr><th></th><th>%s</th><th>%s</th><th></th></tr>') % (
+                _('Type'), _('Expiration'))
+        else:
+            r += htmltext('<p>')
+            r += _('There is currently nothing in your strongbox.')
+            r += htmltext('</p>')
+        has_items_to_validate = False
+        for i, sffile in enumerate(sffiles):
+            expired = False
+            if not sffile.validated_time:
+                has_items_to_validate = True
+                continue
+            if sffile.expiration_time and sffile.expiration_time < time.localtime():
+                expired = True
+            if i%2:
+                classnames = ['odd']
+            else:
+                classnames = ['even']
+            if expired:
+                classnames.append('expired')
+            r += htmltext('<tr class="%s">') % ' '.join(classnames)
+            r += htmltext('<td class="label">')
+            r += sffile.get_display_name()
+            r += htmltext('</td>')
+            if sffile.type_id:
+                r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
+            else:
+                r += htmltext('<td class="type">-</td>')
+            if sffile.expiration_time:
+                r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
+                if expired:
+                    r += ' (%s)' % _('expired')
+                r += htmltext('</td>')
+            else:
+                r += htmltext('<td class="expiration">-</td>')
+            r += htmltext('<td class="actions">')
+            r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
+            r += htmltext('[<a rel="popup" href="remove?id=%s">%s</a>] ') % (sffile.id, _('remove'))
+            r += htmltext('</td>')
+            r += htmltext('</tr>')
+
+        if has_items_to_validate:
+            r += htmltext('<tr><td colspan="4"><h3>%s</h3></td></tr>') % _('Proposed Items')
+            for sffile in sffiles:
+                if sffile.validated_time:
+                    continue
+                if sffile.expiration_time and sffile.expiration_time < time.localtime():
+                    expired = True
+                if i%2:
+                    classnames = ['odd']
+                else:
+                    classnames = ['even']
+                if expired:
+                    classnames.append('expired')
+                r += htmltext('<tr class="%s">') % ' '.join(classnames)
+
+                r += htmltext('<td class="label">')
+                r += sffile.get_display_name()
+                r += htmltext('</td>')
+                if sffile.type_id:
+                    r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
+                else:
+                    r += htmltext('<td class="type">-</td>')
+
+                if sffile.expiration_time:
+                    r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
+                    if expired:
+                        r += ' (%s)' % _('expired')
+                    r += htmltext('</td>')
+                else:
+                    r += htmltext('<td class="expiration">-</td>')
+                r += htmltext('<td class="actions">')
+                r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
+                r += htmltext(' [<a href="validate?id=%s">%s</a>] ') % (sffile.id, _('validate'))
+                r += htmltext(' [<a href="remove?id=%s">%s</a>] ') % (sffile.id, _('reject'))
+                r += htmltext('</td>')
+                r += htmltext('</tr>')
+        if sffiles:
+            r += htmltext('</table>')
+
+        r += htmltext('<h3>%s</h3>') % _('Add a file to the strongbox')
+        form = self.get_form()
+        r += form.render()
+        return r.getvalue()
+
+    def add(self):
+        form = self.get_form()
+        if not form.is_submitted():
+            if get_request().form.get('mode') == 'pick':
+                return redirect('pick')
+            else:
+                return redirect('.')
+
+        sffile = StrongboxItem()
+        sffile.user_id = get_request().user.id
+        sffile.description = form.get_widget('description').parse()
+        sffile.validated_time = time.localtime()
+        sffile.type_id = form.get_widget('type_id').parse()
+        v = form.get_widget('date_time').parse()
+        sffile.set_expiration_time_from_date(v)
+        sffile.store()
+        sffile.set_file(form.get_widget('file').parse())
+        sffile.store()
+        if get_request().form.get('mode') == 'pick':
+            return redirect('pick')
+        else:
+            return redirect('.')
+
+    def download(self):
+        id = get_request().form.get('id')
+        if not id:
+            raise errors.TraversalError()
+        try:
+            sffile = StrongboxItem.get(id)
+        except KeyError:
+            raise errors.TraversalError()
+        if str(sffile.user_id) != str(get_request().user.id):
+            raise errors.TraversalError()
+
+        filename = sffile.file.filename
+        fd = file(filename)
+        size = os.path.getsize(filename)
+        response = get_response()
+        response.set_content_type('application/octet-stream')
+        response.set_header('content-disposition', 'attachment; filename="%s"' % sffile.file.base_filename)
+        return FileStream(fd, size)
+
+    def validate(self):
+        id = get_request().form.get('id')
+        if not id:
+            raise errors.TraversalError()
+        try:
+            sffile = StrongboxItem.get(id)
+        except KeyError:
+            raise errors.TraversalError()
+        if str(sffile.user_id) != str(get_request().user.id):
+            raise errors.TraversalError()
+        sffile.validated_time = time.time()
+        sffile.store()
+        return redirect('.')
+
+    def remove(self):
+        id = get_request().form.get('id')
+        if not id:
+            raise errors.TraversalError()
+        try:
+            sffile = StrongboxItem.get(id)
+        except KeyError:
+            raise errors.TraversalError()
+        if str(sffile.user_id) != str(get_request().user.id):
+            raise errors.TraversalError()
+
+        r = TemplateIO(html=True)
+        form = Form(enctype='multipart/form-data')
+        form.add_hidden('id', get_request().form.get('id'))
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this item from your strongbox.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+        if not form.is_submitted() or form.has_errors():
+            if sffile.type_id:
+                r += htmltext('<h2>%s</h2>') % _('Deleting %(filetype)s: %(filename)s') % {
+                        'filetype': StrongboxType.get(sffile.type_id).label,
+                        'filename': sffile.get_display_name()
+                    }
+            else:
+                r += htmltext('<h2>%s</h2>') % _('Deleting %(filename)s') % {'filename': sffile.get_display_name()}
+            r += form.render()
+            return r.getvalue()
+        else:
+            sffile.remove_self()
+            sffile.remove_file()
+            return redirect('.')
+
+    def picked_file(self):
+        get_response().set_content_type('application/json')
+        sffile = StrongboxItem.get(get_request().form.get('val'))
+        sffile.file.fp = file(sffile.file.filename)
+        if sffile.user_id != get_request().user.id:
+            raise errors.TraversalError()
+        # XXX: this will copy the file, it would be quite nice if it was
+        # possible to just make it a symlink to the sffile
+        token = get_session().add_tempfile(sffile.file)
+        return json.dumps({'token': token, 'filename': sffile.file.base_filename})
+
+    def pick(self):
+        if get_request().form.get('select') == 'true':
+            return self.picked_file()
+        r = TemplateIO(html=True)
+        root_url = get_publisher().get_root_url()
+        sffiles = StrongboxItem.get_with_indexed_value(
+                        str('user_id'), str(get_request().user.id))
+        r += htmltext('<h2>%s</h2>') % _('Pick a file')
+
+        if not sffiles:
+            r += htmltext('<p>')
+            r += _('You do not have any file in your strongbox at the moment.')
+            r += htmltext('</p>')
+            r += htmltext('<div class="buttons">')
+            r += htmltext('<a href="%smyspace/strongbox/" target="_blank">%s</a>') % (root_url,
+                _('Open Strongbox Management'))
+            r += htmltext('</div>')
+        else:
+            r += htmltext('<form id="strongbox-pick">')
+            r += htmltext('<ul>')
+            for sffile in sffiles:
+                r += htmltext('<li><label><input type="radio" name="file" value="%s"/>%s</label>') % (
+                                sffile.id, sffile.get_display_name())
+                r += htmltext(' [<a href="%smyspace/strongbox/download?id=%s">%s</a>] ') % (
+                                root_url, sffile.id, _('view'))
+                r += htmltext('</li>')
+            r += htmltext('</ul>')
+
+            r += htmltext('<div class="buttons">')
+            r += htmltext('<input name="cancel" type="button" value="%s"/>') % _('Cancel')
+            r += ' '
+            r += htmltext('<input name="pick" type="button" value="%s"/>') % _('Pick')
+            r += htmltext('</div>')
+            r += htmltext('</form>')
+        return r.getvalue()
+
+
+class JsonDirectory(Directory):
+    '''Export of several lists in json, related to the current user or the
+       SAMLv2 NameID we'd get in the URL'''
+
+    _q_exports = ['forms']
+
+    user = None
+
+    def _q_traverse(self, path):
+        self.user = get_user_from_api_query_string() or get_request().user
+        if not self.user:
+            raise errors.AccessUnauthorizedError()
+        return Directory._q_traverse(self, path)
+
+    def forms(self):
+        formdefs = FormDef.select(order_by='name', ignore_errors=True)
+        user_forms = []
+        for formdef in formdefs:
+            user_forms.extend(formdef.data_class().get_with_indexed_value(
+                        'user_id', self.user.id))
+        user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
+
+        get_response().set_content_type('application/json')
+
+
+        forms_output = []
+        for form in user_forms:
+            visible_status = form.get_visible_status(user=self.user)
+            # skip drafts and hidden forms
+            if not visible_status:
+                continue
+            name = form.formdef.name
+            id = form.get_display_id()
+            status = visible_status.name
+            title = _('%(name)s #%(id)s (%(status)s)') % {
+                    'name': name,
+                    'id': id,
+                    'status': status
+            }
+            url = form.get_url()
+            d = { 'title': title, 'url': url }
+            d.update(form.get_substitution_variables(minimal=True))
+            forms_output.append(d)
+        return json.dumps(forms_output, cls=misc.JSONEncoder)
+
+
+class MyspaceDirectory(wcs.myspace.MyspaceDirectory):
+    _q_exports = ['', 'profile', 'new', 'password', 'remove', 'drafts', 'forms',
+            'announces', 'strongbox', 'invoices', 'json']
+
+    strongbox = StrongboxDirectory()
+    invoices = MyInvoicesDirectory()
+    json = JsonDirectory()
+
+    def _q_traverse(self, path):
+        get_response().filter['bigdiv'] = 'profile'
+        get_response().breadcrumb.append(('myspace/', _('My Space')))
+
+        # Migrate custom text settings
+        texts_cfg = get_cfg('texts', {})
+        if 'text-aq-top-of-profile' in texts_cfg and (
+                        not 'text-top-of-profile' in texts_cfg):
+            texts_cfg['text-top-of-profile'] = texts_cfg['text-aq-top-of-profile']
+            del texts_cfg['text-aq-top-of-profile']
+            get_publisher().write_cfg()
+
+        return Directory._q_traverse(self, path)
+
+
+    def _q_index(self):
+        user = get_request().user
+        if not user:
+            raise errors.AccessUnauthorizedError()
+        template.html_top(_('My Space'))
+        r = TemplateIO(html=True)
+        if user.anonymous:
+            return redirect('new')
+
+        user_formdef = user.get_formdef()
+
+        user_forms = []
+        if user:
+            formdefs = FormDef.select(order_by='name', ignore_errors=True)
+            user_forms = []
+            for formdef in formdefs:
+                user_forms.extend(formdef.data_class().get_with_indexed_value(
+                            'user_id', user.id))
+            user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
+
+        profile_links = []
+        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
+            if user_formdef:
+                profile_links.append('<a href="#my-profile">%s</a>' % _('My Profile'))
+        if user_forms:
+            profile_links.append('<a href="#my-forms">%s</a>' % _('My Forms'))
+        if get_cfg('misc', {}).get('aq-strongbox'):
+            profile_links.append('<a href="strongbox/">%s</a>' % _('My Strongbox'))
+        if is_payment_supported():
+            profile_links.append('<a href="invoices/">%s</a>' % _('My Invoices'))
+
+        root_url = get_publisher().get_root_url()
+        if user.can_go_in_backoffice():
+            profile_links.append('<a href="%sbackoffice/">%s</a>' % (root_url, _('Back office')))
+        if user.is_admin:
+            profile_links.append('<a href="%sadmin/">%s</a>' % (root_url, _('Admin')))
+
+        if profile_links:
+            r += htmltext('<p id="profile-links">')
+            r += htmltext(' - '.join(profile_links))
+            r += htmltext('</p>')
+
+        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
+            if user_formdef:
+                r += self._my_profile(user_formdef, user)
+
+            r += self._index_buttons(user_formdef)
+
+            try:
+                x = PasswordAccount.get_on_index(get_request().user.id, str('user_id'))
+            except KeyError:
+                pass
+            else:
+                r += htmltext('<p>')
+                r += _('You can delete your account freely from the services portal. '
+                       'This action is irreversible; it will destruct your personal '
+                       'datas and destruct the access to your request history.')
+                r += htmltext(' <strong><a href="remove" rel="popup">%s</a></strong>.') % _('Delete My Account')
+                r += htmltext('</p>')
+
+        options = get_cfg('misc', {}).get('announce_themes')
+        if options:
+            try:
+                subscription = AnnounceSubscription.get_on_index(
+                        get_request().user.id, str('user_id'))
+            except KeyError:
+                pass
+            else:
+                r += htmltext('<p class="command"><a href="announces">%s</a></p>') % _(
+                        'Edit my Subscription to Announces')
+
+        if user_forms:
+            r += htmltext('<h3 id="my-forms">%s</h3>') % _('My Forms')
+            r += root.FormsRootDirectory().user_forms(user_forms)
+
+        return r.getvalue()
+
+    def _my_profile(self, user_formdef, user):
+        r = TemplateIO(html=True)
+        r += htmltext('<h3 id="my-profile">%s</h3>') % _('My Profile')
+
+        r += TextsDirectory.get_html_text('top-of-profile')
+
+        if user.form_data:
+            r += htmltext('<ul>')
+            for field in user_formdef.fields:
+                if not hasattr(field, str('get_view_value')):
+                    continue
+                value = user.form_data.get(field.id)
+                r += htmltext('<li>')
+                r += field.label
+                r += ' : '
+                if value:
+                    r += field.get_view_value(value)
+                r += htmltext('</li>')
+            r += htmltext('</ul>')
+        else:
+            r += htmltext('<p>%s</p>') % _('Empty profile')
+        return r.getvalue()
+
+    def _index_buttons(self, form_data):
+        r = TemplateIO(html=True)
+        passwords_cfg = get_cfg('passwords', {})
+        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
+        if get_session().lasso_session_dump:
+            ident_method = 'idp'
+
+        if form_data and ident_method != 'idp':
+            r += htmltext('<p class="command"><a href="profile" rel="popup">%s</a></p>') % _('Edit My Profile')
+
+        if ident_method == 'password' and passwords_cfg.get('can_change', False):
+            r += htmltext('<p class="command"><a href="password" rel="popup">%s</a></p>') % _('Change My Password')
+
+        return r.getvalue()
+
+    def profile(self):
+        user = get_request().user
+        if not user or user.anonymous:
+            raise errors.AccessUnauthorizedError()
+
+        form = Form(enctype = 'multipart/form-data')
+        formdef = user.get_formdef()
+        formdef.add_fields_to_form(form, form_data = user.form_data)
+
+        form.add_submit('submit', _('Apply Changes'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.profile_submit(form, formdef)
+            return redirect('.')
+
+        template.html_top(_('Edit Profile'))
+        return form.render()
+
+    def profile_submit(self, form, formdef):
+        user = get_request().user
+        data = formdef.get_data(form)
+
+        user.set_attributes_from_formdata(data)
+        user.form_data = data
+
+        user.store()
+
+    def password(self):
+        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
+        if ident_method != 'password':
+            raise errors.TraversalError()
+
+        user = get_request().user
+        if not user or user.anonymous:
+            raise errors.AccessUnauthorizedError()
+
+        form = Form(enctype = 'multipart/form-data')
+        form.add(PasswordWidget, 'new_password', title = _('New Password'),
+                required=True)
+        form.add(PasswordWidget, 'new2_password', title = _('New Password (confirm)'),
+                required=True) 
+
+        form.add_submit('submit', _('Change Password'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            qommon.ident.password.check_password(form, 'new_password')
+            new_password = form.get_widget('new_password').parse()
+            new2_password = form.get_widget('new2_password').parse()
+            if new_password != new2_password:
+                form.set_error('new2_password', _('Passwords do not match'))
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit_password(new_password)
+            return redirect('.')
+
+        template.html_top(_('Change Password'))
+        return form.render()
+
+    def submit_password(self, new_password):
+        passwords_cfg = get_cfg('passwords', {})
+        account = PasswordAccount.get(get_session().username)
+        account.hashing_algo = passwords_cfg.get('hashing_algo')
+        account.set_password(new_password)
+        account.store()
+
+    def new(self):
+        if not get_request().user or not get_request().user.anonymous:
+            raise errors.AccessUnauthorizedError()
+
+        form = Form(enctype = 'multipart/form-data')
+        formdef = get_publisher().user_class.get_formdef()
+        if formdef:
+            formdef.add_fields_to_form(form)
+        else:
+            get_logger().error('missing user formdef (in myspace/new)')
+
+        form.add_submit('submit', _('Register'))
+
+        if form.is_submitted() and not form.has_errors():
+            user = get_publisher().user_class()
+            data = formdef.get_data(form)
+            user.set_attributes_from_formdata(data)
+            user.name_identifiers = get_request().user.name_identifiers
+            user.lasso_dump = get_request().user.lasso_dump
+            user.set_attributes_from_formdata(data)
+            user.form_data = data
+            user.store()
+            get_session().set_user(user.id)
+            root_url = get_publisher().get_root_url()
+            return redirect('%smyspace' % root_url)
+
+        template.html_top(_('Welcome'))
+        return form.render()
+
+
+    def remove(self):
+        user = get_request().user
+        if not user or user.anonymous:
+            raise errors.AccessUnauthorizedError()
+
+        form = Form(enctype = 'multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'Are you really sure you want to remove your account?')))
+        form.add_submit('submit', _('Remove my account'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            user = get_request().user
+            account = PasswordAccount.get_on_index(user.id, str('user_id'))
+            get_session_manager().expire_session() 
+            account.remove_self()
+            return redirect(get_publisher().get_root_url())
+
+        template.html_top(_('Removing Account'))
+        return form.render()
+
+    def announces(self):
+        options = get_cfg('misc', {}).get('announce_themes')
+        if not options:
+            raise errors.TraversalError()
+        user = get_request().user
+        if not user or user.anonymous:
+            raise errors.AccessUnauthorizedError()
+        subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id'))
+        if not subscription:
+            raise errors.TraversalError()
+
+        if subscription.enabled_themes is None:
+            enabled_themes = options
+        else:
+            enabled_themes = subscription.enabled_themes
+
+        form = Form(enctype = 'multipart/form-data')
+        form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'),
+                value=enabled_themes, options=[(x, x, x) for x in options],
+                inline=False, required=False)
+
+        form.add_submit('submit', _('Apply Changes'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            chosen_themes = form.get_widget('themes').parse()
+            if chosen_themes == options:
+                chosen_themes = None
+            subscription.enabled_themes = chosen_themes
+            subscription.store()
+            return redirect('.')
+
+        template.html_top()
+        get_response().breadcrumb.append(('announces', _('Announce Subscription')))
+        return form.render()
+
+
+TextsDirectory.register('aq-myspace-invoice',
+        N_('Message on top of invoices page'),
+        category = N_('Invoices'))
+
diff --git a/auquotidien/modules/payments.py b/auquotidien/modules/payments.py
new file mode 100644
index 0000000..272a1b5
--- /dev/null
+++ b/auquotidien/modules/payments.py
@@ -0,0 +1,597 @@
+import random
+import string
+from datetime import datetime as dt
+import hashlib
+import time
+import urllib
+
+from decimal import Decimal
+
+from quixote import (redirect, get_publisher, get_request, get_session,
+        get_response)
+from quixote.directory import Directory
+from quixote.html import TemplateIO, htmltext
+
+if not set:
+    from sets import Set as set
+
+eopayment = None
+try:
+    import eopayment
+except ImportError:
+    pass
+
+from qommon import _
+from qommon import errors, get_logger, get_cfg, emails
+from qommon.storage import StorableObject
+from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
+    WidgetDict
+from qommon.misc import simplify
+
+from wcs.formdef import FormDef
+from wcs.formdata import Evolution
+from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
+from wcs.users import User
+
+def is_payment_supported():
+    if not eopayment:
+        return False
+    return get_cfg('aq-permissions', {}).get('payments', None) is not None
+
+
+class Regie(StorableObject):
+    _names = 'regies'
+
+    label = None
+    description = None
+    service = None
+    service_options = None
+
+    def get_payment_object(self):
+        return eopayment.Payment(kind=self.service,
+                                 options=self.service_options)
+
+
+class Invoice(StorableObject):
+    _names = 'invoices'
+    _hashed_indexes = ['user_id', 'regie_id']
+    _indexes = ['external_id']
+
+    user_id = None
+    regie_id = None
+    formdef_id = None
+    formdata_id = None
+    subject = None
+    details = None
+    amount = None
+    date = None
+    paid = False
+    paid_date = None
+    canceled = False
+    canceled_date = None
+    canceled_reason = None
+    next_status = None
+    external_id = None
+    request_kwargs = {}
+
+    def __init__(self, id=None, regie_id=None, formdef_id=None):
+        self.id = id
+        self.regie_id = regie_id
+        self.formdef_id = formdef_id
+        if get_publisher() and not self.id:
+            self.id = self.get_new_id()
+
+    def get_user(self):
+        if self.user_id:
+            return User.get(self.user_id, ignore_errors=True)
+        return None
+
+    @property
+    def username(self):
+        user = self.get_user()
+        return user.name if user else ''
+
+    def get_new_id(self, create=False):
+        # format : date-regie-formdef-alea-check
+        r = random.SystemRandom()
+        self.fresh = True
+        while True:
+            id = '-'.join([
+                dt.now().strftime('%Y%m%d'),
+                'r%s' % (self.regie_id or 'x'),
+                'f%s' % (self.formdef_id or 'x'),
+                ''.join([r.choice(string.digits) for x in range(5)])
+                ])
+            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
+            id = id + '-' + crc
+            if not self.has_key(id):
+                return id
+
+    def store(self, *args, **kwargs):
+        if getattr(self, 'fresh', None) is True:
+            del self.fresh
+            notify_new_invoice(self)
+        return super(Invoice, self).store(*args, **kwargs)
+
+    def check_crc(cls, id):
+        try:
+            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
+        except:
+            return False
+    check_crc = classmethod(check_crc)
+
+    def pay(self):
+        self.paid = True
+        self.paid_date = dt.now()
+        self.store()
+        get_logger().info(_('invoice %s paid'), self.id)
+        notify_paid_invoice(self)
+
+    def unpay(self):
+        self.paid = False
+        self.paid_date = None
+        self.store()
+        get_logger().info(_('invoice %s unpaid'), self.id)
+
+    def cancel(self, reason=None):
+        self.canceled = True
+        self.canceled_date = dt.now()
+        if reason:
+            self.canceled_reason = reason
+        self.store()
+        notify_canceled_invoice(self)
+        get_logger().info(_('invoice %s canceled'), self.id)
+
+    def payment_url(self):
+        base_url = get_publisher().get_frontoffice_url()
+        return '%s/invoices/%s' % (base_url, self.id)
+
+
+INVOICE_EVO_VIEW = {
+    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
+    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
+    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
+    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
+}
+
+class InvoiceEvolutionPart:
+    action = None
+    id = None
+    subject = None
+    amount = None
+    transaction = None
+
+    def __init__(self, action, invoice, transaction=None):
+        self.action = action
+        self.id = invoice.id
+        self.subject = invoice.subject
+        self.amount = invoice.amount
+        self.transaction = transaction
+
+    def view(self):
+        vars = {
+            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
+            'id': self.id,
+            'subject': self.subject,
+            'amount': self.amount,
+        }
+        if self.transaction:
+            vars['transaction_order_id'] = self.transaction.order_id
+        return htmltext('<p class="invoice-%s">' % self.action + \
+                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
+
+
+class Transaction(StorableObject):
+    _names = 'transactions'
+    _hashed_indexes = ['invoice_ids']
+    _indexes = ['order_id']
+
+    invoice_ids = None
+
+    order_id = None
+    start = None
+    end = None
+    bank_data = None
+
+    def __init__(self, *args, **kwargs):
+        self.invoice_ids = list()
+        StorableObject.__init__(self, *args, **kwargs)
+
+    def get_new_id(cls, create=False):
+        r = random.SystemRandom()
+        while True:
+            id = ''.join([r.choice(string.digits) for x in range(16)])
+            if not cls.has_key(id):
+                return id
+    get_new_id = classmethod(get_new_id)
+
+class PaymentWorkflowStatusItem(WorkflowStatusItem):
+    description = N_('Payment Creation')
+    key = 'payment'
+    endpoint = False
+    category = ('aq-payment', N_('Payment'))
+    support_substitution_variables = True
+
+    subject = None
+    details = None
+    amount = None
+    regie_id = None
+    next_status = None
+    request_kwargs = {}
+
+    def is_available(self, workflow=None):
+        return is_payment_supported()
+    is_available = classmethod(is_available)
+
+    def render_as_line(self):
+        if self.regie_id:
+            try:
+                return _('Payable to %s' % Regie.get(self.regie_id).label)
+            except KeyError:
+                return _('Payable (not completed)')
+        else:
+            return _('Payable (not completed)')
+
+    def get_parameters(self):
+        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
+                'request_kwargs')
+
+    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
+        if 'subject' in parameters:
+            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
+                     value=self.subject, size=40)
+        if 'details' in parameters:
+            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
+                     value=self.details, cols=80, rows=10)
+        if 'amount' in parameters:
+            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
+        if 'regie_id' in parameters:
+            form.add(SingleSelectWidget, '%sregie_id' % prefix,
+                title=_('Regie'), value=self.regie_id,
+                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
+        if 'next_status' in parameters:
+            form.add(SingleSelectWidget, '%snext_status' % prefix,
+                title=_('Status after validation'), value = self.next_status,
+                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
+                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
+        if 'request_kwargs' in parameters:
+            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
+            hint = ''
+            hint +=_('If the value starts by = it will be '
+                'interpreted as a Python expression.')
+            hint += ' '
+            hint += _('Standard keys are: %s.') % (', '.join(keys))
+            form.add(WidgetDict, 'request_kwargs',
+                title=_('Parameters for the payment system'),
+                hint=hint,
+                value = self.request_kwargs)
+
+    def perform(self, formdata):
+        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
+        invoice.user_id = formdata.user_id
+        invoice.formdata_id = formdata.id
+        invoice.next_status = self.next_status
+        if self.subject:
+            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
+        else:
+            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
+                    'form_name': formdata.formdef.name,
+                    'formdata_id': formdata.id }
+        invoice.details = template_on_formdata(formdata, self.compute(self.details))
+        invoice.amount = Decimal(self.compute(self.amount))
+        invoice.date = dt.now()
+        invoice.request_kwargs = {}
+        if self.request_kwargs:
+            for key, value in self.request_kwargs.iteritems():
+                invoice.request_kwargs[key] = self.compute(value)
+        invoice.store()
+        # add a message in formdata.evolution
+        evo = Evolution()
+        evo.time = time.localtime()
+        evo.status = formdata.status
+        evo.add_part(InvoiceEvolutionPart('create', invoice))
+        if not formdata.evolution:
+            formdata.evolution = []
+        formdata.evolution.append(evo)
+        formdata.store()
+        # redirect the user to "my invoices"
+        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
+
+register_item_class(PaymentWorkflowStatusItem)
+
+class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
+    description = N_('Payment Cancel')
+    key = 'payment-cancel'
+    endpoint = False
+    category = ('aq-payment', N_('Payment'))
+
+    reason = None
+    regie_id = None
+
+    def is_available(self, workflow=None):
+        return is_payment_supported()
+    is_available = classmethod(is_available)
+
+    def render_as_line(self):
+        if self.regie_id:
+            if self.regie_id == '_all':
+                return _('Cancel all Payments')
+            else:
+                try:
+                    return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
+                except KeyError:
+                    return _('Cancel Payments (non completed)')
+        else:
+            return _('Cancel Payments (non completed)')
+
+    def get_parameters(self):
+        return ('reason', 'regie_id')
+
+    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
+        if 'reason' in parameters:
+            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
+                     value=self.reason, size=40)
+        if 'regie_id' in parameters:
+            form.add(SingleSelectWidget, '%sregie_id' % prefix,
+                title=_('Regie'), value=self.regie_id,
+                options = [(None, '---'), ('_all', _('All Regies'))] + \
+                            [(x.id, x.label) for x in Regie.select()])
+
+    def perform(self, formdata):
+        invoices_id = []
+        # get all invoices for the formdata and the selected regie
+        for evo in [evo for evo in formdata.evolution if evo.parts]:
+            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
+                if part.action == 'create':
+                    invoices_id.append(part.id)
+                elif part.id in invoices_id:
+                    invoices_id.remove(part.id)
+        invoices = [Invoice.get(id) for id in invoices_id]
+        # select invoices for the selected regie (if not "all regies")
+        if self.regie_id != '_all':
+            invoices = [i for i in invoices if i.regie_id == self.regie_id]
+        # security filter: check user
+        invoices = [i for i in invoices if i.user_id == formdata.user_id]
+        # security filter: check formdata & formdef
+        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
+                and (i.formdef_id == formdata.formdef.id)]
+        evo = Evolution()
+        evo.time = time.localtime()
+        for invoice in invoices:
+            if not (invoice.paid or invoice.canceled):
+                invoice.cancel(self.reason)
+                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
+        if not formdata.evolution:
+            formdata.evolution = []
+        formdata.evolution.append(evo)
+        formdata.store()
+        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
+
+register_item_class(PaymentCancelWorkflowStatusItem)
+
+
+def request_payment(invoice_ids, url, add_regie=True):
+    for invoice_id in invoice_ids:
+        if not Invoice.check_crc(invoice_id):
+            raise KeyError()
+    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
+    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
+    regie_ids = set([invoice.regie_id for invoice in invoices])
+    # Do not apply if more than one regie is used or no invoice is not paid or canceled
+    if len(invoices) == 0 or len(regie_ids) != 1:
+        url = get_publisher().get_frontoffice_url()
+        if get_session().user:
+            # FIXME: add error messages
+            url += '/myspace/invoices/'
+        return redirect(url)
+    if add_regie:
+        url = '%s%s' % (url, list(regie_ids)[0])
+
+    transaction = Transaction()
+    transaction.store()
+    transaction.invoice_ids = invoice_ids
+    transaction.start = dt.now()
+
+    amount = Decimal(0)
+    for invoice in invoices:
+        amount += Decimal(invoice.amount)
+
+    regie = Regie.get(invoice.regie_id)
+    payment = regie.get_payment_object()
+    # initialize request_kwargs using informations from the first invoice
+    # and update using current user informations
+    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
+    request = get_request()
+    if request.user and request.user.email:
+        request_kwargs['email'] = request.user.email
+    if request.user and request.user.display_name:
+        request_kwargs['name'] = simplify(request.user.display_name)
+    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
+    transaction.order_id = order_id
+    transaction.store()
+
+    for invoice in invoices:
+        if invoice.formdef_id and invoice.formdata_id:
+            formdef = FormDef.get(invoice.formdef_id)
+            formdata = formdef.data_class().get(invoice.formdata_id)
+            evo = Evolution()
+            evo.time = time.localtime()
+            evo.status = formdata.status
+            evo.add_part(InvoiceEvolutionPart('try', invoice,
+                transaction=transaction))
+            if not formdata.evolution:
+                formdata.evolution = []
+            formdata.evolution.append(evo)
+            formdata.store()
+
+    if kind == eopayment.URL:
+        return redirect(data)
+    elif kind == eopayment.FORM:
+        return return_eopayment_form(data)
+    else:
+        raise NotImplementedError()
+
+def return_eopayment_form(form):
+    r = TemplateIO(html=True)
+    r += htmltext('<html><body onload="document.payform.submit()">')
+    r += htmltext('<form action="%s" method="%s" name="payform">') % (form.url, form.method)
+    for field in form.fields:
+        r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
+                field['type'],
+                field['name'],
+                field['value'])
+    r += htmltext('<input type="submit" name="submit" value="%s"/>') % _('Pay')
+    r += htmltext('</body></html>')
+    return r.getvalue()
+
+
+class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
+    description = N_('Payment Validation')
+    key = 'payment-validation'
+    endpoint = False
+    category = ('aq-payment', N_('Payment'))
+
+    next_status = None
+
+    def is_available(self, workflow=None):
+        return is_payment_supported()
+    is_available = classmethod(is_available)
+
+    def render_as_line(self):
+        return _('Wait for payment validation')
+
+    def get_parameters(self):
+        return ('next_status',)
+
+    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
+        if 'next_status' in parameters:
+            form.add(SingleSelectWidget, '%snext_status' % prefix,
+                title=_('Status once validated'), value = self.next_status,
+                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
+
+register_item_class(PaymentValidationWorkflowStatusItem)
+
+
+class PublicPaymentRegieBackDirectory(Directory):
+    def __init__(self, asynchronous):
+        self.asynchronous = asynchronous
+
+    def _q_lookup(self, component):
+        logger = get_logger()
+        request = get_request()
+        query_string = get_request().get_query()
+        if request.get_method() == 'POST' and query_string == '':
+            query_string = urllib.urlencode(request.form)
+        try:
+            regie = Regie.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        if self.asynchronous:
+            logger.debug('received asynchronous notification %r' % query_string)
+        payment = regie.get_payment_object()
+        payment_response = payment.response(query_string)
+        logger.debug('payment response %r', payment_response)
+        order_id = payment_response.order_id
+        bank_data = payment_response.bank_data
+
+        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
+        if transaction is None:
+            raise errors.TraversalError()
+        commit = False
+        if not transaction.end:
+            commit = True
+            transaction.end = dt.now()
+            transaction.bank_data = bank_data
+            transaction.store()
+        if payment_response.signed and payment_response.is_paid() and commit:
+            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
+                                    order_id, payment_response.transaction_id, bank_data))
+
+            for invoice_id in transaction.invoice_ids:
+                # all invoices are now paid
+                invoice = Invoice.get(invoice_id)
+                invoice.pay()
+
+                # workflow for each related formdata
+                if invoice.formdef_id and invoice.formdata_id:
+                    next_status = invoice.next_status
+                    formdef = FormDef.get(invoice.formdef_id)
+                    formdata = formdef.data_class().get(invoice.formdata_id)
+                    wf_status = formdata.get_status()
+                    for item in wf_status.items:
+                        if isinstance(item, PaymentValidationWorkflowStatusItem):
+                            next_status = item.next_status
+                            break
+                    if next_status is not None:
+                        formdata.status = 'wf-%s' % next_status
+                        evo = Evolution()
+                        evo.time = time.localtime()
+                        evo.status = formdata.status
+                        evo.add_part(InvoiceEvolutionPart('pay', invoice,
+                            transaction=transaction))
+                        if not formdata.evolution:
+                            formdata.evolution = []
+                        formdata.evolution.append(evo)
+                        formdata.store()
+                        # performs the items of the new status
+                        formdata.perform_workflow()
+
+        elif payment_response.is_error() and commit:
+            logger.error('transaction %s finished with failure, bank_data:%s' % (
+                                    order_id, bank_data))
+        elif commit:
+            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
+                                    order_id, bank_data))
+        if payment_response.return_content != None and self.asynchronous:
+            get_response().set_content_type('text/plain')
+            return payment_response.return_content
+        else:
+            if payment_response.is_error():
+                # TODO: here return failure message
+                get_session().message = ('info', _('Payment failed'))
+            else:
+                # TODO: Here return success message
+                get_session().message = ('error', _('Payment succeeded'))
+            url = get_publisher().get_frontoffice_url()
+            if get_session().user:
+                url += '/myspace/invoices/'
+            return redirect(url)
+
+class PublicPaymentDirectory(Directory):
+    _q_exports = ['', 'init', 'back', 'back_asynchronous']
+
+    back = PublicPaymentRegieBackDirectory(False)
+    back_asynchronous = PublicPaymentRegieBackDirectory(True)
+
+
+    def init(self):
+        invoice_ids = get_request().form.get('invoice_ids').split(' ')
+
+        for invoice_id in invoice_ids:
+            if not Invoice.check_crc(invoice_id):
+                raise KeyError()
+
+        url = get_publisher().get_frontoffice_url() + '/payment/back/'
+
+        return request_payment(invoice_ids, url)
+
+def notify_new_invoice(invoice):
+    notify_invoice(invoice, 'payment-new-invoice-email')
+
+def notify_paid_invoice(invoice):
+    notify_invoice(invoice, 'payment-invoice-paid-email')
+
+def notify_canceled_invoice(invoice):
+    notify_invoice(invoice, 'payment-invoice-canceled-email')
+
+def notify_invoice(invoice, template):
+    user = invoice.get_user()
+    assert user is not None
+    regie = Regie.get(id=invoice.regie_id)
+    emails.custom_ezt_email(template, { 
+                'user': user,
+                'invoice': invoice,
+                'regie': regie, 
+                'invoice_url': invoice.payment_url() 
+            }, user.email, fire_and_forget = True)
+
diff --git a/auquotidien/modules/payments_ui.py b/auquotidien/modules/payments_ui.py
new file mode 100644
index 0000000..a660777
--- /dev/null
+++ b/auquotidien/modules/payments_ui.py
@@ -0,0 +1,606 @@
+import time
+import pprint
+import locale
+import decimal
+import datetime
+
+from quixote import get_request, get_response, get_session, redirect
+from quixote.directory import Directory, AccessControlled
+from quixote.html import TemplateIO, htmltext
+
+import wcs
+import wcs.admin.root
+from wcs.formdef import FormDef
+
+from qommon import _
+from qommon import errors, misc, template, get_logger
+from qommon.form import *
+from qommon.strftime import strftime
+from qommon.admin.emails import EmailsDirectory
+from qommon.backoffice.menu import html_top
+from qommon import get_cfg
+
+from payments import (eopayment, Regie, is_payment_supported, Invoice,
+        Transaction, notify_paid_invoice)
+
+from qommon.admin.texts import TextsDirectory
+
+if not set:
+    from sets import Set as set
+
+def invoice_as_html(invoice):
+    r = TemplateIO(html=True)
+    r += htmltext('<div id="invoice">')
+    r += htmltext('<h2>%s</h2>') % _('Invoice: %s') % invoice.subject
+    r += htmltext('<h3>%s') % _('Amount: %s') % invoice.amount
+    r += htmltext(' &euro;</h3>')
+    r += htmltext('<!-- DEBUG \n')
+    r += 'Invoice:\n'
+    r += pprint.pformat(invoice.__dict__)
+    for transaction in Transaction.get_with_indexed_value('invoice_ids', invoice.id):
+        r += '\nTransaction:\n'
+        r += pprint.pformat(transaction.__dict__)
+    r += htmltext('\n-->')
+    if invoice.formdef_id and invoice.formdata_id and \
+            get_session().user == invoice.user_id:
+        formdef = FormDef.get(invoice.formdef_id)
+        if formdef:
+            formdata = formdef.data_class().get(invoice.formdata_id, ignore_errors=True)
+            if formdata:
+                name = _('%(form_name)s #%(formdata_id)s') % {
+                        'form_name': formdata.formdef.name,
+                        'formdata_id': formdata.id }
+                r += htmltext('<p class="from">%s <a href="%s">%s</a></p>') % (_('From:'), formdata.get_url(), name)
+    r += htmltext('<p class="regie">%s</p>') % _('Regie: %s') % Regie.get(invoice.regie_id).label
+    r += htmltext('<p class="date">%s</p>') % _('Created on: %s') % misc.localstrftime(invoice.date)
+    if invoice.details:
+        r += htmltext('<p class="details">%s</p>') % _('Details:')
+        r += htmltext('<div class="details">')
+        r += htmltext(invoice.details)
+        r += htmltext('</div>')
+    if invoice.canceled:
+        r += htmltext('<p class="canceled">')
+        r += '%s' % _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
+        if invoice.canceled_reason:
+            r += ' (%s)' % invoice.canceled_reason
+        r += htmltext('</p>')
+    if invoice.paid:
+        r += htmltext('<p class="paid">%s</p>') % _('paid on %s') % misc.localstrftime(invoice.paid_date)
+    r += htmltext('</div>')
+    return r.getvalue()
+
+
+class InvoicesDirectory(Directory):
+    _q_exports = ['', 'multiple']
+
+    def _q_traverse(self, path):
+        if not is_payment_supported():
+            raise errors.TraversalError()
+        get_response().filter['bigdiv'] = 'profile'
+        if get_session().user:
+            # fake breadcrumb
+            get_response().breadcrumb.append(('myspace/', _('My Space')))
+            get_response().breadcrumb.append(('invoices/', _('Invoices')))
+        return Directory._q_traverse(self, path)
+
+    def multiple(self):
+        invoice_ids = get_request().form.get('invoice')
+        if type(invoice_ids) is not list:
+            return redirect('%s' % invoice_ids)
+        return redirect('+'.join(invoice_ids))
+
+    def _q_lookup(self, component):
+        if str('+') in component:
+            invoice_ids = component.split(str('+'))
+        else:
+            invoice_ids = [component]
+        for invoice_id in invoice_ids:
+            if not Invoice.check_crc(invoice_id):
+                raise errors.TraversalError()
+
+        template.html_top(_('Invoices'))
+        r = TemplateIO(html=True)
+        r += TextsDirectory.get_html_text('aq-invoice')
+
+        regies_id = set()
+        for invoice_id in invoice_ids:
+            try:
+                invoice = Invoice.get(invoice_id)
+            except KeyError:
+                raise errors.TraversalError()
+            r += invoice_as_html(invoice)
+            if not (invoice.paid or invoice.canceled):
+                regies_id.add(invoice.regie_id)
+
+        if len(regies_id) == 1:
+            r += htmltext('<p class="command">')
+            r += htmltext('<a href="%s/payment/init?invoice_ids=%s">') % (
+                    get_publisher().get_frontoffice_url(), component)
+            if len(invoice_ids) > 1:
+                r += _('Pay Selected Invoices')
+            else:
+                r += _('Pay')
+            r += htmltext('</a></p>')
+        if len(regies_id) > 1:
+            r += _('You can not pay to different regies.')
+
+        return r.getvalue()
+
+    def _q_index(self):
+        return redirect('..')
+
+
+class RegieDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete', 'options']
+
+    def __init__(self, regie):
+        self.regie = regie
+
+    def _q_index(self):
+        html_top('payments', title = _('Regie: %s') % self.regie.label)
+        r = TemplateIO(html=True)
+        get_response().filter['sidebar'] = self.get_sidebar()
+        r += htmltext('<h2>%s</h2>') % _('Regie: %s') % self.regie.label
+
+        r += get_session().display_message()
+
+        if self.regie.description:
+            r += htmltext('<div class="bo-block">')
+            r += htmltext('<p>')
+            r += self.regie.description
+            r += htmltext('</p>')
+            r += htmltext('</div>')
+
+        if self.regie.service:
+            r += htmltext('<div class="bo-block">')
+            url = get_publisher().get_frontoffice_url() + '/payment/back_asynchronous/'
+            url += str(self.regie.id)
+            r += htmltext('<p>')
+            r += '%s %s' % (_('Banking Service:'), self.regie.service)
+            r += htmltext(' (<a href="options">%s</a>)') % _('options')
+            r += htmltext('</p>')
+            r += htmltext('<p>')
+            r += '%s %s' % (_('Payment notification URL:'), url)
+            r += htmltext('</div>')
+
+        r += self.invoice_listing()
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul>')
+        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
+        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def edit(self):
+        form = self.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit(form)
+            return redirect('..')
+
+        html_top('payments', title = _('Edit Regie: %s') % self.regie.label)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Edit Regie: %s') % self.regie.label
+        r += form.render()
+        return r.getvalue()
+
+
+    def form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'label', title=_('Label'), required=True,
+                value=self.regie.label)
+        form.add(TextWidget, 'description', title=_('Description'),
+                value=self.regie.description, rows=5, cols=60)
+        form.add(SingleSelectWidget, 'service', title=_('Banking Service'),
+                value=self.regie.service, required=True,
+                options = [
+                        ('dummy', _('Dummy (for tests)')),
+                        ('sips', 'SIPS'),
+                        ('systempayv2', 'systempay (Banque Populaire)'),
+                        ('spplus', _('SP+ (Caisse d\'epargne)'))])
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit(self, form):
+        for k in ('label', 'description', 'service'):
+            widget = form.get_widget(k)
+            if widget:
+                setattr(self.regie, k, widget.parse())
+        self.regie.store()
+
+    def delete(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this regie.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            r = TemplateIO(html=True)
+            html_top('payments', title = _('Delete Regie'))
+            r += htmltext('<h2>%s</h2>') % _('Deleting Regie: %s') % self.regie.label
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.regie.remove_self()
+            return redirect('..')
+
+    def option_form(self):
+        form = Form(enctype='multipart/form-data')
+        module = eopayment.get_backend(self.regie.service)
+        service_options = {}
+        for infos in module.description['parameters']:
+            if 'default' in infos:
+                service_options[infos['name']] = infos['default']
+        service_options.update(self.regie.service_options or {})
+
+        banking_titles = {
+            ('dummy', 'direct_notification_url'): N_('Direct Notification URL'),
+            ('dummy', 'siret'): N_('Dummy SIRET'),
+        }
+
+        for infos in module.description['parameters']:
+            name = infos['name']
+            caption = infos.get('caption', name).encode(get_publisher().site_charset)
+            title = banking_titles.get((self.regie.service, name), caption)
+            kwargs = {}
+            widget = StringWidget
+            if infos.get('help_text') is not None:
+                kwargs['hint'] = _(infos['help_text'].encode(get_publisher().site_charset))
+            if infos.get('required', False):
+                kwargs['required'] = True
+            if infos.get('max_length') is not None:
+                kwargs['size'] = infos['max_length']
+            elif infos.get('length') is not None:
+                kwargs['size'] = infos['length']
+            else:
+                kwargs['size'] = 80
+            if kwargs['size'] > 100:
+                widget = TextWidget
+                kwargs['cols'] = 80
+                kwargs['rows'] = 5
+            if 'type' not in infos or infos['type'] is str:
+                form.add(widget, name, title=_(title),
+                         value=service_options.get(name), **kwargs)
+            elif infos['type'] is bool:
+                form.add(CheckboxWidget, name, title=title,
+                         value=service_options.get(name), **kwargs)
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def options(self):
+        r = TemplateIO(html=True)
+        form = self.option_form()
+
+        module = eopayment.get_backend(self.regie.service)
+        try:
+            r += htmltext('<!-- Payment backend description: \n')
+            r += pprint.pformat(module.description)
+            r += htmltext('-->')
+        except:
+            return template.error_page(_('Payment backend do not list its options'))
+            raise errors.TraversalError()
+        r += htmltext('<!-- \n')
+        r += 'Service options\n'
+        r += pprint.pformat(self.regie.service_options)
+        r += htmltext('-->')
+
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+             if self.submit_options(form, module):
+                return redirect('..')
+
+        html_top('payments', title=_('Edit Service Options'))
+        r += htmltext('<h2>%s</h2>') % _('Edit Service Options')
+        r += form.render()
+        return r.getvalue()
+
+    def submit_options(self, form, module):
+        # extra validation
+        error = False
+        for infos in module.description['parameters']:
+            widget = form.get_widget(infos['name'])
+            value = widget.parse()
+            if value and 'validation' in infos:
+                try:
+                    if not infos['validation'](value):
+                        widget.set_error(_('Valeur invalide'))
+                        error = True
+                except ValueError, e:
+                    widget.set_error(_(e.message))
+                    error = True
+        if error:
+            return False
+        if not self.regie.service_options:
+            self.regie.service_options = {}
+        for infos in module.description['parameters']:
+            name = infos['name']
+            value = form.get_widget(name).parse()
+            if value is None:
+                value = ''
+            if hasattr(value, 'strip'):
+                value = value.strip()
+            if infos.get('default') is not None:
+                if value == infos['default']:
+                    self.regie.service_options.pop(name, None)
+                else:
+                    self.regie.service_options[name] = form.get_widget(name).parse()
+            elif not value:
+                self.regie.service_options.pop(name, None)
+            else:
+                self.regie.service_options[name] = form.get_widget(name).parse()
+        self.regie.store()
+        return True
+
+    PAGINATION = 50
+
+    def monetary_amount(self, val):
+        if not val:
+            return ''
+        if isinstance(val, basestring):
+            val = val.replace(',', '.')
+        return '%.2f' % decimal.Decimal(val)
+
+    def get_sort_by(self):
+        request = get_request()
+        sort_by = request.form.get('sort_by')
+        if sort_by not in ('date', 'paid_date', 'username'):
+            sort_by = 'date'
+        return sort_by
+
+    def get_invoices(self):
+        sort_by = self.get_sort_by()
+        invoices = Invoice.get_with_indexed_value('regie_id', self.regie.id,
+                ignore_errors=True)
+        if 'date' in sort_by:
+            reverse = True
+            key = lambda i: getattr(i, sort_by) or datetime.datetime.now()
+        else:
+            reverse = False
+            key = lambda i: getattr(i, sort_by) or ''
+        invoices.sort(reverse=reverse, key=key)
+        return invoices
+
+    def unpay(self, request, invoice):
+        get_logger().info(_('manually set unpaid invoice %(invoice_id)s in regie %(regie)s')
+            % dict(invoice_id=invoice.id, regie=self.regie.id))
+        transaction = Transaction()
+        transaction.invoice_ids = [ invoice.id ]
+        transaction.order_id = 'Manual action'
+        transaction.start = datetime.datetime.now()
+        transaction.end = transaction.start
+        transaction.bank_data = { 
+            'action': 'Set unpaid', 
+            'by': request.user.get_display_name() + ' (%s)' % request.user.id
+        }
+        transaction.store()
+        invoice.unpay()
+
+    def pay(self, request, invoice):
+        get_logger().info(_('manually set paid invoice %(invoice_id)s in regie %(regie)s')
+            % dict(invoice_id=invoice.id, regie=self.regie.id))
+        transaction = Transaction()
+        transaction.invoice_ids = [ invoice.id ]
+        transaction.order_id = 'Manual action'
+        transaction.start = datetime.datetime.now()
+        transaction.end = transaction.start
+        transaction.bank_data = { 
+            'action': 'Set paid', 
+            'by': request.user.get_display_name() + ' (%s)' % request.user.id
+        }
+        transaction.store()
+        invoice.pay()
+
+    def invoice_listing(self):
+        request = get_request()
+        get_response().add_css_include('../../themes/auquotidien/admin.css')
+        if request.get_method() == 'POST':
+            invoice_id = request.form.get('id')
+            invoice = Invoice.get(invoice_id, ignore_errors=True)
+            if invoice:
+                if 'unpay' in request.form:
+                    self.unpay(request, invoice)
+                elif 'pay' in request.form:
+                    self.pay(request, invoice)
+                return redirect('')
+        try:
+            offset = int(request.form.get('offset', 0))
+        except ValueError:
+            offset = 0
+        r = TemplateIO(html=True)
+        r += htmltext('<table id="invoice-listing" borderspacing="0">')
+        r += htmltext('<thead>')
+        r += htmltext('<tr>')
+        r += htmltext('<td><a href="?sort_by=date&offset=%d">Creation</a></td>') % offset
+        r += htmltext('<td>Amount</td>')
+        r += htmltext('<td><a href="?sort_by=paid_date&offset=%d">Paid</a></td>') % offset
+        r += htmltext('<td><a href="?sort_by=username&offset=%d">User</a></td>') % offset
+        r += htmltext('<td>Titre</td>')
+        r += htmltext('<td></td>')
+        r += htmltext('</tr>')
+        r += htmltext('</thead>')
+        invoices = self.get_invoices()
+        for invoice in invoices[offset:offset+self.PAGINATION]:
+            r += htmltext('<tbody class="invoice-rows">')
+            r += htmltext('<tr class="invoice-row"><td>')
+            r += misc.localstrftime(invoice.date)
+            r += htmltext('</td><td class="amount">')
+            r += self.monetary_amount(invoice.amount)
+            r += htmltext('</td><td>')
+            if invoice.paid:
+                r += misc.localstrftime(invoice.paid_date)
+            else:
+                r += ''
+            r += htmltext('</td><td>')
+            user = invoice.get_user()
+            if user:
+                r += user.name
+            r += htmltext('</td><td class="subject">%s</td>') % (invoice.subject or '')
+            r += htmltext('<td>')
+            r += htmltext('<form method="post">')
+            r += htmltext('<input type="hidden" name="id" value="%s"/> ') % invoice.id
+            if invoice.paid:
+                r += htmltext('<input type="submit" name="unpay" value="%s"/>') % _('Set unpaid')
+            else:
+                r += htmltext('<input type="submit" name="pay" value="%s"/>') % _('Set paid')
+            r += htmltext('</form>')
+
+            r += htmltext('</td></tr>')
+            transactions = Transaction.get_with_indexed_value('invoice_ids',
+                    invoice.id)
+            for transaction in sorted(transactions, key=lambda x: x.start):
+                r += htmltext('<tr>')
+                r += htmltext('<td></td>')
+                r += htmltext('<td colspan="5">')
+                r += 'OrderID: %s' % transaction.order_id
+                r += ' Start: %s' % transaction.start
+                if transaction.end:
+                    r += ' End: %s' % transaction.end
+                if transaction.bank_data:
+                    r += ' Bank data: %r' % transaction.bank_data
+                r += htmltext('</td>')
+                r += htmltext('</tr>')
+            r += htmltext('</tbody>')
+        r += htmltext('</tbody></table>')
+        if offset != 0:
+            r += htmltext('<a href="?offset=%d>%s</a> ') % (
+                 max(0, offset-self.PAGINATION), _('Previous'))
+        if offset + self.PAGINATION < len(invoices):
+            r += htmltext('<a href="?offset=%d>%s</a> ') % (
+                 max(0, offset-self.PAGINATION), _('Previous'))
+        return r.getvalue()
+
+
+class RegiesDirectory(Directory):
+    _q_exports = ['', 'new']
+
+    def _q_traverse(self, path):
+        get_response().breadcrumb.append(('regie/', _('Regies')))
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        return redirect('..')
+
+    def new(self):
+        regie_ui = RegieDirectory(Regie())
+
+        form = regie_ui.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            regie_ui.submit(form)
+            return redirect('%s/' % regie_ui.regie.id)
+
+        get_response().breadcrumb.append(('new', _('New Regie')))
+        html_top('payments', title = _('New Regie'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Regie')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            regie = Regie.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(regie.id), regie.label))
+        return RegieDirectory(regie)
+
+
+class PaymentsDirectory(AccessControlled, Directory):
+    _q_exports = ['', 'regie']
+    label = N_('Payments')
+
+    regie = RegiesDirectory()
+
+    def is_accessible(self, user):
+        from .backoffice import check_visibility
+        return check_visibility('payments', user)
+
+    def _q_access(self):
+        user = get_request().user
+        if not user:
+            raise errors.AccessUnauthorizedError()
+
+        if not self.is_accessible(user):
+            raise errors.AccessForbiddenError(
+                    public_msg = _('You are not allowed to access Payments Management'),
+                    location_hint = 'backoffice')
+
+        get_response().breadcrumb.append(('payments/', _('Payments')))
+
+    def _q_index(self):
+        html_top('payments', _('Payments'))
+        get_response().filter['sidebar'] = self.get_sidebar()
+        r = TemplateIO(html=True)
+
+        if not is_payment_supported:
+            r += htmltext('<p class="infonotice">')
+            r += _('Payment is not supported.')
+            r += htmltext('</p>')
+
+        regies = Regie.select()
+        r += htmltext('<h2>%s</h2>') % _('Regies')
+        if not regies:
+            r += htmltext('<p>')
+            r += _('There are no regies defined at the moment.')
+            r += htmltext('</p>')
+        r += htmltext('<ul class="biglist" id="regies-list">')
+        for l in regies:
+            regie_id = l.id
+            r += htmltext('<li class="biglistitem" id="itemId_%s">') % regie_id
+            r += htmltext('<strong class="label"><a href="regie/%s/">%s</a></strong>') % (regie_id, l.label)
+            r += htmltext('</li>')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul id="sidebar-actions">')
+        r += htmltext('  <li><a class="new-item" href="regie/new">%s</a></li>') % _('New Regie')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+
+TextsDirectory.register('aq-invoice',
+        N_('Message on top of an invoice'),
+        category = N_('Invoices'))
+
+EmailsDirectory.register('payment-new-invoice-email',
+        N_('New invoice'),
+        N_('Available variables: user, regie, invoice, invoice_url'),
+        category = N_('Invoices'),
+        default_subject = N_('New invoice'),
+        default_body = N_('''
+A new invoice is available at [invoice_url].
+'''))
+
+EmailsDirectory.register('payment-invoice-paid-email',
+        N_('Paid invoice'),
+        N_('Available variables: user, regie, invoice, invoice_url'),
+        category = N_('Invoices'),
+        default_subject = N_('Paid invoice'),
+        default_body = N_('''
+The invoice [invoice_url] has been paid.
+'''))
+
+EmailsDirectory.register('payment-invoice-canceled-email',
+        N_('Canceled invoice'),
+        N_('Available variables: user, regie, invoice, invoice_url'),
+        category = N_('Invoices'),
+        default_subject = N_('Canceled invoice'),
+        default_body = N_('''
+The invoice [invoice.id] has been canceled.
+'''))
diff --git a/auquotidien/modules/pyatom/__init__.py b/auquotidien/modules/pyatom/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auquotidien/modules/pyatom/afl-2.1.txt b/auquotidien/modules/pyatom/afl-2.1.txt
new file mode 100644
index 0000000..7c180c0
--- /dev/null
+++ b/auquotidien/modules/pyatom/afl-2.1.txt
@@ -0,0 +1,54 @@
+# Larry Rosen has ceased to use or recommend any version
+# of the Academic Free License below version 2.1
+
+The Academic Free License
+v. 2.1
+
+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following notice immediately following the copyright notice for the Original Work:
+
+Licensed under the Academic Free License version 2.1
+
+1) Grant of Copyright License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license to do the following:
+
+a) to reproduce the Original Work in copies;
+
+b) to prepare derivative works ("Derivative Works") based upon the Original Work;
+
+c) to distribute copies of the Original Work and Derivative Works to the public;
+
+d) to perform the Original Work publicly; and
+
+e) to display the Original Work publicly.
+
+2) Grant of Patent License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, to make, use, sell and offer for sale the Original Work and Derivative Works.
+
+3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor hereby agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work, and by publishing the address of that information repository in a notice immediately following the copyright notice that applies to the Original Work.
+
+4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior written permission of the Licensor. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor except as expressly stated herein. No patent license is granted to make, use, sell or offer to sell embodiments of any patent claims other than the licensed claims defined in Section 2. No right is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any Original Work that Licensor otherwise would have a right to license.
+
+5) This section intentionally omitted.
+
+6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately proceeding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to Original Work is granted hereunder except under this disclaimer.
+
+8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to any person for any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to liability for death or personal injury resulting from Licensor's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.
+
+9) Acceptance and Termination. If You distribute copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. Nothing else but this License (or another written agreement between Licensor and You) grants You permission to create Derivative Works based upon the Original Work or to exercise any of the rights granted in Section 1 herein, and any attempt to do so except under the terms of this License (or another written agreement between Licensor and You) is expressly prohibited by U.S. copyright law, the equivalent laws of other countries, and by international treaty. Therefore, by exercising any of the rights granted to You in Section 1 herein, You indicate Your acceptance of this License and all of its terms and conditions.
+
+10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et seq., the equivalent laws of other countries, and international treaty. This section shall survive the termination of this License.
+
+12) Attorneys Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+13) Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. Permission is hereby granted to copy and distribute this license without modification. This license may not be modified without the express written permission of its copyright owner.
+
+ 
+
diff --git a/auquotidien/modules/pyatom/pyatom.py b/auquotidien/modules/pyatom/pyatom.py
new file mode 100644
index 0000000..9a5eceb
--- /dev/null
+++ b/auquotidien/modules/pyatom/pyatom.py
@@ -0,0 +1,1734 @@
+# pyatom.py -- PyAtom library module
+
+"""
+PyAtom
+
+Module to make it really easy to create Atom syndication feeds.
+
+This module is Copyright (C) 2006 Steve R. Hastings.
+Licensed under the Academic Free License version 2.1
+
+You might want to start with the test cases at the end; see how they
+work, and then go back and look at the code in the module.
+
+I hope you find this useful!
+
+Steve R. Hastings
+
+Please send your questions or comments to this email address:
+
+pyatom@langri.com
+"""
+
+
+
+import re
+import sys
+import time
+
+s_pyatom_name = "PyAtom"
+s_pyatom_ver = "0.3.9"
+s_pyatom_name_ver = "%s version %s" % (s_pyatom_name, s_pyatom_ver)
+
+# string constants
+# These string values are used in more than one place.
+
+s_version = "version"
+s_encoding = "encoding"
+s_standalone = "standalone"
+
+s_href = "href"
+s_lang = "xml:lang"
+s_link = "link"
+s_term = "term"
+s_type = "type"
+
+
+
+def set_s_indent(s):
+	"""
+	Set up the globals PyAtom uses to indent its output:
+	s_indent, and s_indent_big
+
+	s_indent is the string to indent one level; default is \\t.
+
+	s_indent_big is s_indent concatenated many times.  PyAtom uses slice
+	copies to get indent strings from s_indent_big.
+	"""
+	global s_indent
+	global s_indent_big
+	s_indent = s
+	s_indent_big = s*256
+
+set_s_indent("\t")
+
+
+
+class TFC(object):
+	"""
+	class TFC: Tag Format Control.
+	Controls how tags are converted to strings.
+
+	Arguments to __init__():
+		level	Specifies what indent level to start at for output.  Default 0.
+		mode	Specifies how to format the output:
+				mode_terse -- minimal output (no indenting)
+				mode_normal -- default
+				mode_verbose -- normal, plus some XML comments
+
+	Normally, if an XML item has no data, nothing is printed, but with
+	mode_verbose you may get a comment like "Collection with 0 entries".
+
+	Methods:
+		b_print_all()
+			Return True if TFC set for full printing.
+		b_print_terse()
+			Return True if TFC set for terse printing.
+		b_print_verbose()
+			Return True if TFC set for verbose printing.
+
+		indent_by(incr)
+			Return a TFC instance that indents by incr columns.
+		s_indent(extra_indent=0)
+			Return an indent string.
+	"""
+	mode_terse, mode_normal, mode_verbose = range(3)
+
+	def __init__(self, level=0, mode=mode_normal):
+		"""
+Arguments:
+	level	Specifies what indent level to start at for output.  Default 0.
+	mode	Specifies how to format the output:
+			mode_terse -- minimal output (no indenting)
+			mode_normal -- default
+			mode_verbose -- normal, plus some XML comments
+
+Normally, if an XML item has no data, nothing is printed, but with
+mode_verbose you may get a comment like "Collection with 0 entries".
+		"""
+		self.level = level
+		self.mode = mode
+
+	def b_print_all(self):
+		"""
+		Return True if TFC set for full printing.
+
+		Some optional things are usually suppressed, but will be printed
+		if the current level is 0.  And everything gets printed when
+		mode_verbose is set.
+		"""
+		return self.level == 0 or self.mode == TFC.mode_verbose
+
+	def b_print_terse(self):
+		"""
+		Return True if TFC set for terse printing.
+		"""
+		return self.mode == TFC.mode_terse
+
+	def b_print_verbose(self):
+		"""
+		Return True if TFC set for verbose printing.
+		"""
+		return self.mode == TFC.mode_verbose
+
+	def indent_by(self, incr):
+		"""
+		Return a TFC instance that indents by incr columns.
+
+		Pass this to a function that takes a TFC to get a temporary indent.
+		"""
+		return TFC(self.level + incr, self.mode)
+	def s_indent(self, extra_indent=0):
+		"""
+		Return an indent string.
+
+		Return a string of white space that indents correctly for the 
+		current TFC settings.  If specified, extra_indent will be added
+		to the current indent level.
+		"""
+		if self.mode == TFC.mode_terse:
+			return ""
+		level = self.level + extra_indent
+		return s_indent_big[0:level]
+
+
+
+pat_nbsp = re.compile(r'&nbsp;')
+def s_entities_to_ws(s):
+	"""
+	Return a copy of s with HTML whitespace entities replaced by a space.
+
+	Currently just gets rid of HTML non-breaking spaces ("&nbsp;").
+	"""
+	if not s:
+		return s
+
+	s = re.sub(pat_nbsp, " ", s)
+	return s
+
+def s_normalize_ws(s):
+	"""
+	Return a copy of string s with each run of whitespace replaced by one space.
+	>>> s = "and    now\n\n\nfor \t  something\v   completely\r\n  different"
+	>>> print s_normalize_ws(s)
+	and now for something completely different
+	>>>
+	"""
+	lst = s.split()
+	s = " ".join(lst)
+	return s
+	
+
+def s_escape_html(s):
+	"""
+	Return a copy of string s with HTML codes escaped.
+
+	This is useful when you want HTML tags printed literally, rather than
+	interpreted.
+
+	>>> print s_escape_html("<head>")
+	&lt;head&gt;
+	>>> print s_escape_html("&nbsp;")
+	&amp;nbsp;
+	"""
+	s = s.replace("&", "&amp;")
+	s = s.replace("<", "&lt;")
+	s = s.replace(">", "&gt;")
+	return s
+
+def s_create_atom_id(t, domain_name, uri=""):
+	"""
+	Create ID using Mark Pilgrim's algorithm.
+
+	Algorithm taken from here:
+	http://diveintomark.org/archives/2004/05/28/howto-atom-id
+	"""
+
+	# ymd (year-month-day) example: 2003-12-13
+	ymd = time.strftime("%Y-%m-%d", t)
+
+	if uri == "":
+		# mush (all mushed together) example: 20031213083000
+		mush = time.strftime("%Y%m%d%H%M%S", t)
+		uri = "/weblog/" + mush
+
+	# s = "tag:" + domain_name + "," + ymd + ":" + uri
+	s = "tag:%s,%s:%s" % (domain_name, ymd, uri)
+
+	s = s.replace("#", "/")
+
+	return s
+
+s_copyright_multiyear = "Copyright %s %d-%d by %s."
+s_copyright_oneyear = "Copyright %s %d by %s."
+def s_copyright(s_owner, s_csym="(C)", end_year=None, start_year=None):
+	"""
+	Return a string with a copyright notice.
+
+	s_owner
+		string with copyright owner's name.
+	s_csym
+		string with copyright symbol. (An HTML entity might be good here.)
+	end_year
+		last year of the copyright.  Default is the current year.
+	start_year
+		first year of the copyright.
+
+	If only end_year is specified, only print one year; if both end_year and
+	start_year are specified, print a range.
+
+	To localize the entire copyright message into another language, change
+	the global variables with the copyright template:
+		s_copyright_multiyear: for a year range
+		s_copyright_oneyear: for a single year
+	"""
+	if not end_year:
+		end_year = time.localtime().tm_year
+
+	if start_year:
+		return s_copyright_multiyear % (s_csym, start_year, end_year, s_owner)
+
+	return s_copyright_oneyear % (s_csym, end_year, s_owner)
+
+
+
+# Here are all of the possible XML items.
+#
+# Supported by PyAtom:
+# XML Declaration: <?xml ... ?>
+# Comments: <!-- ... -->
+# Elements: <tag_name>...</tag_name>
+#
+# Minimal support:
+# Markup Declarations: <!KEYWORD ... >
+# Processing Instructions (PIs): <?KEYWORD ... ?>
+#
+# Not currently supported:
+# INCLUDE and IGNORE directives: <!KEYWORD[ ... ]]>
+# CDATA sections: <![CDATA[ ... ]]>
+#
+
+class XMLItem(object):
+	"""
+	All PyAtom classes inherit from this class.  All it does is provide a
+	few default methods, and be a root for the inheritance tree.
+
+	An XMLItem has several methods that return an XML tag representation of
+	its contents.  Each XMLItem knows how to make a tag for itself.  An
+	XMLItem that contains other XMLItems will ask each one to make a tag;
+	so asking the top-level XMLItem for a tag will cause the entire tree
+	of XMLItems to recursively make tags, and you get a full XML
+	representation with tags appropriately nested and indented.
+	"""
+	def _s_tag(self, tfc):
+		"""
+		A stub which must always be overridden by child classes.
+		"""
+		assert False, "XMLItem instance is too abstract to print."
+
+	def s_tag(self, level):
+		"""
+		Return the item as a string containing an XML tag declaration.
+
+		The XML tag will be indented.
+		Will return an empty string if the item is empty.
+		"""
+		tfc = TFC(level, TFC.mode_normal)
+		return self._s_tag(tfc)
+
+	def s_tag_verbose(self, level):
+		"""
+		Return the item as a string containing an XML tag declaration.
+
+		The XML tag will be indented.
+		May return an XML Comment if the item is empty.
+		"""
+		tfc = TFC(level, TFC.mode_verbose)
+		return self._s_tag(tfc)
+
+	def s_tag_terse(self, level):
+		"""
+		Return the item as a string containing an XML tag declaration.
+
+		The XML tag will not be indented.
+		Will return an empty string if the item is empty.
+		"""
+		tfc = TFC(level, TFC.mode_terse)
+		return self._s_tag(tfc)
+
+	def __str__(self):
+		return self.s_tag(0)
+
+	def level(self):
+		"""
+		Return an integer describing what level this tag is.
+
+		The root tag of an XML document is level 0; document-level comments
+		or other document-level declarations are also level 0.  Tags nested
+		inside the root tag are level 1, tags nested inside those tags are
+		level 2, and so on.
+
+		This is currently only used by the s_tree() functions.  When
+		printing tags normally, the code that walks the tree keeps track of
+		what level is current.
+		"""
+		level = 0
+		while self._parent != None:
+			self = self._parent
+			if self.is_element():
+				level += 1
+		return level
+
+	def s_name(self):
+		"""
+		Return a name for the current item.
+
+		Used only by the s_tree() functions.
+		"""
+		if self._name:
+			return self._name
+		return "unnamed_instance_of_" + type(self).__name__
+
+	def s_tree(self):
+		"""
+		Return a verbose tree showing the current tag and its children.
+
+		This is for debugging; it's not valid XML syntax.
+		"""
+		level = self.level()
+		return "%2d) %s\t%s" % (level, self.s_name(), str(self))
+
+
+
+class DocItem(XMLItem):
+	"""
+	A document-level XML item (appearing above root element).
+
+	Items that can be document-level inherit from this class.
+	"""
+	pass
+
+
+
+class ElementItem(XMLItem):
+	"""
+	An item that may be nested inside an element.
+
+	Items that can be nested inside other elements inherit from this class.
+	"""
+	pass
+
+
+
+class Comment(DocItem,ElementItem):
+	"""
+	An XML comment.
+
+	Attributes:
+		text
+			set the text of the comment
+	"""
+	def __init__(self, text=""):
+		"""
+		text: set the text of the comment
+		"""
+		self._parent = None
+		self._name = ""
+		self.tag_name = "comment"
+		self.text = text
+
+	def _s_tag(self, tfc):
+		if not self:
+			if tfc.b_print_all():
+				return tfc.s_indent() + "<!-- -->"
+			else:
+				return ""
+		else:
+			if self.text.find("\n") >= 0:
+				lst = []
+				lst.append(tfc.s_indent() + "<!--")
+				lst.append(self.text)
+				lst.append(tfc.s_indent() + "-->")
+				return "\n".join(lst)
+			else:
+				s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.text, " -->")
+				return s
+
+		assert False, "not possible to reach this line."
+
+	def __nonzero__(self):
+		# Returns True if there is any comment text.
+		# Returns False otherwise.
+		return not not self.text
+
+	def is_element(self):
+		return True
+
+
+
+# REVIEW: can a PI be an ElementItem?
+class PI(DocItem):
+	"""
+	XML Processing Instruction (PI).
+
+	Attributes:
+		keyword
+		text
+	"""
+	def __init__(self):
+		self._parent = None
+		self._name = ""
+		self.keyword = ""
+		self.text = ""
+
+	def _s_tag(self, tfc):
+		if not self:
+			return ""
+		else:
+			if self.text.find("\n") >= 0:
+				lst = []
+				lst.append("%s%s%s" % (tfc.s_indent(), "<?", self.keyword))
+				lst.append(self.text)
+				lst.append("%s%s" % (tfc.s_indent(), "?>"))
+				return "\n".join(lst)
+			else:
+				s = "%s%s%s %s%s"% \
+						(tfc.s_indent(), "<?", self.keyword, self.text, "?>")
+				return s
+
+		assert False, "not possible to reach this line."
+
+	def __nonzero__(self):
+		# Returns True if there is any keyword.
+		# Returns False otherwise.
+		return not not self.keyword
+
+
+
+# REVIEW: can a MarkupDecl be an ElementItem?
+class MarkupDecl(DocItem):
+	"""
+	XML Markup Declaration.
+
+	Attributes:
+		keyword
+		text
+	"""
+	def __init__(self):
+		self._parent = None
+		self._name = ""
+		self.keyword = ""
+		self.text = ""
+
+	def _s_tag(self, tfc):
+		if not self:
+			return ""
+		else:
+			if self.text.find("\n") >= 0:
+				lst = []
+				lst.append("%s%s%s" % (tfc.s_indent(), "<!", self.keyword))
+				lst.append(self.text)
+				lst.append("%s%s" % (tfc.s_indent(), ">"))
+				return "\n".join(lst)
+			else:
+				s = "%s%s%s %s%s" % \
+						(tfc.s_indent(), "<!", self.keyword, self.text, ">")
+				return s
+
+		assert False, "not possible to reach this line."
+
+	def __nonzero__(self):
+		# Returns True if there is any keyword.
+		# Returns False otherwise.
+		return not not self.keyword
+
+
+
+class CoreElement(ElementItem):
+	"""
+	This is an abstract class.
+
+	All of the XML element classes inherit from this.
+	"""
+	def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []):
+		# dictionary of attributes and their values
+		self.lock = False
+		self._parent = None
+		self._name = ""
+		self.tag_name = tag_name
+		self.def_attr = def_attr
+		self.attrs = {}
+		if def_attr and def_attr_value:
+			self.attrs[def_attr] = def_attr_value
+		self.attr_names = attr_names
+		self.lock = True
+
+	def __nonzero__(self):
+		# Returns True if any attrs are set or there are any contents.
+		# Returns False otherwise.
+		return not not self.attrs or self.has_contents()
+
+	def text_check(self):
+		"""
+		Raise an exception, unless element has text contents.
+
+		Child classes that have text must override this to do nothing.
+		"""
+		raise TypeError, "element does not have text contents"
+
+	def nest_check(self):
+		"""
+		Raise an exception, unless element can nest other elements.
+
+		Child classes that can nest must override this to do nothing.
+		"""
+		raise TypeError, "element cannot nest other elements"
+
+	def __delattr__(self, name):
+		# REVIEW: this should be made to work!
+		raise TypeError, "cannot delete elements"
+
+	def __getattr__(self, name):
+		if name == "lock":
+			# If the "lock" hasn't been created yet, we always want it
+			# to be False, i.e. we are not locked.
+			return False
+		else:
+			raise AttributeError, name
+
+	def __setattr__(self, name, value):
+		# Here's how this works:
+		#
+		# 0) "self.lock" is a boolean, set to False during __init__()
+		# but turned True afterwards.  When it's False, you can add new
+		# members to the class instance without any sort of checks; once
+		# it's set True, __setattr__() starts checking assignments.
+		# By default, when lock is True, you cannot add a new member to
+		# the class instance, and any assignment to an old member has to
+		# be of matching type.  So if you say "a.text = string", the
+		# .text member has to exist and be a string member.
+		#
+		# This is the default __setattr__() for all element types.  It
+		# gets overloaded by the __setattr__() in NestElement, because
+		# for nested elments, it makes sense to be able to add new
+		# elements nested inside.
+		#
+		# This is moderately nice.  But later in NestElement there is a
+		# version of __setattr__() that is *very* nice; check it out.
+		#
+		# 1) This checks assignments to _parent, and makes sure they are
+		# plausible (either an XMLItem, or None).
+
+		try:
+			lock = self.lock
+		except AttributeError:
+			lock = False
+
+		if not lock:
+			self.__dict__[name] = value
+			return
+
+		dict = self.__dict__
+		if not name in dict:
+			# brand-new item
+			if lock:
+				raise TypeError, "element cannot nest other elements"
+
+		if name == "_parent":
+			if not (isinstance(value, XMLItem) or value is None):
+				raise TypeError, "only XMLItem or None is permitted"
+			self.__dict__[name] = value
+			return
+
+		# locked item so do checks
+		if not type(self.__dict__[name]) is type(value):
+			raise TypeError, "value is not the same type"
+
+		self.__dict__[name] = value
+		
+
+	def has_contents(self):
+		return False
+
+	def multiline_contents(self):
+		return False
+
+	def s_contents(self, tfc):
+		assert False, "CoreElement is an abstract class; it has no contents."
+
+	def _s_start_tag_name_attrs(self, tfc):
+		"""
+		Return a string with the start tag name, and any attributes.
+
+		Wrap this in correct punctuation to get a start tag.
+		"""
+		def attr_newline(tfc):
+			if tfc.b_print_terse():
+				return " "
+			else:
+				return "\n" + tfc.s_indent(2)
+
+		lst = []
+		lst.append(self.tag_name)
+
+		if len(self.attrs) == 1:
+			# just one attr so do on one line
+			attr = self.attrs.keys()[0]
+			s_attr = '%s="%s"' % (attr, self.attrs[attr])
+			lst.append(" " + s_attr)
+		elif len(self.attrs) > 1:
+			# more than one attr so do a nice nested tag
+			# 0) show all attrs in the order of attr_names
+			for attr in self.attr_names:
+				if attr in self.attrs.keys():
+					s_attr = '%s="%s"' % (attr, self.attrs[attr])
+					lst.append(attr_newline(tfc) + s_attr)
+			# 1) any attrs not in attr_names?  list them, too
+			for attr in self.attrs:
+				if not attr in self.attr_names:
+					s_attr = '%s="%s"' % (attr, self.attrs[attr])
+					lst.append(attr_newline(tfc) + s_attr)
+
+		return "".join(lst)
+
+	def _s_tag(self, tfc):
+		if not self:
+			if not tfc.b_print_all():
+				return ""
+
+		lst = []
+
+		lst.append(tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc))
+
+		if not self.has_contents():
+			lst.append("/>")
+		else:
+			lst.append(">")
+			if self.multiline_contents():
+				s = "\n%s\n" % self.s_contents(tfc.indent_by(1))
+				lst.append(s + tfc.s_indent())
+			else:
+				lst.append(self.s_contents(tfc))
+			lst.append("</" + self.tag_name + ">")
+
+		return "".join(lst)
+
+	def s_start_tag(self, tfc):
+		return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + ">"
+
+	def s_end_tag(self):
+		return "</" + self.tag_name + ">"
+
+	def s_compact_tag(self, tfc):
+		return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + "/>"
+
+	def is_element(self):
+		return True
+
+
+
+class TextElement(CoreElement):
+	"""
+	An element that cannot have other elements nested inside it.
+
+	Attributes:
+		attr
+		text
+	"""
+	def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []):
+		CoreElement.__init__(self, tag_name, def_attr, def_attr_value,
+				attr_names)
+		self.lock = False
+		self.text = ""
+		self.lock = True
+
+	def text_check(self):
+		pass
+
+	def has_contents(self):
+		return not not self.text
+
+	def multiline_contents(self):
+		return self.text.find("\n") >= 0
+
+	def s_contents(self, tfc):
+		return self.text
+
+
+
+class Nest(ElementItem):
+	"""
+	A data structure that can store Elements, nested inside it.
+
+	Note: this is not, itself, an Element!  Because it is not an XML
+	element, it has no tags.  Its string representation is the
+	representations of the elements nested inside it.
+
+	NestElement and XMLDoc inherit from this.
+	"""
+	def __init__(self):
+		self.lock = False
+		self._parent = None
+		self._name = ""
+		self.elements = []
+		self.lock = True
+	def __len__(self):
+		return len(self.elements)
+	def __getitem__(self, key):
+		return self.elements[key]
+	def __setitem__(self, key, value):
+		self.elements[key] = value
+	def __delitem__(self, key):
+		del(self.elements[key])
+
+	def _do_setattr(self, name, value):
+		if isinstance(value, XMLItem):
+			value._parent = self
+			value._name = name
+			self.elements.append(value)
+		self.__dict__[name] = value
+
+	def __setattr__(self, name, value):
+		# Lots of magic here!  This is important stuff.  Here's how it works:
+		#
+		# 0) self.lock is a boolean, set to False initially and then set
+		# to True at the end of __init__().  When it's False, you can add new
+		# members to the class instance without any sort of checks; once
+		# it's set True, __setattr__() starts checking assignments.  By
+		# default, when lock is True, any assignment to an old member
+		# has to be of matching type.  You can add a new member to the
+		# class instance, but __setattr__() checks to ensure that the
+		# new member is an XMLItem.
+		#
+		# 1) Whether self.lock is set or not, if the value is an XMLitem,
+		# then this will properly add the XMLItem into the tree
+		# structure.  The XMLItem will have _parent set to the parent,
+		# will have _name set to its name in the parent, and will be
+		# added to the parent's elements list.  This is handled by
+		# _do_setattr().
+		#
+		# 2) As a convenience for the user, if the user is assigning a
+		# string, and self is an XMLItem that has a .text value, this
+		# will assign the string to the .text value.  This allows usages
+		# like "e.title = string", which is very nice.  Before I added
+		# this, I frequently wrote that instead of "e.title.text =
+		# string" so I wanted it to just work.  Likewise the user can
+		# assign a time value directly into Timestamp elements.
+		#
+		# 3) This checks assignments to _parent, and makes sure they are
+		# plausible (either an XMLItem, or None).
+
+		try:
+			lock = self.lock
+		except AttributeError:
+			lock = False
+
+		if not lock:
+			self._do_setattr(name, value)
+			return
+
+		dict = self.__dict__
+		if not name in dict:
+			# brand-new item
+			if lock:
+				self.nest_check()
+				if not isinstance(value, XMLItem):
+					raise TypeError, "only XMLItem is permitted"
+			self._do_setattr(name, value)
+			return
+
+		if name == "_parent" or name == "root_element":
+			if not (isinstance(value, XMLItem) or value is None):
+				raise TypeError, "only XMLItem or None is permitted"
+			self.__dict__[name] = value
+			return
+
+		if name == "_name" and type(value) == type(""):
+			self.__dict__[name] = value
+			return
+
+		# for Timestamp elements, allow this:  element = time
+		# (where "time" is a float value, since uses float for times)
+		# Also allow valid timestamp strings.
+		if isinstance(self.__dict__[name], Timestamp):
+			if type(value) == type(1.0):
+				self.__dict__[name].time = value
+				return
+			elif type(value) == type(""):
+				t = utc_time_from_s_timestamp(value)
+				if t:
+					self.__dict__[name].time = t
+				else:
+					raise ValueError, "value must be a valid timestamp string"
+				return
+
+		# Allow string assignment to go to the .text attribute, for
+		# elements that allow it.  All TextElements allow it;
+		# Elements will allow it if they do not nave nested elements.
+		# text_check() raises an error if it's not allowed.
+		if isinstance(self.__dict__[name], CoreElement) and \
+				type(value) == type(""):
+			self.__dict__[name].text_check()
+			self.__dict__[name].text = value
+			return
+
+		# locked item so do checks
+		if not type(self.__dict__[name]) is type(value):
+			raise TypeError, "value is not the same type"
+
+		self.__dict__[name] = value
+		
+	def __delattr__(self, name):
+		# This won't be used often, if ever, but if anyone tries it, it
+		# should work.
+		if isinstance(self.name, XMLItem):
+			o = self.__dict__[name]
+			self.elements.remove(o)
+			del(self.__dict__[name])
+		else:
+			# REVIEW: what error should this raise?
+			raise TypeError, "cannot delete that item"
+
+	def nest_check(self):
+		pass
+
+	def is_element(self):
+		# a Nest is not really an element
+		return False
+
+	def has_contents(self):
+		for element in self.elements:
+			if element:
+				return True
+		# empty iff all of the elements were empty
+		return False
+
+	def __nonzero__(self):
+		return self.has_contents()
+
+	def multiline_contents(self):
+		# if there are any contents, we want multiline for nested tags
+		return self.has_contents()
+
+	def s_contents(self, tfc):
+		if len(self.elements) > 0:
+			# if any nested elements exist, we show those
+			lst = []
+
+			for element in self.elements:
+				s = element._s_tag(tfc)
+				if s:
+					lst.append(s)
+
+			return "\n".join(lst)
+		else:
+			return ""
+
+		assert False, "not possible to reach this line."
+		return ""
+
+	def s_tree(self):
+		level = self.level()
+		tup = (level, self.s_name(), self.__class__.__name__)
+		s = "%2d) %s (instance of %s)" % tup
+		lst = []
+		lst.append(s)
+		for element in self.elements:
+			s = element.s_tree()
+			lst.append(s)
+		return "\n".join(lst)
+
+	def _s_tag(self, tfc):
+		return self.s_contents(tfc)
+
+
+
+
+class NestElement(Nest,CoreElement):
+	"""
+	An element that can have other elements nested inside it.
+
+	Attributes:
+		attr
+		elements: a list of other elements nested inside this one.
+	"""
+	def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]):
+		CoreElement.__init__(self, tag_name, def_attr, def_attr_value,
+				attr_names)
+		self.lock = False
+		self.elements = []
+		self.lock = True
+
+	def is_element(self):
+		return True
+
+	def __nonzero__(self):
+		return CoreElement.__nonzero__(self)
+
+	def _s_tag(self, tfc):
+		return CoreElement._s_tag(self, tfc)
+
+
+
+class Element(NestElement,TextElement):
+	"""
+	A class to represent an arbitrary XML tag.  Can either have other XML
+	elements nested inside it, or else can have a text string value, but
+	never both at the same time.
+
+	This is intended for user-defined XML tags.  The user can just use
+	"Element" for all custom tags.
+
+	PyAtom doesn't use this; PyAtom uses TextElement for tags with a text
+	string value, and NestElement for tags that nest other elements.  Users
+	can do the same, or can just use Element, as they like.
+
+	Attributes:
+		attr
+		elements:	a list of other elements nested inside, if any
+		text:	a text string value, if any
+
+	Note: if text is set, elements will be empty, and vice-versa.  If you
+	have elements nested inside and try to set the .text, this will raise
+	an exception, and vice-versa.
+	"""
+	# A Element can have other elements nested inside it, or it can have
+	# a single ".text" string value.  But never both at the same time.
+	# Once you nest another element, you can no longer use the .text.
+	def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]):
+		NestElement.__init__(self, tag_name, def_attr, def_attr_value,
+				attr_names)
+		self.lock = False
+		self.text = ""
+		self.lock = True
+
+	def nest_check(self):
+		if self.text:
+			raise TypeError, "Element has text contents so cannot nest"
+
+	def text_check(self):
+		if len(self.elements) > 0:
+			raise TypeError, "Element has nested elements so cannot assign text"
+
+	def has_contents(self):
+		return NestElement.has_contents(self) or TextElement.has_contents(self)
+
+	def multiline_contents(self):
+		return NestElement.has_contents(self) or self.text.find("\n") >= 0
+
+	def s_contents(self, tfc):
+		if len(self.elements) > 0:
+			return NestElement.s_contents(self, tfc)
+		elif self.text:
+			return TextElement.s_contents(self, tfc)
+		else:
+			return ""
+		assert False, "not possible to reach this line."
+
+	def s_tree(self):
+		lst = []
+		if len(self.elements) > 0:
+			level = self.level()
+			tup = (level, self.s_name(), self.__class__.__name__)
+			s = "%2d) %s (instance of %s)" % tup
+			lst.append(s)
+			for element in self.elements:
+				s = element.s_tree()
+				lst.append(s)
+			return "\n".join(lst)
+		elif self.text:
+			return XMLItem.s_tree(self)
+		else:
+			level = self.level()
+			tfc = TFC(level)
+			s = "%2d) %s %s" % (level, self.s_name(), "empty Element...")
+			return s
+		assert False, "not possible to reach this line."
+
+
+
+class Collection(XMLItem):
+	"""
+	A Collection contains 0 or more Elements, but isn't an XML element.
+	Use where a run of 0 or more Elements of the same type is legal.
+
+	When you init your Collection, you specify what class of Element it will
+	contain.  Attempts to append an Element of a different class will raise
+	an exception.  Note, however, that the various Element classes all
+	inherit from base classes, and you can specify a class from higher up in
+	the inheritance tree.  You could, if you wanted, make a Collection
+	containing "XMLItem" and then any item defined in PyAtom would be legal
+	in that collection.  (See XMLDoc, which contains two collections of
+	DocItem.)
+
+	Attributes:
+		contains:	the class of element this Collection will contain
+		elements:	a list of other elements nested inside, if any
+
+	Note: The string representation of a Collection is just the string
+	representations of the elements inside it.  However, a verbose string
+	reprentation may have an XML comment like this:
+
+	<!-- Collection of <class> with <n> elements -->
+
+	where <n> is the number of elements in the Collection and <class> is the
+	name of the class in this Collection.
+	"""
+	def __init__(self, element_class):
+		self.lock = False
+		self._parent = None
+		self._name = ""
+		self.elements = []
+		self.contains = element_class
+		self.lock = True
+	def __len__(self):
+		return len(self.elements)
+	def __getitem__(self, key):
+		return self.elements[key]
+	def __setitem__(self, key, value):
+		if not isinstance(value, self.contains):
+			raise TypeError, "object is the wrong type for this collection"
+		self.elements[key] = value
+	def __delitem__(self, key):
+		del(self.elements[key])
+
+	def __nonzero__(self):
+		# there are no attrs so if any element is nonzero, collection is too
+		for element in self.elements:
+			if element:
+				return True
+		return False
+
+	def is_element(self):
+		# A Collection is not really an Element
+		return False
+
+	def s_coll(self):
+		name = self.contains.__name__
+		n = len(self.elements)
+		if n == 1:
+			el = "element"
+		else:
+			el = "elements"
+		return "collection of %s with %d %s" % (name, n, el)
+
+	def append(self, element):
+		if not isinstance(element, self.contains):
+			print >> sys.stderr, "Error: attempted to insert", \
+					type(element).__name__, \
+					"into collection of", self.contains.__name__
+			raise TypeError, "object is the wrong type for this collection"
+		element._parent = self
+		self.elements.append(element)
+
+	def _s_tag(self, tfc):
+		# A collection exists only as a place to put real elements.
+		# There are no start or end tags...
+		# When tfc.b_print_all() is true, we do put an XML comment.
+
+		if not self.elements:
+			if not tfc.b_print_all():
+				return ""
+
+		lst = []
+
+		if tfc.b_print_verbose():
+			s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.s_coll(), " -->")
+			lst.append(s)
+			tfc = tfc.indent_by(1)
+
+		for element in self.elements:
+			s = element._s_tag(tfc)
+			if s:
+				lst.append(s)
+
+		return "\n".join(lst)
+
+	def s_tree(self):
+		level = self.level()
+		s = "%2d) %s %s" % (level, self.s_name(), self.s_coll())
+		lst = []
+		lst.append(s)
+		for element in self.elements:
+			s = element.s_tree()
+			lst.append(s)
+		return "\n".join(lst)
+
+
+
+class XMLDeclaration(XMLItem):
+	# REVIEW: should this print multi-line for multiple attrs?
+	def __init__(self):
+		self._parent = None
+		self._name = ""
+		self.attrs = {}
+		self.attrs[s_version] = "1.0"
+		self.attrs[s_encoding] = "utf-8"
+		self.attr_names = [s_version, s_encoding, s_standalone]
+
+	def _s_tag(self, tfc):
+		# An XMLDeclaration() instance is never empty, so always prints.
+
+		lst = []
+		s = "%s%s" % (tfc.s_indent(), "<?xml")
+		lst.append(s)
+		# 0) show all attrs in the order of attr_names
+		for attr in self.attr_names:
+			if attr in self.attrs.keys():
+				s_attr = ' %s="%s"' % (attr, self.attrs[attr])
+				lst.append(s_attr)
+		# 1) any attrs not in attr_names?  list them, too
+		for attr in self.attrs:
+			if not attr in self.attr_names:
+				s_attr = ' %s="%s"' % (attr, self.attrs[attr])
+				lst.append(s_attr)
+		lst.append("?>")
+
+		return "".join(lst)
+
+	def __nonzero__(self):
+		# Returns True because the XML Declaration is never empty.
+		return True
+
+	def is_element(self):
+		return True
+
+
+
+class XMLDoc(Nest):
+	"""
+	A data structure to represent an XML Document.  It will have the
+	following structure:
+
+	the XML Declaration item
+	0 or more document-level XML items
+	exactly one XML item (the "root tag")
+	0 or more document-level XML items
+
+	document level XML items are: Comment, PI, MarkupDecl
+
+
+	Attributes:
+		xml_decl:	the XMLDeclaration item
+		docitems_above:	a collection of DocItem (items above root_element)
+		root_element:	the XML tag containing your data
+		docitems_below:	a collection of DocItem (items below root_element)
+
+	Note: usually the root_element has lots of other XML items nested inside
+	it!
+	"""
+	def __init__(self, root_element=None):
+		Nest.__init__(self)
+
+		self._name = "XMLDoc"
+
+		self.xml_decl = XMLDeclaration()
+		self.docitems_above = Collection(DocItem)
+
+		if not root_element:
+			root_element = Comment("no root element yet")
+		self.root_element = root_element
+
+		self.docitems_below = Collection(DocItem)
+
+	def __setattr__(self, name, value):
+		# root_element may always be set to any ElementItem
+		if name == "root_element":
+			if not (isinstance(value, ElementItem)):
+				raise TypeError, "only ElementItem is permitted"
+
+			self.lock = False
+			# Item checks out, so assign it.  root_element should only
+			# ever have one element, and we always put the new element
+			# in the same slot in elements[].
+			if "i_root_element" in self.__dict__:
+				# Assign new root_element over old one in elements[]
+				assert self.elements[self.i_root_element] == self.root_element
+				self.elements[self.i_root_element] = value
+			else:
+				# This is the first time root_element was ever set.
+				self.i_root_element = len(self.elements)
+				self.elements.append(value)
+
+			value._parent = self
+			value._name = name
+			self.__dict__[name] = value
+			self.lock = True
+		else:
+			# for all other, fall through to inherited behavior
+			Nest.__setattr__(self, name, value)
+
+	def Validate(self):
+		# XMLDoc never has parent.  Never change this!
+		assert self._parent == None
+		return True
+
+
+
+def local_time_from_utc_time(t):
+	return t - time.timezone
+
+def utc_time_from_local_time(t):
+	return t + time.timezone
+
+def local_time():
+	return time.time() - time.timezone
+
+def utc_time():
+	return time.time()
+
+
+class TimeSeq(object):
+	"""
+	A class to generate a sequence of timestamps.
+
+	Atom feed validators complain if multiple timestamps have the same
+	value, so this provides a convenient way to set a bunch of timestamps
+	all at least one second different from each other.
+	"""
+	def __init__(self, init_time=0):
+		if init_time == 0:
+			self.time = local_time()
+		else:
+			self.time = float(init_time)
+	def next(self):
+		t = self.time
+		self.time += 1.0
+		return t
+
+format_RFC3339 = "%Y-%m-%dT%H:%M:%S"
+
+def parse_time_offset(s):
+	s = s.lstrip().rstrip()
+
+	if (s == '' or s == 'Z' or s == 'z'):
+		return 0
+
+	m = pat_time_offset.search(s)
+	sign = m.group(1)
+	offset_hour = int(m.group(2))
+	offset_min = int(m.group(3))
+	offset = offset_hour * 3600 + offset_min * 60
+	if sign == "-":
+		offset *= -1
+	return offset
+
+def s_timestamp(utc_time, time_offset="Z"):
+	"""
+	Format a time and offset into a string.
+
+	utc_time
+		a floating-point value, time in the UTC time zone
+	s_time_offset
+		a string specifying an offset from UTC.  Examples:
+			z or Z -- offset is 0 ("Zulu" time, UTC, aka GMT)
+			-08:00 -- 8 hours earlier than UTC (Pacific time zone)
+			"" -- empty string is technically not legal, but may work
+
+	Notes:
+		Returned string complies with RFC3339; uses ISO8601 date format.
+		Example: 2003-12-13T18:30:02Z
+		Example: 2003-12-13T18:30:02+02:00
+	"""
+
+	if not utc_time:
+		return ""
+
+	utc_time += parse_time_offset(time_offset)
+
+	try:
+		s = time.strftime(format_RFC3339, time.localtime(utc_time))
+	except:
+		return ""
+
+	return s + time_offset
+
+
+
+pat_RFC3339 = re.compile("(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(.*)")
+pat_time_offset = re.compile("([+-])(\d\d):(\d\d)")
+
+def utc_time_from_s_timestamp(s_date_time_stamp):
+	# parse RFC3339-compatible times that use ISO8601 date format
+	# date time stamp example: 2003-12-13T18:30:02Z
+	# date time stamp example: 2003-12-13T18:30:02+02:00
+	# leaving off the suffix is technically not legal, but allowed
+
+	s_date_time_stamp = s_date_time_stamp.lstrip().rstrip()
+
+	try:
+		m = pat_RFC3339.search(s_date_time_stamp)
+		year = int(m.group(1))
+		mon = int(m.group(2))
+		mday = int(m.group(3))
+		hour = int(m.group(4))
+		min = int(m.group(5))
+		sec = int(m.group(6))
+		tup = (year, mon, mday, hour, min, sec, -1, -1, -1)
+		t = time.mktime(tup)
+
+		s = m.group(7)
+		t += parse_time_offset(s)
+
+		return t
+
+	except:
+		return 0.0
+
+	assert False, "impossible to reach this line"
+
+
+def s_time_offset():
+	"""
+	Return a string with local offset from UTC in RFC3339 format.
+	"""
+
+	# If t is set to local time in seconds since the epoch, then...
+	# ...offset is the value you add to t to get UTC.  This is the
+	# reverse of time.timezone.
+
+	offset = -(time.timezone)
+
+	if offset > 0:
+		sign = "+"
+	else:
+		sign = "-"
+		offset = abs(offset)
+
+	offset_hour = offset // (60 * 60)
+	offset_min = (offset // 60) % 60
+	return "%s%02d:%02d" % (sign, offset_hour, offset_min)
+
+s_offset_local = s_time_offset()
+
+s_offset_default = s_offset_local
+
+def set_default_time_offset(s):
+	global s_offset_default
+	s_offset_default = s
+
+
+class Timestamp(CoreElement):
+	def __init__(self, tag_name, time=0.0):
+		CoreElement.__init__(self, tag_name, None, None)
+		self.lock = False
+		self.time = time
+		self.time_offset = s_offset_default
+		self.lock = True
+
+	def __delattr__(self, name):
+		CoreElement.__delattr_(self, name)
+
+	def __getattr__(self, name):
+		if name == "text":
+			return s_timestamp(self.time, self.time_offset)
+		return CoreElement.__getattr_(self, name)
+
+	def __setattr__(self, name, value):
+		if name == "text":
+			if type(value) != type(""):
+				raise TypeError, "can only assign a string to .text"
+			t = utc_time_from_s_timestamp(value)
+			if t:
+				self.time = utc_time_from_s_timestamp(value)
+			else:
+				raise ValueError, "value must be a valid timestamp string"
+			return
+		CoreElement.__setattr__(self, name, value)
+
+	def has_contents(self):
+		return self.time != 0
+
+	def multiline_contents(self):
+		return False
+
+	def s_contents(self, tfc):
+		return s_timestamp(self.time, self.time_offset)
+
+	def update(self):
+		self.time = local_time()
+		return self
+
+
+
+
+# Below are all the classes to implement Atom using the above tools.
+
+
+
+class AtomText(TextElement):
+	def __init__(self, tag_name):
+		attr_names = [ s_type ]
+		# legal values of type: "text", "html", "xhtml"
+		TextElement.__init__(self, tag_name, None, None, attr_names)
+
+class Title(AtomText):
+	def __init__(self, text=""):
+		AtomText.__init__(self, "title")
+		self.text = text
+		
+class Subtitle(AtomText):
+	def __init__(self, text=""):
+		AtomText.__init__(self, "subtitle")
+		self.text = text
+		
+class Content(AtomText):
+	def __init__(self, text=""):
+		AtomText.__init__(self, "content")
+		self.text = text
+		
+class Summary(AtomText):
+	def __init__(self, text=""):
+		AtomText.__init__(self, "summary")
+		self.text = text
+		
+class Rights(AtomText):
+	def __init__(self, text=""):
+		AtomText.__init__(self, "rights")
+		self.text = text
+		
+class Id(TextElement):
+	def __init__(self, text=""):
+		TextElement.__init__(self, "id", None, None)
+		self.text = text
+		
+class Generator(TextElement):
+	def __init__(self):
+		attr_names = [ "uri", "version" ]
+		TextElement.__init__(self, "generator", None, None, attr_names)
+		
+class Category(TextElement):
+	def __init__(self, term_val=""):
+		attr_names = [s_term, "scheme", "label"]
+		TextElement.__init__(self, "category", s_term, term_val, attr_names)
+
+class Link(TextElement):
+	def __init__(self, href_val=""):
+		attr_names = [
+				s_href, "rel", "type", "hreflang", "title", "length", s_lang]
+		TextElement.__init__(self, "link", s_href, href_val, attr_names)
+
+class Icon(TextElement):
+	def __init__(self):
+		TextElement.__init__(self, "icon", None, None)
+
+class Logo(TextElement):
+	def __init__(self):
+		TextElement.__init__(self, "logo", None, None)
+
+class Name(TextElement):
+	def __init__(self, text=""):
+		TextElement.__init__(self, "name", None, None)
+		self.text = text
+
+class Email(TextElement):
+	def __init__(self):
+		TextElement.__init__(self, "email", None, None)
+
+class Uri(TextElement):
+	def __init__(self):
+		TextElement.__init__(self, "uri", None, None)
+
+
+
+class BasicAuthor(NestElement):
+	def __init__(self, tag_name, name):
+		NestElement.__init__(self, tag_name, None, None)
+		self.name = Name(name)
+		self.email = Email()
+		self.uri = Uri()
+
+class Author(BasicAuthor):
+	def __init__(self, name=""):
+		BasicAuthor.__init__(self, "author", name)
+
+class Contributor(BasicAuthor):
+	def __init__(self, name=""):
+		BasicAuthor.__init__(self, "contributor", name)
+
+
+
+class Updated(Timestamp):
+	def __init__(self, time=0.0):
+		Timestamp.__init__(self, "updated", time)
+
+class Published(Timestamp):
+	def __init__(self, time=0.0):
+		Timestamp.__init__(self, "published", time)
+
+
+
+class FeedElement(NestElement):
+	def __init__(self, tag_name):
+		NestElement.__init__(self, tag_name, None, None)
+
+		self.title = Title("")
+		self.id = Id("")
+		self.updated = Updated()
+		self.authors = Collection(Author)
+		self.links = Collection(Link)
+
+		self.subtitle = Subtitle("")
+		self.categories = Collection(Category)
+		self.contributors = Collection(Contributor)
+		self.generator = Generator()
+		self.icon = Icon()
+		self.logo = Logo()
+		self.rights = Rights("")
+
+class Feed(FeedElement):
+	def __init__(self):
+		FeedElement.__init__(self, "feed")
+		self.attrs["xmlns"] = "http://www.w3.org/2005/Atom"
+		self.title.text = "Title of Feed Goes Here"
+		self.id.text = "ID of Feed Goes Here"
+		self.entries = Collection(Entry)
+
+class Source(FeedElement):
+	def __init__(self):
+		FeedElement.__init__(self, "source")
+
+
+
+class Entry(NestElement):
+	def __init__(self):
+		NestElement.__init__(self, "entry", None, None)
+		self.title = Title("Title of Entry Goes Here")
+		self.id = Id("ID of Entry Goes Here")
+		self.updated = Updated()
+		self.authors = Collection(Author)
+		self.links = Collection(Link)
+
+		self.content = Content("")
+		self.summary = Summary("")
+		self.categories = Collection(Category)
+		self.contributors = Collection(Contributor)
+		self.published = Published()
+		self.source = Source()
+		self.rights = Rights("")
+
+
+
+def diff(s0, name0, s1, name1):
+	from difflib import ndiff
+	lst0 = s0.split("\n")
+	lst1 = s1.split("\n")
+	report = '\n'.join(ndiff(lst0, lst1))
+	return report
+
+
+def run_test_cases():
+
+	# The default is to make time stamps in local time with appropriate
+	# offset; for our tests, we want a default "Z" offset instead.
+	set_default_time_offset("Z")
+
+	failed_tests = 0
+
+
+	# Test: convert current time into a timestamp string and back
+
+	now = local_time()
+	# timestamp format does not allow fractional seconds
+	now = float(int(now))	# truncate any fractional seconds
+	s = s_timestamp(now)
+	t = utc_time_from_s_timestamp(s)
+	if now != t:
+		failed_tests += 1
+		print "test case failed:"
+		print now, "-- original timestamp"
+		print t, "-- converted timestamp does not match"
+
+
+	# Test: convert a timestamp string to a time value and back
+
+	s_time = "2003-12-13T18:30:02Z"
+	t = utc_time_from_s_timestamp(s_time)
+	s = s_timestamp(t)
+	if s_time != s:
+		failed_tests += 1
+		print "test case failed:"
+		print s_time, "-- original timestamp"
+		print s, "-- converted timestamp does not match"
+
+
+	# Test: generate the "Atom-Powered Robots Run Amok" example
+	#
+	# Note: the original had some of the XML declarations in
+	# a different order than PyAtom puts them.  I swapped around
+	# the lines here so they would match the PyAtom order.  Other
+	# than that, this is the example from:
+	#
+	# http://www.atomenabled.org/developers/syndication/#sampleFeed
+
+	s_example = """\
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+	<title>Example Feed</title>
+	<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+	<updated>2003-12-13T18:30:02Z</updated>
+	<author>
+		<name>John Doe</name>
+	</author>
+	<link href="http://example.org/"/>
+	<entry>
+		<title>Atom-Powered Robots Run Amok</title>
+		<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+		<updated>2003-12-13T18:30:02Z</updated>
+		<link href="http://example.org/2003/12/13/atom03"/>
+		<summary>Some text.</summary>
+	</entry>
+</feed>"""
+
+	xmldoc = XMLDoc()
+	
+	feed = Feed()
+	xmldoc.root_element = feed
+
+	feed.title = "Example Feed"
+	feed.id = "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
+	feed.updated = "2003-12-13T18:30:02Z"
+
+	link = Link("http://example.org/")
+	feed.links.append(link)
+
+	author = Author("John Doe")
+	feed.authors.append(author)
+
+
+	entry = Entry()
+	feed.entries.append(entry)
+	entry.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
+	entry.title = "Atom-Powered Robots Run Amok"
+	entry.updated = "2003-12-13T18:30:02Z"
+	entry.summary = "Some text."
+
+	link = Link("http://example.org/2003/12/13/atom03")
+	entry.links.append(link)
+
+
+	s = str(xmldoc)
+	if s_example != s:
+		failed_tests += 1
+		print "test case failed:"
+		print "The generated XML doesn't match the example.  diff follows:"
+		print diff(s_example, "s_example", s, "s")
+
+
+	# Test: verify that xmldoc.Validate() succeeds
+
+	if not xmldoc.Validate():
+		failed_tests += 1
+		print "test case failed:"
+		print "xmldoc.Validate() failed."
+
+
+	# Test: does Element work both nested an non-nested?
+	s_test = """\
+<test>
+	<test:agent number="007">James Bond</test:agent>
+	<test:pet
+			nickname="Mei-Mei"
+			type="cat">Matrix</test:pet>
+</test>"""
+
+	class TestPet(Element):
+		def __init__(self, name=""):
+			Element.__init__(self, "test:pet", None, None)
+			self.text = name
+
+	class TestAgent(Element):
+		def __init__(self, name=""):
+			Element.__init__(self, "test:agent", None, None)
+			self.text = name
+
+	class Test(Element):
+		def __init__(self):
+			Element.__init__(self, "test", None, None)
+			self.test_agent = TestAgent()
+			self.test_pet = TestPet()
+
+	test = Test()
+	test.test_agent = "James Bond"
+	test.test_agent.attrs["number"] = "007"
+	test.test_pet = "Matrix"
+	test.test_pet.attrs["type"] = "cat"
+	test.test_pet.attrs["nickname"] = "Mei-Mei"
+
+	s = str(test)
+	if s_test != s:
+		failed_tests += 1
+		print "test case failed:"
+		print "test output doesn't match.  diff follows:"
+		print diff(s_test, "s_test", s, "s")
+
+
+	if failed_tests > 0:
+		print "self-test failed!"
+	else:
+		print "self-test successful."
+
+
+
+if __name__ == "__main__":
+	run_test_cases()
diff --git a/auquotidien/modules/pyatom/readme.txt b/auquotidien/modules/pyatom/readme.txt
new file mode 100644
index 0000000..a311805
--- /dev/null
+++ b/auquotidien/modules/pyatom/readme.txt
@@ -0,0 +1,64 @@
+PyAtom
+
+
+PyAtom is a Python library module I wrote to make it very easy to create
+an Atom syndication feed.
+
+http://atomenabled.org/developers/syndication/
+
+
+I have released PyAtom under The Academic Free License 2.1.  I intend to
+donate PyAtom to the Python Software Foundation.
+
+
+Notes on PyAtom:
+
+XML is best represented in a tree structure, and PyAtom is a set of
+classes that automatically manage the tree structure for the user.  The
+top level of an Atom feed is an XML "Feed" element with a "<feed>" tag;
+the Feed element has other elements nested inside it that describe the
+feed, and then it has 0 or more Entry elements, each of which has
+elements that describe the Entry.
+
+Take a look at RunPyAtomTestCases(), at the end of pyatom.py, for
+example code showing how to set up a feed with an entry.
+
+To create an XML document with a feed in it, the user does this:
+
+xmldoc = XMLDoc()
+feed = Feed()
+xmldoc.root_element = feed
+
+To assign an entry to a feed, the user just does this:
+
+feed.entries.append(entry)
+
+This adds "entry" to the internal list that keeps track of entries.
+"entry" is now nested inside "feed", which is nested inside "xmldoc".
+
+Later, when the user wants to save the XML in a file, the user can just
+do this:
+
+f = open("file.xml", "w")
+s = str(xmldoc)
+f.write(s)
+
+To make the string from xmldoc, the XMLDoc class walks through the XML
+elements nested inside xmldoc, asking each one to return its string.
+Each element that has other elements nested inside does the same thing.
+The whole tree is recursively walked, and the tags all return strings
+that are indented properly for their level in the tree.
+
+The classes that implement Atom in PyAtom just use the heck out of
+inheritance.  There are abstract base classes that implement broadly
+useful behavior, and lots of classes that just inherit and use this
+behavior; but there are plenty of places where the child classes
+overload the inherited behavior and do something different.  The way
+Python handles inheritance made this a joy to code up.
+
+
+
+If you have any questions about anything here, please contact me using
+this email address:
+
+pyatom@langri.com
diff --git a/auquotidien/modules/root.py b/auquotidien/modules/root.py
new file mode 100644
index 0000000..da39a6d
--- /dev/null
+++ b/auquotidien/modules/root.py
@@ -0,0 +1,1312 @@
+from quixote import get_publisher, get_response, get_request, redirect, get_session
+from quixote.directory import Directory
+from quixote.html import TemplateIO, htmltext
+
+from wcs.qommon.misc import get_variadic_url, simplify
+
+import os
+import re
+import string
+import urlparse
+
+try:
+    import lasso
+except ImportError:
+    pass
+
+import wcs
+import wcs.root
+import qommon
+from qommon import _
+from qommon import get_cfg, get_logger
+from qommon import template
+from qommon import errors
+from qommon.form import *
+from qommon import logger
+from wcs.roles import logged_users_role
+
+from qommon import emails
+from qommon.sms import SMS
+from wcs.categories import Category
+from wcs.formdef import FormDef
+from wcs.data_sources import NamedDataSource
+from qommon.tokens import Token
+from qommon.admin.emails import EmailsDirectory
+from qommon.admin.texts import TextsDirectory
+
+from links import Link
+from announces import Announce, AnnounceSubscription
+from myspace import MyspaceDirectory
+from agenda import AgendaDirectory
+from events import Event, get_default_event_tags
+from payments import PublicPaymentDirectory
+from payments_ui import InvoicesDirectory
+
+import admin
+
+import wcs.forms.root
+from wcs.workflows import Workflow
+from wcs.forms.preview import PreviewDirectory
+
+from saml2 import Saml2Directory
+
+OldRootDirectory = wcs.root.RootDirectory
+
+import qommon.ident.password
+import qommon.ident.idp
+
+
+def category_get_homepage_position(self):
+    if hasattr(self, 'homepage_position') and self.homepage_position:
+        return self.homepage_position
+    if self.url_name == 'consultations':
+        return '2nd'
+    return '1st'
+Category.get_homepage_position = category_get_homepage_position
+
+def category_get_limit(self):
+    if hasattr(self, 'limit') and self.limit is not None:
+        return self.limit
+    return 7
+Category.get_limit = category_get_limit
+
+Category.TEXT_ATTRIBUTES = ['name', 'url_name', 'description', 'homepage_position']
+Category.INT_ATTRIBUTES = ['position', 'limit']
+
+
+class FormsRootDirectory(wcs.forms.root.RootDirectory):
+
+    def _q_index(self, *args):
+        get_response().filter['is_index'] = True
+        return wcs.forms.root.RootDirectory._q_index(self, *args)
+
+    def user_forms(self, user_forms):
+        r = TemplateIO(html=True)
+        base_url = get_publisher().get_root_url()
+
+        draft = [x for x in user_forms if x.is_draft() and not x.formdef.is_disabled()]
+        if draft:
+            r += htmltext('<h4 id="drafts">%s</h4>') % _('My Current Drafts')
+            r += htmltext('<ul>')
+            for f in draft:
+                if f.formdef.category:
+                    category_url = '%s' % f.formdef.category.url_name
+                else:
+                    category_url = '.'
+                r += htmltext('<li><a href="%s%s/%s/%s">%s</a>, %s') % (base_url,
+                    category_url,
+                    f.formdef.url_name, f.id, f.formdef.name,
+                    misc.localstrftime(f.receipt_time))
+                r += htmltext(' (<a href="%s%s/%s/%s?remove-draft">%s</a>)') % (base_url,
+                    category_url,
+                    f.formdef.url_name, f.id, _('delete'))
+                r += htmltext('</li>')
+            r += htmltext('</ul>')
+
+        forms_by_status_name = {}
+        for f in user_forms:
+            if f.is_draft():
+                continue
+            status = f.get_visible_status()
+            if status:
+                status_name = status.name
+            else:
+                status_name = None
+            if status_name in forms_by_status_name:
+                forms_by_status_name[status_name].append(f)
+            else:
+                forms_by_status_name[status_name] = [f]
+        for status_name in forms_by_status_name:
+            if status_name:
+                r += htmltext('<h4>%s</h4>') % _('My forms with status "%s"') % status_name
+            else:
+                r += htmltext('<h4>%s</h4>') % _('My forms with an unknown status') % status_name
+            r += htmltext('<ul>')
+            forms_by_status_name[status_name].sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
+            for f in forms_by_status_name[status_name]:
+                if f.formdef.category_id:
+                    category_url = f.formdef.category.url_name
+                else:
+                    category_url = '.'
+                r += htmltext('<li><a href="%s%s/%s/%s/">%s</a>, %s</li>') % (
+                        base_url,
+                        category_url,
+                        f.formdef.url_name, f.id, f.formdef.name, 
+                        misc.localstrftime(f.receipt_time))
+            r += htmltext('</ul>')
+        return r.getvalue()
+
+
+class AnnounceDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete', 'email']
+
+    def __init__(self, announce):
+        self.announce = announce
+
+    def _q_index(self):
+        template.html_top(_('Announces to citizens'))
+        r = TemplateIO(html=True)
+
+        if self.announce.publication_time:
+            date_heading = '%s - ' % time.strftime(misc.date_format(), self.announce.publication_time)
+        else:
+            date_heading = ''
+
+        r += htmltext('<h3>%s%s</h3>') % (date_heading, self.announce.title)
+
+        r += htmltext('<p>')
+        r += self.announce.text
+        r += htmltext('</p>')
+
+        r += htmltext('<p>')
+        r += htmltext('<a href="../">%s</a>') % _('Back')
+        r += htmltext('</p>')
+        return r.getvalue()
+
+
+class AnnouncesDirectory(Directory):
+    _q_exports = ['', 'subscribe', 'email', 'atom', 'sms', 'emailconfirm',
+            'email_unsubscribe', 'sms_unsubscribe', 'smsconfirm', 'rawlist']
+
+    def _q_traverse(self, path):
+        get_response().breadcrumb.append(('announces/', _('Announces')))
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        template.html_top(_('Announces to citizens'))
+        r = TemplateIO(html=True)
+        r += self.announces_list()
+        r += htmltext('<ul id="announces-links">')
+        r += htmltext('<li><a href="subscribe">%s</a></li>') % _('Receiving those Announces')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def _get_announce_subscription(self):
+        """ """
+        sub = None
+        if get_request().user:
+            subs = AnnounceSubscription.select(lambda x: x.user_id == get_request().user.id)
+            if subs:
+                sub = subs[0]
+        return sub
+
+    def rawlist(self):
+        get_response().filter = None
+        return self.announces_list()
+
+    def announces_list(self):
+        announces = Announce.get_published_announces()
+        if not announces:
+            raise errors.TraversalError()
+
+        # XXX: will need pagination someday
+        r = TemplateIO(html=True)
+        for item in announces:
+            r += htmltext('<div class="announce-item">\n')
+            r += htmltext('<h4>')
+            if item.publication_time:
+                r += time.strftime(misc.date_format(), item.publication_time)
+                r += ' - '
+            r += item.title
+            r += htmltext('</h4>\n')
+            r += htmltext('<p>\n')
+            r += item.text
+            r += htmltext('\n</p>\n')
+            r += htmltext('</div>\n')
+        return r.getvalue()
+
+
+    def sms(self):
+        sms_mode = get_cfg('sms', {}).get('mode', 'none')
+
+        if sms_mode == 'none':
+            raise errors.TraversalError()
+
+        get_response().breadcrumb.append(('sms', _('SMS')))
+        template.html_top(_('Receiving announces by SMS'))
+        r = TemplateIO(html=True)
+
+        if sms_mode == 'demo':
+            r += TextsDirectory.get_html_text('aq-sms-demo')
+        else:
+            announces_cfg = get_cfg('announces',{})
+            mobile_mask = announces_cfg.get('mobile_mask')
+            if mobile_mask:
+                mobile_mask = ' (' + mobile_mask + ')'
+            else:
+                mobile_mask = ''
+            form = Form(enctype='multipart/form-data')
+            form.add(StringWidget, 'mobile', title = _('Mobile number %s') % mobile_mask, size=12, required=True)
+            form.add_submit('submit', _('Subscribe'))
+            form.add_submit('cancel', _('Cancel'))
+
+            if form.get_submit() == 'cancel':
+                return redirect('subscribe')
+
+            if form.is_submitted() and not form.has_errors():
+                s = self.sms_submit(form)
+                if s == False:
+                    r += form.render()
+                else:
+                    return redirect("smsconfirm")
+            else:
+                r += form.render()
+        return r.getvalue()
+
+    def sms_submit(self, form):
+        mobile = form.get_widget("mobile").parse()
+        # clean the string, remove any extra character
+        mobile = re.sub('[^0-9+]','',mobile)
+        # if a mask was set, validate
+        announces_cfg = get_cfg('announces',{})
+        mobile_mask = announces_cfg.get('mobile_mask')
+        if mobile_mask:
+            mobile_regexp = re.sub('X','[0-9]', mobile_mask) + '$'
+            if not re.match(mobile_regexp, mobile):
+                form.set_error("mobile", _("Phone number invalid ! It must match ") + mobile_mask)
+                return False
+        if mobile.startswith('00'):
+            mobile = '+' + mobile[2:]
+        else:
+            # Default to france international prefix
+            if not mobile.startswith('+'):
+                mobile = re.sub("^0", "+33", mobile)
+        sub = self._get_announce_subscription()
+        if not sub:
+            sub = AnnounceSubscription()
+        if get_request().user:
+            sub.user_id = get_request().user.id
+
+        if mobile:
+            sub.sms = mobile
+
+        if not get_request().user:
+            sub.enabled = False
+
+        sub.store()
+
+        # Asking sms confirmation
+        token = Token(3 * 86400, 4, string.digits)
+        token.type = 'announces-subscription-confirmation'
+        token.subscription_id = sub.id
+        token.store()
+
+        message = _("Confirmation code : %s") % str(token.id)
+        sms_cfg = get_cfg('sms', {})
+        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
+        mode = sms_cfg.get('mode', 'none')
+        sms = SMS.get_sms_class(mode)
+        try:
+            sms.send(sender, [mobile], message)
+        except errors.SMSError, e:
+            get_logger().error(e)
+            form.set_error("mobile", _("Send SMS confirmation failed"))
+            sub.remove("sms")
+            return False
+
+    def smsconfirm(self):
+        template.html_top(_('Receiving announces by SMS confirmation'))
+        r = TemplateIO(html=True)
+        r += htmltext("<p>%s</p>") % _("You will receive a confirmation code by SMS.")
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'code', title = _('Confirmation code (4 characters)'), size=12, required=True)
+        form.add_submit('submit', _('Subscribe'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+
+        if form.is_submitted() and not form.has_errors():
+            token = None
+            id = form.get_widget("code").parse()
+            try:
+                token = Token.get(id)
+            except KeyError:
+                form.set_error("code",  _('Invalid confirmation code.'))
+            else:
+                if token.type != 'announces-subscription-confirmation':
+                    form.set_error("code",  _('Invalid confirmation code.'))
+                else:
+                    sub = AnnounceSubscription.get(token.subscription_id)
+                    token.remove_self()
+                    sub.enabled_sms = True
+                    sub.store()
+                    return redirect('.')
+            r += form.render()
+        else:
+            r += form.render()
+
+        return r.getvalue()
+
+    def sms_unsubscribe(self):
+        sub = self._get_announce_subscription()
+
+        form = Form(enctype='multipart/form-data')
+        if not sub:
+            return redirect('..')
+
+        form.add_submit('submit', _('Unsubscribe'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+
+        get_response().breadcrumb.append(('sms', _('SMS Unsubscription')))
+        template.html_top()
+        r = TemplateIO(html=True)
+
+        if form.is_submitted() and not form.has_errors():
+            if sub:
+                sub.remove("sms")
+
+            root_url = get_publisher().get_root_url()
+            r += htmltext('<p>')
+            r += _('You have been unsubscribed from announces')
+            r += htmltext('</p>')
+            if not get_response().iframe_mode:
+                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
+        else:
+            r += htmltext('<p>')
+            r += _('Do you want to stop receiving announces by sms ?')
+            r += htmltext('</p>')
+            r += form.render()
+
+        return r.getvalue()
+
+
+    def subscribe(self):
+        get_response().breadcrumb.append(('subscribe', _('Subscription')))
+        template.html_top(_('Receiving Announces'))
+        r = TemplateIO(html=True)
+
+        r += TextsDirectory.get_html_text('aq-announces-subscription')
+
+        sub = self._get_announce_subscription()
+
+        r += htmltext('<ul id="announce-modes">')
+        if sub and sub.email:
+            r += htmltext(' <li>')
+            r += htmltext('<span id="par_mail">%s</span>') % _('Email (currently subscribed)')
+            r += htmltext(' <a href="email_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
+        else:
+            r += htmltext(' <li><a href="email" id="par_mail" rel="popup">%s</a></li>') % _('Email')
+        if sub and sub.sms:
+            r += htmltext(' <li>')
+            if sub.enabled_sms:
+                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently subscribed)') % sub.sms
+            else:
+                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently not confirmed)') % sub.sms
+                r += htmltext(' <a href="smsconfirm" rel="popup">%s</a> ') % _('Confirmation')
+            r += htmltext(' <a href="sms_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
+        elif get_cfg('sms', {}).get('mode', 'none') != 'none':
+            r += htmltext(' <li><a href="sms" id="par_sms">%s</a></li>') % _('SMS')
+        r += htmltext(' <li><a class="feed-link" href="atom" id="par_rss">%s</a>') % _('Feed')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def email(self):
+        get_response().breadcrumb.append(('email', _('Email Subscription')))
+        template.html_top(_('Receiving Announces by email'))
+        r = TemplateIO(html=True)
+
+        form = Form(enctype='multipart/form-data')
+        if get_request().user:
+            if get_request().user.email:
+                r += htmltext('<p>')
+                r += _('You are logged in and your email is %s, ok to subscribe ?') % \
+                        get_request().user.email
+                r += htmltext('</p>')
+                form.add_submit('submit', _('Subscribe'))
+            else:
+                r += htmltext('<p>')
+                r += _("You are logged in but there is no email address in your profile.")
+                r += htmltext('</p>')
+                form.add(EmailWidget, 'email', title = _('Email'), required = True)
+                form.add_submit('submit', _('Subscribe'))
+                form.add_submit('submit-remember', _('Subscribe and add this email to my profile'))
+        else:
+            r += htmltext('<p>')
+            r += _('FIXME will only be used for this purpose etc.')
+            r += htmltext('</p>')
+            form.add(EmailWidget, 'email', title = _('Email'), required = True)
+            form.add_submit('submit', _('Subscribe'))
+
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('subscribe')
+
+        if form.is_submitted() and not form.has_errors():
+            s = self.email_submit(form)
+            if s is not False:
+                return s
+        else:
+            r += form.render()
+
+        return r.getvalue()
+
+    def email_submit(self, form):
+        sub = self._get_announce_subscription()
+        if not sub:
+            sub = AnnounceSubscription()
+
+        if get_request().user:
+            sub.user_id = get_request().user.id
+
+        if form.get_widget('email'):
+            sub.email = form.get_widget('email').parse()
+        elif get_request().user.email:
+            sub.email = get_request().user.email
+
+        if not get_request().user:
+            sub.enabled = False
+
+        sub.store()
+
+        if get_request().user:
+            r = TemplateIO(html=True)
+            root_url = get_publisher().get_root_url()
+            r += htmltext('<p>')
+            r += _('You have been subscribed to the announces.')
+            r += htmltext('</p>')
+            if not get_response().iframe_mode:
+                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
+            return r.getvalue()
+
+        # asking email confirmation before subscribing someone
+        token = Token(3 * 86400)
+        token.type = 'announces-subscription-confirmation'
+        token.subscription_id = sub.id
+        token.store()
+        data = {
+            'confirm_url': get_request().get_url() + 'confirm?t=%s&a=cfm' % token.id,
+            'cancel_url': get_request().get_url() + 'confirm?t=%s&a=cxl' % token.id,
+            'time': misc.localstrftime(time.localtime(token.expiration)),
+        }
+
+        emails.custom_ezt_email('announces-subscription-confirmation',
+                data, sub.email, exclude_current_user = False)
+
+        r = TemplateIO(html=True)
+        root_url = get_publisher().get_root_url()
+        r += htmltext('<p>')
+        r += _('You have been sent an email for confirmation')
+        r += htmltext('</p>')
+        if not get_response().iframe_mode:
+            r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
+        return r.getvalue()
+
+    def emailconfirm(self):
+        tokenv = get_request().form.get('t')
+        action = get_request().form.get('a')
+
+        root_url = get_publisher().get_root_url()
+
+        try:
+            token = Token.get(tokenv)
+        except KeyError:
+            return template.error_page(
+                    _('The token you submitted does not exist, has expired, or has been cancelled.'),
+                    continue_to = (root_url, _('home page')))
+
+        if token.type != 'announces-subscription-confirmation':
+            return template.error_page(
+                    _('The token you submitted is not appropriate for the requested task.'),
+                    continue_to = (root_url, _('home page')))
+
+        sub = AnnounceSubscription.get(token.subscription_id)
+
+        if action == 'cxl':
+            r = TemplateIO(html=True)
+            root_url = get_publisher().get_root_url()
+            template.html_top(_('Email Subscription'))
+            r += htmltext('<h1>%s</h1>') % _('Request Cancelled')
+            r += htmltext('<p>%s</p>') % _('The request for subscription has been cancelled.')
+            r += htmltext('<p>')
+            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
+            r += htmltext('</p>')
+            token.remove_self()
+            sub.remove_self()
+            return r.getvalue()
+
+        if action == 'cfm':
+            token.remove_self()
+            sub.enabled = True
+            sub.store()
+            r = TemplateIO(html=True)
+            root_url = get_publisher().get_root_url()
+            template.html_top(_('Email Subscription'))
+            r += htmltext('<h1>%s</h1>') % _('Subscription Confirmation')
+            r += htmltext('<p>%s</p>') % _('Your subscription to announces is now effective.')
+            r += htmltext('<p>')
+            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
+            r += htmltext('</p>')
+            return r.getvalue()
+
+    def atom(self):
+        response = get_response()
+        response.set_content_type('application/atom+xml')
+
+        from pyatom import pyatom
+        xmldoc = pyatom.XMLDoc()
+        feed = pyatom.Feed()
+        xmldoc.root_element = feed
+        feed.title = get_cfg('misc', {}).get('sitename') or 'Publik'
+        feed.id = get_request().get_url()
+
+        author_email = get_cfg('emails', {}).get('reply_to')
+        if not author_email:
+            author_email = get_cfg('emails', {}).get('from')
+        if author_email:
+            feed.authors.append(pyatom.Author(author_email))
+
+        announces = Announce.get_published_announces()
+
+        if announces and announces[0].modification_time:
+            feed.updated = misc.format_time(announces[0].modification_time,
+                        '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
+                        gmtime = True)
+        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
+
+        for item in announces:
+            entry = item.get_atom_entry()
+            if entry:
+                feed.entries.append(entry)
+
+        return str(feed)
+
+    def email_unsubscribe(self):
+        sub = self._get_announce_subscription()
+
+        form = Form(enctype='multipart/form-data')
+        if not sub:
+            form.add(EmailWidget, 'email', title = _('Email'), required = True)
+
+        form.add_submit('submit', _('Unsubscribe'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+
+        get_response().breadcrumb.append(('email', _('Email Unsubscription')))
+        template.html_top()
+        r = TemplateIO(html=True)
+
+        if form.is_submitted() and not form.has_errors():
+            if sub:
+                sub.remove("email")
+            else:
+                email = form.get_widget('email').parse()
+                for s in AnnounceSubscription.select():
+                    if s.email == email:
+                        s.remove("email")
+
+            root_url = get_publisher().get_root_url()
+            r += htmltext('<p>')
+            r += _('You have been unsubscribed from announces')
+            r += htmltext('</p>')
+            if not get_response().iframe_mode:
+                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
+
+        else:
+            r += htmltext('<p>')
+            r += _('Do you want to stop receiving announces by email?')
+            r += htmltext('</p>')
+            r += form.render()
+
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            announce = Announce.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+
+        if announce.hidden:
+            raise errors.TraversalError()
+
+        get_response().breadcrumb.append((str(announce.id), announce.title))
+        return AnnounceDirectory(announce)
+
+OldRegisterDirectory = wcs.root.RegisterDirectory
+
+class AlternateRegisterDirectory(OldRegisterDirectory):
+    def _q_traverse(self, path):
+        get_response().filter['bigdiv'] = 'new_member'
+        return OldRegisterDirectory._q_traverse(self, path)
+
+    def _q_index(self):
+        get_logger().info('register')
+        ident_methods = get_cfg('identification', {}).get('methods', [])
+
+        if len(ident_methods) == 0:
+            idps = get_cfg('idp', {})
+            if len(idps) == 0:
+                return template.error_page(_('Authentication subsystem is not yet configured.'))
+            ident_methods = ['idp'] # fallback to old behaviour; saml.
+
+        if len(ident_methods) == 1:
+            method = ident_methods[0]
+        else:
+            method = 'password'
+
+        return qommon.ident.register(method)
+
+OldLoginDirectory = wcs.root.LoginDirectory
+
+class AlternateLoginDirectory(OldLoginDirectory):
+    def _q_traverse(self, path):
+        get_response().filter['bigdiv'] = 'member'
+        return OldLoginDirectory._q_traverse(self, path)
+
+    def _q_index(self):
+        get_logger().info('login')
+        ident_methods = get_cfg('identification', {}).get('methods', [])
+
+        if get_request().form.get('ReturnUrl'):
+            get_request().form['next'] = get_request().form.pop('ReturnUrl')
+
+        if 'IsPassive' in get_request().form and 'idp' in ident_methods:
+            # if isPassive is given in query parameters, we restrict ourselves
+            # to saml login.
+            ident_methods = ['idp']
+
+        if len(ident_methods) > 1 and 'idp' in ident_methods:
+            # if there is more than one identification method, and there is a
+            # possibility of SSO, if we got there as a consequence of an access
+            # unauthorized url on admin/ or backoffice/, then idp auth method
+            # is chosen forcefully.
+            after_url = get_request().form.get('next')
+            if after_url:
+                root_url = get_publisher().get_root_url()
+                after_path = urlparse.urlparse(after_url)[2]
+                after_path = after_path[len(root_url):]
+                if after_path.startswith(str('admin')) or \
+                        after_path.startswith(str('backoffice')):
+                    ident_methods = ['idp']
+
+        # don't display authentication system choice
+        if len(ident_methods) == 1:
+            method = ident_methods[0]
+            try:
+                return qommon.ident.login(method)
+            except KeyError:
+                get_logger().error('failed to login with method %s' % method)
+                return errors.TraversalError()
+
+        if sorted(ident_methods) == ['idp', 'password']:
+            r = TemplateIO(html=True)
+            get_response().breadcrumb.append(('login', _('Login')))
+            identities_cfg = get_cfg('identities', {})
+            form = Form(enctype = 'multipart/form-data', id = 'login-form', use_tokens = False)
+            if identities_cfg.get('email-as-username', False):
+                form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
+            else:
+                form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
+            form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
+            form.add_submit('submit', _('Connect'))
+            if form.is_submitted() and not form.has_errors():
+                tmp = qommon.ident.password.MethodDirectory().login_submit(form)
+                if not form.has_errors():
+                    return tmp
+
+            r += htmltext('<div id="login-password">')
+            r += get_session().display_message()
+            r += form.render()
+
+            base_url = get_publisher().get_root_url()
+            r += htmltext('<p><a href="%sident/password/forgotten">%s</a></p>') % (
+                    base_url, _('Forgotten password ?'))
+
+            r += htmltext('</div>')
+
+            # XXX: this part only supports a single IdP
+            r += htmltext('<div id="login-sso">')
+            r += TextsDirectory.get_html_text('aq-sso-text')
+            form = Form(enctype='multipart/form-data',
+                    action = '%sident/idp/login' % base_url)
+            form.add_hidden('method', 'idp')
+            for kidp, idp in get_cfg('idp', {}).items():
+                p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
+                        misc.get_abs_path(idp['metadata']),
+                        misc.get_abs_path(idp.get('publickey')), None)
+                form.add_hidden('idp', p.providerId)
+                break
+            form.add_submit('submit', _('Connect'))
+
+            r += form.render()
+            r += htmltext('</div>')
+
+            get_request().environ['REQUEST_METHOD'] = 'GET'
+
+            r += htmltext("""<script type="text/javascript">
+              document.getElementById('login-form')['username'].focus();
+            </script>""")
+            return r.getvalue()
+        else:
+            return OldLoginDirectory._q_index(self)
+
+
+OldIdentDirectory = wcs.root.IdentDirectory
+class AlternateIdentDirectory(OldIdentDirectory):
+    def _q_traverse(self, path):
+        get_response().filter['bigdiv'] = 'member'
+        return OldIdentDirectory._q_traverse(self, path)
+
+
+class AlternatePreviewDirectory(PreviewDirectory):
+    def _q_traverse(self, path):
+        get_response().filter['bigdiv'] = 'rub_service'
+        return super(AlternatePreviewDirectory, self)._q_traverse(path)
+
+
+class AlternateRootDirectory(OldRootDirectory):
+    _q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout',
+            'saml', 'register', 'ident', 'afterjobs',
+            ('informations-editeur', 'informations_editeur'),
+            ('announces', 'announces_dir'),
+            'accessibility', 'contact', 'help',
+            'myspace', 'services', 'agenda', 'categories', 'user',
+            ('tmp-upload', 'tmp_upload'), 'json', '__version__',
+            'themes', 'pages', 'payment', 'invoices', 'roles',
+            'api', 'code', 'fargo', 'tryauth', 'auth', 'preview',
+            ('reload-top', 'reload_top'), 'static',
+            ('i18n.js', 'i18n_js')]
+
+    admin = admin.AdminRootDirectory()
+    announces_dir = AnnouncesDirectory()
+    register = AlternateRegisterDirectory()
+    login = AlternateLoginDirectory()
+    ident = AlternateIdentDirectory()
+    myspace = MyspaceDirectory()
+    agenda = AgendaDirectory()
+    saml = Saml2Directory()
+    payment = PublicPaymentDirectory()
+    invoices = InvoicesDirectory()
+    code = wcs.forms.root.TrackingCodesDirectory()
+    preview = AlternatePreviewDirectory()
+
+    def get_substitution_variables(self):
+        d = {}
+        def print_links(fd):
+            fd.write(str(self.links()))
+        d['links'] = print_links
+        return d
+
+    def _q_traverse(self, path):
+        self.feed_substitution_parts()
+
+        # set app_label to Publik if none was specified (this is used in
+        # backoffice header top line)
+        if not get_publisher().get_site_option('app_label'):
+            if not get_publisher().site_options.has_section('options'):
+                get_publisher().site_options.add_section('options')
+            get_publisher().site_options.set('options', 'app_label', 'Publik')
+
+        response = get_response()
+        if not hasattr(response, 'filter'):
+            response.filter = {}
+
+        response.filter['auquotidien'] = True
+        response.filter['gauche'] = self.box_side(path)
+        response.filter['keywords'] = template.get_current_theme().get('keywords')
+        get_publisher().substitutions.feed(self)
+
+        response.breadcrumb = [ ('', _('Home')) ]
+
+        if not self.admin:
+            self.admin = get_publisher().admin_directory_class()
+
+        if not self.backoffice:
+            self.backoffice = get_publisher().backoffice_directory_class()
+
+        try:
+            return Directory._q_traverse(self, path)
+        except errors.TraversalError, e:
+            try:
+                f = FormDef.get_by_urlname(path[0])
+            except KeyError:
+                pass
+            else:
+                base_url = get_publisher().get_root_url()
+
+                uri_rest = get_request().environ.get('REQUEST_URI')
+                if not uri_rest:
+                    # REQUEST_URI doesn't exist when using internal HTTP server
+                    # (--http)
+                    uri_rest = get_request().get_path()
+                    if get_request().get_query():
+                        uri_rest += '?' + get_request().get_query()
+                if uri_rest.startswith(base_url):
+                    uri_rest = uri_rest[len(base_url):]
+                if f.category:
+                    return redirect('%s%s/%s' % (base_url, f.category.url_name, uri_rest))
+
+            raise e
+
+
+    def _q_lookup(self, component):
+        # is this a category ?
+        try:
+            category = Category.get_by_urlname(component)
+        except KeyError:
+            pass
+        else:
+            return FormsRootDirectory(category)
+
+        # is this a formdef ?
+        try:
+            formdef = FormDef.get_by_urlname(component)
+        except KeyError:
+            pass
+        else:
+            # if there's no category, or the request is a POST, directly call
+            # into FormsRootDirectory.
+            if formdef.category_id is None or get_request().get_method() == 'POST':
+                get_response().filter['bigdiv'] = 'rub_service'
+                return FormsRootDirectory()._q_lookup(component)
+
+            # if there is category, let it fall back to raise TraversalError,
+            # it will get caught in _q_traverse that will redirect it to an
+            # URL embedding the category
+
+        return None
+
+    def json(self):
+        return FormsRootDirectory().json()
+
+    def categories(self):
+        return FormsRootDirectory().categories()
+
+    def _q_index(self):
+        if get_request().is_json():
+            return FormsRootDirectory().json()
+
+        root_url = get_publisher().get_root_url()
+        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
+            return redirect('%smyspace/new' % root_url)
+
+        redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
+        if redirect_url:
+            return redirect(misc.get_variadic_url(redirect_url,
+                get_publisher().substitutions.get_context_variables()))
+
+        if get_response().iframe_mode:
+            # never display home page in an iframe
+            return redirect('%sservices' % root_url)
+
+        template.html_top()
+        r = TemplateIO(html=True)
+        get_response().filter['is_index'] = True
+
+        if not 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
+            t = TextsDirectory.get_html_text('aq-home-page')
+            if not t:
+                if get_request().user:
+                    t = TextsDirectory.get_html_text('welcome-logged')
+                else:
+                    t = TextsDirectory.get_html_text('welcome-unlogged')
+            if t:
+                r += htmltext('<div id="home-page-intro">')
+                r += t
+                r += htmltext('</div>')
+
+        r += htmltext('<div id="centre">')
+        r += self.box_services(position='1st')
+        r += htmltext('</div>')
+        r += htmltext('<div id="droite">')
+        r += self.myspace_snippet()
+        r += self.box_services(position='2nd')
+        r += self.consultations()
+        r += self.announces()
+        r += htmltext('</div>')
+
+        user = get_request().user
+        if user and user.can_go_in_backoffice():
+            get_response().filter['backoffice'] = True
+
+        return r.getvalue()
+
+    def services(self):
+        template.html_top()
+        get_response().filter['bigdiv'] = 'rub_service'
+        return self.box_services(level = 2)
+
+    def box_services(self, level=3, position=None):
+        ## Services
+        if get_request().user and get_request().user.roles:
+            accepted_roles = get_request().user.roles
+        else:
+            accepted_roles = []
+
+        cats = Category.select(order_by = 'name')
+        cats = [x for x in cats if x.url_name != 'consultations']
+        Category.sort_by_position(cats)
+
+        all_formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
+                order_by = 'name')
+        if get_response().page_template_key == 'mobile':
+            # if we are in 'mobile' mode, and some formdefs have a 'mobile'
+            # keyword, we limit the display to those
+            if any((x for x in all_formdefs if x.keywords and 'mobile' in x.keywords)):
+                all_formdefs = [x for x in all_formdefs if x.keywords and 'mobile' in x.keywords]
+
+        if position:
+            t = self.display_list_of_formdefs(
+                            [x for x in cats if x.get_homepage_position() == position],
+                            all_formdefs, accepted_roles)
+        else:
+            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
+
+        if not t:
+            return
+
+        r = TemplateIO(html=True)
+
+        if position == '2nd':
+            r += htmltext('<div id="services-2nd">')
+        else:
+            r += htmltext('<div id="services">')
+        if level == 2:
+            r += htmltext('<h2>%s</h2>') % _('Services')
+        else:
+            r += htmltext('<h3>%s</h3>') % _('Services')
+
+        if get_response().iframe_mode:
+            if get_request().user:
+                message = TextsDirectory.get_html_text('welcome-logged')
+            else:
+                message = TextsDirectory.get_html_text('welcome-unlogged')
+
+            if message:
+                r += htmltext('<div id="welcome-message">')
+                r += message
+                r += htmltext('</div>')
+        elif 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
+            homepage_text = TextsDirectory.get_html_text('aq-home-page')
+            if homepage_text:
+                r += htmltext('<div id="home-page-intro">')
+                r += homepage_text
+                r += htmltext('</div>')
+
+        r += htmltext('<ul>')
+        r += t
+        r += htmltext('</ul>')
+
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
+        r = TemplateIO(html=True)
+        for category in cats:
+            if category.url_name == 'consultations':
+                self.consultations_category = category
+                continue
+            formdefs = [x for x in all_formdefs if str(x.category_id) == str(category.id)]
+            formdefs_advertise = []
+
+            for formdef in formdefs[:]:
+                if formdef.is_disabled(): # is a redirection
+                    continue
+                if not formdef.roles:
+                    continue
+                if not get_request().user:
+                    if formdef.always_advertise:
+                        formdefs_advertise.append(formdef)
+                    formdefs.remove(formdef)
+                    continue
+                if logged_users_role().id in formdef.roles:
+                    continue
+                for q in accepted_roles:
+                    if q in formdef.roles:
+                        break
+                else:
+                    if formdef.always_advertise:
+                        formdefs_advertise.append(formdef)
+                    formdefs.remove(formdef)
+
+            if not formdefs and not formdefs_advertise:
+                continue
+
+            keywords = {}
+            for formdef in formdefs:
+                for keyword in formdef.keywords_list:
+                    keywords[keyword] = True
+
+            r += htmltext('<li id="category-%s" data-keywords="%s">') % (
+                    category.url_name, ' '.join(keywords))
+            r += htmltext('<strong>')
+            r += htmltext('<a href="%s/">') % category.url_name
+            r += category.name
+            r += htmltext('</a></strong>\n')
+            r += category.get_description_html_text()
+            r += htmltext('<ul>')
+            limit = category.get_limit()
+            for formdef in formdefs[:limit]:
+                r += htmltext('<li data-keywords="%s">') % ' '.join(formdef.keywords_list)
+                classes = []
+                if formdef.is_disabled() and formdef.disabled_redirection:
+                    classes.append('redirection')
+                r += htmltext('<a class="%s" href="%s/%s/">%s</a>') % (
+                        ' '.join(classes), category.url_name, formdef.url_name, formdef.name)
+                r += htmltext('</li>\n')
+            if len(formdefs) < limit:
+                for formdef in formdefs_advertise[:limit-len(formdefs)]:
+                    r += htmltext('<li class="required-authentication">')
+                    r += htmltext('<a href="%s/%s/">%s</a>') % (category.url_name, formdef.url_name, formdef.name)
+                    r += htmltext('<span> (%s)</span>') % _('authentication required')
+                    r += htmltext('</li>\n')
+            if (len(formdefs)+len(formdefs_advertise)) > limit:
+                r += htmltext('<li class="all-forms"><a href="%s/" title="%s">%s</a></li>') % (category.url_name,
+                        _('Access to all forms of the "%s" category') % category.name,
+                        _('Access to all forms in this category'))
+            r += htmltext('</ul>')
+            r += htmltext('</li>\n')
+
+        return r.getvalue()
+
+    def consultations(self):
+        cats = [x for x in Category.select() if x.url_name == 'consultations']
+        if not cats:
+            return
+        consultations_category = cats[0]
+        formdefs = FormDef.select(lambda x: (
+                    str(x.category_id) == str(consultations_category.id) and
+                        (not x.is_disabled() or x.disabled_redirection)),
+                    order_by = 'name')
+        if not formdefs:
+            return
+        ## Consultations
+        r = TemplateIO(html=True)
+        r += htmltext('<div id="consultations">')
+        r += htmltext('<h3>%s</h3>') % _('Consultations')
+        r += consultations_category.get_description_html_text()
+        r += htmltext('<ul>')
+        for formdef in formdefs:
+            r += htmltext('<li>')
+            r += htmltext('<a href="%s/%s/">%s</a>') % (consultations_category.url_name,
+                formdef.url_name, formdef.name)
+            r += htmltext('</li>')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def box_side(self, path):
+        r = TemplateIO(html=True)
+        root_url = get_publisher().get_root_url()
+
+        if self.has_anonymous_access_codes() and path == [''] and (
+                'include-tracking-code-form' in get_response().filter.get('keywords', [])):
+            r += htmltext('<form id="follow-form" action="%scode/load">') % root_url
+            r += htmltext('<h3>%s</h3>') % _('Tracking code')
+            r += htmltext('<input size="12" name="code" placeholder="%s"/>') % _('ex: RPQDFVCD')
+            r += htmltext('<input type="submit" value="%s"/>') % _('Load')
+            r += htmltext('</form>')
+
+        r += self.links()
+
+        cats = Category.select(order_by = 'name')
+        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
+        Category.sort_by_position(cats)
+        if cats:
+            r += htmltext('<div id="side-services">')
+            r += htmltext('<h3>%s</h3>') % _('Services')
+            r += htmltext('<ul>')
+            for cat in cats:
+                r += htmltext('<li><a href="%s/">%s</a></li>') % (cat.url_name, cat.name)
+            r += htmltext('</ul>')
+            r += htmltext('</div>')
+
+        if Event.keys(): # if there are events, add a link to the agenda
+            tags = get_cfg('misc', {}).get('event_tags')
+            if not tags:
+                tags = get_default_event_tags()
+            r += htmltext('<h3 id="agenda-link"><a href="%sagenda/">%s</a></h3>') % (root_url, _('Agenda'))
+
+            if path and path[0] == 'agenda':
+                r += htmltext('<p class="tags">')
+                for tag in tags:
+                    r += htmltext('<a href="%sagenda/tag/%s">%s</a> ') % (root_url, tag, tag)
+                r += htmltext('</p>')
+                r += self.agenda.display_remote_calendars()
+
+                r += htmltext('<p>')
+                r += htmltext('  <a href="%sagenda/filter">%s</a>') % (root_url, _('Advanced Filter'))
+                r += htmltext('</p>')
+
+        v = r.getvalue()
+        if v:
+            r = TemplateIO(html=True)
+            r += htmltext('<div id="sidebox">')
+            r += v
+            r += htmltext('</div>')
+            return r.getvalue()
+
+        return None
+
+    def has_anonymous_access_codes(self):
+        return any((x for x in FormDef.select() if x.enable_tracking_codes))
+
+    def links(self):
+        links = Link.select()
+        if not links:
+            return ''
+
+        Link.sort_by_position(links)
+
+        r = TemplateIO(html=True)
+
+        r += htmltext('<div id="links">')
+        if links[0].url:
+            # first link has an URL, so it's not a title, so we display a
+            # generic title
+            r += htmltext('<h3>%s</h3>') % _('Useful links')
+        has_ul = False
+        vars = get_publisher().substitutions.get_context_variables()
+        for link in links:
+            if not link.url:
+                # acting title
+                if has_ul:
+                    r += htmltext('</ul>')
+                r += htmltext('<h3>%s</h3>') % link.title
+                r += htmltext('<ul>')
+                has_ul = True
+            else:
+                if not has_ul:
+                    r += htmltext('<ul>')
+                    has_ul = True
+                r += htmltext('<li class="link-%s"><a href="%s">%s</a></li>') % (
+                        simplify(link.title), get_variadic_url(link.url, vars), link.title)
+        if has_ul:
+            r += htmltext('</ul>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def announces(self):
+        announces = Announce.get_published_announces()
+        if not announces:
+            return
+
+        r = TemplateIO(html=True)
+        r += htmltext('<div id="announces">')
+        r += htmltext('<h3>%s</h3>') % _('Announces to citizens')
+        for item in announces[:3]:
+            r += htmltext('<div class="announce-item">')
+            r += htmltext('<h4>')
+            if item.publication_time:
+                r += time.strftime(misc.date_format(), item.publication_time)
+                r += ' - '
+            r += item.title
+            r += htmltext('</h4>')
+            r += htmltext('<p>')
+            r += item.text
+            r += htmltext('</p>')
+            r += htmltext('</div>')
+
+        r += htmltext('<ul id="announces-links">')
+        r += htmltext('<li><a href="announces/subscribe">%s</a></li>') % _('Receiving those Announces')
+        r += htmltext('<li><a href="announces/">%s</a></li>') % _('Previous Announces')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def myspace_snippet(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<div id="myspace">')
+        r += htmltext('<h3>%s</h3>') % _('My Space')
+        r += htmltext('<ul>')
+        if get_request().user and not get_request().user.anonymous:
+            r += htmltext('  <li><a href="myspace/" id="member">%s</a></li>') % _('Access to your personal space')
+            r += htmltext('  <li><a href="logout" id="logout">%s</a></li>') % _('Logout')
+        else:
+            r += htmltext('  <li><a href="register/" id="inscr">%s</a></li>') % _('Registration')
+            r += htmltext('  <li><a href="login/" id="login">%s</a></li>') % _('Login')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def page_view(self, key, title, urlname = None):
+        if not urlname:
+            urlname = key[3:].replace(str('_'), str('-'))
+        get_response().breadcrumb.append((urlname, title))
+        template.html_top(title)
+        r = TemplateIO(html=True)
+        r += htmltext('<div class="article">')
+        r += htmltext(TextsDirectory.get_html_text(key))
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def informations_editeur(self):
+        get_response().filter['bigdiv'] = 'info'
+        return self.page_view('aq-editor-info', _('Editor Informations'),
+                urlname = 'informations_editeur')
+
+    def accessibility(self):
+        get_response().filter['bigdiv'] = 'accessibility'
+        return self.page_view('aq-accessibility', _('Accessibility Statement'))
+
+    def contact(self):
+        get_response().filter['bigdiv'] = 'contact'
+        return self.page_view('aq-contact', _('Contact'))
+
+    def help(self):
+        get_response().filter['bigdiv'] = 'help'
+        return self.page_view('aq-help', _('Help'))
+
+
+from qommon.publisher import get_publisher_class
+get_publisher_class().root_directory_class = AlternateRootDirectory
+get_publisher_class().after_login_url = 'myspace/'
+get_publisher_class().use_sms_feature = True
+
+# help links
+get_publisher_class().backoffice_help_url = {
+    'fr': 'https://doc-publik.entrouvert.com/',
+}
+
+
+EmailsDirectory.register('announces-subscription-confirmation',
+        N_('Confirmation of Announces Subscription'),
+        N_('Available variables: change_url, cancel_url, time, sitename'),
+        default_subject = N_('Announce Subscription Request'),
+        default_body = N_("""\
+You have (or someone impersonating you has) requested to subscribe to
+announces from [sitename].  To confirm this request, visit the
+following link:
+
+[confirm_url]
+
+If you are not the person who made this request, or you wish to cancel
+this request, visit the following link:
+
+[cancel_url]
+
+If you do nothing, the request will lapse after 3 days (precisely on
+[time]).
+"""))
+
+
+TextsDirectory.register('aq-announces-subscription',
+        N_('Text on announces subscription page'),
+        default = N_('''\
+<p>
+FIXME
+'</p>'''))
+
+TextsDirectory.register('aq-sms-demo',
+        N_('Text when subscribing to announces SMS and configured as demo'),
+        default = N_('''
+<p>
+Receiving announces by SMS is not possible in this demo
+</p>'''))
+
+TextsDirectory.register('aq-editor-info', N_('Editor Informations'))
+TextsDirectory.register('aq-accessibility', N_('Accessibility Statement'))
+TextsDirectory.register('aq-contact', N_('Contact Information'))
+TextsDirectory.register('aq-help', N_('Help'))
+TextsDirectory.register('aq-sso-text',  N_('Connecting with Identity Provider'),
+        default = N_('''<h3>Connecting with Identity Provider</h3>
+<p>You can also use your identity provider to connect.
+</p>'''))
+
+TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg = True)
diff --git a/auquotidien/modules/saml2.py b/auquotidien/modules/saml2.py
new file mode 100644
index 0000000..477f17f
--- /dev/null
+++ b/auquotidien/modules/saml2.py
@@ -0,0 +1,89 @@
+try:
+    import lasso
+except ImportError:
+    pass
+
+from qommon import get_cfg, get_logger
+import qommon.saml2
+
+
+class Saml2Directory(qommon.saml2.Saml2Directory):
+    def extract_attributes(self, session, login):
+        '''Separate attributes as two dictionaries: one for last value, one for
+           the list of values.'''
+        d = {}
+        m = {}
+
+        lasso_session = lasso.Session.newFromDump(session.lasso_session_dump)
+        try:
+            assertion = lasso_session.getAssertions(None)[0]
+        except:
+            get_logger().warn('failed to lookup assertion')
+            return d, m
+
+        try:
+            for attribute in assertion.attributeStatement[0].attribute:
+                try:
+                    d[attribute.name] = attribute.attributeValue[0].any[0].content
+                    for attribute_value in attribute.attributeValue:
+                        l = m.setdefault(attribute.name, [])
+                        l.append(attribute_value.any[0].content)
+                except IndexError:
+                    pass
+        except IndexError:
+            pass
+        return d, m
+
+    def fill_user_attributes(self, session, login, user):
+        qommon.saml2.Saml2Directory.fill_user_attributes(self, session, login, user)
+
+        idp = qommon.saml2.get_remote_provider_cfg(login)
+        if not idp.get('attribute-mapping'):
+            self.legacy_fill_user_attributes(session, login, user)
+
+    def legacy_fill_user_attributes(self, session, login, user):
+        '''Fill fields using a legacy attribute to field varname mapping'''
+        d, m = self.extract_attributes(session, login)
+        users_cfg = get_cfg('users', {}) or {}
+        get_logger().debug('using legacy attribute filling')
+
+        # standard attributes
+        user.name = d.get('cn')
+        user.email = d.get('mail')
+
+        # email field
+        field_email = users_cfg.get('field_email')
+        if field_email:
+            user.form_data[field_email] = d.get('mail') or d.get('email')
+
+        # name field, this only works if there's a single field for the name
+        field_name_values = users_cfg.get('field_name')
+        if field_name_values:
+            if type(field_name_values) is str: # it was a string in previous versions
+                field_name_values = [field_name_values]
+            if len(field_name_values) == 1:
+                user.form_data[field_name_values[0]] = d.get('cn')
+
+        # other fields, matching is done on known LDAP attribute names and
+        # common variable names
+        extra_field_mappings = [
+                ('gn', ('firstname', 'prenom')),
+                ('givenName', ('firstname', 'prenom')),
+                ('surname', ('surname', 'name', 'nom',)),
+                ('sn', ('surname', 'name', 'nom',)),
+                ('personalTitle', ('personalTitle', 'civilite',)),
+                ('l', ('location', 'commune', 'ville',)),
+                ('streetAddress', ('streetAddress', 'address', 'adresse', 'street',)),
+                ('street', ('streetAddress', 'address', 'adresse', 'street',)),
+                ('postalCode', ('postalCode', 'codepostal', 'cp',)),
+                ('telephoneNumber', ('telephoneNumber', 'telephonefixe', 'telephone',)),
+                ('mobile', ('mobile', 'telephonemobile',)),
+                ('faxNumber', ('faxNumber', 'fax')),
+        ]
+
+        for attribute_key, field_varnames in extra_field_mappings:
+            if not attribute_key in d:
+                continue
+            for field in user.get_formdef().fields:
+                if field.varname in field_varnames:
+                    user.form_data[field.id] = d.get(attribute_key)
diff --git a/auquotidien/modules/strongbox.py b/auquotidien/modules/strongbox.py
new file mode 100644
index 0000000..4d6ecde
--- /dev/null
+++ b/auquotidien/modules/strongbox.py
@@ -0,0 +1,98 @@
+import os
+import time
+
+from quixote import get_publisher
+from qommon.storage import StorableObject
+from qommon import misc
+
+class StrongboxType(StorableObject):
+    _names = 'strongbox-types'
+
+    id = None
+    label = None
+    validation_months = 0
+
+
+class StrongboxFile():
+    id = None
+    orig_filename = None
+    base_filename = None
+    content_type = None
+    charset = None
+
+    def __init__(self, id, f):
+        self.id = id
+        self.orig_filename = f.orig_filename
+        self.base_filename = f.base_filename
+        self.content_type = f.content_type
+        self.charset = f.charset
+        self.fp = f.fp
+
+    def __getstate__(self):
+        odict = self.__dict__.copy()
+        if not odict.has_key('fp'):
+            return odict
+
+        # XXX: add some locking
+        del odict['fp']
+        dirname = os.path.join(get_publisher().app_dir, 'strongbox-files')
+        if not os.path.exists(dirname):
+            os.mkdir(dirname)
+        odict['filename'] = os.path.join(dirname, str(self.id))
+        self.fp.seek(0)
+        fd = file(odict['filename'], 'w')
+        fd.write(self.fp.read())
+        fd.close()
+        return odict
+
+
+class StrongboxItem(StorableObject):
+    _names = 'strongbox-items'
+    _hashed_indexes = ['user_id']
+
+    user_id = None
+    description = None
+    file = None
+    type_id = None
+    expiration_time = None
+    proposed_by = None
+    proposed_time = None
+    validated_time = None
+
+    def get_display_name(self):
+        if self.description:
+            return self.description
+        return self.file.base_filename
+
+    def set_file(self, file):
+        self.file = StrongboxFile(self.id, file)
+
+    def remove_file(self):
+        os.unlink(self.file.filename)
+
+    def set_expiration_time_from_date(self, value):
+        if not value:
+            self.expiration_time = None
+            return
+        if not StrongboxType.has_key(self.type_id):
+            return
+
+        if not StrongboxType.get(self.type_id).validation_months:
+            self.expiration_time = None
+            return
+
+        date = time.strptime(value, misc.date_format())
+        year, month, day = date[:3]
+        month += StrongboxType.get(self.type_id).validation_months
+        while month > 12:
+           year += 1
+           month -= 12
+        while True:
+            try:
+                self.expiration_time = time.strptime(
+                       '%04d-%02d-%02d' % (year, month, day), '%Y-%m-%d')
+            except ValueError:
+                day -= 1
+                continue
+            break
+
diff --git a/auquotidien/modules/strongbox_ui.py b/auquotidien/modules/strongbox_ui.py
new file mode 100644
index 0000000..71456e2
--- /dev/null
+++ b/auquotidien/modules/strongbox_ui.py
@@ -0,0 +1,285 @@
+import time
+
+from quixote import get_request, get_response, get_session, redirect
+from quixote.directory import Directory, AccessControlled
+from quixote.html import TemplateIO, htmltext
+
+import wcs
+import wcs.admin.root
+
+from qommon import _
+from qommon import errors, misc
+from qommon.form import *
+from qommon.strftime import strftime
+from qommon.backoffice.menu import html_top
+from qommon import get_cfg
+
+from strongbox import StrongboxType, StrongboxItem
+
+
+
+class StrongboxTypeDirectory(Directory):
+    _q_exports = ['', 'edit', 'delete']
+
+    def __init__(self, strongboxtype):
+        self.strongboxtype = strongboxtype
+
+    def _q_index(self):
+        html_top('strongbox', title = _('Item Type: %s') % self.strongboxtype.label)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Item Type: %s') % self.strongboxtype.label
+        get_response().filter['sidebar'] = self.get_sidebar()
+        r += get_session().display_message()
+
+        if self.strongboxtype.validation_months:
+            r += htmltext('<div class="bo-block">')
+            r += htmltext('<ul>')
+            r += htmltext('<li>')
+            r += _('Number of months of validity:')
+            r += ' '
+            r += self.strongboxtype.validation_months
+            r += htmltext('</li>')
+            r += htmltext('</ul>')
+            r += htmltext('</div>')
+
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul>')
+        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
+        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def edit(self):
+        form = self.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            self.submit(form)
+            return redirect('..')
+
+        html_top('strongbox', title = _('Edit Item Type: %s') % self.strongboxtype.label)
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Edit Item Type: %s') % self.strongboxtype.label
+        r += form.render()
+        return r.getvalue()
+
+    def form(self):
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'label', title = _('Label'), required = True,
+                value = self.strongboxtype.label)
+        form.add(IntWidget, 'validation_months', title=_('Number of months of validity'),
+                value=self.strongboxtype.validation_months,
+                hint=_('Use 0 if there is no expiration'))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        return form
+
+    def submit(self, form):
+        for k in ('label', 'validation_months'):
+            widget = form.get_widget(k)
+            if widget:
+                setattr(self.strongboxtype, k, widget.parse())
+        self.strongboxtype.store()
+
+    def delete(self):
+        form = Form(enctype='multipart/form-data')
+        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
+                        'You are about to irrevocably delete this item type.')))
+        form.add_submit('submit', _('Submit'))
+        form.add_submit('cancel', _('Cancel'))
+        if form.get_submit() == 'cancel':
+            return redirect('..')
+        if not form.is_submitted() or form.has_errors():
+            get_response().breadcrumb.append(('delete', _('Delete')))
+            html_top('strongbox', title = _('Delete Item Type'))
+            r = TemplateIO(html=True)
+            r += htmltext('<h2>%s</h2>') % _('Deleting Item Type: %s') % self.strongboxtype.label
+            r += form.render()
+            return r.getvalue()
+        else:
+            self.strongboxtype.remove_self()
+            return redirect('..')
+
+
+class StrongboxTypesDirectory(Directory):
+    _q_exports = ['', 'new']
+
+    def _q_traverse(self, path):
+        get_response().breadcrumb.append(('types/', _('Item Types')))
+        return Directory._q_traverse(self, path)
+
+    def _q_index(self):
+        return redirect('..')
+
+    def new(self):
+        type_ui = StrongboxTypeDirectory(StrongboxType())
+
+        form = type_ui.form()
+        if form.get_submit() == 'cancel':
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            type_ui.submit(form)
+            return redirect('%s/' % type_ui.strongboxtype.id)
+
+        get_response().breadcrumb.append(('new', _('New Item Type')))
+        html_top('strongbox', title = _('New Item Type'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('New Item Type')
+        r += form.render()
+        return r.getvalue()
+
+    def _q_lookup(self, component):
+        try:
+            strongboxtype = StrongboxType.get(component)
+        except KeyError:
+            raise errors.TraversalError()
+        get_response().breadcrumb.append((str(strongboxtype.id), strongboxtype.label))
+        return StrongboxTypeDirectory(strongboxtype)
+
+
+class StrongboxDirectory(AccessControlled, Directory):
+    _q_exports = ['', 'types', 'add', 'add_to']
+    label = N_('Strongbox')
+
+    types = StrongboxTypesDirectory()
+
+    def is_accessible(self, user):
+        from .backoffice import check_visibility
+        return check_visibility('strongbox', user)
+
+    def _q_access(self):
+        user = get_request().user
+        if not user:
+            raise errors.AccessUnauthorizedError()
+
+        if not self.is_accessible(user):
+            raise errors.AccessForbiddenError(
+                    public_msg = _('You are not allowed to access Strongbox Management'),
+                    location_hint = 'backoffice')
+
+        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
+
+
+    def _q_index(self):
+        html_top('strongbox', _('Strongbox'))
+        r = TemplateIO(html=True)
+        get_response().filter['sidebar'] = self.get_sidebar()
+
+        r += get_session().display_message()
+
+        r += htmltext('<div class="splitcontent-left">')
+        r += htmltext('<div class="bo-block">')
+        r += htmltext('<h2>%s</h2>') % _('Propose a file to a user')
+        form = Form(enctype='multipart/form-data')
+        form.add(StringWidget, 'q', title = _('User'), required=True)
+        form.add_submit('search', _('Search'))
+        r += form.render()
+        if form.is_submitted() and not form.has_errors():
+            q = form.get_widget('q').parse()
+            users = self.search_for_users(q)
+            if users:
+                if len(users) == 1:
+                    return redirect('add_to?user_id=%s' % users[0].id)
+                if len(users) < 50:
+                    r += _('(first 50 users only)')
+                r += htmltext('<ul>')
+                for u in users:
+                    r += htmltext('<li><a href="add_to?user_id=%s">%s</a></li>') % (u.id, u.display_name)
+                r += htmltext('</ul>')
+            else:
+                r += _('No user found.')
+        r += htmltext('</div>')
+        r += htmltext('</div>')
+
+        r += htmltext('<div class="splitcontent-right">')
+        r += htmltext('<div class="bo-block">')
+        types = StrongboxType.select()
+        r += htmltext('<h2>%s</h2>') % _('Item Types')
+        if not types:
+            r += htmltext('<p>')
+            r += _('There is no item types defined at the moment.')
+            r += htmltext('</p>')
+
+        r += htmltext('<ul class="biglist" id="strongbox-list">')
+        for l in types:
+            type_id = l.id
+            r += htmltext('<li class="biglistitem" id="itemId_%s">') % type_id
+            r += htmltext('<strong class="label"><a href="types/%s/">%s</a></strong>') % (type_id, l.label)
+            r += htmltext('</li>')
+        r += htmltext('</ul>')
+        r += htmltext('</div>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def get_sidebar(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<ul id="sidebar-actions">')
+        r += htmltext('  <li><a class="new-item" href="types/new">%s</a></li>') % _('New Item Type')
+        r += htmltext('</ul>')
+        return r.getvalue()
+
+    def search_for_users(self, q):
+        if hasattr(get_publisher().user_class, 'search'):
+            return get_publisher().user_class.search(q)
+        if q:
+            users = [x for x in get_publisher().user_class.select()
+                     if q in (x.name or '') or q in (x.email or '')]
+            return users
+        else:
+            return []
+
+    def get_form(self):
+        types = [(x.id, x.label) for x in StrongboxType.select()]
+        form = Form(action='add', enctype='multipart/form-data')
+        form.add(StringWidget, 'description', title=_('Description'), size=60)
+        form.add(FileWidget, 'file', title=_('File'), required=True)
+        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
+                 options = [(None, _('Not specified'))] + types)
+        form.add(DateWidget, 'date_time', title = _('Document Date'))
+        form.add_submit('submit', _('Upload'))
+        return form
+
+    def add(self):
+        form = self.get_form()
+        form.add(StringWidget, 'user_id', title=_('User'))
+        if not form.is_submitted():
+           return redirect('.')
+
+        sffile = StrongboxItem()
+        sffile.user_id = form.get_widget('user_id').parse()
+        sffile.description = form.get_widget('description').parse()
+        sffile.proposed_time = time.localtime()
+        sffile.proposed_id = get_request().user.id
+        sffile.type_id = form.get_widget('type_id').parse()
+        v = form.get_widget('date_time').parse()
+        sffile.set_expiration_time_from_date(v)
+        sffile.store()
+        sffile.set_file(form.get_widget('file').parse())
+        sffile.store()
+        return redirect('.')
+
+    def add_to(self):
+        form = Form(enctype='multipart/form-data', action='add_to')
+        form.add(StringWidget, 'user_id', title = _('User'), required=True)
+        try:
+            user_id = form.get_widget('user_id').parse()
+            user = get_publisher().user_class.get(user_id)
+        except:
+            return redirect('.')
+        if not user:
+            return redirect('.')
+        get_request().form = {}
+        get_request().environ['REQUEST_METHOD'] = 'GET'
+
+        html_top('strongbox', _('Strongbox'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s %s</h2>') % (_('Propose a file to:'), user.display_name)
+        form = self.get_form()
+        form.add(HiddenWidget, 'user_id', title=_('User'), value=user.id)
+        r += form.render()
+        return r.getvalue()
diff --git a/auquotidien/modules/template.py b/auquotidien/modules/template.py
new file mode 100644
index 0000000..7048247
--- /dev/null
+++ b/auquotidien/modules/template.py
@@ -0,0 +1,82 @@
+from quixote import get_request, get_publisher, get_response
+from qommon.publisher import get_publisher_class
+from quixote.html import htmltext
+
+from qommon import _
+from qommon import template
+from qommon.admin.texts import TextsDirectory
+from wcs.categories import Category
+
+wcs_error_page = template.error_page
+
+def render_response(publisher, body):
+    response = publisher.get_request().response
+    body = str(body)
+
+    for key in ('bigdiv', 'gauche'):
+        if not response.filter.has_key(key):
+            response.filter[key] = None
+
+    root_url = publisher.get_root_url()
+    wcs_path = publisher.get_request().get_path()[len(root_url):]
+    section = wcs_path.split('/')[0]
+
+    if section in ('backoffice', 'admin'):
+        return template.decorate(body, response)
+
+    section_title = ''
+    page_title = response.filter.get('title')
+    if 'error-page' in response.filter:
+        pass
+    elif section == 'consultations':
+        section_title = '<h2 id="consultations">%s</h2>\n' % _('Consultations')
+        response.filter['bigdiv'] = 'rub_consultation'
+    elif section == 'announces':
+        response.filter['bigdiv'] = 'rub_annonce'
+        section_title = '<h2 id="announces">%s</h2>\n' % _('Announces to citizens')
+        if page_title == _('Announces to citizens'):
+            page_title = ''
+    elif section == 'agenda':
+        response.filter['bigdiv'] = 'rub_agenda'
+        section_title = '<h2 id="agenda">%s</h2>\n' % _('Agenda')
+        if page_title == _('Agenda'):
+            page_title = ''
+    elif section and len(section) > 1:
+        # XXX: this works but is not efficient
+        if Category.has_urlname(section):
+            section_title = '<h2 id="services">%s</h2>\n' % _('Services')
+            response.filter['bigdiv'] = 'rub_service'
+
+    if not 'auquotidien-no-titles-in-section' in response.filter.get('keywords', []):
+        if page_title:
+            if section_title:
+                page_title = '<h3>%s</h3>' % page_title
+            else:
+                page_title = '<h2 id="info">%s</h2>' % page_title
+        else:
+            page_title = ''
+
+        body = '\n'.join((section_title, page_title, body))
+
+    if hasattr(response, 'breadcrumb') and response.breadcrumb:
+        if len(response.breadcrumb) == 1:
+            response.breadcrumb = None
+
+    return template.decorate(body, response)
+
+
+def error_page(*args, **kwargs):
+    get_response().filter = {}
+    get_response().filter['error-page'] = None
+    get_response().filter['keywords'] = template.get_current_theme().get('keywords')
+    get_response().filter['title'] = template.get_current_theme().get('keywords')
+    get_response().filter['gauche'] = TextsDirectory.get_html_text('aq-error-assistance')
+    error_page = wcs_error_page(*args, **kwargs)
+    title = get_response().filter['title']
+    get_response().filter['title'] = None
+    return htmltext('<div id="info"><h2>%s</h2>' % title) + error_page + htmltext('</div>')
+
+template.error_page = error_page
+get_publisher_class().render_response = render_response
+
+TextsDirectory.register('aq-error-assistance', N_('Assistance text next to errors'))
diff --git a/debian/rules b/debian/rules
index 9701d1f..adf9d23 100755
--- a/debian/rules
+++ b/debian/rules
@@ -31,9 +31,6 @@ install: build
 	dh_installdirs
 
 	$(PYTHON) setup.py install --prefix=$(DESTDIR)/usr --no-compile
-	for i in $(CURDIR)/debian/wcs-au-quotidien/usr/lib/python2.*; do \
-		mv $$i/site-packages/extra \
-			$$i/site-packages/extra-wcs-au-quotidien; done
 	cd po && make install prefix=$(CURDIR)/debian/wcs-au-quotidien/
 	install -d -m 755 $(DESTDIR)/var/lib/wcs-au-quotidien $(DESTDIR)/etc/wcs
 	install -m 644 wcs-au-quotidien.cfg-sample $(DESTDIR)/etc/wcs/wcs-au-quotidien.cfg
diff --git a/debian/wcs-au-quotidien.init b/debian/wcs-au-quotidien.init
index df4769a..b4969b5 100755
--- a/debian/wcs-au-quotidien.init
+++ b/debian/wcs-au-quotidien.init
@@ -19,7 +19,7 @@ PIDFILE=/var/run/$NAME.pid
 SCRIPTNAME=/etc/init.d/$NAME
 PYTHON_VERSION=`/usr/bin/env python -c \
     "import sys; print '%d.%d' % (sys.version_info[0], sys.version_info[1])"`
-OPTIONS="--extra /usr/lib/pymodules/python$PYTHON_VERSION/extra-wcs-au-quotidien/"
+OPTIONS="--extra /usr/lib/pymodules/python$PYTHON_VERSION/auquotidien/"
 WCS_USER=wcs-au-quotidien
 WCS_GROUP=wcs-au-quotidien
 CONFIG_FILE=/etc/wcs/wcs-au-quotidien.cfg
diff --git a/extra/auquotidien.py b/extra/auquotidien.py
deleted file mode 100644
index bf12784..0000000
--- a/extra/auquotidien.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from quixote import get_publisher
-
-from qommon import _
-from qommon.publisher import get_publisher_class, get_request
-from qommon.misc import get_cfg
-
-import modules.admin
-import modules.backoffice
-import modules.links_ui
-import modules.announces_ui
-import modules.categories_admin
-import modules.events_ui
-import modules.payments_ui
-import modules.strongbox_ui
-import modules.formpage
-import modules.template
-import modules.root
-import modules.payments
-import modules.connectors
-import modules.abelium_domino_ui
-import modules.abelium_domino_vars
-import modules.abelium_domino_synchro
-
-get_publisher_class().register_translation_domain('auquotidien')
-get_publisher_class().default_configuration_path = 'au-quotidien-wcs-settings.xml'
-
-rdb = get_publisher_class().backoffice_directory_class
-
-rdb.items = []
-
-rdb.register_directory('announces', modules.announces_ui.AnnouncesDirectory())
-rdb.register_menu_item('announces/', _('Announces'))
-
-rdb.register_directory('links', modules.links_ui.LinksDirectory())
-rdb.register_menu_item('links/', _('Links'))
-
-rdb.register_directory('events', modules.events_ui.EventsDirectory())
-rdb.register_menu_item('events/', _('Events'))
-
-rdb.register_directory('payments', modules.payments_ui.PaymentsDirectory())
-rdb.register_menu_item('payments/', _('Payments'))
-
-rdb.register_directory('strongbox', modules.strongbox_ui.StrongboxDirectory())
-rdb.register_menu_item('strongbox/', _('Strongbox'))
-
-rdb.register_directory('settings', modules.admin.SettingsDirectory())
-rdb.register_directory('categories', modules.categories_admin.CategoriesDirectory())
-
-import wcs.admin.settings
-wcs.admin.settings.SettingsDirectory.domino = modules.abelium_domino_ui.AbeliumDominoDirectory()
-wcs.admin.settings.SettingsDirectory._q_exports.append('domino')
-
-import wcs.categories
-wcs.categories.Category.XML_NODES = [('name', 'str'), ('url_name', 'str'),
-        ('description', 'str'), ('position', 'int'),
-        ('homepage_position', 'str'), ('redirect_url', 'str'), ('limit', 'int')]
diff --git a/extra/modules/__init__.py b/extra/modules/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/extra/modules/abelium_domino_synchro.py b/extra/modules/abelium_domino_synchro.py
deleted file mode 100644
index b821857..0000000
--- a/extra/modules/abelium_domino_synchro.py
+++ /dev/null
@@ -1,164 +0,0 @@
-import sys
-from datetime import datetime
-import collections
-from decimal import Decimal
-
-from qommon import _
-from qommon.cron import CronJob
-from qommon.publisher import get_publisher_class
-from qommon import get_logger
-
-from wcs.users import User
-
-from abelium_domino_ui import (get_client, is_activated, get_invoice_regie,
-        abelium_domino_ws)
-from payments import Invoice, Transaction
-
-DOMINO_ID_PREFIX = 'DOMINO-'
-
-def synchronize_domino(publisher):
-    regie = get_invoice_regie(publisher)
-    logger = get_logger()
-    if not is_activated(publisher) or not regie:
-        return
-    client = get_client(publisher)
-    if client is None:
-        logger.warning('Unable to create a DominoWS object')
-        return
-    client.clear_cache()
-    users = User.values()
-    users_by_mail = dict(((user.email, user) for user in users))
-    users_by_code_interne = {}
-    for user in users:
-        if hasattr(user, 'abelium_domino_code_famille'):
-            users_by_code_interne[user.abelium_domino_code_famille] = user
-    try:
-        invoices = client.invoices
-    except abelium_domino_ws.DominoException, e:
-        publisher.notify_of_exception(sys.exc_info(), context='[DOMINO]')
-        logger.error('domino cron: failure to retrieve invoice list from domino '
-                'for synchronization [error:%s]', e)
-        return
-    # import new invoices
-    logger.info('domino cron: retrieved %i invoices', len(invoices))
-    for invoice_id, invoice in invoices.iteritems():
-        user = None
-        if invoice.family.code_interne in users_by_code_interne:
-            user = users_by_code_interne[invoice.family.code_interne]
-        if user is None:
-            for email in (invoice.family.email_pere, invoice.family.email_mere,
-                    invoice.family.adresse_internet):
-                user = users_by_mail.get(email)
-                if user:
-                    break
-            else:
-                continue
-        external_id = '%s%s' % (DOMINO_ID_PREFIX, invoice.id)
-        payment_invoice = Invoice.get_on_index(external_id, 'external_id', ignore_errors=True)
-        if payment_invoice:
-            continue
-        if invoice.reste_du == Decimal(0) or invoice.reste_du < Decimal(0):
-            continue
-        payment_invoice = Invoice()
-        payment_invoice.user_id = user.id
-        payment_invoice.external_id = external_id
-        payment_invoice.regie_id = regie.id
-        payment_invoice.formdef_id = None
-        payment_invoice.formdata_id = None
-        payment_invoice.amount = invoice.reste_du
-        payment_invoice.date = invoice.creation
-        payment_invoice.domino_synchro_date = datetime.now()
-        if 'etablissement' in invoice._detail:
-            etablissement = invoice._detail['etablissement'].encode('utf-8')
-            payment_invoice.subject = _('%s - Childcare services') % etablissement
-        else:
-            payment_invoice.subject = _('Childcare services')
-        if invoice._detail.get('lignes'):
-            details = []
-            details.append('<table class="invoice-details"><thead>')
-            tpl = '''<tr>
-    <td>%(designation)s</td>
-    <td>%(quantite)s</td>
-    <td>%(prix)s</td>
-    <td>%(montant)s</td>
-</tr>'''
-            captions = {
-                    'designation': _('Caption'),
-                    'quantite': _('Quantity'),
-                    'prix': _('Price'),
-                    'amount': _('Amount')
-            }
-            details.append(tpl % captions)
-            details.append('</thead>')
-            details.append('<tbody>')
-            for ligne in invoice._detail['lignes']:
-                def encode(x):
-                    a, b = x
-                    b = b.encode('utf-8')
-                    return (a,b)
-                ligne = map(encode, ligne)
-                ligne = dict(ligne)
-                base = collections.defaultdict(lambda:'')
-                base.update(ligne)
-                details.append(tpl % base)
-            details.append('</tbody></table>')
-            payment_invoice.details = '\n'.join(details)
-        payment_invoice.store()
-        logger.info('domino cron: remote invoice %s for family %s added to user %s invoices with id %s',
-                invoice.id, invoice.family.id, user.id, payment_invoice.id)
-
-    # update invoices
-    invoices_ids = dict(invoices.iteritems())
-    for payment_invoice in Invoice.values():
-        if payment_invoice.external_id is None or not payment_invoice.external_id.startswith(DOMINO_ID_PREFIX):
-            continue # not a payment related to domino we skip
-        i = payment_invoice.external_id[len(DOMINO_ID_PREFIX):]
-        i = int(i)
-        invoice = invoices_ids.get(i)
-        if payment_invoice.paid:
-            if not invoice: 
-                # invoice has been paid (locally or not) but remote invoice has
-                # been deleted do, we do nothing.
-                continue
-            if getattr(payment_invoice, 'domino_knows_its_paid', None) or getattr(payment_invoice, 'paid_by_domino', None):
-                # synchronization of payment already done, skip
-                continue
-            transactions = Transaction.get_with_indexed_value('invoice_ids', payment_invoice.id)
-            if not transactions:
-                logger.warning("domino cron: invoice %s is marked paid but does "
-                        "not have any linked transaction.", payment_invoice.id)
-                details = '' # no details about the payment, problem
-            else:
-                details = repr(transactions[0].__dict__)
-            if invoice.montant != payment_invoice.amount:
-                pass # add warning logs
-            try:
-                client.pay_invoice([invoice], invoice.montant, details,
-                        payment_invoice.paid_date)
-            except abelium_domino_ws.DominoException, e:
-                logger.error('domino cron: invoice %s has been paid, but the remote system '
-                        'is unreachable, notification will be done again '
-                        'later [error: %s]', invoice.id, e)
-            else:
-                # memorize the date of synchronization
-                payment_invoice.domino_knows_its_paid = datetime.now()
-                payment_invoice.store()
-                logger.info('domino cron: domino: invoice %s has been paid; remote system has been '
-                        'notified', payment_invoice.id)
-        else: # unpaid
-            if not invoice:
-                logger.info('domino cron: remote invoice %s disapearred, so its '
-                        'still-unpaid local counterpart invoice %s was deleted.',
-                        i, payment_invoice.id)
-                payment_invoice.remove_self()
-            elif invoice.paid():
-                payment_invoice.paid_by_domino = True
-                payment_invoice.pay()
-                logger.info('domino cron: remote invoice %s has beend paid, '
-                        'local invoice %s of user %s is now marked as paid.',
-                        invoice.id, payment_invoice.id, payment_invoice.user_id)
-            else: # not invoice.paid()
-                pass # still waiting for the payment
-
-get_publisher_class().register_cronjob(CronJob(function=synchronize_domino,
-    hours=range(0, 24), minutes=range(0, 60, 30)))
diff --git a/extra/modules/abelium_domino_ui.py b/extra/modules/abelium_domino_ui.py
deleted file mode 100644
index e297e8e..0000000
--- a/extra/modules/abelium_domino_ui.py
+++ /dev/null
@@ -1,170 +0,0 @@
-from quixote import get_publisher, redirect, get_request
-from quixote.directory import Directory, AccessControlled
-from quixote.html import TemplateIO, htmltext
-
-from qommon import _
-from qommon import get_cfg, get_logger
-from qommon.form import Form, StringWidget, CheckboxWidget, SingleSelectWidget
-from qommon.backoffice.menu import html_top
-from quixote.html import htmltext
-
-from payments import Regie
-
-
-# constants
-ABELIUM_DOMINO = 'abelium_domino'
-ACTIVATED = 'activated'
-WSDL_URL = 'wsdl_url'
-SERVICE_URL = 'service_url'
-DOMAIN = 'domain'
-LOGIN = 'login'
-PASSWORD = 'password'
-INVOICE_REGIE = 'invoice_regie'
-
-try:
-    import abelium_domino_ws
-except ImportError, e:
-    abelium_domino_ws = None
-    import_error = e
-
-def get_abelium_cfg(publisher):
-    if not publisher:
-        publisher = get_publisher()
-    return publisher.cfg.get(ABELIUM_DOMINO, {})
-
-def is_activated(publisher=None):
-    cfg = get_abelium_cfg(publisher)
-    return cfg.get(ACTIVATED, False) and abelium_domino_ws is not None
-
-def get_client(publisher=None):
-    publisher = publisher or get_publisher()
-
-    cfg = get_abelium_cfg(publisher)
-    try:
-        publisher._ws_cache = abelium_domino_ws.DominoWs(
-            url=cfg.get(WSDL_URL, ''),
-            domain=cfg.get(DOMAIN,''),
-            login=cfg.get(LOGIN, ''),
-            password=cfg.get(PASSWORD, ''),
-            location=cfg.get(SERVICE_URL),
-            logger=get_logger())
-    except IOError:
-        return None
-    return publisher._ws_cache
-
-def get_family(user, publisher=None):
-    family = None
-    if user is None:
-        return None
-    client = get_client(publisher)
-    if not client:
-        return None
-    if hasattr(user, 'abelium_domino_code_famille'):
-        family = client.get_family_by_code_interne(
-            user.abelium_domino_code_famille)
-    if family is None and user.email:
-        family = client.get_family_by_mail(user.email)
-    return family
-
-def get_invoice_regie(publisher=None):
-    cfg = get_abelium_cfg(publisher)
-    regie_id = cfg.get(INVOICE_REGIE)
-    if not regie_id:
-        return None
-    return Regie.get(regie_id, ignore_errors=True)
-
-class AbeliumDominoDirectory(Directory):
-    _q_exports = [ '' , 'debug' ]
-    label = N_('Domino')
-
-    def debug(self):
-        from abelium_domino_vars import SESSION_CACHE
-        html_top(ABELIUM_DOMINO)
-        r = TemplateIO(html=True)
-        r += htmltext('<form method="post"><button>Lancer le cron</button></form>')
-        if get_request().get_method() == 'POST':
-            try:
-                from abelium_domino_synchro import synchronize_domino
-                synchronize_domino(get_publisher())
-            except Exception, e:
-                r += htmltext('<pre>%s</pre>') % repr(e)
-        r += htmltext('<p>code interne: %s</p>') % getattr(get_request().user, str('abelium_domino_code_famille'), None)
-        r += htmltext('<dl>')
-        context = get_publisher().substitutions.get_context_variables()
-        for var in sorted(context.keys()):
-            value = context[var]
-            if value:
-                r += htmltext('<dt>%s</dt>') % var
-                r += htmltext('<dd>%s</dt>') % value
-        r += htmltext('</dl>')
-        delattr(get_request().session, SESSION_CACHE)
-
-    def _q_index(self):
-        publisher = get_publisher()
-        cfg = get_cfg(ABELIUM_DOMINO, {})
-        form = self.form(cfg)
-
-        title = _('Abelium Domino')
-        html_top(ABELIUM_DOMINO, title = title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % title
-
-        if form.is_submitted() and not form.has_errors():
-            if form.get_widget('cancel').parse():
-                return redirect('..')
-            if form.get_widget('submit').parse():
-                for name in [f[0] for f in self.form_desc] + [INVOICE_REGIE]:
-                    widget = form.get_widget(name)
-                    if widget:
-                        cfg[name] = widget.parse()
-                publisher.cfg[ABELIUM_DOMINO] = cfg
-                publisher.write_cfg()
-                return redirect('.')
-
-        if abelium_domino_ws:
-            r += form.render()
-        else:
-            message = _('The Abelium Domino module is not '
-                    'activated because of this error when '
-                    'loading it: %r') % import_error
-            r += htmltext('<p class="errornotice">%s</p>') % message
-        r += htmltext('<dl style="display: none">')
-        context = get_publisher().substitutions.get_context_variables()
-        for var in sorted(context.keys()):
-            value = context[var]
-            if value:
-                r += htmltext('<dt>%s</dt>') % var
-                r += htmltext('<dd>%s</dt>') % value
-        r += htmltext('</dl>')
-        return r.getvalue()
-
-    form_desc = (
-            # name, required, title, kind
-            (ACTIVATED, False, _('Activated'), bool),
-            (WSDL_URL, True, _('WSDL URL'), str),
-            (SERVICE_URL, False, _('Service URL'), str),
-            (DOMAIN, True, _('Domain'), str),
-            (LOGIN, True, _('Login'), str),
-            (PASSWORD, True, _('Password'), str),
-    )
-
-
-    def form(self, initial_value={}):
-        form = Form(enctype='multipart/form-data')
-        kinds = { str: StringWidget, bool: CheckboxWidget }
-        for name, required, title, kind in self.form_desc:
-            widget = kinds[kind]
-            form.add(widget, name, required=required, title=title,
-                    value=initial_value.get(name, ''))
-        options = [(regie.id, regie.label) \
-                        for regie in Regie.values()]
-        options.insert(0, (None, _('None')))
-        form.add(SingleSelectWidget, INVOICE_REGIE,
-                title=_('Regie which will receive payments'),
-                value=initial_value.get(INVOICE_REGIE),
-                options=options)
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        return form
diff --git a/extra/modules/abelium_domino_vars.py b/extra/modules/abelium_domino_vars.py
deleted file mode 100644
index 6f32bf3..0000000
--- a/extra/modules/abelium_domino_vars.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from decimal import Decimal
-import logging
-
-from quixote.publish import get_publisher
-
-from qommon import _
-from qommon.substitution import Substitutions
-from wcs.publisher import WcsPublisher
-
-from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
-
-SESSION_CACHE = 'abelium_domino_variable_cache'
-
-class DominoVariables(object):
-    VARIABLE_TEMPLATE = 'domino_var_%s'
-    CHILD_VARIABLE_TEMPLATE = 'domino_var_%s_enfant%s'
-
-    CHILD_COLUMNS = abelium_domino_ws.Child.COLUMNS
-    FAMILY_COLUMNS = abelium_domino_ws.Family.COLUMNS \
-        + abelium_domino_ws.Family.MORE_COLUMNS
-    def __init__(self, publisher=None, request=None):
-        self.publisher = publisher
-        self.request = request
-
-    def get_substitution_variables(self):
-        vars = {}
-        if not is_activated() or not self.request or not self.request.user \
-                or not getattr(self.request.user, 'email'):
-            return vars
-
-        # test cache
-        cache = getattr(self.request.session, SESSION_CACHE, None)
-        if cache is not None:
-            return cache
-        # call the web service
-        try:
-            charset = get_publisher().site_charset
-            family = get_family(self.request.user)
-            if family:
-                family.complete()
-                for i, child in enumerate(family.children):
-                    for remote_name, name, converter, desc in self.CHILD_COLUMNS:
-                        v = getattr(child, name, None)
-                        if v is None:
-                            continue
-                        if hasattr(v, 'encode'):
-                            v = v.encode(charset)
-                        vars[self.CHILD_VARIABLE_TEMPLATE % (name, i+1)] = v
-                vars[self.VARIABLE_TEMPLATE % 'nombre_enfants'] = len(family.children)
-                for remote_name, name, converted, desc in self.FAMILY_COLUMNS:
-                    if hasattr(family, name):
-                        v = getattr(family, name)
-                        if v is None:
-                            continue
-                        if hasattr(v, 'encode'):
-                            v = v.encode(charset)
-                        vars[self.VARIABLE_TEMPLATE % name] = v
-                amount = Decimal(0)
-                for invoice in family.invoices:
-                    amount += invoice.reste_du
-                if amount:
-                    vars['user_famille_reste_du'] = str(amount)
-        except abelium_domino_ws.DominoException:
-            logging.exception('unable to call the domino ws for user %s', self.request.user.id)
-        setattr(self.request.session, SESSION_CACHE, vars)
-        self.request.session.store()
-        return vars
-
-    def get_substitution_variables_list(cls):
-        if not is_activated():
-            return ()
-        vars = []
-        for remote_name, name, converted, desc in cls.FAMILY_COLUMNS:
-            vars.append((_('Domino'), cls.VARIABLE_TEMPLATE % name, desc))
-        for remote_name, name, converted, desc in cls.CHILD_COLUMNS:
-            vars.append((_('Domino'), cls.CHILD_VARIABLE_TEMPLATE % (name, '{0,1,2,..}'), desc))
-        return vars
-    get_substitution_variables_list = classmethod(get_substitution_variables_list)
-
-Substitutions.register_dynamic_source(DominoVariables)
-WcsPublisher.register_extra_source(DominoVariables)
diff --git a/extra/modules/abelium_domino_workflow.py b/extra/modules/abelium_domino_workflow.py
deleted file mode 100644
index f1be0a7..0000000
--- a/extra/modules/abelium_domino_workflow.py
+++ /dev/null
@@ -1,151 +0,0 @@
-import re
-import time
-
-from quixote import get_request, get_publisher, get_session
-from quixote.directory import Directory
-
-from qommon import _
-from qommon.substitution import Substitutions
-from qommon.form import Form, ValidatedStringWidget
-import qommon.misc
-from qommon import get_logger
-
-from wcs.workflows import Workflow, WorkflowStatusJumpItem, register_item_class
-from wcs.forms.common import FormStatusPage
-
-from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
-import abelium_domino_ws
-
-class InternalCodeStringWidget(ValidatedStringWidget):
-    regex = '\d*'
-
-class AbeliumDominoRegisterFamilyWorkflowStatusItem(WorkflowStatusJumpItem):
-    status = None
-    description = N_('Abelium Domino: Register a Family')
-    key = 'abelium-domino-register-family'
-    category = ('aq-abelium', N_('Abelium'))
-    label = None
-
-    def render_as_line(self):
-        return _('Register a Family into Abelium Domino')
-
-    def get_family(self, formdata):
-        try:
-            user = formdata.get_user()
-            if user:
-                family = get_family(user)
-                if family:
-                    family.complete()
-                return family
-        except abelium_domino_ws.DominoException:
-            pass
-        return None
-
-    def fill_form(self, form, formdata, user):
-        family = self.get_family(formdata)
-        if 'family_id' not in form._names:
-            form.add(InternalCodeStringWidget, 'family_id',
-                     title=_('Family internal code'),
-                     value=family and family.code_interne.encode('utf8'),
-                     hint=_('If a family internal code is present, the '
-                            'family is updated, if not it is created'))
-            form.add_submit('create_update_button%s' % self.id,
-                    _('Create or update the family'))
-
-    def update(self, form, formdata, user, evo):
-        fid_widget = form.get_widget('family_id')
-        code_interne = fid_widget.parse()
-        try:
-            code_interne = int(code_interne)
-        except ValueError:
-            raise ValueError('Le code interne est invalide')
-        code_interne = '%05d' % code_interne
-        family = get_client().get_family_by_code_interne(code_interne)
-        if not family:
-            raise ValueError('Le code interne est invalide')
-        family.complete()
-        self.extract_family(form, formdata, user, evo, family)
-        family.save()
-        return family
-
-    def create(self, form, formdata, user, evo):
-        family = abelium_domino_ws.Family(client=get_client())
-        self.extract_family(form, formdata, user, evo, family)
-        return family
-
-    def extract_family(self, form, formdata, user, evo, family):
-        formdef = formdata.formdef
-        children = [abelium_domino_ws.Child() for i in range(5)]
-        max_i = 0
-        for field in formdef.fields:
-            value = formdata.data.get(field.id)
-            if value in (None, ''):
-                continue
-            if hasattr(field, 'date_in_the_past'):
-                value = time.strftime('%Y%m%d', value)
-            value = unicode(value, 'utf8')
-            if field.prefill and \
-               field.prefill.get('type') == 'formula':
-                v = field.prefill.get('value', '').strip()
-                i = None
-                name = None
-                m = re.search('domino_var_([^ ]*)_enfant([0-9]*)', v)
-                m2 = re.search('domino_var_([^ ]*)', v)
-                if m:
-                    name, i = m.groups()
-                    try:
-                        i = int(i)
-                    except ValueError:
-                        continue
-                    max_i = max(i, max_i)
-                    print 'enfant', name, i-1, value
-                    setattr(children[i-1], name, value)
-                elif m2:
-                    name = m2.group(1)
-                    print 'family', name, value
-                    setattr(family, name, value)
-        for child1, child2 in zip(family.children, children[:max_i]):
-            child1.__dict__.update(child2.__dict__)
-        family.save()
-        if max_i > len(family.children): # add new children
-            for child in children[len(family.children):max_i]:
-                family.add_child(child)
-
-    def submit_form(self, form, formdata, user, evo):
-        logger = get_logger()
-        if form.get_submit() != 'create_update_button%s' % self.id:
-            return
-        try:
-            if form.get_widget('family_id').parse():
-                family = self.update(form, formdata, user, evo)
-                msg = _('Sucessfully updated the family %s')
-                log_msg = _('Sucessfully updated the family %(code)s of %(user)s')
-            else:
-                family = self.create(form, formdata, user, evo)
-                msg = _('Sucessfully created the family %s')
-                log_msg = _('Sucessfully created the family %(code)s of %(user)s')
-            code_interne = family.code_interne.encode('utf8')
-            msg = msg % code_interne
-            logger.info(log_msg, {'code': code_interne, 'user': formdata.get_user()})
-            form_user = formdata.get_user()
-            form_user.abelium_domino_code_famille = code_interne
-            form_user.store()
-        except Exception, e:
-            if form.get_widget('family_id').parse():
-                msg = _('Unable to update family: %s') % str(e)
-            else:
-                msg = _('Unable to create family: %s') % str(e)
-            evo.comment = msg
-            logger.exception(msg  % formdata.get_user())
-        else:
-            evo.comment = msg
-            wf_status = self.get_target_status()
-            if wf_status:
-                evo.status = 'wf-%s' % wf_status[0].id
-        return False
-
-    def is_available(self, workflow=None):
-        return get_publisher().has_site_option('domino')
-    is_available = classmethod(is_available)
-
-register_item_class(AbeliumDominoRegisterFamilyWorkflowStatusItem)
diff --git a/extra/modules/abelium_domino_ws.py b/extra/modules/abelium_domino_ws.py
deleted file mode 100644
index fc006bf..0000000
--- a/extra/modules/abelium_domino_ws.py
+++ /dev/null
@@ -1,648 +0,0 @@
-# -*- coding: utf-8 -*-
-from decimal import Decimal
-import time
-import datetime
-from xml.etree import ElementTree as etree
-import logging
-
-try:
-    from suds.client import Client
-    from suds.bindings.binding import Binding
-    # Webdev is bugged and using an HTML generator to produce XML content, 
-    Binding.replyfilter = lambda self, x: x.replace('&nbsp;', '&#160;')
-except ImportError:
-    Client = None
-    Binding = None
-
-logger = logging.getLogger(__name__)
-
-# cleaning and parsing functions
-
-LINE_SEPARATOR = '\n'
-COLUMN_SEPARATOR = '\t'
-
-def unicode_and_strip(x):
-    return unicode(x).strip()
-
-def strip_and_int(x):
-    try:
-        return int(x.strip())
-    except ValueError:
-        return None
-
-def strip_and_date(x):
-    try:
-        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
-    except ValueError:
-        return None
-
-def parse_date(date_string):
-    if date_string:
-        return datetime.datetime.strptime(date_string, "%Y%m%d")
-    else:
-        None
-
-class DominoException(Exception):
-    pass
-
-def object_cached(function):
-    '''Decorate an object method so that its results is cached on the object
-       instance after the first call.
-    '''
-    def decorated_function(self, *args, **kwargs):
-        cache_name = '__%s_cache' % function.__name__
-        if not hasattr(self, cache_name):
-            setattr(self, cache_name, (time.time(), {}))
-        t, d = getattr(self, cache_name)
-        if time.time() - t > 80:
-            setattr(self, cache_name, (time.time(), {}))
-            t, d = getattr(self, cache_name)
-        k = tuple(*args) + tuple(sorted(kwargs.items()))
-        if not k in d:
-            d[k] = function(self, *args, **kwargs)
-        return d[k]
-    return decorated_function
-
-# Data model
-class SimpleObject(object):
-    '''Base class for object returned by the web service'''
-
-    '''Describe basic columns'''
-    COLUMNS = ()
-    '''Describe extended object columns'''
-    MORE_COLUMNS = ()
-
-    def __init__(self, **kwargs):
-        self.__dict__.update(kwargs)
-
-    def __repr__(self):
-        c = {}
-        for remote_name, name, converter, desc in self.COLUMNS:
-            if hasattr(self, name):
-                c[name] = getattr(self, name)
-        return str(c)
-
-    def serialize(self):
-        l = []
-        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
-            if local_name == 'id':
-                continue
-            v = getattr(self, local_name, None)
-            if v is None:
-                continue
-            if isinstance(v, (datetime.date, datetime.datetime)):
-                v = v.strftime('%Y%m%d')
-            if remote_name.endswith('_DA') and '-' in v:
-                v = v.replace('-', '')
-            l.append(u'{0}: "{1}"'.format(remote_name, v))
-        return u','.join(l)
-
-    def debug(self):
-        '''Output a debugging view of this object'''
-        res = ''
-        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
-            if hasattr(self, name):
-                res += name + ':' + repr(getattr(self, name)) + '\n'
-        return res
-
-    def __int__(self):
-        '''Return the object id'''
-        return self.id
-
-class UrgentContact(SimpleObject):
-    COLUMNS = (
-            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
-            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
-            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
-            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
-            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
-            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
-            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
-            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
-            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
-            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
-            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
-            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
-            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
-            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
-            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
-            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
-    )
-
-class Child(SimpleObject):
-    COLUMNS = (
-            ('IDENFANTS', 'id', strip_and_int, 'Identifiant de ENFANTS'),
-            ('NOM_CH', 'nom', unicode_and_strip, 'Nom'),
-            ('PRENOM_CH', 'prenom', unicode_and_strip, 'Prénom'),
-            ('NAISSANCE_DA', 'date_naissance', strip_and_date, 'Date de Naissance'),
-            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'Commentaires / Notes'),
-            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
-            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'Code Postal'),
-            ('VILLE_CH', 'ville', unicode_and_strip, 'Ville'),
-            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'Code Interne'),
-            ('LIEUNAISSANCE_CH', 'lieu_naissance', unicode_and_strip, 'Lieu de Naissance'),
-            ('DEPNAISSANCE_CH', 'departement_naissance', unicode_and_strip, 'Département Naissance'),
-            ('NUMSECU_CH', 'num_securite_sociale', unicode_and_strip, 'N° de SECU'),
-            ('NATIONALITE_CH', 'nationalite', unicode_and_strip, 'Nationalité'),
-            ('PRENOM2_CH', 'prenom2', unicode_and_strip, 'Prénom 2'),
-            ('SEXE_CH', 'sexe', unicode_and_strip, 'Sexe'),
-            ('IDTABLELIBRE1', 'IDTABLELIBRE1', unicode_and_strip, 'IDTABLELIBRE1'),
-            ('IDTABLELIBRE2', 'IDTABLELIBRE2', unicode_and_strip, 'IDTABLELIBRE2'),
-            ('IDTABLELIBRE3', 'IDTABLELIBRE3', unicode_and_strip, 'IDTABLELIBRE3'),
-            ('IDTABLELIBRE4', 'IDTABLELIBRE4', unicode_and_strip, 'IDTABLELIBRE4'),
-            ('CHAMPLIBRE1_CH', 'CHAMPLIBRE1_CH', unicode_and_strip, 'Valeur Champ Libre 1'),
-            ('CHAMPLIBRE2_CH', 'CHAMPLIBRE2_CH', unicode_and_strip, 'Valeur Champ Libre 2'),
-            ('CHAMPCALCULE1_CH', 'CHAMPCALCULE1_CH', unicode_and_strip, 'Valeur Champ Calculé 1'),
-            ('CHAMPCALCULE2_CH', 'CHAMPCALCULE2_CH', unicode_and_strip, 'Valeur Champ Calculé 2'),
-            ('SOMMEIL_ME', 'sommeil', unicode_and_strip, 'Sommeil'),
-            ('ACTIVITE_ME', 'activite', unicode_and_strip, 'Activités'),
-            ('HABITUDE_ME', 'habitude', unicode_and_strip, 'Habitudes'),
-            ('PHOTO_CH', 'photographie', unicode_and_strip, 'Photographie'),
-            ('NUMCOMPTE_CH', 'numcompte', unicode_and_strip, 'N° Compte Comptable'),
-            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'Téléphone'),
-            ('IDFAMILLES2', 'id_famille2', unicode_and_strip, 'Identifiant famille 2'),
-            ('PERE_CH', 'pere', unicode_and_strip, 'Nom du père'),
-            ('MERE_CH', 'mere', unicode_and_strip, 'Nom de la mère'),
-            ('AUTOPARENTALEMERE_IN', 'autorisation_parentale_mere', unicode_and_strip, 'Autorisation Parentale Mère'),
-            ('AUTOPARENTALEPERE_IN', 'autorisation_parentale_pere', unicode_and_strip, 'Autorisation Parentale de Père'),
-            ('IDPORTAIL_ENFANTS', 'id_portail_enfants', unicode_and_strip, 'Identifiant de PORTAIL_ENFANTS'),
-            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'Adresse Internet'),
-    )
-
-    def save(self):
-        if hasattr(self, 'id'):
-            self.client.update_child(self)
-        else:
-            self.id = self.client.add_child(self)
-        self.client.clear_cache()
-
-class Family(SimpleObject):
-    COLUMNS = (
-            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
-            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
-            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
-            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
-            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
-            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
-    )
-
-    MORE_COLUMNS = (
-            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
-            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
-            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
-            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
-            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
-            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
-            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
-            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
-            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
-            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
-            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
-            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
-            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
-            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
-            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
-            ('REVENUMENSUEL_MO', 'revenu_mensuel', unicode_and_strip, 'revenu mensuel de la famille'),
-            ('REVENUANNUEL_MO', 'revenu_annuel', unicode_and_strip, 'revenu annuel de la famille'),
-            ('QUOTIENTFAMILIAL_MO', 'quotient_familial', unicode_and_strip, 'quotient familial'),
-            ('NBTOTALENFANTS_EN', 'nb_total_enfants', unicode_and_strip, 'nombre total d\'enfants'),
-            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge', unicode_and_strip, 'nombre d\'enfants à charge'),
-            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
-            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
-            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere', unicode_and_strip, 'autorisation parentale de père'),
-            ('DATENAISPERE_DA', 'date_naissance_pere', strip_and_date, 'date de naisance du père'),
-            ('DEPNAISPERE_EN', 'departement_naissance_pere', unicode_and_strip, 'département de naissance du père'),
-            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
-            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
-            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
-            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
-            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
-            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
-            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
-            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
-            ('TELPERE_LR_IN', 'tel_pere_liste_rouge', unicode_and_strip, 'téléphone liste rouge père'),
-            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge père'),
-            ('TEL_LR_IN', 'tel_liste_rourge', unicode_and_strip, 'téléphone liste rouge'),
-            ('TEL2_LR_IN', 'tel2_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge'),
-            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
-            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
-            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere', unicode_and_strip, 'autorisation parentale mère'),
-            ('DATENAISMERE_DA', 'date_naissance_mere', strip_and_date, 'date de naissance de la mère'),
-            ('DEPNAISMERE_EN', 'departement_naissance_mere', unicode_and_strip, 'département de naissance de la mère'),
-            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
-            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
-            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere', unicode_and_strip, 'revenu mensuel du père'),
-            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
-            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
-            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
-            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
-            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere', unicode_and_strip, 'revenu mensuel mère'),
-            ('REVANNUELPERE_MO', 'revenu_annuel_pere', unicode_and_strip, 'revenu annuel père'),
-            ('REVANNUELMERE_MO', 'revenu_annuel_mere', unicode_and_strip, 'revenu annuel mère'),
-            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
-            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
-            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge', unicode_and_strip, 'téléphone liste rouge mère'),
-            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge mère'),
-            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
-            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
-            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
-            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
-            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
-            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
-            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
-            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
-            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
-            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
-            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
-            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
-            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
-            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
-            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
-            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
-            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
-            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
-            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
-            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
-            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
-            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
-            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
-            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
-            ('TOTALDU_MO', 'total_du', unicode_and_strip, 'total dû'),
-            ('TOTALREGLE_MO', 'total_regle', unicode_and_strip, 'total réglé'),
-            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
-            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
-            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
-            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
-            ('RIVOLI_EN', 'code_rivoli', unicode_and_strip, 'identifiant code rivoli'),
-            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
-            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
-            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
-            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
-            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'commentaires / notes'),
-            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
-            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
-            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
-            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
-            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
-            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
-            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
-            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
-            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
-            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
-            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
-            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
-            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
-            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
-            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
-            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
-            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
-            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
-            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
-            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
-            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
-            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
-            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
-            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
-            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
-            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
-            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
-#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
-#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
-            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
-            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
-            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
-            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
-            ('ECHEANCEASSURANCE_DA', 'echeance_assurance', unicode_and_strip, 'date echéance assurance'),
-            ('RM_MIKADO_MO', 'rm_mikado', unicode_and_strip, 'revenus mensuels mikado'),
-            ('RA_MIKADO_MO', 'ra_mikado', unicode_and_strip, 'revenus annuels mikado'),
-            ('QF_MIKADO_MO', 'qf_mikado', unicode_and_strip, 'quotient familial mikado'),
-            ('RM_DIABOLO_MO', 'rm_diabolo', unicode_and_strip, 'revenus mensuels diabolo'),
-            ('RA_DIABOLO_MO', 'ra_diabolo', unicode_and_strip, 'revenus annuels diabolo'),
-            ('QF_DIABOLO_MO', 'qf_diabolo', unicode_and_strip, 'quotient familial diabolo'),
-            ('RM_OLIGO_MO', 'rm_oligo', unicode_and_strip, 'revenus mensuels oligo'),
-            ('RA_OLIGO_MO', 'ra_oligo', unicode_and_strip, 'revenus annuels oligo'),
-            ('QF_OLIGO_MO', 'qf_oligo', unicode_and_strip, 'quotient familial oligo'),
-            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado', unicode_and_strip, 'date d\'application des revenus de mikado'),
-            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo', unicode_and_strip, 'date d\'application des revenus de diabolo'),
-            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo', unicode_and_strip, 'date d\'application des revenus de oligo'),
-    )
-
-    def __init__(self, *args, **kwargs):
-        self.children = []
-        super(Family, self).__init__(*args, **kwargs)
-
-    def complete(self):
-        k = [a for a,b,c,d in self.MORE_COLUMNS]
-        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
-                columns=self.MORE_COLUMNS, instances=(self,)))
-        l = self.client.get_children(self.id).values()
-        self.children = sorted(l, key=lambda c: c.id)
-        return self
-
-    @property
-    def invoices(self):
-        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
-
-    def add_child(self, child):
-        if hasattr(self, 'id'):
-            child.id_famille = self.id
-            child.client = self.client
-        self.children.append(child)
-
-    def save(self):
-        if hasattr(self, 'id'):
-            self.client.update_family(self)
-        else:
-            self.code_interne = self.client.new_code_interne()
-            self.id = self.client.add_family(self)
-        for child in self.children:
-            child.id_famille = self.id
-            child.save()
-        self.client.clear_cache()
-
-class Invoice(SimpleObject):
-    COLUMNS = (
-        ('', 'id_famille', int, ''),
-        ('', 'id', int, ''),
-        ('', 'numero', str, ''),
-        ('', 'debut_periode', parse_date, ''),
-        ('', 'fin_periode', parse_date, ''),
-        ('', 'creation', parse_date, ''),
-        ('', 'echeance', parse_date, ''),
-        ('', 'montant', Decimal, ''),
-        ('', 'reste_du', Decimal, ''),
-    )
-    _detail = {}
-
-    def detail(self):
-        if not self._detail:
-            self.client.factures_detail([self])
-        return self._detail
-
-    @property
-    def family(self):
-        return self.client.families[self.id_famille]
-
-    def paid(self):
-        return self.reste_du == Decimal(0)
-
-class DominoWs(object):
-    '''Interface to the WebService exposed by Abelium Domino.
-
-       It allows to retrieve family and invoices.
-
-       Beware that it does a lot of caching to limit call to the webservice, so
-       if you need fresh data, call clear_cache()
-
-       All family are in the families dictionnary and all invoices in the
-       invoices dictionnary.
-    '''
-
-    def __init__(self, url, domain, login, password, location=None,
-            logger=logger):
-        if not Client:
-            raise ValueError('You need python suds')
-        self.logger = logger
-        self.logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)',
-                url, domain, login, password, location)
-        self.url = url
-        self.domain = domain
-        self.login = login
-        self.password = password
-        self.client = Client(url, location=location, timeout=60)
-        self.client.options.cache.setduration(seconds=60)
-
-    def clear_cache(self):
-        '''Remove cached attributes from the instance.'''
-
-        for key, value in self.__dict__.items():
-            if key.startswith('__') and key.endswith('_cache'):
-                del self.__dict__[key]
-
-    def call(self, function_name, *args):
-        '''Call SOAP method named function_name passing args list as parameters.
-
-           Any error is converted into the DominoException class.'''
-        print 'call', function_name, args
-
-        try:
-            self.logger.debug(('soap call to %s(%s)' % (function_name, args)).encode('utf-8'))
-            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
-            self.logger.debug((u'result: %s' % data).encode('utf-8'))
-            self.data = data
-        except IOError, e:
-            raise DominoException('Erreur IO', e)
-        if data is None:
-           data = ''
-        if data.startswith('ERREUR'):
-            raise DominoException(data[9:].encode('utf8'))
-        return data
-
-    def parse_tabular_data(self, data):
-        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
-           Empty lines (ignoring spaces) are ignored.
-        '''
-
-        rows = data.split(LINE_SEPARATOR)
-        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
-        return rows
-
-    def __call__(self, function_name, cls=None, args=[], instances=None, columns=None):
-        '''Call SOAP method named function_name, splitlines, map tab separated
-        values to _map keys in a dictionnary, and use this dictionnary to
-        initialize an object of class cls.
-
-        - If instances is present, the given instances are updated with the
-          returned content, in order, row by row.
-        - If cls is not None and instances is None, a new instance of the class
-          cls is instancied for every row and initialized with the content of
-          the row.
-        - If cls and instances are None, the raw data returned by the SOAP call
-          is returned.
-        '''
-
-        data = self.call(function_name, *args)
-        if cls or instances:
-            rows = self.parse_tabular_data(data)
-            kwargs = {}
-            if instances:
-                rows = zip(rows, instances)
-            for row in rows:
-                if instances:
-                    row, instance = row
-                if not row[0]:
-                    continue
-                for a, b in zip(columns or cls.COLUMNS, row):
-                    x, name, converter, desc = a
-                    kwargs[name] = converter(b.strip())
-                if instances:
-                    instance.__dict__.update(kwargs)
-                    yield instance
-                else:
-                    yield cls(client=self, **kwargs)
-        else:
-            yield data
-
-    def add_family(self, family):
-        result = self.call('AJOUTER_FAMILLE', family.serialize())
-        return int(result.strip())
-
-    def update_family(self, family):
-        if not hasattr(family, 'id'):
-            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
-        result = self.call('MODIFIER_FAMILLE', unicode(family.id), family.serialize())
-        return result.strip() == 'OK'
-
-    def add_child(self, child):
-        result = self.call('AJOUTER_ENFANT', child.serialize())
-        return int(result.strip())
-
-    def update_child(self, child):
-        if not hasattr(child, 'id'):
-            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
-        result = self.call('MODIFIER_ENFANT', unicode(child.id), child.serialize())
-        return result.strip() == 'OK'
-
-    @property
-    @object_cached
-    def families(self):
-        '''Dictionary of all families indexed by their id.
-
-           After the first use, the value is cached. Use clear_cache() to reset
-           it.
-        '''
-
-        return self.get_families()
-
-    def get_families(self, id_famille=0, full=False):
-        '''Get families informations.
-           There is no caching.
-
-           id_famille - if not 0, the family with this id is retrieved. If 0
-           all families are retrieved. Default to 0.
-           full - If True return all the columns of the family table. Default
-           to False.
-        '''
-        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
-        families = self('LISTER_FAMILLES',
-                Family,
-                args=(','.join([x[0] for x in columns]), id_famille))
-        return dict([(int(x), x) for x in families])
-
-    def get_children(self, id_famille=0):
-        columns = Child.COLUMNS
-        if id_famille == 0:
-            children = self('LISTER_ENFANTS',
-                Child,
-                args=((','.join([x[0] for x in columns])),))
-        else:
-            children = self('LISTER_ENFANTS_FAMILLE',
-                Child,
-                args=(id_famille, (','.join([x[0] for x in columns]))))
-        return dict([(int(x), x) for x in children])
-
-    def get_urgent_contacts(self, id_enfant):
-        columns = UrgentContact.COLUMNS
-        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
-                UrgentContact,
-                args=((id_enfant, ','.join([x[0] for x in columns]))))
-        return dict([(int(x), x) for x in urgent_contacts])
-
-    @property
-    @object_cached
-    def invoices(self):
-        '''Dictionnary of all invoices indexed by their id.
-
-           After the first use, the value is cached. Use clear_cache() to reset
-           it.
-        '''
-        invoices = self.get_invoices()
-        for invoice in invoices.values():
-            invoice.famille = self.families[invoice.id_famille]
-        return invoices
-
-    def new_code_interne(self):
-        max_ci = 0
-        for family in self.families.values():
-            try:
-                max_ci = max(max_ci, int(family.code_interne))
-            except:
-                pass
-        return '%05d' % (max_ci+1)
-
-    def get_invoices(self, id_famille=0, state='TOUTES'):
-        '''Get invoices informations.
-
-           id_famille - If value is not 0, only invoice for the family with
-           this id are retrieved. If value is 0, invoices for all families are
-           retrieved. Default to 0.
-           etat - state of the invoices to return, possible values are
-           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
-        '''
-        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
-            args=(id_famille, state))
-        invoices = list(invoices)
-        for invoice in invoices:
-            invoice.famille = self.families[invoice.id_famille]
-        return dict(((int(x), x) for x in invoices))
-
-    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
-    def factures_detail(self, invoices):
-        '''Retrieve details of some invoice'''
-        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
-        try:
-            tree = etree.fromstring(data.encode('utf8'))
-            for invoice, facture_node in zip(invoices, tree.findall('facture')):
-                rows = []
-                for ligne in facture_node.findall('detail_facture/ligne'):
-                    row = []
-                    rows.append(row)
-                    for header in self.FACTURE_DETAIL_HEADERS:
-                        if header in ligne.attrib:
-                            row.append((header, ligne.attrib[header]))
-                etablissement = facture_node.find('detail_etablissements/etablissement')
-                if etablissement is not None:
-                    nom = etablissement.get('nom').strip()
-                else:
-                    nom = ''
-                d = { 'etablissement': nom, 'lignes': rows }
-                invoice._detail = d
-        except Exception, e:
-            raise DominoException('Exception when retrieving invoice details', e)
-
-    def get_family_by_mail(self, email):
-        '''Return the first whose one email attribute matches the given email'''
-        for famille in self.families.values():
-            if email in (famille.email_pere, famille.email_mere,
-                    famille.adresse_internet):
-                return famille
-        return None
-
-    def get_family_by_code_interne(self, code_interne):
-        '''Return the first whose one email attribute matches the given email'''
-        for famille in self.families.values():
-            if getattr(famille, 'code_interne', None) == code_interne:
-                return famille
-        return None
-
-    def pay_invoice(self, id_invoices, amount, other_information, date=None):
-        '''Notify Domino of the payment of some invoices.
-
-           id_invoices - integer if of the invoice or Invoice instances
-           amount - amount as a Decimal object
-           other_information - free content to attach to the payment, like a
-           bank transaction number for correlation.
-           date - date of the payment, must be a datetime object. If None,
-           now() is used. Default to None.
-        '''
-
-        if not date:
-            date = datetime.datetime.now()
-        due = sum([self.invoices[int(id_invoice)].reste_du
-                 for id_invoice in id_invoices])
-        if Decimal(amount) == Decimal(due):
-            return self('SOLDER_FACTURE', None, args=(str(amount), 
-                    ''.join([ '%s;' % int(x) for x in id_invoices]),
-                    date.strftime('%Y-%m-%d'), other_information))
-        else:
-            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
diff --git a/extra/modules/admin.py b/extra/modules/admin.py
deleted file mode 100644
index b238469..0000000
--- a/extra/modules/admin.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import os
-
-from quixote import get_publisher, redirect
-from quixote.directory import Directory
-from quixote.html import htmltext, TemplateIO
-
-import wcs.admin.root
-import wcs.root
-from wcs.roles import get_user_roles
-
-from qommon import _
-from qommon import errors, get_cfg
-from qommon.form import *
-
-import wcs.admin.settings
-from wcs.formdef import FormDef
-from wcs.categories import Category
-from qommon.backoffice.menu import html_top
-
-from events import get_default_event_tags
-import re
-from abelium_domino_ui import AbeliumDominoDirectory
-
-class AdminRootDirectory(wcs.admin.root.RootDirectory):
-    def __init__(self):
-        self._q_exports = wcs.admin.root.RootDirectory._q_exports
-        self.menu_items[-1] = ('/', N_('Publik'))
-
-    def get_intro_text(self):
-        return _('Welcome on Publik administration interface')
-
-
-class PanelDirectory(Directory):
-    _q_exports = ['', 'update', 'announces', 'permissions', 'event_keywords',
-            'announce_themes', 'strongbox', 'clicrdv', 'domino']
-    label = N_('Control Panel')
-
-    domino = AbeliumDominoDirectory()
-
-    def _verify_mask(self, form):
-        if form.is_submitted():
-            if not re.match("[0-9Xx]*$", form.get('mobile_mask') or ''):
-                form.set_error('mobile_mask', _('Invalid value'))
-            else:
-                string_value = form.get_widget('mobile_mask').value
-                if string_value:
-                    form.get_widget('mobile_mask').value = string_value.upper()
-
-    def announces(self):
-        announces_cfg = get_cfg('announces', {})
-        sms_cfg = get_cfg('sms', {})
-        form = Form(enctype='multipart/form-data')
-        hint = ""
-        if sms_cfg.get('mode','') in ("none",""):
-            hint = htmltext(_('You must also <a href="%s">configure your SMS provider</a>') % "../settings/sms")
-
-        form.add(CheckboxWidget, 'sms_support', title = _('SMS support'),
-                hint = hint, value = announces_cfg.get('sms_support', 0))
-        form.add(StringWidget, 'mobile_mask', title = _('Mask for mobile numbers'),
-                hint = _('example: 06XXXXXXXX'),
-                value = announces_cfg.get('mobile_mask',''))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        self._verify_mask(form)
-
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('aq/announces', _('Announces Options')))
-            html_top('settings', _('Announces Options'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Announces Options')
-            r += form.render()
-            return r.getvalue()
-        else:
-            from wcs.admin.settings import cfg_submit
-            cfg_submit(form, 'announces', ('sms_support','mobile_mask'))
-            return redirect('..')
-
-    def permissions(self):
-        permissions_cfg = get_cfg('aq-permissions', {})
-        form = Form(enctype='multipart/form-data')
-        form.add(SingleSelectWidget, 'forms', title = _('Admin role for forms'),
-                value = permissions_cfg.get('forms', None),
-                options = [(None, _('Nobody'), None)] + get_user_roles())
-        form.add(SingleSelectWidget, 'events', title = _('Admin role for events'),
-                value = permissions_cfg.get('events', None),
-                options = [(None, _('Nobody'), None)] + get_user_roles())
-        form.add(SingleSelectWidget, 'links', title = _('Admin role for links'),
-                value = permissions_cfg.get('links', None),
-                options = [(None, _('Nobody'), None)] + get_user_roles())
-        form.add(SingleSelectWidget, 'announces', title = _('Admin role for announces'),
-                value = permissions_cfg.get('announces', None),
-                options = [(None, _('Nobody'), None)] + get_user_roles())
-        form.add(SingleSelectWidget, 'payments', title = _('Admin role for payments'),
-                value = permissions_cfg.get('payments', None),
-                options = [(None, _('Nobody'), None)] + get_user_roles())
-        form.add(SingleSelectWidget, 'strongbox', title = _('Admin role for strongbox'),
-                value = permissions_cfg.get('strongbox', None),
-                options = [(None, _('Nobody'), None)] + get_user_roles())
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('aq/permissions', _('Permissions')))
-            html_top('settings', _('Permissions'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Permissions')
-            r += form.render()
-            return r.getvalue()
-        else:
-            from wcs.admin.settings import cfg_submit
-            cfg_submit(form, 'aq-permissions',
-                        ('forms', 'events', 'links', 'announces', 'payments', 'strongbox'))
-            return redirect('..')
-
-    def event_keywords(self):
-        misc_cfg = get_cfg('misc', {})
-        form = Form(enctype='multipart/form-data')
-        form.add(WidgetList, 'event_tags', title = _('Event Keywords'),
-                value = misc_cfg.get('event_tags', get_default_event_tags()),
-                elemnt_type = StringWidget,
-                add_element_label = _('Add Keyword'),
-                element_kwargs = {str('render_br'): False, str('size'): 30})
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('aq/event_keywords', _('Event Keywords')))
-            html_top('settings', _('Event Keywords'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Event Keywords')
-            r += form.render()
-            return r.getvalue()
-        else:
-            from wcs.admin.settings import cfg_submit
-            cfg_submit(form, 'misc', ('event_tags',))
-            return redirect('..')
-
-    def announce_themes(self):
-        misc_cfg = get_cfg('misc', {})
-        form = Form(enctype='multipart/form-data')
-        form.add(WidgetList, 'announce_themes', title = _('Announce Themes'),
-                value = misc_cfg.get('announce_themes', []),
-                elemnt_type = StringWidget,
-                add_element_label = _('Add Theme'),
-                element_kwargs = {str('render_br'): False, str('size'): 30})
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('aq/announce_themes', _('Announce Themes')))
-            html_top('settings', _('Announce Themes'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Announce Themes')
-            r += form.render()
-            return r.getvalue()
-        else:
-            from wcs.admin.settings import cfg_submit
-            cfg_submit(form, 'misc', ('announce_themes',))
-            return redirect('..')
-
-    def strongbox(self):
-        if not get_publisher().has_site_option('strongbox'):
-            raise errors.TraversalError()
-        misc_cfg = get_cfg('misc', {})
-        form = Form(enctype='multipart/form-data')
-        form.add(CheckboxWidget, 'aq-strongbox', title=_('Strongbox Support'),
-                value=misc_cfg.get('aq-strongbox'), required=False)
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('aq/strongbox', _('Strongbox Support')))
-            html_top('settings', _('Strongbox Support'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Strongbox Support')
-            r += form.render()
-            return r.getvalue()
-        else:
-            from wcs.admin.settings import cfg_submit
-            cfg_submit(form, 'misc', ('aq-strongbox',))
-            return redirect('..')
-
-    def clicrdv(self):
-        if not get_publisher().has_site_option('clicrdv'):
-            raise errors.TraversalError()
-        misc_cfg = get_cfg('misc', {})
-        form = Form(enctype='multipart/form-data')
-        form.add(SingleSelectWidget, 'aq-clicrdv-server', title=_('ClicRDV Server'),
-                value=misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), required=True,
-                options=[(str('www.clicrdv.com'), _('Production Server')),
-                         (str('sandbox.clicrdv.com'), _('Sandbox Server'))])
-        form.add(StringWidget, 'aq-clicrdv-api-key', title=_('API Key'),
-                value=misc_cfg.get('aq-clicrdv-api-key'), required=False,
-                size=40, hint=_('Empty to disable ClicRDV support'))
-        form.add(StringWidget, 'aq-clicrdv-api-username', title=_('Username'),
-                value=misc_cfg.get('aq-clicrdv-api-username'), required=False)
-        form.add(StringWidget, 'aq-clicrdv-api-password', title=_('Password'),
-                value=misc_cfg.get('aq-clicrdv-api-password'), required=False)
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_widget('cancel').parse():
-            return redirect('..')
-
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('aq/clicrdv', _('ClicRDV Integration')))
-            html_top('settings', _('ClicRDV Integration'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('ClicRDV Integration')
-            r += form.render()
-            r += htmltext('<p>%s</p>') % _('Available Interventions: ')
-            try:
-                from clicrdv import get_all_intervention_sets
-                intervention_sets = get_all_intervention_sets()
-                r += htmltext('<ul>')
-                for s in intervention_sets:
-                    r += htmltext('<li><strong>clicrdv_get_interventions_in_set(%s)</strong> - %s') % (
-                            s['id'], s['name'])
-                    r += htmltext('<ul>')
-                    for n, intervention in s['interventions']:
-                        r += htmltext('<li>%s (id: %s)</li>') % (intervention, n)
-                    r += htmltext('</ul></li>')
-                r += htmltext('</ul>')
-            except Exception, e:
-                r += htmltext('<p>%s (%s)</p>') % (
-                        _('Cannot access to ClicRDV service'), str(e))
-            return r.getvalue()
-        else:
-            from wcs.admin.settings import cfg_submit
-            cfg_submit(form, 'misc', ('aq-clicrdv-server',
-                                      'aq-clicrdv-api-key',
-                                      'aq-clicrdv-api-username',
-                                      'aq-clicrdv-api-password'))
-            return redirect('..')
-
-
-class SettingsDirectory(wcs.admin.settings.SettingsDirectory):
-    def _q_index(self):
-        r = TemplateIO(html=True)
-        r += htmltext(super(SettingsDirectory, self)._q_index())
-        r += htmltext('<div class="splitcontent-right">')
-        r += htmltext('<div class="bo-block">')
-        r += htmltext('<h2>%s</h2>') % _('Extra Options')
-        r += htmltext('<ul>')
-        r += htmltext('<li><a href="aq/announces">%s</a></li>') % _('Announces Options')
-        r += htmltext('<li><a href="aq/permissions">%s</a></li>') % _('Permissions')
-        r += htmltext('<li><a href="aq/event_keywords">%s</a></li>') % _('Event Keywords')
-        r += htmltext('<li><a href="aq/announce_themes">%s</a></li>') % _('Announce Themes')
-        if get_publisher().has_site_option('strongbox'):
-            r += htmltext('<li><a href="aq/strongbox">%s</a></li>') % _('Strongbox Support')
-        if get_publisher().has_site_option('clicrdv'):
-            r += htmltext('<li><a href="aq/clicrdv">%s</a></li>') % _('ClicRDV Integration')
-        if get_publisher().has_site_option('domino'):
-            r += htmltext('<li><a href="aq/domino">%s</a></li>') % _('Abelium Domino Integration')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        if component == 'aq':
-            return PanelDirectory()
-        return super(SettingsDirectory, self)._q_lookup(component)
-
-import categories_admin
diff --git a/extra/modules/agenda.py b/extra/modules/agenda.py
deleted file mode 100644
index 6f82c9b..0000000
--- a/extra/modules/agenda.py
+++ /dev/null
@@ -1,307 +0,0 @@
-import time
-import datetime
-from sets import Set
-
-from quixote.directory import Directory
-from quixote import get_publisher, get_request, redirect, get_session, get_response
-from quixote.html import htmltext, TemplateIO
-
-from qommon import _
-from qommon import misc, template, errors, get_cfg
-from qommon.form import *
-
-from events import Event, RemoteCalendar, get_default_event_tags
-
-
-class TagDirectory(Directory):
-    def _q_lookup(self, component):
-        events = Event.select()
-        for remote_calendar in RemoteCalendar.select():
-            if remote_calendar.events:
-                events.extend(remote_calendar.events)
-        self.events = [x for x in events if component in (x.keywords or [])]
-        self.events.sort(lambda x,y: cmp(x.date_start, y.date_start))
-        self.tag = component
-        return self.display_events()
-
-    def display_events(self):
-        template.html_top(_('Agenda'))
-        r = TemplateIO(html=True)
-        if len(self.events) > 1:
-            r += htmltext('<p id="nb-events">')
-            r += _('%(nb)d events with %(keyword)s keyword') % {
-                    'nb': len(self.events),
-                    'keyword': self.tag
-            }
-            r += htmltext('</p>')
-
-        if self.events:
-            r += htmltext('<dl id="events">')
-            for ev in self.events:
-                r += htmltext(ev.as_html_dt_dd())
-            r += htmltext('</dl>')
-        else:
-            r += htmltext('<p id="nb-events">')
-            r += _('No event registered with the %s keyword.') % self.tag
-            r += htmltext('</p>')
-        return r.getvalue()
-
-
-class AgendaDirectory(Directory):
-    _q_exports = ['', 'icalendar', 'tag', 'atom', 'filter']
-
-    year = None
-    month = None
-
-    tag = TagDirectory()
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append(('agenda/', _('Agenda')))
-        self.year, self.month = time.localtime()[:2]
-        if len(path) >= 1 and path[0].isdigit():
-            self.year, self.month = (None, None)
-            self.year = int(path[0])
-            get_response().breadcrumb.append(('%s/' % self.year, self.year))
-            path = path[1:]
-            if len(path) >= 1 and path[0] in [str(x) for x in range(1, 13)]:
-                self.month = int(path[0])
-                get_response().breadcrumb.append(('%s/' % self.month,
-                            misc.get_month_name(self.month)))
-                path = path[1:]
-            if len(path) == 0:
-                return redirect(get_request().get_path() + '/')
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        if self.month:
-            r = TemplateIO(html=True)
-            r += htmltext(self.display_month_links())
-            r += htmltext(self.display_month())
-            return r.getvalue()
-        else:
-            return redirect('..')
-
-    def display_month(self):
-        template.html_top(_('Agenda'))
-        events = Event.select()
-        remote_cal = get_request().form.get('cal')
-        if remote_cal != 'local':
-            if remote_cal:
-                try:
-                    events = RemoteCalendar.get(remote_cal).events
-                except KeyError:
-                    raise errors.TraversalError()
-                if not events:
-                    events = []
-            else:
-                for remote_calendar in RemoteCalendar.select():
-                    if remote_calendar.events:
-                        events.extend(remote_calendar.events)
-        events = [x for x in events if x.in_month(self.year, self.month)]
-        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
-
-        r = TemplateIO(html=True)
-        if events:
-            if len(events) > 1:
-                r += htmltext('<p id="nb-events">')
-                r += _('%(nb)d events for %(month_name)s %(year)s') % {
-                        'nb': len(events),
-                        'month_name': misc.get_month_name(self.month),
-                        'year': self.year}
-                r += htmltext('</p>')
-
-            r += htmltext('<dl id="events">')
-            for ev in events:
-                r += htmltext(ev.as_html_dt_dd())
-            r += htmltext('</dl>')
-        else:
-            r += htmltext('<p id="nb-events">')
-            r += _('No event registered for the month of %s.') % '%s %s' % (
-                    misc.get_month_name(self.month), self.year)
-            r += htmltext('</p>')
-
-        root_url = get_publisher().get_root_url()
-        r += htmltext('<div id="agenda-subs">')
-        r += htmltext('<p>')
-        r += _('You can subscribe to this calendar:')
-        r += htmltext('</p>')
-        r += htmltext('<ul>')
-        r += htmltext('  <li><a href="%sagenda/icalendar" id="par_ical">%s</a></li>') % (
-                root_url, _('iCalendar'))
-        r += htmltext('  <li><a href="%sagenda/atom" id="par_rss">%s</a></li>') % (
-                root_url, _('Feed'))
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def display_month_links(self):
-        today = datetime.date(*(time.localtime()[:2] + (1,)))
-        r = TemplateIO(html=True)
-        r += htmltext('<ul id="month-links">')
-        for i in range(12):
-            r += htmltext('<li>')
-            if (today.year, today.month) == (self.year, self.month):
-                r += htmltext('<strong>')
-                r += '%s %s' % (misc.get_month_name(today.month), today.year)
-                r += htmltext('</strong>')
-            else:
-                root_url = get_publisher().get_root_url()
-                r += htmltext('<a href="%sagenda/%s/%s/">') % (root_url, today.year, today.month)
-                r += '%s %s' % (misc.get_month_name(today.month), today.year)
-                r += htmltext('</a>')
-            r += htmltext('</li>')
-            today += datetime.timedelta(31)
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def display_remote_calendars(self):
-        r = TemplateIO(html=True)
-        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
-        if not remote_calendars:
-            return
-        remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
-        r += htmltext('<p class="tags">')
-        remote_cal = get_request().form.get('cal')
-        agenda_root_url = get_publisher().get_root_url() + 'agenda/'
-        if remote_cal:
-            r += htmltext('<a href="%s">%s</a> ') % (agenda_root_url, _('All'))
-        else:
-            r += htmltext('<strong><a href="%s">%s</a></strong> ') % (agenda_root_url, _('All'))
-        if remote_cal != 'local':
-            r += htmltext('<a href="%s?cal=local">%s</a> ') % (agenda_root_url, _('Local'))
-        else:
-            r += htmltext('<strong><a href="%s?cal=local">%s</a></strong> ') % (agenda_root_url, _('Local'))
-        for cal in remote_calendars:
-            if remote_cal == str(cal.id):
-                r += htmltext('<strong><a href="%s?cal=%s">%s</a></strong> ') % (
-                        agenda_root_url, cal.id, cal.label)
-            else:
-                r += htmltext('<a href="%s?cal=%s">%s</a> ') % (agenda_root_url, cal.id, cal.label)
-        r += htmltext('</p>')
-        return r.getvalue()
-
-    def icalendar(self):
-        if not Event.keys():
-            raise errors.TraversalError()
-        response = get_response()
-        response.set_content_type('text/calendar', 'utf-8')
-        vcal = Event.as_vcalendar()
-        if type(vcal) is unicode:
-            return vcal.encode('utf-8')
-        else:
-            return vcal
-
-    def atom(self):
-        response = get_response()
-        response.set_content_type('application/atom+xml')
-
-        from pyatom import pyatom
-        xmldoc = pyatom.XMLDoc()
-        feed = pyatom.Feed()
-        xmldoc.root_element = feed
-        feed.title = get_cfg('misc', {}).get('sitename', 'Publik') + ' - ' + _('Agenda')
-        feed.id = get_request().get_url()
-
-        author_email = get_cfg('emails', {}).get('reply_to')
-        if not author_email:
-            author_email = get_cfg('emails', {}).get('from')
-        if author_email:
-            feed.authors.append(pyatom.Author(author_email))
-
-        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
-
-        year, month = time.localtime()[:2]
-        nyear, nmonth = year, month+1
-        if nmonth > 12:
-            nyear, nmonth = nyear+1, 1
-
-        events = [x for x in Event.select() if x.in_month(year, month) or x.in_month(nyear, nmonth)]
-        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
-        events.reverse()
-
-        for item in events:
-            entry = item.get_atom_entry()
-            if entry is not None:
-                feed.entries.append(entry)
-
-        return str(feed)
-
-    def filter(self, no_event=False):
-        template.html_top(_('Agenda'))
-        tags = get_cfg('misc', {}).get('event_tags')
-        if not tags:
-            tags = get_default_event_tags()
-        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
-
-        form = Form(enctype='multipart/form-data')
-        if tags and remote_calendars:
-            form.widgets.append(HtmlWidget('<table id="agenda-filter"><tr><td>'))
-        if tags:
-            form.add(CheckboxesWidget, 'tags', title=_('Tags'),
-                    options=[(x,x) for x in tags],
-                    inline=False)
-        if tags and remote_calendars:
-            form.widgets.append(HtmlWidget('</td><td>'))
-        if remote_calendars:
-            remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
-            form.add(CheckboxesWidget, 'calendars', title=_('Calendars'),
-                    options=[('local', _('Local'))] + [(x.id, x.label) for x in remote_calendars],
-                    inline=False)
-        if tags and remote_calendars:
-            form.widgets.append(HtmlWidget('</td></tr></table>'))
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_widget('cancel').parse():
-            return redirect('.')
-
-        if no_event or not form.is_submitted():
-            r = TemplateIO(html=True)
-            if no_event:
-                r += htmltext('<p id="nb-events">')
-                r += _('No events matching the filter.')
-                r += htmltext('</p>')
-            r += form.render()
-            return r.getvalue()
-        else:
-            return self.filter_submitted(form, tags, remote_calendars)
-
-    def filter_submitted(self, form, tags, remote_calendars):
-        if remote_calendars:
-            selected_remote_calendars = form.get_widget('calendars').parse()
-            events = []
-            for remote_calendar in selected_remote_calendars:
-                if remote_calendar == 'local':
-                    events.extend(Event.select())
-                else:
-                    try:
-                        events.extend(RemoteCalendar.get(remote_calendar).events)
-                    except KeyError:
-                        pass
-        else:
-            events = Event.select()
-
-        events = [x for x in events if x.after_today()]
-
-        if tags:
-            selected_tags = Set(form.get_widget('tags').parse())
-            if selected_tags and len(selected_tags) != len(tags):
-                events = [x for x in events if Set(x.keywords).intersection(selected_tags)]
-
-        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
-
-        r = TemplateIO(html=True)
-
-        if len(events) > 1:
-            r += htmltext('<p id="nb-events">')
-            r += htmltext(_('%(nb)d events')) % {'nb': len(events)}
-            r += htmltext('</p>')
-
-        if events:
-            r += htmltext('<dl id="events">')
-            for ev in events:
-                r += htmltext(ev.as_html_dt_dd())
-            r += htmltext('</dl>')
-            return r.getvalue()
-        else:
-            return self.filter(no_event=True)
diff --git a/extra/modules/announces.py b/extra/modules/announces.py
deleted file mode 100644
index 4a1306d..0000000
--- a/extra/modules/announces.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import time
-
-from quixote import get_publisher
-
-from quixote.html import htmlescape
-
-from qommon import _
-from qommon.storage import StorableObject
-from qommon import get_cfg, get_logger
-from qommon import errors
-from qommon import misc
-
-from qommon import emails
-from qommon.sms import SMS
-from qommon.admin.emails import EmailsDirectory
-
-class AnnounceSubscription(StorableObject):
-    _names = 'announce-subscriptions'
-    _indexes = ['user_id']
-
-    user_id = None
-    email = None
-    sms = None
-    enabled = True
-    enabled_sms = False
-    enabled_themes = None
-
-    def remove(self, type=None):
-        """ type (string) : email or sms """
-        if type == "email":
-            self.email = None
-        elif type == "sms":
-            self.sms = None
-            self.enabled_sms = False
-        if not type or (not self.sms and not self.email):
-            self.remove_self()
-        else:
-            self.store()
-        
-    def get_user(self):
-        if self.user_id:
-            try:
-                return get_publisher().user_class.get(self.user_id)
-            except KeyError:
-                return None
-        return None
-    user = property(get_user)
-
-
-class Announce(StorableObject):
-    _names = 'announces'
-
-    title = None
-    text = None
-
-    hidden = False
-
-    publication_time = None
-    modification_time = None
-    expiration_time = None
-    sent_by_email_time = None
-    sent_by_sms_time = None
-    theme = None
-
-    position = None
-
-    def sort_by_position(cls, links):
-        def cmp_position(x, y):
-            if x.position == y.position:
-                return 0
-            if x.position is None:
-                return 1
-            if y.position is None:
-                return -1
-            return cmp(x.position, y.position)
-        links.sort(cmp_position)
-    sort_by_position = classmethod(sort_by_position)
-
-    def get_atom_entry(self):
-        from pyatom import pyatom
-        entry = pyatom.Entry()
-        entry.id = self.get_url()
-        entry.title = self.title
-
-        entry.content.attrs['type'] = 'html'
-        entry.content.text = str('<p>' + htmlescape(
-                    unicode(self.text, get_publisher().site_charset).encode('utf-8')) + '</p>')
-
-        link = pyatom.Link(self.get_url())
-        entry.links.append(link)
-
-        if self.publication_time:
-            entry.published = misc.format_time(self.publication_time,
-                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
-                    gmtime = True)
-
-        if self.modification_time:
-            entry.updated = misc.format_time(self.modification_time,
-                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
-                    gmtime = True)
-
-        return entry
-
-    def get_url(self):
-        return '%s/announces/%s/' % (get_publisher().get_frontoffice_url(), self.id)
-
-    def store(self):
-        self.modification_time = time.gmtime()
-        StorableObject.store(self)
-
-    def email(self, job=None):
-        self.sent_by_email_time = time.gmtime()
-        StorableObject.store(self)
-
-        data = {
-            'title': self.title,
-            'text': self.text
-        }
-
-        subscribers = AnnounceSubscription.select(lambda x: x.enabled)
-
-        rcpts = []
-        for l in subscribers:
-            if self.theme:
-                if l.enabled_themes is not None:
-                    if self.theme not in l.enabled_themes:
-                        continue
-            if l.user and l.user.email:
-                rcpts.append(l.user.email)
-            elif l.email:
-                rcpts.append(l.email)
-
-        emails.custom_ezt_email('aq-announce', data, email_rcpt = rcpts, hide_recipients = True)
-
-    def sms(self, job=None):
-        self.sent_by_sms_time = time.gmtime()
-        StorableObject.store(self)
-
-        subscribers = AnnounceSubscription.select(lambda x: x.enabled_sms)
-
-        rcpts = []
-        for sub in subscribers:
-            if self.theme:
-                if sub.enabled_themes is not None:
-                    if self.theme not in sub.enabled_themes:
-                        continue
-            if sub.sms:
-                rcpts.append(sub.sms)
-
-        sms_cfg = get_cfg('sms', {})
-        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
-        message = "%s: %s" % (self.title, self.text)
-        mode = sms_cfg.get('mode', 'none')
-        sms = SMS.get_sms_class(mode)
-        try:
-            sms.send(sender, rcpts, message[:160])
-        except errors.SMSError, e:
-            get_logger().error(e)
-
-    def get_published_announces(cls):
-        announces = cls.select(lambda x: not x.hidden)
-        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
-                    y.publication_time or y.modification_time))
-        announces = [x for x in announces if x.publication_time < time.gmtime()
-                     and (x.expiration_time is None or x.expiration_time > time.gmtime())]
-        announces.reverse()
-        return announces
-    get_published_announces = classmethod(get_published_announces)
-
-
-EmailsDirectory.register('aq-announce',
-        N_('Publication of announce to subscriber'),
-        N_('Available variables: title, text'),
-        default_subject = N_('Announce: [title]'),
-        default_body = N_("""\
-[text]
-
--- 
-This is an announce sent to you by your city, you can opt to not receive
-those messages anymore on the city website.
-"""))
-
diff --git a/extra/modules/announces_ui.py b/extra/modules/announces_ui.py
deleted file mode 100644
index d916a3b..0000000
--- a/extra/modules/announces_ui.py
+++ /dev/null
@@ -1,459 +0,0 @@
-from quixote import get_request, get_response, get_session, redirect
-from quixote.directory import Directory, AccessControlled
-from quixote.html import htmltext, TemplateIO
-
-import wcs
-
-from qommon import _
-from qommon.backoffice.menu import html_top
-from qommon.admin.menu import command_icon
-from qommon import get_cfg
-from qommon import errors
-from qommon.form import *
-from qommon.afterjobs import AfterJob
-
-from announces import Announce, AnnounceSubscription
-
-
-class SubscriptionDirectory(Directory):
-    _q_exports = ['delete_email', "delete_sms"]
-
-    def __init__(self, subscription):
-        self.subscription = subscription
-
-    def delete_email(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to delete this subscription.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('announces', title = _('Delete Subscription'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.subscription.remove("email")
-            return redirect('..')
-
-    def delete_sms(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to delete this subscription.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('announces', title = _('Delete Subscription'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.subscription.remove("sms")
-            return redirect('..')
-
-
-class SubscriptionsDirectory(Directory):
-    _q_exports = ['']
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append(('subscriptions', _('Subscriptions')))
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        html_top('announces', _('Announces Subscribers'))
-        r = TemplateIO(html=True)
-
-        r += htmltext('<h2>%s</h2>') % _('Announces Subscribers')
-
-        subscribers = AnnounceSubscription.select()
-        r += htmltext('<ul class="biglist" id="subscribers-list">')
-        for l in subscribers:
-            if l.email:
-                if l.enabled is False:
-                    r += htmltext('<li class="disabled">')
-                else:
-                    r += htmltext('<li>')
-                r += htmltext('<strong class="label">')
-                if l.user:
-                    r += l.user.display_name
-                elif l.email:
-                    r += l.email
-                r += htmltext('</strong>')
-                r += htmltext('<p class="details">')
-                if l.user:
-                    r += l.user.email
-                r += htmltext('</p>')
-                r += htmltext('<p class="commands">')
-                r += command_icon('%s/delete_email' % l.id, 'remove', popup = True)
-                r += htmltext('</p></li>')
-                r += htmltext('</li>')
-            if l.sms:
-                if l.enabled_sms is False:
-                    r += htmltext('<li class="disabled">')
-                else:
-                    r += htmltext('<li>')
-                r += htmltext('<strong class="label">')
-                if l.user:
-                    r += l.user.display_name
-                elif l.email:
-                    r += l.email
-                r += htmltext('</strong>')
-                r += htmltext('<p class="details">')
-                r += l.sms
-                r += htmltext('</p>')
-                r += htmltext('<p class="commands">')
-                r += command_icon('%s/delete_sms' % l.id, 'remove', popup = True)
-                r += htmltext('</p></li>')
-                r += htmltext('</li>')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            sub = AnnounceSubscription.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(sub.id), str(sub.id)))
-        return SubscriptionDirectory(sub)
-
-    def listing(self):
-        return redirect('.')
-
-class AnnounceDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete', 'email', 'sms']
-
-    def __init__(self, announce):
-        self.announce = announce
-
-    def _q_index(self):
-        form = Form(enctype='multipart/form-data')
-        get_response().filter['sidebar'] = self.get_sidebar()
-
-        if self.announce.sent_by_email_time is None:
-            form.add_submit('email', _('Send email'))
-
-        announces_cfg = get_cfg('announces', {})
-        if announces_cfg.get('sms_support', 0) and self.announce.sent_by_sms_time is None:
-            form.add_submit('sms', _('Send SMS'))
-
-        if form.get_submit() == 'edit':
-            return redirect('edit')
-        if form.get_submit() == 'delete':
-            return redirect('delete')
-        if form.get_submit() == 'email':
-            return redirect('email')
-        if form.get_submit() == 'sms':
-            return redirect('sms')
-
-        html_top('announces', title = _('Announce: %s') % self.announce.title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Announce: %s') % self.announce.title
-        r += htmltext('<div class="bo-block">')
-        r += htmltext('<p>')
-        r += self.announce.text
-        r += htmltext('</p>')
-        r += htmltext('</div>')
-
-        if form.get_submit_widgets():
-            r += form.render()
-
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul>')
-        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
-        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def email(self):
-        if get_request().form.get('job'):
-            try:
-                job = AfterJob.get(get_request().form.get('job'))
-            except KeyError:
-                return redirect('..')
-            html_top('announces', title = _('Announce: %s') % self.announce.title)
-            r = TemplateIO(html=True)
-            get_response().add_javascript(['jquery.js', 'afterjob.js'])
-            r += htmltext('<dl class="job-status">')
-            r += htmltext('<dt>')
-            r += _(job.label)
-            r += htmltext('</dt>')
-            r += htmltext('<dd>')
-            r += htmltext('<span class="afterjob" id="%s">') % job.id
-            r += _(job.status)
-            r += htmltext('</span>')
-            r += htmltext('</dd>')
-            r += htmltext('</dl>')
-
-            r += htmltext('<div class="done">')
-            r += htmltext('<a href="../">%s</a>') % _('Back')
-            r += htmltext('</div>')
-
-            return r.getvalue()
-        else:
-            job = get_response().add_after_job(
-                    str(N_('Sending emails for announce')),
-                    self.announce.email)
-            return redirect('email?job=%s' % job.id)
-
-    def sms(self):
-        if get_request().form.get('job'):
-            try:
-                job = AfterJob.get(get_request().form.get('job'))
-            except KeyError:
-                return redirect('..')
-            html_top('announces', title = _('Announce: %s') % self.announce.title)
-            get_response().add_javascript(['jquery.js', 'afterjob.js'])
-            r = TemplateIO(html=True)
-            r += htmltext('<dl class="job-status">')
-            r += htmltext('<dt>')
-            r += _(job.label)
-            r += htmltext('</dt>')
-            r += htmltext('<dd>')
-            r += htmltext('<span class="afterjob" id="%s">') % job.id
-            r += _(job.status)
-            r += htmltext('</span>')
-            r += htmltext('</dd>')
-            r += htmltext('</dl>')
-
-            r += htmltext('<div class="done">')
-            r += htmltext('<a href="../">%s</a>') % _('Back')
-            r += htmltext('</div>')
-
-            return r.getvalue()
-        else:
-            job = get_response().add_after_job(
-                    str(N_('Sending sms for announce')),
-                    self.announce.sms)
-            return redirect('sms?job=%s' % job.id)
-
-    def edit(self):
-        form = self.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit(form)
-            return redirect('..')
-
-        html_top('announces', title = _('Edit Announce: %s') % self.announce.title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Edit Announce: %s') % self.announce.title
-        r += form.render()
-        return r.getvalue()
-
-    def form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'title', title = _('Title'), required = True,
-                value = self.announce.title)
-        if self.announce.publication_time:
-            pub_time = time.strftime(misc.date_format(), self.announce.publication_time)
-        else:
-            pub_time = None
-        form.add(DateWidget, 'publication_time', title = _('Publication Time'),
-                value = pub_time)
-        if self.announce.expiration_time:
-            exp_time = time.strftime(misc.date_format(), self.announce.expiration_time)
-        else:
-            exp_time = None
-        form.add(DateWidget, 'expiration_time', title = _('Expiration Time'),
-                value = exp_time)
-        form.add(TextWidget, 'text', title = _('Text'), required = True,
-                value = self.announce.text, rows = 10, cols = 70)
-        if get_cfg('misc', {}).get('announce_themes'):
-            form.add(SingleSelectWidget, 'theme', title = _('Announce Theme'),
-                    value = self.announce.theme,
-                    options = get_cfg('misc', {}).get('announce_themes'))
-        form.add(CheckboxWidget, 'hidden', title = _('Hidden'),
-                value = self.announce.hidden)
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit(self, form):
-        for k in ('title', 'text', 'hidden', 'theme'):
-            widget = form.get_widget(k)
-            if widget:
-                setattr(self.announce, k, widget.parse())
-        for k in ('publication_time', 'expiration_time'):
-            widget = form.get_widget(k)
-            if widget:
-                wid_time = widget.parse()
-                if wid_time:
-                    setattr(self.announce, k, time.strptime(wid_time, misc.date_format()))
-                else:
-                    setattr(self.announce, k, None)
-        self.announce.store()
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this announce.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('announces', title = _('Delete Announce'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Announce: %s') % self.announce.title
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.announce.remove_self()
-            return redirect('..')
-
-
-class AnnouncesDirectory(AccessControlled, Directory):
-    _q_exports = ['', 'new', 'listing', 'subscriptions', 'update_order', 'log']
-    label = N_('Announces')
-
-    subscriptions = SubscriptionsDirectory()
-
-    def is_accessible(self, user):
-        from .backoffice import check_visibility
-        return check_visibility('announces', user)
-
-    def _q_access(self):
-        user = get_request().user
-        if not user:
-            raise errors.AccessUnauthorizedError()
-
-        if not self.is_accessible(user):
-            raise errors.AccessForbiddenError(
-                    public_msg = _('You are not allowed to access Announces Management'),
-                    location_hint = 'backoffice')
-
-        get_response().breadcrumb.append(('announces/', _('Announces')))
-
-    def _q_index(self):
-        html_top('announces', _('Announces'))
-        r = TemplateIO(html=True)
-
-        get_response().filter['sidebar'] = self.get_sidebar()
-
-        announces = Announce.select()
-        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
-                    y.publication_time or y.modification_time))
-        announces.reverse()
-
-        r += htmltext('<ul class="biglist" id="announces-list">')
-        for l in announces:
-            announce_id = l.id
-            if l.hidden:
-                r += htmltext('<li class="disabled" class="biglistitem" id="itemId_%s">') % announce_id
-            else:
-                r += htmltext('<li class="biglistitem" id="itemId_%s">') % announce_id
-            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (l.id, l.title)
-            if l.publication_time:
-                r += htmltext('<p class="details">')
-                r += time.strftime(misc.date_format(), l.publication_time)
-                r += htmltext('</p>')
-            r += htmltext('</li>')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul id="sidebar-actions">')
-        r += htmltext(' <li><a class="new-item" href="new">%s</a></li>') % _('New')
-        r += htmltext(' <li><a href="subscriptions/">%s</a></li>') % _('Subscriptions')
-        r += htmltext(' <li><a href="log">%s</a></li>') % _('Log')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def log(self):
-        announces = Announce.select()
-        log = []
-        for l in announces:
-            if l.publication_time:
-                log.append((l.publication_time, _('Publication'), l))
-            if l.sent_by_email_time:
-                log.append((l.sent_by_email_time, _('Email'), l))
-            if l.sent_by_sms_time:
-                log.append((l.sent_by_sms_time, _('SMS'), l))
-        log.sort()
-
-        get_response().breadcrumb.append(('log', _('Log')))
-        html_top('announces', title = _('Log'))
-        r = TemplateIO(html=True)
-
-        r += htmltext('<table>')
-        r += htmltext('<thead>')
-        r += htmltext('<tr>')
-        r += htmltext('<th>%s</th>') % _('Time')
-        r += htmltext('<th>%s</th>') % _('Type')
-        r += htmltext('<td></td>')
-        r += htmltext('</tr>')
-        r += htmltext('</thead>')
-        r += htmltext('<tbody>')
-        for log_time, log_type, log_announce in log:
-            r += htmltext('<tr>')
-            r += htmltext('<td>')
-            r += misc.localstrftime(log_time)
-            r += htmltext('</td>')
-            r += htmltext('<td>')
-            r += log_type
-            r += htmltext('</td>')
-            r += htmltext('<td>')
-            r += htmltext('<a href="%s">%s</a>') % (log_announce.id, log_announce.title)
-            r += htmltext('</td>')
-            r += htmltext('</tr>')
-        r += htmltext('</tbody>')
-        r += htmltext('</table>')
-        return r.getvalue()
-
-    def update_order(self):
-        request = get_request()
-        new_order = request.form['order'].strip(';').split(';')
-        announces = Announce.select()
-        dict = {}
-        for l in announces:
-            dict[str(l.id)] = l
-        for i, o in enumerate(new_order):
-            dict[o].position = i + 1
-            dict[o].store()
-        return 'ok'
-
-    def new(self):
-        announce = Announce()
-        announce.publication_time = time.gmtime()
-        announce_ui = AnnounceDirectory(announce)
-
-        form = announce_ui.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            announce_ui.submit(form)
-            return redirect('%s/' % announce_ui.announce.id)
-
-        get_response().breadcrumb.append(('new', _('New Announce')))
-        html_top('announces', title = _('New Announce'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Announce')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            announce = Announce.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(announce.id), announce.title))
-        return AnnounceDirectory(announce)
-
-    def listing(self):
-        return redirect('.')
-
diff --git a/extra/modules/backoffice.py b/extra/modules/backoffice.py
deleted file mode 100644
index 0b41830..0000000
--- a/extra/modules/backoffice.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import os
-
-from quixote import get_publisher, redirect
-from quixote.directory import Directory
-from quixote.html import TemplateIO, htmltext
-
-from qommon import _
-from qommon.publisher import get_publisher_class
-
-import wcs.backoffice.management
-import wcs.root
-from wcs.categories import Category
-from wcs.formdef import FormDef
-
-from qommon import get_cfg, errors
-from qommon.form import *
-
-CURRENT_USER = object()
-
-def check_visibility(target, user=CURRENT_USER):
-    if user is CURRENT_USER:
-        user = get_request().user
-    if not user:
-        return False
-    target = target.strip('/')
-    if target == 'management':
-        target = 'forms'
-    if target == 'strongbox':
-        if not get_publisher().has_site_option(target):
-            # strongbox disabled in site-options.cfg
-            return False
-        if not get_cfg('misc', {}).get('aq-strongbox'):
-            # strongbox disabled in settings panel
-            return False
-    admin_role = get_cfg('aq-permissions', {}).get(target, None)
-    if not admin_role:
-        return False
-    if not (user.is_admin or admin_role in (user.roles or [])):
-        return False
-    return True
-
-
-class BackofficeRootDirectory(wcs.backoffice.root.RootDirectory):
-    def get_intro_text(self):
-        return _('Welcome on Publik back office interface')
-
-    def _q_index(self):
-        if len(self.get_menu_items()) == 1:
-            return redirect(self.get_menu_items()[0]['url'])
-        return wcs.backoffice.root.RootDirectory._q_index(self)
-
-    def home(self):
-        return redirect('management/')
-
-get_publisher_class().backoffice_directory_class = BackofficeRootDirectory
diff --git a/extra/modules/categories_admin.py b/extra/modules/categories_admin.py
deleted file mode 100644
index 8fc3aa5..0000000
--- a/extra/modules/categories_admin.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# w.c.s. - web application for online forms
-# Copyright (C) 2005-2010  Entr'ouvert
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, see <http://www.gnu.org/licenses/>.
-
-from quixote import redirect
-from quixote.directory import Directory
-from quixote.html import TemplateIO, htmltext
-
-from qommon import _
-from qommon import misc
-from wcs.categories import Category
-from qommon.form import *
-from qommon.backoffice.menu import html_top
-from qommon.admin.menu import command_icon, error_page
-import wcs.admin.categories
-
-class CategoryUI:
-    def __init__(self, category):
-        self.category = category
-        if self.category is None:
-            self.category = Category()
-
-    def get_form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'name', title = _('Category Name'), required = True, size=30,
-                value = self.category.name)
-        form.add(TextWidget, 'description', title = _('Description'), cols = 80, rows = 10,
-                value = self.category.description)
-
-        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
-        if not homepage_redirect_url:
-            form.add(SingleSelectWidget, 'homepage_position',
-                        title=_('Position on homepage'),
-                        value=self.category.get_homepage_position(),
-                        options = [('1st', _('First Column')),
-                                   ('2nd', _('Second Column')),
-                                   ('side', _('Sidebar')),
-                                   ('none', _('None'))])
-            form.add(IntWidget, 'limit',
-                        title=_('Limit number of items displayed on homepage'),
-                        value=self.category.get_limit())
-
-        form.add(StringWidget, 'redirect_url', size=32,
-                title=_('URL Redirection'),
-                hint=_('If set, redirect the site category page to the given URL.'),
-                value=self.category.redirect_url)
-
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit_form(self, form):
-        if self.category.id:
-            self.category.name = form.get_widget('name').parse()
-            category = self.category
-        else:
-            category = Category(name = form.get_widget('name').parse())
-
-        name = form.get_widget('name').parse()
-        category_names = [x.name for x in Category.select() if x.id != category.id]
-        if name in category_names:
-            form.get_widget('name').set_error(_('This name is already used'))
-            raise ValueError()
-
-        category.description = form.get_widget('description').parse()
-        category.redirect_url = form.get_widget('redirect_url').parse()
-
-        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
-        if not homepage_redirect_url:
-            category.homepage_position = form.get_widget('homepage_position').parse()
-            category.limit = form.get_widget('limit').parse()
-
-        category.store()
-
-
-
-class CategoryPage(wcs.admin.categories.CategoryPage):
-    def __init__(self, component):
-        self.category = Category.get(component)
-        self.category_ui = CategoryUI(self.category)
-        get_response().breadcrumb.append((component + '/', self.category.name))
-
-
-class CategoriesDirectory(wcs.admin.categories.CategoriesDirectory):
-    label = N_('Categories')
-
-    def new(self):
-        get_response().breadcrumb.append( ('categories/', _('Categories')) )
-        get_response().breadcrumb.append( ('new', _('New')) )
-        category_ui = CategoryUI(None)
-        form = category_ui.get_form()
-        if form.get_widget('cancel').parse():
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            try:
-                category_ui.submit_form(form)
-            except ValueError:
-                pass
-            else:
-                return redirect('.')
-
-        html_top('categories', title = _('New Category'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Category')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        get_response().breadcrumb.append( ('categories/', _('Categories')) )
-        return CategoryPage(component)
diff --git a/extra/modules/clicrdv.py b/extra/modules/clicrdv.py
deleted file mode 100644
index 3beae71..0000000
--- a/extra/modules/clicrdv.py
+++ /dev/null
@@ -1,365 +0,0 @@
-import base64
-import datetime
-import urllib2
-
-try:
-    import json
-except ImportError:
-    import simplejson as json
-
-import time
-import vobject
-
-from qommon import _
-from qommon import get_cfg
-from qommon.misc import format_time
-from qommon.form import *
-
-from wcs.data_sources import register_data_source_function
-from wcs.formdata import Evolution
-from wcs.forms.common import FormStatusPage
-from wcs.workflows import Workflow, WorkflowStatusItem, register_item_class
-
-def get_clicrdv_req(url):
-    misc_cfg = get_cfg('misc', {})
-
-    url = 'https://%s/api/v1/%s' % (
-                misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), url)
-    if '?' in url:
-       url = url + '&apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
-    else:
-       url = url + '?apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
-
-    req = urllib2.Request(url)
-    username = misc_cfg.get('aq-clicrdv-api-username')
-    password = misc_cfg.get('aq-clicrdv-api-password')
-    authheader = 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
-    req.add_header('Authorization', authheader)
-    return req
-
-def get_json(url):
-    return json.load(urllib2.urlopen(get_clicrdv_req(url)))
-
-def as_str(s):
-    if type(s) is unicode:
-        return s.encode(get_publisher().site_charset)
-    return s
-
-def get_all_intervention_sets():
-    interventions_set = []
-    for interventionset in sorted(get_json('interventionsets').get('records'), 
-            lambda x,y: cmp(x['sort'],y['sort'])):
-        interventions = []
-        for intervention in sorted(get_json('interventionsets/%s/interventions' % interventionset.get('id')).get('records'),
-                lambda x,y: cmp(x['sort'], y['sort'])):
-            if intervention.get('deleted') == True:
-                continue
-            name = '%s' % as_str(intervention.get('publicname'))
-            if not name:
-                name = '%s' % as_str(intervention.get('name'))
-            interventions.append((intervention.get('id'), as_str(name)))
-        interventions_set.append({
-                'id': interventionset.get('id'),
-                'group_id': interventionset.get('group_id'),
-                'name': as_str(interventionset.get('name')),
-                'publicname': as_str(interventionset.get('publicname')) or '',
-                'description': as_str(interventionset.get('description')) or '',
-                'interventions': interventions
-                })
-    return interventions_set
-
-def get_all_interventions():
-    interventions = []
-    for s in get_all_intervention_sets():
-        for i, publicname in s['interventions']:
-            intervention_label = '%s - %s' % (s['publicname'], publicname)
-            interventions.append((i, as_str(intervention_label)))
-    return interventions
-
-def get_interventions_in_set(interventionset_id):
-    interventions = []
-    interventions_json = get_json('interventionsets/%s/interventions' % interventionset_id)
-    for intervention in interventions_json.get('records'):
-        if intervention.get('deleted') != True:
-            name = '%s' % as_str(intervention.get('publicname'))
-            if not name:
-                name = '%s' % as_str(intervention.get('name'))
-            interventions.append((intervention.get('id'), name))
-    return interventions
-
-def get_available_timeslots(intervention, date_start=None, date_end=None):
-    timeslots = []
-    iid = intervention
-    gid = get_json('interventions/%s' % iid).get('group_id')
-    request_url = 'availabletimeslots?intervention_ids[]=%s&group_id=%s' % (iid, gid)
-    if date_start is None:
-        date_start = datetime.datetime.today().strftime('%Y-%m-%d')
-    if date_end is None:
-        date_end = (datetime.datetime.today() + datetime.timedelta(366)).strftime('%Y-%m-%d')
-    if date_start:
-        request_url = request_url + '&start=%s' % urllib2.quote(date_start)
-    if date_end:
-        request_url = request_url + '&end=%s' % urllib2.quote(date_end)
-    for timeslot in get_json(request_url).get('availabletimeslots'):
-        timeslots.append(timeslot.get('start'))
-    timeslots.sort()
-    return timeslots
-
-def get_available_dates(intervention):
-    dates = []
-    for timeslot in get_available_timeslots(intervention):
-        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
-        date_tuple = (time.strftime('%Y-%m-%d', parsed),
-                      format_time(parsed, '%(weekday_name)s %(day)0.2d/%(month)0.2d/%(year)s'))
-        if date_tuple in dates:
-            continue
-        dates.append(date_tuple)
-    return dates
-
-def get_available_times(intervention, date):
-    times = []
-    timeslots = get_available_timeslots(intervention,
-                    date_start='%s 00:00:00' % date,
-                    date_end='%s 23:59:59' % date)
-    for timeslot in timeslots:
-        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
-        time_tuple = (time.strftime('%H:%M:%S', parsed),
-                      time.strftime('%Hh%M', parsed))
-        times.append(time_tuple)
-    times.sort()
-    return times
-
-register_data_source_function(get_all_interventions, 'clicrdv_get_all_interventions')
-register_data_source_function(get_interventions_in_set, 'clicrdv_get_interventions_in_set')
-register_data_source_function(get_available_dates, 'clicrdv_get_available_dates')
-register_data_source_function(get_available_times, 'clicrdv_get_available_times')
-
-def form_download_event(self):
-    self.check_receiver()
-
-    found = False
-    for evo in self.filled.evolution:
-        if evo.parts:
-            for p in evo.parts:
-                if not isinstance(p, AppointmentPart):
-                    continue
-                cal = vobject.iCalendar()
-                cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
-                vevent = vobject.newFromBehavior('vevent')
-                vevent.add('uid').value = 'clicrdv-%s' % p.id
-                vevent.add('summary').value = p.json_dict.get('group_name')
-                vevent.add('dtstart').value = datetime.datetime.strptime(
-                                p.json_dict.get('start'), '%Y-%m-%d %H:%M:%S')
-                vevent.add('dtend').value = datetime.datetime.strptime(
-                                p.json_dict.get('end'), '%Y-%m-%d %H:%M:%S')
-                vevent.add('location').value = p.json_dict.get('location')
-                cal.add(vevent)
-
-                response = get_response()
-                response.set_content_type('text/calendar')
-                return cal.serialize()
-
-    raise TraversalError()
-
-
-class AppointmentPart(object):
-    def __init__(self, json_dict):
-        self.id = json_dict.get('id')
-        self.json_dict = json_dict
-
-    def view(self):
-        return htmltext('<p class="appointment"><a href="clicrdvevent">%s</a></p>' % (
-                                _('Download Appointment')))
-
-
-class AppointmentErrorPart(object):
-    def __init__(self, msg):
-        self.msg = msg
-
-    def view(self):
-        return htmltext('<p class="appointment-error">%s</p>' % str(self.msg))
-
-
-class ClicRdvCreateAppointment(WorkflowStatusItem):
-    description = N_('Create a ClicRDV Appointment')
-    key = 'clicrdv-create'
-    category = ('aq-clicrdv', N_('ClicRDV'))
-
-    endpoint = False
-
-    var_firstname = None
-    var_lastname = None
-    var_email = None
-    var_firstphone = None
-    var_secondphone = None
-    var_datetime = None
-    var_intervention_id = None
-    status_on_success = None
-    status_on_failure = None
-
-    def init(cls):
-        FormStatusPage._q_extra_exports.append('clicrdvevent')
-        FormStatusPage.clicrdvevent = form_download_event
-    init = classmethod(init)
-
-    def is_available(self, workflow=None):
-        return get_publisher().has_site_option('clicrdv')
-    is_available = classmethod(is_available)
-
-    def render_as_line(self):
-        return _('Create an appointment in ClicRDV')
-
-    def get_parameters(self):
-        return ('var_firstname', 'var_lastname', 'var_email', 'var_firstphone',
-                'var_secondphone', 'var_datetime', 'var_intervention_id',
-                'status_on_success', 'status_on_failure')
-
-    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
-        parameter_labels = {
-            'var_firstname': N_('First Name'),
-            'var_lastname': N_('Last Name'),
-            'var_email': N_('Email'),
-            'var_firstphone': N_('Phone (1st)'),
-            'var_secondphone':  N_('Phone (2nd)'),
-            'var_datetime': N_('Date/time'),
-            'var_intervention_id': N_('Intervention Id'),
-        }
-        for parameter in self.get_parameters():
-            if not parameter in parameter_labels:
-                continue
-            if parameter in parameters:
-                form.add(StringWidget, '%s%s' % (prefix, parameter),
-                         title=_(parameter_labels.get(parameter)),
-                         value=getattr(self, parameter),
-                         required=False)
-        if 'status_on_success' in parameters:
-            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
-                    title=_('Status On Success'), value=self.status_on_success,
-                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
-        if 'status_on_failure' in parameters:
-            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
-                    title=_('Status On Failure'), value=self.status_on_failure,
-                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
-
-    def perform(self, formdata):
-        args = {}
-        for parameter in self.get_parameters():
-            args[parameter] = self.compute(getattr(self, parameter))
-            if not args.get(parameter):
-                del args[parameter]
-        message = {'appointment':
-                      {'fiche': {'firstname': args.get('var_firstname', '-'),
-                                 'lastname': args.get('var_lastname', '-'),
-                                 'email': args.get('var_email'),
-                                 'firstphone': args.get('var_firstphone'),
-                                 'secondphone': args.get('var_secondphone'),
-                                },
-                       'date': args.get('var_datetime'),
-                       'intervention_ids': [int(args.get('var_intervention_id'))],
-                       # 'comments': '-',
-                       'websource': 'Publik'}
-                  }
-
-        req = get_clicrdv_req('appointments')
-        req.add_data(json.dumps(message))
-        req.add_header('Content-Type', 'application/json')
-
-        try:
-            fd = urllib2.urlopen(req)
-        except urllib2.HTTPError, e:
-            success = False
-            try:
-                msg = json.load(e.fp)[0].get('error')
-            except:
-                msg = _('unknown error')
-
-            if formdata.evolution:
-                evo = formdata.evolution[-1]
-            else:
-                formdata.evolution = []
-                evo = Evolution()
-                evo.time = time.localtime()
-                evo.status = formdata.status
-                formdata.evolution.append(evo)
-            evo.add_part(AppointmentErrorPart(msg))
-        else:
-            success = True
-            response = json.load(fd)
-            appointment_id = response.get('records')[0].get('id')
-
-            # add a message in formdata.evolution
-            if formdata.evolution:
-                evo = formdata.evolution[-1]
-            else:
-                formdata.evolution = []
-                evo = Evolution()
-                evo.time = time.localtime()
-                evo.status = formdata.status
-                formdata.evolution.append(evo)
-            evo.add_part(AppointmentPart(response.get('records')[0]))
-
-        formdata.store()
-
-        if (success and self.status_on_success) or (success is False and self.status_on_failure):
-            if success:
-                formdata.status = 'wf-%s' % self.status_on_success
-            else:
-                formdata.status = 'wf-%s' % self.status_on_failure
-
-register_item_class(ClicRdvCreateAppointment)
-
-
-class ClicRdvCancelAppointment(WorkflowStatusItem):
-    description = N_('Cancel a ClicRDV Appointment')
-    key = 'clicrdv-cancel'
-    category = ('aq-clicrdv', N_('ClicRDV'))
-
-    endpoint = False
-
-    status_on_success = None
-    status_on_failure = None
-
-    def get_parameters(self):
-        return ('status_on_success', 'status_on_failure')
-
-    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
-        if 'status_on_success' in parameters:
-            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
-                    title=_('Status On Success'), value=self.status_on_success,
-                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
-        if 'status_on_failure' in parameters:
-            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
-                    title=_('Status On Failure'), value=self.status_on_failure,
-                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
-
-    def is_available(self, workflow=None):
-        return get_publisher().has_site_option('clicrdv')
-    is_available = classmethod(is_available)
-
-    def render_as_line(self):
-        return _('Cancel an appointment in ClicRDV')
-
-    def perform(self, formdata):
-        success = True
-
-        for evo in [evo for evo in formdata.evolution if evo.parts]:
-            for part in [part for part in evo.parts if isinstance(part, AppointmentPart)]:
-                appointment_id = part.id
-                try:
-                    req = get_clicrdv_req('appointments/%s' % appointment_id)
-                    req.get_method = (lambda: 'DELETE')
-                    fd = urllib2.urlopen(req)
-                    none = fd.read()
-                except urllib2.URLError:
-                    # clicrdv will return a "Bad Request" (HTTP 400) response
-                    # when it's not possible to remove an appointment
-                    # (for example because it's too late)
-                    success = False
-
-        if (success and self.status_on_success) or (success is False and self.status_on_failure):
-            if success:
-                formdata.status = 'wf-%s' % self.status_on_success
-            else:
-                formdata.status = 'wf-%s' % self.status_on_failure
-
-register_item_class(ClicRdvCancelAppointment)
diff --git a/extra/modules/connectors.py b/extra/modules/connectors.py
deleted file mode 100644
index 271312e..0000000
--- a/extra/modules/connectors.py
+++ /dev/null
@@ -1,2 +0,0 @@
-import clicrdv
-import abelium_domino_workflow
diff --git a/extra/modules/events.py b/extra/modules/events.py
deleted file mode 100644
index 930707d..0000000
--- a/extra/modules/events.py
+++ /dev/null
@@ -1,238 +0,0 @@
-import time
-import datetime
-import urllib2
-import vobject
-
-from quixote import get_request, get_publisher, get_response
-from quixote.html import htmltext, TemplateIO, htmlescape
-
-from qommon import _
-from qommon.publisher import get_publisher_class
-from qommon.storage import StorableObject
-from qommon.cron import CronJob
-from qommon import misc
-
-class Event(StorableObject):
-    _names = 'events'
-
-    title = None
-    description = None
-    url = None
-    date_start = None
-    date_end = None
-    location = None
-    organizer = None
-    more_infos = None
-    keywords = None
-
-    def in_month(self, year, month):
-        if not self.date_end: # a single date
-            return tuple(self.date_start[:2]) == (year, month)
-        else:
-            # an interval
-            if tuple(self.date_start[:2]) > (year, month): # start later
-                return False
-            if tuple(self.date_end[:2]) < (year, month): # ended before
-                return False
-        return True
-
-    def after_today(self):
-        today = time.localtime()[:3]
-        if not self.date_end:
-            return tuple(self.date_start[:3]) > today
-        return tuple(self.date_end[:3]) > today
-
-    def format_date(self):
-        d = {
-            'year_start': self.date_start[0],
-            'month_start': misc.get_month_name(self.date_start[1]),
-            'day_start': self.date_start[2]
-            }
-        if self.date_end and self.date_start[:3] != self.date_end[:3]:
-            d.update({
-                'year_end': self.date_end[0],
-                'month_end': misc.get_month_name(self.date_end[1]),
-                'day_end': self.date_end[2]
-                })
-            d2 = datetime.date(*self.date_start[:3]) + datetime.timedelta(days=1)
-            if tuple(self.date_end[:3]) == (d2.year, d2.month, d2.day):
-                # two consecutive days
-                if self.date_start[1] == self.date_end[1]:
-                    return _('On %(month_start)s %(day_start)s and %(day_end)s') % d
-                else:
-                    return _('On %(month_start)s %(day_start)s and %(month_end)s %(day_end)s') % d
-            else:
-                if self.date_start[0] == self.date_end[0]: # same year
-                    if self.date_start[1] == self.date_end[1]: # same month
-                        return _('From %(month_start)s %(day_start)s to %(day_end)s') % d
-                    else:
-                        return _('From %(month_start)s %(day_start)s '
-                                 'to %(month_end)s %(day_end)s') % d
-                else:
-                    return _('From %(month_start)s %(day_start)s %(year_start)s '
-                             'to %(month_end)s %(day_end)s %(year_end)s') % d
-        else:
-            return _('On %(month_start)s %(day_start)s') % d
-
-    def as_vevent(self):
-        vevent = vobject.newFromBehavior('vevent')
-        site_charset = get_publisher().site_charset
-        vevent.add('uid').value = '%04d%02d%02d-%s@%s' % (self.date_start[:3] + (self.id,
-            get_request().get_server().lower().split(':')[0].rstrip('.')))
-        vevent.add('summary').value = unicode(self.title, site_charset)
-        vevent.add('dtstart').value = datetime.date(*self.date_start[:3])
-        vevent.dtstart.value_param = 'DATE'
-        if self.date_end:
-            vevent.add('dtend').value = datetime.date(*self.date_end[:3])
-            vevent.dtend.value_param = 'DATE'
-        if self.description:
-            vevent.add('description').value = unicode(self.description.strip(), site_charset)
-        if self.url:
-            vevent.add('url').value = unicode(self.url, site_charset)
-        if self.location:
-            vevent.add('location').value = unicode(self.location, site_charset)
-        if self.organizer:
-            vevent.add('organizer').value = unicode(self.organizer, site_charset)
-        if self.keywords:
-            vevent.add('categories').value = [unicode(x, site_charset) for x in self.keywords]
-        vevent.add('class').value = 'PUBLIC'
-        return vevent
-
-    def as_vcalendar(cls):
-        cal = vobject.iCalendar()
-        cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
-        for x in cls.select():
-            cal.add(x.as_vevent())
-        return cal.serialize()
-    as_vcalendar = classmethod(as_vcalendar)
-    
-    def as_html_dt_dd(self):
-        root_url = get_publisher().get_root_url()
-        r = TemplateIO(html=True)
-        r += htmltext('<dt>')
-        r += self.format_date()
-        r += htmltext('</dt>')
-        r += htmltext('<p><dd><strong>%s</strong>') % self.title
-        if self.description:
-            r += ' - ' + self.description
-        r += htmltext('</p>')
-        if (self.location or self.organizer or self.more_infos or self.keywords):
-            r += htmltext('<ul>')
-            if self.location:
-                r += htmltext('<li>%s: %s</li>') % (_('Location'), self.location)
-            if self.organizer:
-                r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.organizer)
-            if self.more_infos:
-                r += htmltext('<li>%s</li>') % self.more_infos
-            if self.keywords:
-                r += htmltext('<li>')
-                for k in self.keywords:
-                    r += htmltext('<a class="tag" href="%sagenda/tag/%s">%s</a> ') % (root_url, k, k)
-                r += htmltext('</li>')
-            r += htmltext('</ul>')
-
-        if self.url:
-            if get_response().iframe_mode:
-                r += htmltext('<a class="external" href="%s" target="_top">%s</a>') % (
-                        self.url, _('More information'))
-            else:
-                r += htmltext('<a class="external" href="%s">%s</a>') % (
-                        self.url, _('More information'))
-        r += htmltext('</dd>')
-        return r.getvalue()
-
-    def get_url(self):
-        return '%s/agenda/events/%s/' % (get_publisher().get_frontoffice_url(), self.id)
-
-    def get_atom_entry(self):
-        from pyatom import pyatom
-        entry = pyatom.Entry()
-        entry.id = self.get_url()
-        entry.title = self.title
-
-        entry.content.attrs['type'] = 'html'
-        entry.content.text = str('<p>' + htmlescape(
-                    unicode(self.description, get_publisher().site_charset).encode('utf-8')) + '</p>')
-
-        return entry
-
-
-class RemoteCalendar(StorableObject):
-    _names = 'remote_calendars'
-
-    label = None
-    url = None
-    content = None
-    events = None
-    error = None  # (time, string, params)
-
-    def download_and_parse(self, job=None):
-        old_content = self.content
-
-        try:
-            self.content = urllib2.urlopen(self.url).read()
-        except urllib2.HTTPError, e:
-            self.error = (time.localtime(), N_('HTTP Error %s on download'), (e.code,))
-            self.store()
-            return
-        except urllib2.URLError, e:
-            self.error = (time.localtime(), N_('Error on download'), ())
-            self.store()
-            return
-
-        if self.error:
-            self.error = None
-            self.store()
-
-        if self.content == old_content:
-            return
-
-        self.events = []
-        try:
-            parsed_cal = vobject.readOne(self.content)
-        except vobject.base.ParseError:
-            self.error = (time.localtime(), N_('Failed to parse file'), ())
-            self.store()
-            return
-
-        site_charset = get_publisher().site_charset
-        for vevent in parsed_cal.vevent_list:
-            ev = Event()
-            ev.title = vevent.summary.value.encode(site_charset, 'replace')
-            try:
-                ev.url = vevent.url.value.encode(site_charset, 'replace')
-            except AttributeError:
-                pass
-            ev.date_start = vevent.dtstart.value.timetuple()
-            try:
-                ev.date_end = vevent.dtend.value.timetuple()
-            except AttributeError:
-                pass
-            try:
-                ev.description = vevent.description.value.encode(site_charset, 'replace')
-            except AttributeError:
-                pass
-            try:
-                ev.keywords = [x.encode(site_charset) for x in vevent.categories.value]
-            except AttributeError:
-                pass
-            self.events.append(ev)
-        self.store()
-
-        
-    def get_error_message(self):
-        if not self.error:
-            return None
-        return '(%s) %s' % (misc.localstrftime(self.error[0]),
-                _(self.error[1]) % self.error[2])
-
-
-def update_remote_calendars(publisher):
-    for source in RemoteCalendar.select():
-        source.download_and_parse()
-
-def get_default_event_tags():
-    return [_('All Public'), _('Adults'), _('Children'), _('Free')]
-
-get_publisher_class().register_cronjob(CronJob(update_remote_calendars, minutes = [0]))
-
diff --git a/extra/modules/events_ui.py b/extra/modules/events_ui.py
deleted file mode 100644
index 664e65c..0000000
--- a/extra/modules/events_ui.py
+++ /dev/null
@@ -1,418 +0,0 @@
-import time
-
-from quixote import get_request, get_response, get_session, redirect
-from quixote.directory import Directory, AccessControlled
-from quixote.html import TemplateIO, htmltext
-
-import wcs
-import wcs.admin.root
-
-from qommon import _
-from qommon.backoffice.menu import html_top
-from qommon.admin.menu import command_icon
-from qommon import get_cfg
-from qommon import errors, misc
-from qommon.form import *
-from qommon.strftime import strftime
-
-from events import Event, RemoteCalendar, get_default_event_tags
-
-
-
-class RemoteCalendarDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete', 'update']
-
-    def __init__(self, calendar):
-        self.calendar = calendar
-
-    def _q_index(self):
-        form = Form(enctype='multipart/form-data')
-        form.add_submit('edit', _('Edit'))
-        form.add_submit('delete', _('Delete'))
-        form.add_submit('update', _('Update'))
-        form.add_submit('back', _('Back'))
-
-        if form.get_submit() == 'edit':
-            return redirect('edit')
-        if form.get_submit() == 'update':
-            return redirect('update')
-        if form.get_submit() == 'delete':
-            return redirect('delete')
-        if form.get_submit() == 'back':
-            return redirect('..')
-
-        html_top('events', title = _('Remote Calendar: %s') % self.calendar.label)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Remote Calendar: %s') % self.calendar.label
-
-        r += get_session().display_message()
-
-        r += htmltext('<p>')
-        self.calendar.url
-        if self.calendar.error:
-            r += htmltext(' - <span class="error-message">%s</span>') % self.calendar.get_error_message()
-        r += htmltext('</p>')
-
-        if not self.calendar.content:
-            r += htmltext('<p>')
-            r += _('No content has been retrieved yet.')
-            r += htmltext('</p>')
-        else:
-            r += htmltext('<ul>')
-            for ev in sorted(self.calendar.events, lambda x,y: cmp(x.date_start, y.date_start)):
-                r += htmltext('<li>')
-                if ev.date_start:
-                    r += strftime(misc.date_format(), ev.date_start)
-                if ev.date_end and ev.date_start[:3] != ev.date_end[:3]:
-                    r += ' - '
-                    r += strftime(misc.date_format(), ev.date_start)
-
-                r += ' : '
-                if ev.url:
-                    r += htmltext('<a href="%s">%s</a>') % (ev.url, ev.title)
-                else:
-                    r += ev.title
-                r += htmltext('</li>')
-            r += htmltext('</ul>')
-
-        r += form.render()
-        return r.getvalue()
-
-    def edit(self):
-        form = self.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit(form)
-            return redirect('..')
-
-        html_top('events', title = _('Edit Remote Calendar: %s') % self.calendar.label)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Edit Remote Calendar: %s') % self.calendar.label
-        r += form.render()
-        return r.getvalue()
-
-    def form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'label', title = _('Label'), required = True,
-                value = self.calendar.label)
-        form.add(StringWidget, 'url', title = _('URL'), required = True,
-                value = self.calendar.url, size = 40)
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit(self, form):
-        for k in ('label', 'url'):
-            widget = form.get_widget(k)
-            if widget:
-                setattr(self.calendar, k, widget.parse())
-        self.calendar.store()
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this remote calendar.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('events', title = _('Delete Remote Calendar'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Remote Calendar: %s') % self.calendar.label
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.calendar.remove_self()
-            return redirect('..')
-
-    def update(self):
-        get_session().message = ('info',
-                _('Calendar update has been requested, reload in a few moments'))
-        get_response().add_after_job('updating remote calendar',
-                self.calendar.download_and_parse,
-                fire_and_forget = True)
-        return redirect('.')
-
-
-
-class RemoteCalendarsDirectory(Directory):
-    _q_exports = ['', 'new']
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append(('remote/', _('Remote Calendars')))
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        return redirect('..')
-
-    def new(self):
-        calendar_ui = RemoteCalendarDirectory(RemoteCalendar())
-
-        form = calendar_ui.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            calendar_ui.submit(form)
-            return redirect('%s/' % calendar_ui.calendar.id)
-
-        get_response().breadcrumb.append(('new', _('New Remote Calendar')))
-        html_top('events', title = _('New Remote Calendar'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Remote Calendar')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            event = RemoteCalendar.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(event.id), event.label))
-        return RemoteCalendarDirectory(event)
-
-
-class EventDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete']
-
-    def __init__(self, event):
-        self.event = event
-
-    def _q_index(self):
-        form = Form(enctype='multipart/form-data')
-        form.add_submit('edit', _('Edit'))
-        form.add_submit('delete', _('Delete'))
-        form.add_submit('back', _('Back'))
-
-        if form.get_submit() == 'edit':
-            return redirect('edit')
-        if form.get_submit() == 'delete':
-            return redirect('delete')
-        if form.get_submit() == 'back':
-            return redirect('..')
-
-        html_top('events', title = _('Event: %s') % self.event.title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Event: %s') % self.event.title
-        r += htmltext('<p>')
-        r += self.event.description
-        r += htmltext('</p>')
-        r += htmltext('<ul>')
-        if self.event.location:
-            r += htmltext('<li>%s: %s</li>') % (_('Location'), self.event.location)
-        if self.event.organizer:
-            r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.event.organizer)
-        if self.event.url:
-            r += htmltext('<li>%s: <a href="%s">%s</a></li>') % (_('URL'), self.event.url, self.event.url)
-        r += htmltext('</ul>')
-
-        if self.event.more_infos:
-            r += htmltext('<p>')
-            r += self.event.more_infos
-            r += htmltext('</p>')
-
-        r += form.render()
-        return r.getvalue()
-
-    def edit(self):
-        form = self.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit(form)
-            return redirect('..')
-
-        html_top('events', title = _('Edit Event: %s') % self.event.title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Edit Event: %s') % self.event.title
-        r += form.render()
-        return r.getvalue()
-
-    def form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'title', title = _('Title'), required = True,
-                value = self.event.title)
-        form.add(TextWidget, 'description', title = _('Description'),
-                cols = 70, rows = 10,
-                required = True, value = self.event.description)
-        form.add(StringWidget, 'url', title = _('URL'), required = False,
-                value = self.event.url, size = 40)
-        form.add(DateWidget, 'date_start', title = _('Start Date'), required = True,
-                value = strftime(misc.date_format(), self.event.date_start))
-        form.add(DateWidget, 'date_end', title = _('End Date'), required = False,
-                value = strftime(misc.date_format(), self.event.date_end))
-        form.add(TextWidget, 'location', title = _('Location'),
-                cols = 70, rows = 4,
-                required = False, value = self.event.location)
-        form.add(StringWidget, 'organizer', title = _('Organizer'), required = False,
-                value = self.event.organizer, size = 40)
-        form.add(TextWidget, 'more_infos', title = _('More informations'),
-                cols = 70, rows = 10,
-                required = False, value = self.event.more_infos)
-        form.add(TagsWidget, 'keywords', title = _('Keywords'),
-                value = self.event.keywords, size = 50,
-                known_tags = get_cfg('misc', {}).get('event_tags', get_default_event_tags()))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit(self, form):
-        for k in ('title', 'description', 'url', 'date_start', 'date_end',
-                    'organizer', 'location', 'more_infos', 'keywords'):
-            widget = form.get_widget(k)
-            if widget:
-                if k in ('date_start', 'date_end'):
-                    # convert dates to 9-item tuples
-                    v = widget.parse()
-                    if v:
-                        setattr(self.event, k, time.strptime(v, misc.date_format()))
-                    else:
-                        setattr(self.event, k, None)
-                else:
-                    setattr(self.event, k, widget.parse())
-        self.event.store()
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this event.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('events', title = _('Delete Event'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Event: %s') % self.event.title
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.event.remove_self()
-            return redirect('..')
-
-
-
-
-class EventsDirectory(AccessControlled, Directory):
-    _q_exports = ['', 'new', 'listing', 'remote']
-    label = N_('Events')
-
-    remote = RemoteCalendarsDirectory()
-
-    def is_accessible(self, user):
-        from .backoffice import check_visibility
-        return check_visibility('events', user)
-
-    def _q_access(self):
-        user = get_request().user
-        if not user:
-            raise errors.AccessUnauthorizedError()
-
-        if not self.is_accessible(user):
-            raise errors.AccessForbiddenError(
-                    public_msg = _('You are not allowed to access Events Management'),
-                    location_hint = 'backoffice')
-
-        get_response().breadcrumb.append(('events/', _('Events')))
-
-
-    def _q_index(self):
-        html_top('events', _('Events'))
-        r = TemplateIO(html=True)
-
-        get_response().filter['sidebar'] = self.get_sidebar()
-
-        r += htmltext('<div class="splitcontent-left">')
-
-        r += htmltext('<div class="bo-block">')
-        events = Event.select()
-        r += htmltext('<h2>%s</h2>') % _('Events')
-        if not events:
-            r += htmltext('<p>')
-            r += _('There is no event defined at the moment.')
-            r += htmltext('</p>')
-        r += htmltext('<ul class="biglist" id="events-list">')
-        for l in events:
-            event_id = l.id
-            r += htmltext('<li class="biglistitem" id="itemId_%s">') % event_id
-            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (event_id, l.title)
-            r += ' - '
-            r += l.format_date()
-            r += htmltext('<p class="commands">')
-            r += command_icon('%s/edit' % event_id, 'edit')
-            r += command_icon('%s/delete' % event_id, 'remove')
-            r += htmltext('</p></li>')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        r += htmltext('</div>')
-
-        r += htmltext('<div class="splitcontent-right">')
-        r += htmltext('<div class="bo-block">')
-        rcalendars = RemoteCalendar.select()
-        r += htmltext('<h2>%s</h2>') % _('Remote Calendars')
-        if not rcalendars:
-            r += htmltext('<p>')
-            r += _('There is no remote calendars defined at the moment.')
-            r += htmltext('</p>')
-
-        r += htmltext('<ul class="biglist" id="events-list">')
-        for l in rcalendars:
-            rcal_id = l.id
-            r += htmltext('<li class="biglistitem" id="itemId_%s">') % rcal_id
-            r += htmltext('<strong class="label"><a href="remote/%s/">%s</a></strong>') % (rcal_id, l.label)
-            r += htmltext('<p class="details">')
-            r += l.url
-            if l.error:
-                r += htmltext('<br /><span class="error-message">%s</span>') % l.get_error_message()
-            r += htmltext('</p>')
-            r += htmltext('<p class="commands">')
-            r += command_icon('remote/%s/edit' % rcal_id, 'edit')
-            r += command_icon('remote/%s/delete' % rcal_id, 'remove')
-            r += htmltext('</p></li>')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul id="sidebar-actions">')
-        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Event')
-        r += htmltext('  <li><a class="new-item" href="remote/new">%s</a></li>') % _('New Remote Calendar')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def new(self):
-        event_ui = EventDirectory(Event())
-
-        form = event_ui.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            event_ui.submit(form)
-            return redirect('%s/' % event_ui.event.id)
-
-        get_response().breadcrumb.append(('new', _('New Event')))
-        html_top('events', title = _('New Event'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Event')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            event = Event.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(event.id), event.title))
-        return EventDirectory(event)
-
-    def listing(self):
-        return redirect('.')
diff --git a/extra/modules/formpage.py b/extra/modules/formpage.py
deleted file mode 100644
index 7905e84..0000000
--- a/extra/modules/formpage.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from quixote import get_publisher, get_request, redirect
-from quixote.directory import Directory
-from quixote.html import htmltext
-
-import os
-
-import wcs
-import wcs.forms.root
-import wcs.forms.preview
-from qommon import _
-from qommon import template
-from qommon import errors
-from qommon.form import *
-from wcs.roles import logged_users_role
-
-from qommon import emails
-
-OldFormPage = wcs.forms.root.FormPage
-
-class AlternateFormPage(OldFormPage):
-    def form_side(self, *args, **kwargs):
-        form_side_html = OldFormPage.form_side(self, *args, **kwargs)
-        # add a 'Steps' title
-        form_side_html = str(form_side_html).replace('<ol>', '<h2>%s</h2>\n<ol>' % _('Steps'))
-        get_response().filter['gauche'] = form_side_html
-        get_response().filter['steps'] = form_side_html
-        return
-
-wcs.forms.root.FormPage = AlternateFormPage
-wcs.forms.root.PublicFormStatusPage.form_page_class = AlternateFormPage
-wcs.forms.preview.PreviewFormPage.__bases__ = (AlternateFormPage,)
-
-
-OldFormsRootDirectory = wcs.forms.root.RootDirectory
-
-class AlternateFormsRootDirectory(OldFormsRootDirectory):
-    def form_list(self, *args, **kwargs):
-        form_list = OldFormsRootDirectory.form_list(self, *args, **kwargs)
-        return htmltext(str(form_list).replace('h2>', 'h3>'))
-
-wcs.forms.root.RootDirectory = AlternateFormsRootDirectory
diff --git a/extra/modules/links.py b/extra/modules/links.py
deleted file mode 100644
index 90557d7..0000000
--- a/extra/modules/links.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from qommon.storage import StorableObject
-
-class Link(StorableObject):
-    _names = 'links'
-
-    title = None
-    url = None
-    position = None
-
-    def sort_by_position(cls, links):
-        def cmp_position(x, y):
-            if x.position == y.position:
-                return 0
-            if x.position is None:
-                return 1
-            if y.position is None:
-                return -1
-            return cmp(x.position, y.position)
-        links.sort(cmp_position)
-    sort_by_position = classmethod(sort_by_position)
-
diff --git a/extra/modules/links_ui.py b/extra/modules/links_ui.py
deleted file mode 100644
index cccf59b..0000000
--- a/extra/modules/links_ui.py
+++ /dev/null
@@ -1,194 +0,0 @@
-from quixote import get_request, get_response, get_session, redirect
-from quixote.directory import Directory, AccessControlled
-from quixote.html import TemplateIO, htmltext
-
-import wcs
-import wcs.admin.root
-
-from qommon import _
-from qommon import errors
-from qommon.form import *
-from qommon.backoffice.menu import html_top
-from qommon.admin.menu import command_icon
-from qommon import get_cfg
-
-from links import Link
-
-
-class LinkDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete']
-
-    def __init__(self, link):
-        self.link = link
-
-    def _q_index(self):
-        form = Form(enctype='multipart/form-data')
-        form.add_submit('edit', _('Edit'))
-        form.add_submit('delete', _('Delete'))
-        form.add_submit('back', _('Back'))
-
-        if form.get_submit() == 'edit':
-            return redirect('edit')
-        if form.get_submit() == 'delete':
-            return redirect('delete')
-        if form.get_submit() == 'back':
-            return redirect('..')
-
-        html_top('links', title = _('Link: %s') % self.link.title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Link: %s') % self.link.title
-        r += htmltext('<p>')
-        r += self.link.url
-        r += htmltext('</p>')
-
-        r += form.render()
-        return r.getvalue()
-
-    def edit(self):
-        form = self.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit(form)
-            return redirect('..')
-
-        html_top('links', title = _('Edit Link: %s') % self.link.title)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Edit Link: %s') % self.link.title
-        r += form.render()
-        return r.getvalue()
-
-
-    def form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'title', title = _('Title'), required = True,
-                value = self.link.title)
-        form.add(StringWidget, 'url', title = _('URL'), required=False,
-                value = self.link.url,
-                hint=_('Leave empty to create a title'))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit(self, form):
-        for k in ('title', 'url'):
-            widget = form.get_widget(k)
-            if widget:
-                setattr(self.link, k, widget.parse())
-        self.link.store()
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this link.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('links', title = _('Delete Link'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Link: %s') % self.link.title
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.link.remove_self()
-            return redirect('..')
-
-
-class LinksDirectory(AccessControlled, Directory):
-    _q_exports = ['', 'new', 'listing', 'update_order']
-    label = N_('Links')
-
-    def is_accessible(self, user):
-        from .backoffice import check_visibility
-        return check_visibility('links', user)
-
-    def _q_access(self):
-        user = get_request().user
-        if not user:
-            raise errors.AccessUnauthorizedError()
-
-        if not self.is_accessible(user):
-            raise errors.AccessForbiddenError(
-                    public_msg = _('You are not allowed to access Links Management'),
-                    location_hint = 'backoffice')
-
-        get_response().breadcrumb.append(('links/', _('Links')))
-
-
-    def _q_index(self):
-        html_top('links', _('Links'))
-        r = TemplateIO(html=True)
-        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js'])
-        get_response().filter['sidebar'] = self.get_sidebar()
-
-        links = Link.select()
-        Link.sort_by_position(links)
-
-        r += htmltext('<ul class="biglist sortable" id="links-list">')
-        for l in links:
-            link_id = l.id
-            r += htmltext('<li class="biglistitem" id="itemId_%s">') % link_id
-            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (link_id, l.title)
-            r += htmltext('<p class="details">')
-            r += l.url
-            r += htmltext('</p>')
-            r += htmltext('<p class="commands">')
-            r += command_icon('%s/edit' % link_id, 'edit')
-            r += command_icon('%s/delete' % link_id, 'remove')
-            r += htmltext('</p></li>')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul id="sidebar-actions">')
-        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Link')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def update_order(self):
-        request = get_request()
-        new_order = request.form['order'].strip(';').split(';')
-        links = Link.select()
-        dict = {}
-        for l in links:
-            dict[str(l.id)] = l
-        for i, o in enumerate(new_order):
-            dict[o].position = i + 1
-            dict[o].store()
-        return 'ok'
-
-
-    def new(self):
-        link_ui = LinkDirectory(Link())
-
-        form = link_ui.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            link_ui.submit(form)
-            return redirect('%s/' % link_ui.link.id)
-
-        get_response().breadcrumb.append(('new', _('New Link')))
-        html_top('links', title = _('New Link'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Link')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            link = Link.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(link.id), link.title))
-        return LinkDirectory(link)
-
-    def listing(self):
-        return redirect('.')
-
diff --git a/extra/modules/myspace.py b/extra/modules/myspace.py
deleted file mode 100644
index 3ec2e06..0000000
--- a/extra/modules/myspace.py
+++ /dev/null
@@ -1,721 +0,0 @@
-try:
-    import lasso
-except ImportError:
-    pass
-
-import json
-
-from quixote import get_publisher, get_request, redirect, get_response, get_session_manager, get_session
-from quixote.directory import AccessControlled, Directory
-from quixote.html import TemplateIO, htmltext
-from quixote.util import StaticFile, FileStream
-
-from qommon import _
-from qommon import template
-from qommon.form import *
-from qommon import get_cfg, get_logger
-from qommon import errors
-from wcs.api import get_user_from_api_query_string
-
-import qommon.ident.password
-from qommon.ident.password_accounts import PasswordAccount
-
-from qommon.admin.texts import TextsDirectory
-
-from wcs.formdef import FormDef
-import wcs.myspace
-import root
-
-from announces import AnnounceSubscription
-from strongbox import StrongboxItem, StrongboxType
-from payments import Invoice, Regie, is_payment_supported
-
-class MyInvoicesDirectory(Directory):
-    _q_exports = ['']
-
-    def _q_traverse(self, path):
-        if not is_payment_supported():
-            raise errors.TraversalError()
-        get_response().breadcrumb.append(('invoices/', _('Invoices')))
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        user = get_request().user
-        if not user or user.anonymous:
-            raise errors.AccessUnauthorizedError()
-
-        template.html_top(_('Invoices'))
-        r = TemplateIO(html=True)
-        r += TextsDirectory.get_html_text('aq-myspace-invoice')
-
-        r += get_session().display_message()
-
-        invoices = []
-        invoices.extend(Invoice.get_with_indexed_value(
-            str('user_id'), str(user.id)))
-
-        def cmp_invoice(a, b):
-            t = cmp(a.regie_id, b.regie_id)
-            if t != 0:
-                return t
-            return -cmp(a.date, b.date)
-
-        invoices.sort(cmp_invoice)
-
-        last_regie_id = None
-        unpaid = False
-        for invoice in invoices:
-            if invoice.regie_id != last_regie_id:
-                if last_regie_id:
-                    r += htmltext('</ul>')
-                    if unpaid:
-                        r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
-                    r += htmltext('</form>')
-                last_regie_id = invoice.regie_id
-                r += htmltext('<h3>%s</h3>') % Regie.get(last_regie_id).label
-                unpaid = False
-                r += htmltext('<form action="%s/invoices/multiple">' % get_publisher().get_frontoffice_url())
-                r += htmltext('<ul>')
-
-            r += htmltext('<li>')
-            if not (invoice.paid or invoice.canceled):
-                r += htmltext('<input type="checkbox" name="invoice" value="%s"/>' % invoice.id)
-                unpaid = True
-            r += misc.localstrftime(invoice.date)
-            r += ' - '
-            r += '%s' % invoice.subject
-            r += ' - '
-            r += '%s' % invoice.amount
-            r += htmltext(' &euro;')
-            r += ' - '
-            button = '<span class="paybutton">%s</span>' % _('Pay')
-            if invoice.canceled:
-                r += _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
-                r += ' - '
-                button = _('Details')
-            if invoice.paid:
-                r += _('paid on %s') % misc.localstrftime(invoice.paid_date)
-                r += ' - '
-                button = _('Details')
-            r += htmltext('<a href="%s/invoices/%s">%s</a>' % (get_publisher().get_frontoffice_url(),
-                    invoice.id, button))
-            r += htmltext('</li>')
-
-        if last_regie_id:
-            r += htmltext('</ul>')
-            if unpaid:
-                r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
-            r += htmltext('</form>')
-
-        return r.getvalue()
-
-
-class StrongboxDirectory(Directory):
-    _q_exports = ['', 'add', 'download', 'remove', 'pick', 'validate']
-
-    def _q_traverse(self, path):
-        if not get_cfg('misc', {}).get('aq-strongbox'):
-            raise errors.TraversalError()
-        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
-        return Directory._q_traverse(self, path)
-
-    def get_form(self):
-        types = [(x.id, x.label) for x in StrongboxType.select()]
-        form = Form(action='add', enctype='multipart/form-data')
-        form.add(StringWidget, 'description', title=_('Description'), size=60)
-        form.add(FileWidget, 'file', title=_('File'), required=True)
-        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
-                 options = [(None, _('Not specified'))] + types)
-        form.add(DateWidget, 'date_time', title = _('Document Date'))
-        form.add_submit('submit', _('Upload'))
-        return form
-
-    def _q_index(self):
-        template.html_top(_('Strongbox'))
-        r = TemplateIO(html=True)
-
-        # TODO: a paragraph of explanations here could be useful
-
-        sffiles = StrongboxItem.get_with_indexed_value(
-                        str('user_id'), str(get_request().user.id))
-        if sffiles:
-            r += htmltext('<table id="strongbox-items">')
-            r += htmltext('<tr><th></th><th>%s</th><th>%s</th><th></th></tr>') % (
-                _('Type'), _('Expiration'))
-        else:
-            r += htmltext('<p>')
-            r += _('There is currently nothing in your strongbox.')
-            r += htmltext('</p>')
-        has_items_to_validate = False
-        for i, sffile in enumerate(sffiles):
-            expired = False
-            if not sffile.validated_time:
-                has_items_to_validate = True
-                continue
-            if sffile.expiration_time and sffile.expiration_time < time.localtime():
-                expired = True
-            if i%2:
-                classnames = ['odd']
-            else:
-                classnames = ['even']
-            if expired:
-                classnames.append('expired')
-            r += htmltext('<tr class="%s">') % ' '.join(classnames)
-            r += htmltext('<td class="label">')
-            r += sffile.get_display_name()
-            r += htmltext('</td>')
-            if sffile.type_id:
-                r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
-            else:
-                r += htmltext('<td class="type">-</td>')
-            if sffile.expiration_time:
-                r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
-                if expired:
-                    r += ' (%s)' % _('expired')
-                r += htmltext('</td>')
-            else:
-                r += htmltext('<td class="expiration">-</td>')
-            r += htmltext('<td class="actions">')
-            r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
-            r += htmltext('[<a rel="popup" href="remove?id=%s">%s</a>] ') % (sffile.id, _('remove'))
-            r += htmltext('</td>')
-            r += htmltext('</tr>')
-
-        if has_items_to_validate:
-            r += htmltext('<tr><td colspan="4"><h3>%s</h3></td></tr>') % _('Proposed Items')
-            for sffile in sffiles:
-                if sffile.validated_time:
-                    continue
-                if sffile.expiration_time and sffile.expiration_time < time.localtime():
-                    expired = True
-                if i%2:
-                    classnames = ['odd']
-                else:
-                    classnames = ['even']
-                if expired:
-                    classnames.append('expired')
-                r += htmltext('<tr class="%s">') % ' '.join(classnames)
-
-                r += htmltext('<td class="label">')
-                r += sffile.get_display_name()
-                r += htmltext('</td>')
-                if sffile.type_id:
-                    r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
-                else:
-                    r += htmltext('<td class="type">-</td>')
-
-                if sffile.expiration_time:
-                    r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
-                    if expired:
-                        r += ' (%s)' % _('expired')
-                    r += htmltext('</td>')
-                else:
-                    r += htmltext('<td class="expiration">-</td>')
-                r += htmltext('<td class="actions">')
-                r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
-                r += htmltext(' [<a href="validate?id=%s">%s</a>] ') % (sffile.id, _('validate'))
-                r += htmltext(' [<a href="remove?id=%s">%s</a>] ') % (sffile.id, _('reject'))
-                r += htmltext('</td>')
-                r += htmltext('</tr>')
-        if sffiles:
-            r += htmltext('</table>')
-
-        r += htmltext('<h3>%s</h3>') % _('Add a file to the strongbox')
-        form = self.get_form()
-        r += form.render()
-        return r.getvalue()
-
-    def add(self):
-        form = self.get_form()
-        if not form.is_submitted():
-            if get_request().form.get('mode') == 'pick':
-                return redirect('pick')
-            else:
-                return redirect('.')
-
-        sffile = StrongboxItem()
-        sffile.user_id = get_request().user.id
-        sffile.description = form.get_widget('description').parse()
-        sffile.validated_time = time.localtime()
-        sffile.type_id = form.get_widget('type_id').parse()
-        v = form.get_widget('date_time').parse()
-        sffile.set_expiration_time_from_date(v)
-        sffile.store()
-        sffile.set_file(form.get_widget('file').parse())
-        sffile.store()
-        if get_request().form.get('mode') == 'pick':
-            return redirect('pick')
-        else:
-            return redirect('.')
-
-    def download(self):
-        id = get_request().form.get('id')
-        if not id:
-            raise errors.TraversalError()
-        try:
-            sffile = StrongboxItem.get(id)
-        except KeyError:
-            raise errors.TraversalError()
-        if str(sffile.user_id) != str(get_request().user.id):
-            raise errors.TraversalError()
-
-        filename = sffile.file.filename
-        fd = file(filename)
-        size = os.path.getsize(filename)
-        response = get_response()
-        response.set_content_type('application/octet-stream')
-        response.set_header('content-disposition', 'attachment; filename="%s"' % sffile.file.base_filename)
-        return FileStream(fd, size)
-
-    def validate(self):
-        id = get_request().form.get('id')
-        if not id:
-            raise errors.TraversalError()
-        try:
-            sffile = StrongboxItem.get(id)
-        except KeyError:
-            raise errors.TraversalError()
-        if str(sffile.user_id) != str(get_request().user.id):
-            raise errors.TraversalError()
-        sffile.validated_time = time.time()
-        sffile.store()
-        return redirect('.')
-
-    def remove(self):
-        id = get_request().form.get('id')
-        if not id:
-            raise errors.TraversalError()
-        try:
-            sffile = StrongboxItem.get(id)
-        except KeyError:
-            raise errors.TraversalError()
-        if str(sffile.user_id) != str(get_request().user.id):
-            raise errors.TraversalError()
-
-        r = TemplateIO(html=True)
-        form = Form(enctype='multipart/form-data')
-        form.add_hidden('id', get_request().form.get('id'))
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this item from your strongbox.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-        if not form.is_submitted() or form.has_errors():
-            if sffile.type_id:
-                r += htmltext('<h2>%s</h2>') % _('Deleting %(filetype)s: %(filename)s') % {
-                        'filetype': StrongboxType.get(sffile.type_id).label,
-                        'filename': sffile.get_display_name()
-                    }
-            else:
-                r += htmltext('<h2>%s</h2>') % _('Deleting %(filename)s') % {'filename': sffile.get_display_name()}
-            r += form.render()
-            return r.getvalue()
-        else:
-            sffile.remove_self()
-            sffile.remove_file()
-            return redirect('.')
-
-    def picked_file(self):
-        get_response().set_content_type('application/json')
-        sffile = StrongboxItem.get(get_request().form.get('val'))
-        sffile.file.fp = file(sffile.file.filename)
-        if sffile.user_id != get_request().user.id:
-            raise errors.TraversalError()
-        # XXX: this will copy the file, it would be quite nice if it was
-        # possible to just make it a symlink to the sffile
-        token = get_session().add_tempfile(sffile.file)
-        return json.dumps({'token': token, 'filename': sffile.file.base_filename})
-
-    def pick(self):
-        if get_request().form.get('select') == 'true':
-            return self.picked_file()
-        r = TemplateIO(html=True)
-        root_url = get_publisher().get_root_url()
-        sffiles = StrongboxItem.get_with_indexed_value(
-                        str('user_id'), str(get_request().user.id))
-        r += htmltext('<h2>%s</h2>') % _('Pick a file')
-
-        if not sffiles:
-            r += htmltext('<p>')
-            r += _('You do not have any file in your strongbox at the moment.')
-            r += htmltext('</p>')
-            r += htmltext('<div class="buttons">')
-            r += htmltext('<a href="%smyspace/strongbox/" target="_blank">%s</a>') % (root_url,
-                _('Open Strongbox Management'))
-            r += htmltext('</div>')
-        else:
-            r += htmltext('<form id="strongbox-pick">')
-            r += htmltext('<ul>')
-            for sffile in sffiles:
-                r += htmltext('<li><label><input type="radio" name="file" value="%s"/>%s</label>') % (
-                                sffile.id, sffile.get_display_name())
-                r += htmltext(' [<a href="%smyspace/strongbox/download?id=%s">%s</a>] ') % (
-                                root_url, sffile.id, _('view'))
-                r += htmltext('</li>')
-            r += htmltext('</ul>')
-
-            r += htmltext('<div class="buttons">')
-            r += htmltext('<input name="cancel" type="button" value="%s"/>') % _('Cancel')
-            r += ' '
-            r += htmltext('<input name="pick" type="button" value="%s"/>') % _('Pick')
-            r += htmltext('</div>')
-            r += htmltext('</form>')
-        return r.getvalue()
-
-
-class JsonDirectory(Directory):
-    '''Export of several lists in json, related to the current user or the
-       SAMLv2 NameID we'd get in the URL'''
-
-    _q_exports = ['forms']
-
-    user = None
-
-    def _q_traverse(self, path):
-        self.user = get_user_from_api_query_string() or get_request().user
-        if not self.user:
-            raise errors.AccessUnauthorizedError()
-        return Directory._q_traverse(self, path)
-
-    def forms(self):
-        formdefs = FormDef.select(order_by='name', ignore_errors=True)
-        user_forms = []
-        for formdef in formdefs:
-            user_forms.extend(formdef.data_class().get_with_indexed_value(
-                        'user_id', self.user.id))
-        user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
-
-        get_response().set_content_type('application/json')
-
-
-        forms_output = []
-        for form in user_forms:
-            visible_status = form.get_visible_status(user=self.user)
-            # skip drafts and hidden forms
-            if not visible_status:
-                continue
-            name = form.formdef.name
-            id = form.get_display_id()
-            status = visible_status.name
-            title = _('%(name)s #%(id)s (%(status)s)') % {
-                    'name': name,
-                    'id': id,
-                    'status': status
-            }
-            url = form.get_url()
-            d = { 'title': title, 'url': url }
-            d.update(form.get_substitution_variables(minimal=True))
-            forms_output.append(d)
-        return json.dumps(forms_output, cls=misc.JSONEncoder)
-
-
-class MyspaceDirectory(wcs.myspace.MyspaceDirectory):
-    _q_exports = ['', 'profile', 'new', 'password', 'remove', 'drafts', 'forms',
-            'announces', 'strongbox', 'invoices', 'json']
-
-    strongbox = StrongboxDirectory()
-    invoices = MyInvoicesDirectory()
-    json = JsonDirectory()
-
-    def _q_traverse(self, path):
-        get_response().filter['bigdiv'] = 'profile'
-        get_response().breadcrumb.append(('myspace/', _('My Space')))
-
-        # Migrate custom text settings
-        texts_cfg = get_cfg('texts', {})
-        if 'text-aq-top-of-profile' in texts_cfg and (
-                        not 'text-top-of-profile' in texts_cfg):
-            texts_cfg['text-top-of-profile'] = texts_cfg['text-aq-top-of-profile']
-            del texts_cfg['text-aq-top-of-profile']
-            get_publisher().write_cfg()
-
-        return Directory._q_traverse(self, path)
-
-
-    def _q_index(self):
-        user = get_request().user
-        if not user:
-            raise errors.AccessUnauthorizedError()
-        template.html_top(_('My Space'))
-        r = TemplateIO(html=True)
-        if user.anonymous:
-            return redirect('new')
-
-        user_formdef = user.get_formdef()
-
-        user_forms = []
-        if user:
-            formdefs = FormDef.select(order_by='name', ignore_errors=True)
-            user_forms = []
-            for formdef in formdefs:
-                user_forms.extend(formdef.data_class().get_with_indexed_value(
-                            'user_id', user.id))
-            user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
-
-        profile_links = []
-        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
-            if user_formdef:
-                profile_links.append('<a href="#my-profile">%s</a>' % _('My Profile'))
-        if user_forms:
-            profile_links.append('<a href="#my-forms">%s</a>' % _('My Forms'))
-        if get_cfg('misc', {}).get('aq-strongbox'):
-            profile_links.append('<a href="strongbox/">%s</a>' % _('My Strongbox'))
-        if is_payment_supported():
-            profile_links.append('<a href="invoices/">%s</a>' % _('My Invoices'))
-
-        root_url = get_publisher().get_root_url()
-        if user.can_go_in_backoffice():
-            profile_links.append('<a href="%sbackoffice/">%s</a>' % (root_url, _('Back office')))
-        if user.is_admin:
-            profile_links.append('<a href="%sadmin/">%s</a>' % (root_url, _('Admin')))
-
-        if profile_links:
-            r += htmltext('<p id="profile-links">')
-            r += htmltext(' - '.join(profile_links))
-            r += htmltext('</p>')
-
-        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
-            if user_formdef:
-                r += self._my_profile(user_formdef, user)
-
-            r += self._index_buttons(user_formdef)
-
-            try:
-                x = PasswordAccount.get_on_index(get_request().user.id, str('user_id'))
-            except KeyError:
-                pass
-            else:
-                r += htmltext('<p>')
-                r += _('You can delete your account freely from the services portal. '
-                       'This action is irreversible; it will destruct your personal '
-                       'datas and destruct the access to your request history.')
-                r += htmltext(' <strong><a href="remove" rel="popup">%s</a></strong>.') % _('Delete My Account')
-                r += htmltext('</p>')
-
-        options = get_cfg('misc', {}).get('announce_themes')
-        if options:
-            try:
-                subscription = AnnounceSubscription.get_on_index(
-                        get_request().user.id, str('user_id'))
-            except KeyError:
-                pass
-            else:
-                r += htmltext('<p class="command"><a href="announces">%s</a></p>') % _(
-                        'Edit my Subscription to Announces')
-
-        if user_forms:
-            r += htmltext('<h3 id="my-forms">%s</h3>') % _('My Forms')
-            r += root.FormsRootDirectory().user_forms(user_forms)
-
-        return r.getvalue()
-
-    def _my_profile(self, user_formdef, user):
-        r = TemplateIO(html=True)
-        r += htmltext('<h3 id="my-profile">%s</h3>') % _('My Profile')
-
-        r += TextsDirectory.get_html_text('top-of-profile')
-
-        if user.form_data:
-            r += htmltext('<ul>')
-            for field in user_formdef.fields:
-                if not hasattr(field, str('get_view_value')):
-                    continue
-                value = user.form_data.get(field.id)
-                r += htmltext('<li>')
-                r += field.label
-                r += ' : '
-                if value:
-                    r += field.get_view_value(value)
-                r += htmltext('</li>')
-            r += htmltext('</ul>')
-        else:
-            r += htmltext('<p>%s</p>') % _('Empty profile')
-        return r.getvalue()
-
-    def _index_buttons(self, form_data):
-        r = TemplateIO(html=True)
-        passwords_cfg = get_cfg('passwords', {})
-        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
-        if get_session().lasso_session_dump:
-            ident_method = 'idp'
-
-        if form_data and ident_method != 'idp':
-            r += htmltext('<p class="command"><a href="profile" rel="popup">%s</a></p>') % _('Edit My Profile')
-
-        if ident_method == 'password' and passwords_cfg.get('can_change', False):
-            r += htmltext('<p class="command"><a href="password" rel="popup">%s</a></p>') % _('Change My Password')
-
-        return r.getvalue()
-
-    def profile(self):
-        user = get_request().user
-        if not user or user.anonymous:
-            raise errors.AccessUnauthorizedError()
-
-        form = Form(enctype = 'multipart/form-data')
-        formdef = user.get_formdef()
-        formdef.add_fields_to_form(form, form_data = user.form_data)
-
-        form.add_submit('submit', _('Apply Changes'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.profile_submit(form, formdef)
-            return redirect('.')
-
-        template.html_top(_('Edit Profile'))
-        return form.render()
-
-    def profile_submit(self, form, formdef):
-        user = get_request().user
-        data = formdef.get_data(form)
-
-        user.set_attributes_from_formdata(data)
-        user.form_data = data
-
-        user.store()
-
-    def password(self):
-        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
-        if ident_method != 'password':
-            raise errors.TraversalError()
-
-        user = get_request().user
-        if not user or user.anonymous:
-            raise errors.AccessUnauthorizedError()
-
-        form = Form(enctype = 'multipart/form-data')
-        form.add(PasswordWidget, 'new_password', title = _('New Password'),
-                required=True)
-        form.add(PasswordWidget, 'new2_password', title = _('New Password (confirm)'),
-                required=True) 
-
-        form.add_submit('submit', _('Change Password'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            qommon.ident.password.check_password(form, 'new_password')
-            new_password = form.get_widget('new_password').parse()
-            new2_password = form.get_widget('new2_password').parse()
-            if new_password != new2_password:
-                form.set_error('new2_password', _('Passwords do not match'))
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit_password(new_password)
-            return redirect('.')
-
-        template.html_top(_('Change Password'))
-        return form.render()
-
-    def submit_password(self, new_password):
-        passwords_cfg = get_cfg('passwords', {})
-        account = PasswordAccount.get(get_session().username)
-        account.hashing_algo = passwords_cfg.get('hashing_algo')
-        account.set_password(new_password)
-        account.store()
-
-    def new(self):
-        if not get_request().user or not get_request().user.anonymous:
-            raise errors.AccessUnauthorizedError()
-
-        form = Form(enctype = 'multipart/form-data')
-        formdef = get_publisher().user_class.get_formdef()
-        if formdef:
-            formdef.add_fields_to_form(form)
-        else:
-            get_logger().error('missing user formdef (in myspace/new)')
-
-        form.add_submit('submit', _('Register'))
-
-        if form.is_submitted() and not form.has_errors():
-            user = get_publisher().user_class()
-            data = formdef.get_data(form)
-            user.set_attributes_from_formdata(data)
-            user.name_identifiers = get_request().user.name_identifiers
-            user.lasso_dump = get_request().user.lasso_dump
-            user.set_attributes_from_formdata(data)
-            user.form_data = data
-            user.store()
-            get_session().set_user(user.id)
-            root_url = get_publisher().get_root_url()
-            return redirect('%smyspace' % root_url)
-
-        template.html_top(_('Welcome'))
-        return form.render()
-
-
-    def remove(self):
-        user = get_request().user
-        if not user or user.anonymous:
-            raise errors.AccessUnauthorizedError()
-
-        form = Form(enctype = 'multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'Are you really sure you want to remove your account?')))
-        form.add_submit('submit', _('Remove my account'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            user = get_request().user
-            account = PasswordAccount.get_on_index(user.id, str('user_id'))
-            get_session_manager().expire_session() 
-            account.remove_self()
-            return redirect(get_publisher().get_root_url())
-
-        template.html_top(_('Removing Account'))
-        return form.render()
-
-    def announces(self):
-        options = get_cfg('misc', {}).get('announce_themes')
-        if not options:
-            raise errors.TraversalError()
-        user = get_request().user
-        if not user or user.anonymous:
-            raise errors.AccessUnauthorizedError()
-        subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id'))
-        if not subscription:
-            raise errors.TraversalError()
-
-        if subscription.enabled_themes is None:
-            enabled_themes = options
-        else:
-            enabled_themes = subscription.enabled_themes
-
-        form = Form(enctype = 'multipart/form-data')
-        form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'),
-                value=enabled_themes, options=[(x, x, x) for x in options],
-                inline=False, required=False)
-
-        form.add_submit('submit', _('Apply Changes'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            chosen_themes = form.get_widget('themes').parse()
-            if chosen_themes == options:
-                chosen_themes = None
-            subscription.enabled_themes = chosen_themes
-            subscription.store()
-            return redirect('.')
-
-        template.html_top()
-        get_response().breadcrumb.append(('announces', _('Announce Subscription')))
-        return form.render()
-
-
-TextsDirectory.register('aq-myspace-invoice',
-        N_('Message on top of invoices page'),
-        category = N_('Invoices'))
-
diff --git a/extra/modules/payments.py b/extra/modules/payments.py
deleted file mode 100644
index 272a1b5..0000000
--- a/extra/modules/payments.py
+++ /dev/null
@@ -1,597 +0,0 @@
-import random
-import string
-from datetime import datetime as dt
-import hashlib
-import time
-import urllib
-
-from decimal import Decimal
-
-from quixote import (redirect, get_publisher, get_request, get_session,
-        get_response)
-from quixote.directory import Directory
-from quixote.html import TemplateIO, htmltext
-
-if not set:
-    from sets import Set as set
-
-eopayment = None
-try:
-    import eopayment
-except ImportError:
-    pass
-
-from qommon import _
-from qommon import errors, get_logger, get_cfg, emails
-from qommon.storage import StorableObject
-from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
-    WidgetDict
-from qommon.misc import simplify
-
-from wcs.formdef import FormDef
-from wcs.formdata import Evolution
-from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
-from wcs.users import User
-
-def is_payment_supported():
-    if not eopayment:
-        return False
-    return get_cfg('aq-permissions', {}).get('payments', None) is not None
-
-
-class Regie(StorableObject):
-    _names = 'regies'
-
-    label = None
-    description = None
-    service = None
-    service_options = None
-
-    def get_payment_object(self):
-        return eopayment.Payment(kind=self.service,
-                                 options=self.service_options)
-
-
-class Invoice(StorableObject):
-    _names = 'invoices'
-    _hashed_indexes = ['user_id', 'regie_id']
-    _indexes = ['external_id']
-
-    user_id = None
-    regie_id = None
-    formdef_id = None
-    formdata_id = None
-    subject = None
-    details = None
-    amount = None
-    date = None
-    paid = False
-    paid_date = None
-    canceled = False
-    canceled_date = None
-    canceled_reason = None
-    next_status = None
-    external_id = None
-    request_kwargs = {}
-
-    def __init__(self, id=None, regie_id=None, formdef_id=None):
-        self.id = id
-        self.regie_id = regie_id
-        self.formdef_id = formdef_id
-        if get_publisher() and not self.id:
-            self.id = self.get_new_id()
-
-    def get_user(self):
-        if self.user_id:
-            return User.get(self.user_id, ignore_errors=True)
-        return None
-
-    @property
-    def username(self):
-        user = self.get_user()
-        return user.name if user else ''
-
-    def get_new_id(self, create=False):
-        # format : date-regie-formdef-alea-check
-        r = random.SystemRandom()
-        self.fresh = True
-        while True:
-            id = '-'.join([
-                dt.now().strftime('%Y%m%d'),
-                'r%s' % (self.regie_id or 'x'),
-                'f%s' % (self.formdef_id or 'x'),
-                ''.join([r.choice(string.digits) for x in range(5)])
-                ])
-            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
-            id = id + '-' + crc
-            if not self.has_key(id):
-                return id
-
-    def store(self, *args, **kwargs):
-        if getattr(self, 'fresh', None) is True:
-            del self.fresh
-            notify_new_invoice(self)
-        return super(Invoice, self).store(*args, **kwargs)
-
-    def check_crc(cls, id):
-        try:
-            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
-        except:
-            return False
-    check_crc = classmethod(check_crc)
-
-    def pay(self):
-        self.paid = True
-        self.paid_date = dt.now()
-        self.store()
-        get_logger().info(_('invoice %s paid'), self.id)
-        notify_paid_invoice(self)
-
-    def unpay(self):
-        self.paid = False
-        self.paid_date = None
-        self.store()
-        get_logger().info(_('invoice %s unpaid'), self.id)
-
-    def cancel(self, reason=None):
-        self.canceled = True
-        self.canceled_date = dt.now()
-        if reason:
-            self.canceled_reason = reason
-        self.store()
-        notify_canceled_invoice(self)
-        get_logger().info(_('invoice %s canceled'), self.id)
-
-    def payment_url(self):
-        base_url = get_publisher().get_frontoffice_url()
-        return '%s/invoices/%s' % (base_url, self.id)
-
-
-INVOICE_EVO_VIEW = {
-    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
-    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
-    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
-    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
-}
-
-class InvoiceEvolutionPart:
-    action = None
-    id = None
-    subject = None
-    amount = None
-    transaction = None
-
-    def __init__(self, action, invoice, transaction=None):
-        self.action = action
-        self.id = invoice.id
-        self.subject = invoice.subject
-        self.amount = invoice.amount
-        self.transaction = transaction
-
-    def view(self):
-        vars = {
-            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
-            'id': self.id,
-            'subject': self.subject,
-            'amount': self.amount,
-        }
-        if self.transaction:
-            vars['transaction_order_id'] = self.transaction.order_id
-        return htmltext('<p class="invoice-%s">' % self.action + \
-                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
-
-
-class Transaction(StorableObject):
-    _names = 'transactions'
-    _hashed_indexes = ['invoice_ids']
-    _indexes = ['order_id']
-
-    invoice_ids = None
-
-    order_id = None
-    start = None
-    end = None
-    bank_data = None
-
-    def __init__(self, *args, **kwargs):
-        self.invoice_ids = list()
-        StorableObject.__init__(self, *args, **kwargs)
-
-    def get_new_id(cls, create=False):
-        r = random.SystemRandom()
-        while True:
-            id = ''.join([r.choice(string.digits) for x in range(16)])
-            if not cls.has_key(id):
-                return id
-    get_new_id = classmethod(get_new_id)
-
-class PaymentWorkflowStatusItem(WorkflowStatusItem):
-    description = N_('Payment Creation')
-    key = 'payment'
-    endpoint = False
-    category = ('aq-payment', N_('Payment'))
-    support_substitution_variables = True
-
-    subject = None
-    details = None
-    amount = None
-    regie_id = None
-    next_status = None
-    request_kwargs = {}
-
-    def is_available(self, workflow=None):
-        return is_payment_supported()
-    is_available = classmethod(is_available)
-
-    def render_as_line(self):
-        if self.regie_id:
-            try:
-                return _('Payable to %s' % Regie.get(self.regie_id).label)
-            except KeyError:
-                return _('Payable (not completed)')
-        else:
-            return _('Payable (not completed)')
-
-    def get_parameters(self):
-        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
-                'request_kwargs')
-
-    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
-        if 'subject' in parameters:
-            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
-                     value=self.subject, size=40)
-        if 'details' in parameters:
-            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
-                     value=self.details, cols=80, rows=10)
-        if 'amount' in parameters:
-            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
-        if 'regie_id' in parameters:
-            form.add(SingleSelectWidget, '%sregie_id' % prefix,
-                title=_('Regie'), value=self.regie_id,
-                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
-        if 'next_status' in parameters:
-            form.add(SingleSelectWidget, '%snext_status' % prefix,
-                title=_('Status after validation'), value = self.next_status,
-                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
-                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
-        if 'request_kwargs' in parameters:
-            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
-            hint = ''
-            hint +=_('If the value starts by = it will be '
-                'interpreted as a Python expression.')
-            hint += ' '
-            hint += _('Standard keys are: %s.') % (', '.join(keys))
-            form.add(WidgetDict, 'request_kwargs',
-                title=_('Parameters for the payment system'),
-                hint=hint,
-                value = self.request_kwargs)
-
-    def perform(self, formdata):
-        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
-        invoice.user_id = formdata.user_id
-        invoice.formdata_id = formdata.id
-        invoice.next_status = self.next_status
-        if self.subject:
-            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
-        else:
-            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
-                    'form_name': formdata.formdef.name,
-                    'formdata_id': formdata.id }
-        invoice.details = template_on_formdata(formdata, self.compute(self.details))
-        invoice.amount = Decimal(self.compute(self.amount))
-        invoice.date = dt.now()
-        invoice.request_kwargs = {}
-        if self.request_kwargs:
-            for key, value in self.request_kwargs.iteritems():
-                invoice.request_kwargs[key] = self.compute(value)
-        invoice.store()
-        # add a message in formdata.evolution
-        evo = Evolution()
-        evo.time = time.localtime()
-        evo.status = formdata.status
-        evo.add_part(InvoiceEvolutionPart('create', invoice))
-        if not formdata.evolution:
-            formdata.evolution = []
-        formdata.evolution.append(evo)
-        formdata.store()
-        # redirect the user to "my invoices"
-        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
-
-register_item_class(PaymentWorkflowStatusItem)
-
-class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
-    description = N_('Payment Cancel')
-    key = 'payment-cancel'
-    endpoint = False
-    category = ('aq-payment', N_('Payment'))
-
-    reason = None
-    regie_id = None
-
-    def is_available(self, workflow=None):
-        return is_payment_supported()
-    is_available = classmethod(is_available)
-
-    def render_as_line(self):
-        if self.regie_id:
-            if self.regie_id == '_all':
-                return _('Cancel all Payments')
-            else:
-                try:
-                    return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
-                except KeyError:
-                    return _('Cancel Payments (non completed)')
-        else:
-            return _('Cancel Payments (non completed)')
-
-    def get_parameters(self):
-        return ('reason', 'regie_id')
-
-    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
-        if 'reason' in parameters:
-            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
-                     value=self.reason, size=40)
-        if 'regie_id' in parameters:
-            form.add(SingleSelectWidget, '%sregie_id' % prefix,
-                title=_('Regie'), value=self.regie_id,
-                options = [(None, '---'), ('_all', _('All Regies'))] + \
-                            [(x.id, x.label) for x in Regie.select()])
-
-    def perform(self, formdata):
-        invoices_id = []
-        # get all invoices for the formdata and the selected regie
-        for evo in [evo for evo in formdata.evolution if evo.parts]:
-            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
-                if part.action == 'create':
-                    invoices_id.append(part.id)
-                elif part.id in invoices_id:
-                    invoices_id.remove(part.id)
-        invoices = [Invoice.get(id) for id in invoices_id]
-        # select invoices for the selected regie (if not "all regies")
-        if self.regie_id != '_all':
-            invoices = [i for i in invoices if i.regie_id == self.regie_id]
-        # security filter: check user
-        invoices = [i for i in invoices if i.user_id == formdata.user_id]
-        # security filter: check formdata & formdef
-        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
-                and (i.formdef_id == formdata.formdef.id)]
-        evo = Evolution()
-        evo.time = time.localtime()
-        for invoice in invoices:
-            if not (invoice.paid or invoice.canceled):
-                invoice.cancel(self.reason)
-                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
-        if not formdata.evolution:
-            formdata.evolution = []
-        formdata.evolution.append(evo)
-        formdata.store()
-        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
-
-register_item_class(PaymentCancelWorkflowStatusItem)
-
-
-def request_payment(invoice_ids, url, add_regie=True):
-    for invoice_id in invoice_ids:
-        if not Invoice.check_crc(invoice_id):
-            raise KeyError()
-    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
-    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
-    regie_ids = set([invoice.regie_id for invoice in invoices])
-    # Do not apply if more than one regie is used or no invoice is not paid or canceled
-    if len(invoices) == 0 or len(regie_ids) != 1:
-        url = get_publisher().get_frontoffice_url()
-        if get_session().user:
-            # FIXME: add error messages
-            url += '/myspace/invoices/'
-        return redirect(url)
-    if add_regie:
-        url = '%s%s' % (url, list(regie_ids)[0])
-
-    transaction = Transaction()
-    transaction.store()
-    transaction.invoice_ids = invoice_ids
-    transaction.start = dt.now()
-
-    amount = Decimal(0)
-    for invoice in invoices:
-        amount += Decimal(invoice.amount)
-
-    regie = Regie.get(invoice.regie_id)
-    payment = regie.get_payment_object()
-    # initialize request_kwargs using informations from the first invoice
-    # and update using current user informations
-    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
-    request = get_request()
-    if request.user and request.user.email:
-        request_kwargs['email'] = request.user.email
-    if request.user and request.user.display_name:
-        request_kwargs['name'] = simplify(request.user.display_name)
-    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
-    transaction.order_id = order_id
-    transaction.store()
-
-    for invoice in invoices:
-        if invoice.formdef_id and invoice.formdata_id:
-            formdef = FormDef.get(invoice.formdef_id)
-            formdata = formdef.data_class().get(invoice.formdata_id)
-            evo = Evolution()
-            evo.time = time.localtime()
-            evo.status = formdata.status
-            evo.add_part(InvoiceEvolutionPart('try', invoice,
-                transaction=transaction))
-            if not formdata.evolution:
-                formdata.evolution = []
-            formdata.evolution.append(evo)
-            formdata.store()
-
-    if kind == eopayment.URL:
-        return redirect(data)
-    elif kind == eopayment.FORM:
-        return return_eopayment_form(data)
-    else:
-        raise NotImplementedError()
-
-def return_eopayment_form(form):
-    r = TemplateIO(html=True)
-    r += htmltext('<html><body onload="document.payform.submit()">')
-    r += htmltext('<form action="%s" method="%s" name="payform">') % (form.url, form.method)
-    for field in form.fields:
-        r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
-                field['type'],
-                field['name'],
-                field['value'])
-    r += htmltext('<input type="submit" name="submit" value="%s"/>') % _('Pay')
-    r += htmltext('</body></html>')
-    return r.getvalue()
-
-
-class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
-    description = N_('Payment Validation')
-    key = 'payment-validation'
-    endpoint = False
-    category = ('aq-payment', N_('Payment'))
-
-    next_status = None
-
-    def is_available(self, workflow=None):
-        return is_payment_supported()
-    is_available = classmethod(is_available)
-
-    def render_as_line(self):
-        return _('Wait for payment validation')
-
-    def get_parameters(self):
-        return ('next_status',)
-
-    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
-        if 'next_status' in parameters:
-            form.add(SingleSelectWidget, '%snext_status' % prefix,
-                title=_('Status once validated'), value = self.next_status,
-                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
-
-register_item_class(PaymentValidationWorkflowStatusItem)
-
-
-class PublicPaymentRegieBackDirectory(Directory):
-    def __init__(self, asynchronous):
-        self.asynchronous = asynchronous
-
-    def _q_lookup(self, component):
-        logger = get_logger()
-        request = get_request()
-        query_string = get_request().get_query()
-        if request.get_method() == 'POST' and query_string == '':
-            query_string = urllib.urlencode(request.form)
-        try:
-            regie = Regie.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        if self.asynchronous:
-            logger.debug('received asynchronous notification %r' % query_string)
-        payment = regie.get_payment_object()
-        payment_response = payment.response(query_string)
-        logger.debug('payment response %r', payment_response)
-        order_id = payment_response.order_id
-        bank_data = payment_response.bank_data
-
-        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
-        if transaction is None:
-            raise errors.TraversalError()
-        commit = False
-        if not transaction.end:
-            commit = True
-            transaction.end = dt.now()
-            transaction.bank_data = bank_data
-            transaction.store()
-        if payment_response.signed and payment_response.is_paid() and commit:
-            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
-                                    order_id, payment_response.transaction_id, bank_data))
-
-            for invoice_id in transaction.invoice_ids:
-                # all invoices are now paid
-                invoice = Invoice.get(invoice_id)
-                invoice.pay()
-
-                # workflow for each related formdata
-                if invoice.formdef_id and invoice.formdata_id:
-                    next_status = invoice.next_status
-                    formdef = FormDef.get(invoice.formdef_id)
-                    formdata = formdef.data_class().get(invoice.formdata_id)
-                    wf_status = formdata.get_status()
-                    for item in wf_status.items:
-                        if isinstance(item, PaymentValidationWorkflowStatusItem):
-                            next_status = item.next_status
-                            break
-                    if next_status is not None:
-                        formdata.status = 'wf-%s' % next_status
-                        evo = Evolution()
-                        evo.time = time.localtime()
-                        evo.status = formdata.status
-                        evo.add_part(InvoiceEvolutionPart('pay', invoice,
-                            transaction=transaction))
-                        if not formdata.evolution:
-                            formdata.evolution = []
-                        formdata.evolution.append(evo)
-                        formdata.store()
-                        # performs the items of the new status
-                        formdata.perform_workflow()
-
-        elif payment_response.is_error() and commit:
-            logger.error('transaction %s finished with failure, bank_data:%s' % (
-                                    order_id, bank_data))
-        elif commit:
-            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
-                                    order_id, bank_data))
-        if payment_response.return_content != None and self.asynchronous:
-            get_response().set_content_type('text/plain')
-            return payment_response.return_content
-        else:
-            if payment_response.is_error():
-                # TODO: here return failure message
-                get_session().message = ('info', _('Payment failed'))
-            else:
-                # TODO: Here return success message
-                get_session().message = ('error', _('Payment succeeded'))
-            url = get_publisher().get_frontoffice_url()
-            if get_session().user:
-                url += '/myspace/invoices/'
-            return redirect(url)
-
-class PublicPaymentDirectory(Directory):
-    _q_exports = ['', 'init', 'back', 'back_asynchronous']
-
-    back = PublicPaymentRegieBackDirectory(False)
-    back_asynchronous = PublicPaymentRegieBackDirectory(True)
-
-
-    def init(self):
-        invoice_ids = get_request().form.get('invoice_ids').split(' ')
-
-        for invoice_id in invoice_ids:
-            if not Invoice.check_crc(invoice_id):
-                raise KeyError()
-
-        url = get_publisher().get_frontoffice_url() + '/payment/back/'
-
-        return request_payment(invoice_ids, url)
-
-def notify_new_invoice(invoice):
-    notify_invoice(invoice, 'payment-new-invoice-email')
-
-def notify_paid_invoice(invoice):
-    notify_invoice(invoice, 'payment-invoice-paid-email')
-
-def notify_canceled_invoice(invoice):
-    notify_invoice(invoice, 'payment-invoice-canceled-email')
-
-def notify_invoice(invoice, template):
-    user = invoice.get_user()
-    assert user is not None
-    regie = Regie.get(id=invoice.regie_id)
-    emails.custom_ezt_email(template, { 
-                'user': user,
-                'invoice': invoice,
-                'regie': regie, 
-                'invoice_url': invoice.payment_url() 
-            }, user.email, fire_and_forget = True)
-
diff --git a/extra/modules/payments_ui.py b/extra/modules/payments_ui.py
deleted file mode 100644
index a660777..0000000
--- a/extra/modules/payments_ui.py
+++ /dev/null
@@ -1,606 +0,0 @@
-import time
-import pprint
-import locale
-import decimal
-import datetime
-
-from quixote import get_request, get_response, get_session, redirect
-from quixote.directory import Directory, AccessControlled
-from quixote.html import TemplateIO, htmltext
-
-import wcs
-import wcs.admin.root
-from wcs.formdef import FormDef
-
-from qommon import _
-from qommon import errors, misc, template, get_logger
-from qommon.form import *
-from qommon.strftime import strftime
-from qommon.admin.emails import EmailsDirectory
-from qommon.backoffice.menu import html_top
-from qommon import get_cfg
-
-from payments import (eopayment, Regie, is_payment_supported, Invoice,
-        Transaction, notify_paid_invoice)
-
-from qommon.admin.texts import TextsDirectory
-
-if not set:
-    from sets import Set as set
-
-def invoice_as_html(invoice):
-    r = TemplateIO(html=True)
-    r += htmltext('<div id="invoice">')
-    r += htmltext('<h2>%s</h2>') % _('Invoice: %s') % invoice.subject
-    r += htmltext('<h3>%s') % _('Amount: %s') % invoice.amount
-    r += htmltext(' &euro;</h3>')
-    r += htmltext('<!-- DEBUG \n')
-    r += 'Invoice:\n'
-    r += pprint.pformat(invoice.__dict__)
-    for transaction in Transaction.get_with_indexed_value('invoice_ids', invoice.id):
-        r += '\nTransaction:\n'
-        r += pprint.pformat(transaction.__dict__)
-    r += htmltext('\n-->')
-    if invoice.formdef_id and invoice.formdata_id and \
-            get_session().user == invoice.user_id:
-        formdef = FormDef.get(invoice.formdef_id)
-        if formdef:
-            formdata = formdef.data_class().get(invoice.formdata_id, ignore_errors=True)
-            if formdata:
-                name = _('%(form_name)s #%(formdata_id)s') % {
-                        'form_name': formdata.formdef.name,
-                        'formdata_id': formdata.id }
-                r += htmltext('<p class="from">%s <a href="%s">%s</a></p>') % (_('From:'), formdata.get_url(), name)
-    r += htmltext('<p class="regie">%s</p>') % _('Regie: %s') % Regie.get(invoice.regie_id).label
-    r += htmltext('<p class="date">%s</p>') % _('Created on: %s') % misc.localstrftime(invoice.date)
-    if invoice.details:
-        r += htmltext('<p class="details">%s</p>') % _('Details:')
-        r += htmltext('<div class="details">')
-        r += htmltext(invoice.details)
-        r += htmltext('</div>')
-    if invoice.canceled:
-        r += htmltext('<p class="canceled">')
-        r += '%s' % _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
-        if invoice.canceled_reason:
-            r += ' (%s)' % invoice.canceled_reason
-        r += htmltext('</p>')
-    if invoice.paid:
-        r += htmltext('<p class="paid">%s</p>') % _('paid on %s') % misc.localstrftime(invoice.paid_date)
-    r += htmltext('</div>')
-    return r.getvalue()
-
-
-class InvoicesDirectory(Directory):
-    _q_exports = ['', 'multiple']
-
-    def _q_traverse(self, path):
-        if not is_payment_supported():
-            raise errors.TraversalError()
-        get_response().filter['bigdiv'] = 'profile'
-        if get_session().user:
-            # fake breadcrumb
-            get_response().breadcrumb.append(('myspace/', _('My Space')))
-            get_response().breadcrumb.append(('invoices/', _('Invoices')))
-        return Directory._q_traverse(self, path)
-
-    def multiple(self):
-        invoice_ids = get_request().form.get('invoice')
-        if type(invoice_ids) is not list:
-            return redirect('%s' % invoice_ids)
-        return redirect('+'.join(invoice_ids))
-
-    def _q_lookup(self, component):
-        if str('+') in component:
-            invoice_ids = component.split(str('+'))
-        else:
-            invoice_ids = [component]
-        for invoice_id in invoice_ids:
-            if not Invoice.check_crc(invoice_id):
-                raise errors.TraversalError()
-
-        template.html_top(_('Invoices'))
-        r = TemplateIO(html=True)
-        r += TextsDirectory.get_html_text('aq-invoice')
-
-        regies_id = set()
-        for invoice_id in invoice_ids:
-            try:
-                invoice = Invoice.get(invoice_id)
-            except KeyError:
-                raise errors.TraversalError()
-            r += invoice_as_html(invoice)
-            if not (invoice.paid or invoice.canceled):
-                regies_id.add(invoice.regie_id)
-
-        if len(regies_id) == 1:
-            r += htmltext('<p class="command">')
-            r += htmltext('<a href="%s/payment/init?invoice_ids=%s">') % (
-                    get_publisher().get_frontoffice_url(), component)
-            if len(invoice_ids) > 1:
-                r += _('Pay Selected Invoices')
-            else:
-                r += _('Pay')
-            r += htmltext('</a></p>')
-        if len(regies_id) > 1:
-            r += _('You can not pay to different regies.')
-
-        return r.getvalue()
-
-    def _q_index(self):
-        return redirect('..')
-
-
-class RegieDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete', 'options']
-
-    def __init__(self, regie):
-        self.regie = regie
-
-    def _q_index(self):
-        html_top('payments', title = _('Regie: %s') % self.regie.label)
-        r = TemplateIO(html=True)
-        get_response().filter['sidebar'] = self.get_sidebar()
-        r += htmltext('<h2>%s</h2>') % _('Regie: %s') % self.regie.label
-
-        r += get_session().display_message()
-
-        if self.regie.description:
-            r += htmltext('<div class="bo-block">')
-            r += htmltext('<p>')
-            r += self.regie.description
-            r += htmltext('</p>')
-            r += htmltext('</div>')
-
-        if self.regie.service:
-            r += htmltext('<div class="bo-block">')
-            url = get_publisher().get_frontoffice_url() + '/payment/back_asynchronous/'
-            url += str(self.regie.id)
-            r += htmltext('<p>')
-            r += '%s %s' % (_('Banking Service:'), self.regie.service)
-            r += htmltext(' (<a href="options">%s</a>)') % _('options')
-            r += htmltext('</p>')
-            r += htmltext('<p>')
-            r += '%s %s' % (_('Payment notification URL:'), url)
-            r += htmltext('</div>')
-
-        r += self.invoice_listing()
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul>')
-        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
-        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def edit(self):
-        form = self.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit(form)
-            return redirect('..')
-
-        html_top('payments', title = _('Edit Regie: %s') % self.regie.label)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Edit Regie: %s') % self.regie.label
-        r += form.render()
-        return r.getvalue()
-
-
-    def form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'label', title=_('Label'), required=True,
-                value=self.regie.label)
-        form.add(TextWidget, 'description', title=_('Description'),
-                value=self.regie.description, rows=5, cols=60)
-        form.add(SingleSelectWidget, 'service', title=_('Banking Service'),
-                value=self.regie.service, required=True,
-                options = [
-                        ('dummy', _('Dummy (for tests)')),
-                        ('sips', 'SIPS'),
-                        ('systempayv2', 'systempay (Banque Populaire)'),
-                        ('spplus', _('SP+ (Caisse d\'epargne)'))])
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit(self, form):
-        for k in ('label', 'description', 'service'):
-            widget = form.get_widget(k)
-            if widget:
-                setattr(self.regie, k, widget.parse())
-        self.regie.store()
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this regie.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            r = TemplateIO(html=True)
-            html_top('payments', title = _('Delete Regie'))
-            r += htmltext('<h2>%s</h2>') % _('Deleting Regie: %s') % self.regie.label
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.regie.remove_self()
-            return redirect('..')
-
-    def option_form(self):
-        form = Form(enctype='multipart/form-data')
-        module = eopayment.get_backend(self.regie.service)
-        service_options = {}
-        for infos in module.description['parameters']:
-            if 'default' in infos:
-                service_options[infos['name']] = infos['default']
-        service_options.update(self.regie.service_options or {})
-
-        banking_titles = {
-            ('dummy', 'direct_notification_url'): N_('Direct Notification URL'),
-            ('dummy', 'siret'): N_('Dummy SIRET'),
-        }
-
-        for infos in module.description['parameters']:
-            name = infos['name']
-            caption = infos.get('caption', name).encode(get_publisher().site_charset)
-            title = banking_titles.get((self.regie.service, name), caption)
-            kwargs = {}
-            widget = StringWidget
-            if infos.get('help_text') is not None:
-                kwargs['hint'] = _(infos['help_text'].encode(get_publisher().site_charset))
-            if infos.get('required', False):
-                kwargs['required'] = True
-            if infos.get('max_length') is not None:
-                kwargs['size'] = infos['max_length']
-            elif infos.get('length') is not None:
-                kwargs['size'] = infos['length']
-            else:
-                kwargs['size'] = 80
-            if kwargs['size'] > 100:
-                widget = TextWidget
-                kwargs['cols'] = 80
-                kwargs['rows'] = 5
-            if 'type' not in infos or infos['type'] is str:
-                form.add(widget, name, title=_(title),
-                         value=service_options.get(name), **kwargs)
-            elif infos['type'] is bool:
-                form.add(CheckboxWidget, name, title=title,
-                         value=service_options.get(name), **kwargs)
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def options(self):
-        r = TemplateIO(html=True)
-        form = self.option_form()
-
-        module = eopayment.get_backend(self.regie.service)
-        try:
-            r += htmltext('<!-- Payment backend description: \n')
-            r += pprint.pformat(module.description)
-            r += htmltext('-->')
-        except:
-            return template.error_page(_('Payment backend do not list its options'))
-            raise errors.TraversalError()
-        r += htmltext('<!-- \n')
-        r += 'Service options\n'
-        r += pprint.pformat(self.regie.service_options)
-        r += htmltext('-->')
-
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-             if self.submit_options(form, module):
-                return redirect('..')
-
-        html_top('payments', title=_('Edit Service Options'))
-        r += htmltext('<h2>%s</h2>') % _('Edit Service Options')
-        r += form.render()
-        return r.getvalue()
-
-    def submit_options(self, form, module):
-        # extra validation
-        error = False
-        for infos in module.description['parameters']:
-            widget = form.get_widget(infos['name'])
-            value = widget.parse()
-            if value and 'validation' in infos:
-                try:
-                    if not infos['validation'](value):
-                        widget.set_error(_('Valeur invalide'))
-                        error = True
-                except ValueError, e:
-                    widget.set_error(_(e.message))
-                    error = True
-        if error:
-            return False
-        if not self.regie.service_options:
-            self.regie.service_options = {}
-        for infos in module.description['parameters']:
-            name = infos['name']
-            value = form.get_widget(name).parse()
-            if value is None:
-                value = ''
-            if hasattr(value, 'strip'):
-                value = value.strip()
-            if infos.get('default') is not None:
-                if value == infos['default']:
-                    self.regie.service_options.pop(name, None)
-                else:
-                    self.regie.service_options[name] = form.get_widget(name).parse()
-            elif not value:
-                self.regie.service_options.pop(name, None)
-            else:
-                self.regie.service_options[name] = form.get_widget(name).parse()
-        self.regie.store()
-        return True
-
-    PAGINATION = 50
-
-    def monetary_amount(self, val):
-        if not val:
-            return ''
-        if isinstance(val, basestring):
-            val = val.replace(',', '.')
-        return '%.2f' % decimal.Decimal(val)
-
-    def get_sort_by(self):
-        request = get_request()
-        sort_by = request.form.get('sort_by')
-        if sort_by not in ('date', 'paid_date', 'username'):
-            sort_by = 'date'
-        return sort_by
-
-    def get_invoices(self):
-        sort_by = self.get_sort_by()
-        invoices = Invoice.get_with_indexed_value('regie_id', self.regie.id,
-                ignore_errors=True)
-        if 'date' in sort_by:
-            reverse = True
-            key = lambda i: getattr(i, sort_by) or datetime.datetime.now()
-        else:
-            reverse = False
-            key = lambda i: getattr(i, sort_by) or ''
-        invoices.sort(reverse=reverse, key=key)
-        return invoices
-
-    def unpay(self, request, invoice):
-        get_logger().info(_('manually set unpaid invoice %(invoice_id)s in regie %(regie)s')
-            % dict(invoice_id=invoice.id, regie=self.regie.id))
-        transaction = Transaction()
-        transaction.invoice_ids = [ invoice.id ]
-        transaction.order_id = 'Manual action'
-        transaction.start = datetime.datetime.now()
-        transaction.end = transaction.start
-        transaction.bank_data = { 
-            'action': 'Set unpaid', 
-            'by': request.user.get_display_name() + ' (%s)' % request.user.id
-        }
-        transaction.store()
-        invoice.unpay()
-
-    def pay(self, request, invoice):
-        get_logger().info(_('manually set paid invoice %(invoice_id)s in regie %(regie)s')
-            % dict(invoice_id=invoice.id, regie=self.regie.id))
-        transaction = Transaction()
-        transaction.invoice_ids = [ invoice.id ]
-        transaction.order_id = 'Manual action'
-        transaction.start = datetime.datetime.now()
-        transaction.end = transaction.start
-        transaction.bank_data = { 
-            'action': 'Set paid', 
-            'by': request.user.get_display_name() + ' (%s)' % request.user.id
-        }
-        transaction.store()
-        invoice.pay()
-
-    def invoice_listing(self):
-        request = get_request()
-        get_response().add_css_include('../../themes/auquotidien/admin.css')
-        if request.get_method() == 'POST':
-            invoice_id = request.form.get('id')
-            invoice = Invoice.get(invoice_id, ignore_errors=True)
-            if invoice:
-                if 'unpay' in request.form:
-                    self.unpay(request, invoice)
-                elif 'pay' in request.form:
-                    self.pay(request, invoice)
-                return redirect('')
-        try:
-            offset = int(request.form.get('offset', 0))
-        except ValueError:
-            offset = 0
-        r = TemplateIO(html=True)
-        r += htmltext('<table id="invoice-listing" borderspacing="0">')
-        r += htmltext('<thead>')
-        r += htmltext('<tr>')
-        r += htmltext('<td><a href="?sort_by=date&offset=%d">Creation</a></td>') % offset
-        r += htmltext('<td>Amount</td>')
-        r += htmltext('<td><a href="?sort_by=paid_date&offset=%d">Paid</a></td>') % offset
-        r += htmltext('<td><a href="?sort_by=username&offset=%d">User</a></td>') % offset
-        r += htmltext('<td>Titre</td>')
-        r += htmltext('<td></td>')
-        r += htmltext('</tr>')
-        r += htmltext('</thead>')
-        invoices = self.get_invoices()
-        for invoice in invoices[offset:offset+self.PAGINATION]:
-            r += htmltext('<tbody class="invoice-rows">')
-            r += htmltext('<tr class="invoice-row"><td>')
-            r += misc.localstrftime(invoice.date)
-            r += htmltext('</td><td class="amount">')
-            r += self.monetary_amount(invoice.amount)
-            r += htmltext('</td><td>')
-            if invoice.paid:
-                r += misc.localstrftime(invoice.paid_date)
-            else:
-                r += ''
-            r += htmltext('</td><td>')
-            user = invoice.get_user()
-            if user:
-                r += user.name
-            r += htmltext('</td><td class="subject">%s</td>') % (invoice.subject or '')
-            r += htmltext('<td>')
-            r += htmltext('<form method="post">')
-            r += htmltext('<input type="hidden" name="id" value="%s"/> ') % invoice.id
-            if invoice.paid:
-                r += htmltext('<input type="submit" name="unpay" value="%s"/>') % _('Set unpaid')
-            else:
-                r += htmltext('<input type="submit" name="pay" value="%s"/>') % _('Set paid')
-            r += htmltext('</form>')
-
-            r += htmltext('</td></tr>')
-            transactions = Transaction.get_with_indexed_value('invoice_ids',
-                    invoice.id)
-            for transaction in sorted(transactions, key=lambda x: x.start):
-                r += htmltext('<tr>')
-                r += htmltext('<td></td>')
-                r += htmltext('<td colspan="5">')
-                r += 'OrderID: %s' % transaction.order_id
-                r += ' Start: %s' % transaction.start
-                if transaction.end:
-                    r += ' End: %s' % transaction.end
-                if transaction.bank_data:
-                    r += ' Bank data: %r' % transaction.bank_data
-                r += htmltext('</td>')
-                r += htmltext('</tr>')
-            r += htmltext('</tbody>')
-        r += htmltext('</tbody></table>')
-        if offset != 0:
-            r += htmltext('<a href="?offset=%d>%s</a> ') % (
-                 max(0, offset-self.PAGINATION), _('Previous'))
-        if offset + self.PAGINATION < len(invoices):
-            r += htmltext('<a href="?offset=%d>%s</a> ') % (
-                 max(0, offset-self.PAGINATION), _('Previous'))
-        return r.getvalue()
-
-
-class RegiesDirectory(Directory):
-    _q_exports = ['', 'new']
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append(('regie/', _('Regies')))
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        return redirect('..')
-
-    def new(self):
-        regie_ui = RegieDirectory(Regie())
-
-        form = regie_ui.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            regie_ui.submit(form)
-            return redirect('%s/' % regie_ui.regie.id)
-
-        get_response().breadcrumb.append(('new', _('New Regie')))
-        html_top('payments', title = _('New Regie'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Regie')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            regie = Regie.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(regie.id), regie.label))
-        return RegieDirectory(regie)
-
-
-class PaymentsDirectory(AccessControlled, Directory):
-    _q_exports = ['', 'regie']
-    label = N_('Payments')
-
-    regie = RegiesDirectory()
-
-    def is_accessible(self, user):
-        from .backoffice import check_visibility
-        return check_visibility('payments', user)
-
-    def _q_access(self):
-        user = get_request().user
-        if not user:
-            raise errors.AccessUnauthorizedError()
-
-        if not self.is_accessible(user):
-            raise errors.AccessForbiddenError(
-                    public_msg = _('You are not allowed to access Payments Management'),
-                    location_hint = 'backoffice')
-
-        get_response().breadcrumb.append(('payments/', _('Payments')))
-
-    def _q_index(self):
-        html_top('payments', _('Payments'))
-        get_response().filter['sidebar'] = self.get_sidebar()
-        r = TemplateIO(html=True)
-
-        if not is_payment_supported:
-            r += htmltext('<p class="infonotice">')
-            r += _('Payment is not supported.')
-            r += htmltext('</p>')
-
-        regies = Regie.select()
-        r += htmltext('<h2>%s</h2>') % _('Regies')
-        if not regies:
-            r += htmltext('<p>')
-            r += _('There are no regies defined at the moment.')
-            r += htmltext('</p>')
-        r += htmltext('<ul class="biglist" id="regies-list">')
-        for l in regies:
-            regie_id = l.id
-            r += htmltext('<li class="biglistitem" id="itemId_%s">') % regie_id
-            r += htmltext('<strong class="label"><a href="regie/%s/">%s</a></strong>') % (regie_id, l.label)
-            r += htmltext('</li>')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul id="sidebar-actions">')
-        r += htmltext('  <li><a class="new-item" href="regie/new">%s</a></li>') % _('New Regie')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-
-TextsDirectory.register('aq-invoice',
-        N_('Message on top of an invoice'),
-        category = N_('Invoices'))
-
-EmailsDirectory.register('payment-new-invoice-email',
-        N_('New invoice'),
-        N_('Available variables: user, regie, invoice, invoice_url'),
-        category = N_('Invoices'),
-        default_subject = N_('New invoice'),
-        default_body = N_('''
-A new invoice is available at [invoice_url].
-'''))
-
-EmailsDirectory.register('payment-invoice-paid-email',
-        N_('Paid invoice'),
-        N_('Available variables: user, regie, invoice, invoice_url'),
-        category = N_('Invoices'),
-        default_subject = N_('Paid invoice'),
-        default_body = N_('''
-The invoice [invoice_url] has been paid.
-'''))
-
-EmailsDirectory.register('payment-invoice-canceled-email',
-        N_('Canceled invoice'),
-        N_('Available variables: user, regie, invoice, invoice_url'),
-        category = N_('Invoices'),
-        default_subject = N_('Canceled invoice'),
-        default_body = N_('''
-The invoice [invoice.id] has been canceled.
-'''))
diff --git a/extra/modules/pyatom/__init__.py b/extra/modules/pyatom/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/extra/modules/pyatom/afl-2.1.txt b/extra/modules/pyatom/afl-2.1.txt
deleted file mode 100644
index 7c180c0..0000000
--- a/extra/modules/pyatom/afl-2.1.txt
+++ /dev/null
@@ -1,54 +0,0 @@
-# Larry Rosen has ceased to use or recommend any version
-# of the Academic Free License below version 2.1
-
-The Academic Free License
-v. 2.1
-
-This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following notice immediately following the copyright notice for the Original Work:
-
-Licensed under the Academic Free License version 2.1
-
-1) Grant of Copyright License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license to do the following:
-
-a) to reproduce the Original Work in copies;
-
-b) to prepare derivative works ("Derivative Works") based upon the Original Work;
-
-c) to distribute copies of the Original Work and Derivative Works to the public;
-
-d) to perform the Original Work publicly; and
-
-e) to display the Original Work publicly.
-
-2) Grant of Patent License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, to make, use, sell and offer for sale the Original Work and Derivative Works.
-
-3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor hereby agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work, and by publishing the address of that information repository in a notice immediately following the copyright notice that applies to the Original Work.
-
-4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior written permission of the Licensor. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor except as expressly stated herein. No patent license is granted to make, use, sell or offer to sell embodiments of any patent claims other than the licensed claims defined in Section 2. No right is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any Original Work that Licensor otherwise would have a right to license.
-
-5) This section intentionally omitted.
-
-6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
-
-7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately proceeding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to Original Work is granted hereunder except under this disclaimer.
-
-8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to any person for any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to liability for death or personal injury resulting from Licensor's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.
-
-9) Acceptance and Termination. If You distribute copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. Nothing else but this License (or another written agreement between Licensor and You) grants You permission to create Derivative Works based upon the Original Work or to exercise any of the rights granted in Section 1 herein, and any attempt to do so except under the terms of this License (or another written agreement between Licensor and You) is expressly prohibited by U.S. copyright law, the equivalent laws of other countries, and by international treaty. Therefore, by exercising any of the rights granted to You in Section 1 herein, You indicate Your acceptance of this License and all of its terms and conditions.
-
-10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
-
-11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et seq., the equivalent laws of other countries, and international treaty. This section shall survive the termination of this License.
-
-12) Attorneys Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
-
-13) Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
-
-14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
-
-This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. Permission is hereby granted to copy and distribute this license without modification. This license may not be modified without the express written permission of its copyright owner.
-
- 
-
diff --git a/extra/modules/pyatom/pyatom.py b/extra/modules/pyatom/pyatom.py
deleted file mode 100644
index 9a5eceb..0000000
--- a/extra/modules/pyatom/pyatom.py
+++ /dev/null
@@ -1,1734 +0,0 @@
-# pyatom.py -- PyAtom library module
-
-"""
-PyAtom
-
-Module to make it really easy to create Atom syndication feeds.
-
-This module is Copyright (C) 2006 Steve R. Hastings.
-Licensed under the Academic Free License version 2.1
-
-You might want to start with the test cases at the end; see how they
-work, and then go back and look at the code in the module.
-
-I hope you find this useful!
-
-Steve R. Hastings
-
-Please send your questions or comments to this email address:
-
-pyatom@langri.com
-"""
-
-
-
-import re
-import sys
-import time
-
-s_pyatom_name = "PyAtom"
-s_pyatom_ver = "0.3.9"
-s_pyatom_name_ver = "%s version %s" % (s_pyatom_name, s_pyatom_ver)
-
-# string constants
-# These string values are used in more than one place.
-
-s_version = "version"
-s_encoding = "encoding"
-s_standalone = "standalone"
-
-s_href = "href"
-s_lang = "xml:lang"
-s_link = "link"
-s_term = "term"
-s_type = "type"
-
-
-
-def set_s_indent(s):
-	"""
-	Set up the globals PyAtom uses to indent its output:
-	s_indent, and s_indent_big
-
-	s_indent is the string to indent one level; default is \\t.
-
-	s_indent_big is s_indent concatenated many times.  PyAtom uses slice
-	copies to get indent strings from s_indent_big.
-	"""
-	global s_indent
-	global s_indent_big
-	s_indent = s
-	s_indent_big = s*256
-
-set_s_indent("\t")
-
-
-
-class TFC(object):
-	"""
-	class TFC: Tag Format Control.
-	Controls how tags are converted to strings.
-
-	Arguments to __init__():
-		level	Specifies what indent level to start at for output.  Default 0.
-		mode	Specifies how to format the output:
-				mode_terse -- minimal output (no indenting)
-				mode_normal -- default
-				mode_verbose -- normal, plus some XML comments
-
-	Normally, if an XML item has no data, nothing is printed, but with
-	mode_verbose you may get a comment like "Collection with 0 entries".
-
-	Methods:
-		b_print_all()
-			Return True if TFC set for full printing.
-		b_print_terse()
-			Return True if TFC set for terse printing.
-		b_print_verbose()
-			Return True if TFC set for verbose printing.
-
-		indent_by(incr)
-			Return a TFC instance that indents by incr columns.
-		s_indent(extra_indent=0)
-			Return an indent string.
-	"""
-	mode_terse, mode_normal, mode_verbose = range(3)
-
-	def __init__(self, level=0, mode=mode_normal):
-		"""
-Arguments:
-	level	Specifies what indent level to start at for output.  Default 0.
-	mode	Specifies how to format the output:
-			mode_terse -- minimal output (no indenting)
-			mode_normal -- default
-			mode_verbose -- normal, plus some XML comments
-
-Normally, if an XML item has no data, nothing is printed, but with
-mode_verbose you may get a comment like "Collection with 0 entries".
-		"""
-		self.level = level
-		self.mode = mode
-
-	def b_print_all(self):
-		"""
-		Return True if TFC set for full printing.
-
-		Some optional things are usually suppressed, but will be printed
-		if the current level is 0.  And everything gets printed when
-		mode_verbose is set.
-		"""
-		return self.level == 0 or self.mode == TFC.mode_verbose
-
-	def b_print_terse(self):
-		"""
-		Return True if TFC set for terse printing.
-		"""
-		return self.mode == TFC.mode_terse
-
-	def b_print_verbose(self):
-		"""
-		Return True if TFC set for verbose printing.
-		"""
-		return self.mode == TFC.mode_verbose
-
-	def indent_by(self, incr):
-		"""
-		Return a TFC instance that indents by incr columns.
-
-		Pass this to a function that takes a TFC to get a temporary indent.
-		"""
-		return TFC(self.level + incr, self.mode)
-	def s_indent(self, extra_indent=0):
-		"""
-		Return an indent string.
-
-		Return a string of white space that indents correctly for the 
-		current TFC settings.  If specified, extra_indent will be added
-		to the current indent level.
-		"""
-		if self.mode == TFC.mode_terse:
-			return ""
-		level = self.level + extra_indent
-		return s_indent_big[0:level]
-
-
-
-pat_nbsp = re.compile(r'&nbsp;')
-def s_entities_to_ws(s):
-	"""
-	Return a copy of s with HTML whitespace entities replaced by a space.
-
-	Currently just gets rid of HTML non-breaking spaces ("&nbsp;").
-	"""
-	if not s:
-		return s
-
-	s = re.sub(pat_nbsp, " ", s)
-	return s
-
-def s_normalize_ws(s):
-	"""
-	Return a copy of string s with each run of whitespace replaced by one space.
-	>>> s = "and    now\n\n\nfor \t  something\v   completely\r\n  different"
-	>>> print s_normalize_ws(s)
-	and now for something completely different
-	>>>
-	"""
-	lst = s.split()
-	s = " ".join(lst)
-	return s
-	
-
-def s_escape_html(s):
-	"""
-	Return a copy of string s with HTML codes escaped.
-
-	This is useful when you want HTML tags printed literally, rather than
-	interpreted.
-
-	>>> print s_escape_html("<head>")
-	&lt;head&gt;
-	>>> print s_escape_html("&nbsp;")
-	&amp;nbsp;
-	"""
-	s = s.replace("&", "&amp;")
-	s = s.replace("<", "&lt;")
-	s = s.replace(">", "&gt;")
-	return s
-
-def s_create_atom_id(t, domain_name, uri=""):
-	"""
-	Create ID using Mark Pilgrim's algorithm.
-
-	Algorithm taken from here:
-	http://diveintomark.org/archives/2004/05/28/howto-atom-id
-	"""
-
-	# ymd (year-month-day) example: 2003-12-13
-	ymd = time.strftime("%Y-%m-%d", t)
-
-	if uri == "":
-		# mush (all mushed together) example: 20031213083000
-		mush = time.strftime("%Y%m%d%H%M%S", t)
-		uri = "/weblog/" + mush
-
-	# s = "tag:" + domain_name + "," + ymd + ":" + uri
-	s = "tag:%s,%s:%s" % (domain_name, ymd, uri)
-
-	s = s.replace("#", "/")
-
-	return s
-
-s_copyright_multiyear = "Copyright %s %d-%d by %s."
-s_copyright_oneyear = "Copyright %s %d by %s."
-def s_copyright(s_owner, s_csym="(C)", end_year=None, start_year=None):
-	"""
-	Return a string with a copyright notice.
-
-	s_owner
-		string with copyright owner's name.
-	s_csym
-		string with copyright symbol. (An HTML entity might be good here.)
-	end_year
-		last year of the copyright.  Default is the current year.
-	start_year
-		first year of the copyright.
-
-	If only end_year is specified, only print one year; if both end_year and
-	start_year are specified, print a range.
-
-	To localize the entire copyright message into another language, change
-	the global variables with the copyright template:
-		s_copyright_multiyear: for a year range
-		s_copyright_oneyear: for a single year
-	"""
-	if not end_year:
-		end_year = time.localtime().tm_year
-
-	if start_year:
-		return s_copyright_multiyear % (s_csym, start_year, end_year, s_owner)
-
-	return s_copyright_oneyear % (s_csym, end_year, s_owner)
-
-
-
-# Here are all of the possible XML items.
-#
-# Supported by PyAtom:
-# XML Declaration: <?xml ... ?>
-# Comments: <!-- ... -->
-# Elements: <tag_name>...</tag_name>
-#
-# Minimal support:
-# Markup Declarations: <!KEYWORD ... >
-# Processing Instructions (PIs): <?KEYWORD ... ?>
-#
-# Not currently supported:
-# INCLUDE and IGNORE directives: <!KEYWORD[ ... ]]>
-# CDATA sections: <![CDATA[ ... ]]>
-#
-
-class XMLItem(object):
-	"""
-	All PyAtom classes inherit from this class.  All it does is provide a
-	few default methods, and be a root for the inheritance tree.
-
-	An XMLItem has several methods that return an XML tag representation of
-	its contents.  Each XMLItem knows how to make a tag for itself.  An
-	XMLItem that contains other XMLItems will ask each one to make a tag;
-	so asking the top-level XMLItem for a tag will cause the entire tree
-	of XMLItems to recursively make tags, and you get a full XML
-	representation with tags appropriately nested and indented.
-	"""
-	def _s_tag(self, tfc):
-		"""
-		A stub which must always be overridden by child classes.
-		"""
-		assert False, "XMLItem instance is too abstract to print."
-
-	def s_tag(self, level):
-		"""
-		Return the item as a string containing an XML tag declaration.
-
-		The XML tag will be indented.
-		Will return an empty string if the item is empty.
-		"""
-		tfc = TFC(level, TFC.mode_normal)
-		return self._s_tag(tfc)
-
-	def s_tag_verbose(self, level):
-		"""
-		Return the item as a string containing an XML tag declaration.
-
-		The XML tag will be indented.
-		May return an XML Comment if the item is empty.
-		"""
-		tfc = TFC(level, TFC.mode_verbose)
-		return self._s_tag(tfc)
-
-	def s_tag_terse(self, level):
-		"""
-		Return the item as a string containing an XML tag declaration.
-
-		The XML tag will not be indented.
-		Will return an empty string if the item is empty.
-		"""
-		tfc = TFC(level, TFC.mode_terse)
-		return self._s_tag(tfc)
-
-	def __str__(self):
-		return self.s_tag(0)
-
-	def level(self):
-		"""
-		Return an integer describing what level this tag is.
-
-		The root tag of an XML document is level 0; document-level comments
-		or other document-level declarations are also level 0.  Tags nested
-		inside the root tag are level 1, tags nested inside those tags are
-		level 2, and so on.
-
-		This is currently only used by the s_tree() functions.  When
-		printing tags normally, the code that walks the tree keeps track of
-		what level is current.
-		"""
-		level = 0
-		while self._parent != None:
-			self = self._parent
-			if self.is_element():
-				level += 1
-		return level
-
-	def s_name(self):
-		"""
-		Return a name for the current item.
-
-		Used only by the s_tree() functions.
-		"""
-		if self._name:
-			return self._name
-		return "unnamed_instance_of_" + type(self).__name__
-
-	def s_tree(self):
-		"""
-		Return a verbose tree showing the current tag and its children.
-
-		This is for debugging; it's not valid XML syntax.
-		"""
-		level = self.level()
-		return "%2d) %s\t%s" % (level, self.s_name(), str(self))
-
-
-
-class DocItem(XMLItem):
-	"""
-	A document-level XML item (appearing above root element).
-
-	Items that can be document-level inherit from this class.
-	"""
-	pass
-
-
-
-class ElementItem(XMLItem):
-	"""
-	An item that may be nested inside an element.
-
-	Items that can be nested inside other elements inherit from this class.
-	"""
-	pass
-
-
-
-class Comment(DocItem,ElementItem):
-	"""
-	An XML comment.
-
-	Attributes:
-		text
-			set the text of the comment
-	"""
-	def __init__(self, text=""):
-		"""
-		text: set the text of the comment
-		"""
-		self._parent = None
-		self._name = ""
-		self.tag_name = "comment"
-		self.text = text
-
-	def _s_tag(self, tfc):
-		if not self:
-			if tfc.b_print_all():
-				return tfc.s_indent() + "<!-- -->"
-			else:
-				return ""
-		else:
-			if self.text.find("\n") >= 0:
-				lst = []
-				lst.append(tfc.s_indent() + "<!--")
-				lst.append(self.text)
-				lst.append(tfc.s_indent() + "-->")
-				return "\n".join(lst)
-			else:
-				s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.text, " -->")
-				return s
-
-		assert False, "not possible to reach this line."
-
-	def __nonzero__(self):
-		# Returns True if there is any comment text.
-		# Returns False otherwise.
-		return not not self.text
-
-	def is_element(self):
-		return True
-
-
-
-# REVIEW: can a PI be an ElementItem?
-class PI(DocItem):
-	"""
-	XML Processing Instruction (PI).
-
-	Attributes:
-		keyword
-		text
-	"""
-	def __init__(self):
-		self._parent = None
-		self._name = ""
-		self.keyword = ""
-		self.text = ""
-
-	def _s_tag(self, tfc):
-		if not self:
-			return ""
-		else:
-			if self.text.find("\n") >= 0:
-				lst = []
-				lst.append("%s%s%s" % (tfc.s_indent(), "<?", self.keyword))
-				lst.append(self.text)
-				lst.append("%s%s" % (tfc.s_indent(), "?>"))
-				return "\n".join(lst)
-			else:
-				s = "%s%s%s %s%s"% \
-						(tfc.s_indent(), "<?", self.keyword, self.text, "?>")
-				return s
-
-		assert False, "not possible to reach this line."
-
-	def __nonzero__(self):
-		# Returns True if there is any keyword.
-		# Returns False otherwise.
-		return not not self.keyword
-
-
-
-# REVIEW: can a MarkupDecl be an ElementItem?
-class MarkupDecl(DocItem):
-	"""
-	XML Markup Declaration.
-
-	Attributes:
-		keyword
-		text
-	"""
-	def __init__(self):
-		self._parent = None
-		self._name = ""
-		self.keyword = ""
-		self.text = ""
-
-	def _s_tag(self, tfc):
-		if not self:
-			return ""
-		else:
-			if self.text.find("\n") >= 0:
-				lst = []
-				lst.append("%s%s%s" % (tfc.s_indent(), "<!", self.keyword))
-				lst.append(self.text)
-				lst.append("%s%s" % (tfc.s_indent(), ">"))
-				return "\n".join(lst)
-			else:
-				s = "%s%s%s %s%s" % \
-						(tfc.s_indent(), "<!", self.keyword, self.text, ">")
-				return s
-
-		assert False, "not possible to reach this line."
-
-	def __nonzero__(self):
-		# Returns True if there is any keyword.
-		# Returns False otherwise.
-		return not not self.keyword
-
-
-
-class CoreElement(ElementItem):
-	"""
-	This is an abstract class.
-
-	All of the XML element classes inherit from this.
-	"""
-	def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []):
-		# dictionary of attributes and their values
-		self.lock = False
-		self._parent = None
-		self._name = ""
-		self.tag_name = tag_name
-		self.def_attr = def_attr
-		self.attrs = {}
-		if def_attr and def_attr_value:
-			self.attrs[def_attr] = def_attr_value
-		self.attr_names = attr_names
-		self.lock = True
-
-	def __nonzero__(self):
-		# Returns True if any attrs are set or there are any contents.
-		# Returns False otherwise.
-		return not not self.attrs or self.has_contents()
-
-	def text_check(self):
-		"""
-		Raise an exception, unless element has text contents.
-
-		Child classes that have text must override this to do nothing.
-		"""
-		raise TypeError, "element does not have text contents"
-
-	def nest_check(self):
-		"""
-		Raise an exception, unless element can nest other elements.
-
-		Child classes that can nest must override this to do nothing.
-		"""
-		raise TypeError, "element cannot nest other elements"
-
-	def __delattr__(self, name):
-		# REVIEW: this should be made to work!
-		raise TypeError, "cannot delete elements"
-
-	def __getattr__(self, name):
-		if name == "lock":
-			# If the "lock" hasn't been created yet, we always want it
-			# to be False, i.e. we are not locked.
-			return False
-		else:
-			raise AttributeError, name
-
-	def __setattr__(self, name, value):
-		# Here's how this works:
-		#
-		# 0) "self.lock" is a boolean, set to False during __init__()
-		# but turned True afterwards.  When it's False, you can add new
-		# members to the class instance without any sort of checks; once
-		# it's set True, __setattr__() starts checking assignments.
-		# By default, when lock is True, you cannot add a new member to
-		# the class instance, and any assignment to an old member has to
-		# be of matching type.  So if you say "a.text = string", the
-		# .text member has to exist and be a string member.
-		#
-		# This is the default __setattr__() for all element types.  It
-		# gets overloaded by the __setattr__() in NestElement, because
-		# for nested elments, it makes sense to be able to add new
-		# elements nested inside.
-		#
-		# This is moderately nice.  But later in NestElement there is a
-		# version of __setattr__() that is *very* nice; check it out.
-		#
-		# 1) This checks assignments to _parent, and makes sure they are
-		# plausible (either an XMLItem, or None).
-
-		try:
-			lock = self.lock
-		except AttributeError:
-			lock = False
-
-		if not lock:
-			self.__dict__[name] = value
-			return
-
-		dict = self.__dict__
-		if not name in dict:
-			# brand-new item
-			if lock:
-				raise TypeError, "element cannot nest other elements"
-
-		if name == "_parent":
-			if not (isinstance(value, XMLItem) or value is None):
-				raise TypeError, "only XMLItem or None is permitted"
-			self.__dict__[name] = value
-			return
-
-		# locked item so do checks
-		if not type(self.__dict__[name]) is type(value):
-			raise TypeError, "value is not the same type"
-
-		self.__dict__[name] = value
-		
-
-	def has_contents(self):
-		return False
-
-	def multiline_contents(self):
-		return False
-
-	def s_contents(self, tfc):
-		assert False, "CoreElement is an abstract class; it has no contents."
-
-	def _s_start_tag_name_attrs(self, tfc):
-		"""
-		Return a string with the start tag name, and any attributes.
-
-		Wrap this in correct punctuation to get a start tag.
-		"""
-		def attr_newline(tfc):
-			if tfc.b_print_terse():
-				return " "
-			else:
-				return "\n" + tfc.s_indent(2)
-
-		lst = []
-		lst.append(self.tag_name)
-
-		if len(self.attrs) == 1:
-			# just one attr so do on one line
-			attr = self.attrs.keys()[0]
-			s_attr = '%s="%s"' % (attr, self.attrs[attr])
-			lst.append(" " + s_attr)
-		elif len(self.attrs) > 1:
-			# more than one attr so do a nice nested tag
-			# 0) show all attrs in the order of attr_names
-			for attr in self.attr_names:
-				if attr in self.attrs.keys():
-					s_attr = '%s="%s"' % (attr, self.attrs[attr])
-					lst.append(attr_newline(tfc) + s_attr)
-			# 1) any attrs not in attr_names?  list them, too
-			for attr in self.attrs:
-				if not attr in self.attr_names:
-					s_attr = '%s="%s"' % (attr, self.attrs[attr])
-					lst.append(attr_newline(tfc) + s_attr)
-
-		return "".join(lst)
-
-	def _s_tag(self, tfc):
-		if not self:
-			if not tfc.b_print_all():
-				return ""
-
-		lst = []
-
-		lst.append(tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc))
-
-		if not self.has_contents():
-			lst.append("/>")
-		else:
-			lst.append(">")
-			if self.multiline_contents():
-				s = "\n%s\n" % self.s_contents(tfc.indent_by(1))
-				lst.append(s + tfc.s_indent())
-			else:
-				lst.append(self.s_contents(tfc))
-			lst.append("</" + self.tag_name + ">")
-
-		return "".join(lst)
-
-	def s_start_tag(self, tfc):
-		return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + ">"
-
-	def s_end_tag(self):
-		return "</" + self.tag_name + ">"
-
-	def s_compact_tag(self, tfc):
-		return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + "/>"
-
-	def is_element(self):
-		return True
-
-
-
-class TextElement(CoreElement):
-	"""
-	An element that cannot have other elements nested inside it.
-
-	Attributes:
-		attr
-		text
-	"""
-	def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []):
-		CoreElement.__init__(self, tag_name, def_attr, def_attr_value,
-				attr_names)
-		self.lock = False
-		self.text = ""
-		self.lock = True
-
-	def text_check(self):
-		pass
-
-	def has_contents(self):
-		return not not self.text
-
-	def multiline_contents(self):
-		return self.text.find("\n") >= 0
-
-	def s_contents(self, tfc):
-		return self.text
-
-
-
-class Nest(ElementItem):
-	"""
-	A data structure that can store Elements, nested inside it.
-
-	Note: this is not, itself, an Element!  Because it is not an XML
-	element, it has no tags.  Its string representation is the
-	representations of the elements nested inside it.
-
-	NestElement and XMLDoc inherit from this.
-	"""
-	def __init__(self):
-		self.lock = False
-		self._parent = None
-		self._name = ""
-		self.elements = []
-		self.lock = True
-	def __len__(self):
-		return len(self.elements)
-	def __getitem__(self, key):
-		return self.elements[key]
-	def __setitem__(self, key, value):
-		self.elements[key] = value
-	def __delitem__(self, key):
-		del(self.elements[key])
-
-	def _do_setattr(self, name, value):
-		if isinstance(value, XMLItem):
-			value._parent = self
-			value._name = name
-			self.elements.append(value)
-		self.__dict__[name] = value
-
-	def __setattr__(self, name, value):
-		# Lots of magic here!  This is important stuff.  Here's how it works:
-		#
-		# 0) self.lock is a boolean, set to False initially and then set
-		# to True at the end of __init__().  When it's False, you can add new
-		# members to the class instance without any sort of checks; once
-		# it's set True, __setattr__() starts checking assignments.  By
-		# default, when lock is True, any assignment to an old member
-		# has to be of matching type.  You can add a new member to the
-		# class instance, but __setattr__() checks to ensure that the
-		# new member is an XMLItem.
-		#
-		# 1) Whether self.lock is set or not, if the value is an XMLitem,
-		# then this will properly add the XMLItem into the tree
-		# structure.  The XMLItem will have _parent set to the parent,
-		# will have _name set to its name in the parent, and will be
-		# added to the parent's elements list.  This is handled by
-		# _do_setattr().
-		#
-		# 2) As a convenience for the user, if the user is assigning a
-		# string, and self is an XMLItem that has a .text value, this
-		# will assign the string to the .text value.  This allows usages
-		# like "e.title = string", which is very nice.  Before I added
-		# this, I frequently wrote that instead of "e.title.text =
-		# string" so I wanted it to just work.  Likewise the user can
-		# assign a time value directly into Timestamp elements.
-		#
-		# 3) This checks assignments to _parent, and makes sure they are
-		# plausible (either an XMLItem, or None).
-
-		try:
-			lock = self.lock
-		except AttributeError:
-			lock = False
-
-		if not lock:
-			self._do_setattr(name, value)
-			return
-
-		dict = self.__dict__
-		if not name in dict:
-			# brand-new item
-			if lock:
-				self.nest_check()
-				if not isinstance(value, XMLItem):
-					raise TypeError, "only XMLItem is permitted"
-			self._do_setattr(name, value)
-			return
-
-		if name == "_parent" or name == "root_element":
-			if not (isinstance(value, XMLItem) or value is None):
-				raise TypeError, "only XMLItem or None is permitted"
-			self.__dict__[name] = value
-			return
-
-		if name == "_name" and type(value) == type(""):
-			self.__dict__[name] = value
-			return
-
-		# for Timestamp elements, allow this:  element = time
-		# (where "time" is a float value, since uses float for times)
-		# Also allow valid timestamp strings.
-		if isinstance(self.__dict__[name], Timestamp):
-			if type(value) == type(1.0):
-				self.__dict__[name].time = value
-				return
-			elif type(value) == type(""):
-				t = utc_time_from_s_timestamp(value)
-				if t:
-					self.__dict__[name].time = t
-				else:
-					raise ValueError, "value must be a valid timestamp string"
-				return
-
-		# Allow string assignment to go to the .text attribute, for
-		# elements that allow it.  All TextElements allow it;
-		# Elements will allow it if they do not nave nested elements.
-		# text_check() raises an error if it's not allowed.
-		if isinstance(self.__dict__[name], CoreElement) and \
-				type(value) == type(""):
-			self.__dict__[name].text_check()
-			self.__dict__[name].text = value
-			return
-
-		# locked item so do checks
-		if not type(self.__dict__[name]) is type(value):
-			raise TypeError, "value is not the same type"
-
-		self.__dict__[name] = value
-		
-	def __delattr__(self, name):
-		# This won't be used often, if ever, but if anyone tries it, it
-		# should work.
-		if isinstance(self.name, XMLItem):
-			o = self.__dict__[name]
-			self.elements.remove(o)
-			del(self.__dict__[name])
-		else:
-			# REVIEW: what error should this raise?
-			raise TypeError, "cannot delete that item"
-
-	def nest_check(self):
-		pass
-
-	def is_element(self):
-		# a Nest is not really an element
-		return False
-
-	def has_contents(self):
-		for element in self.elements:
-			if element:
-				return True
-		# empty iff all of the elements were empty
-		return False
-
-	def __nonzero__(self):
-		return self.has_contents()
-
-	def multiline_contents(self):
-		# if there are any contents, we want multiline for nested tags
-		return self.has_contents()
-
-	def s_contents(self, tfc):
-		if len(self.elements) > 0:
-			# if any nested elements exist, we show those
-			lst = []
-
-			for element in self.elements:
-				s = element._s_tag(tfc)
-				if s:
-					lst.append(s)
-
-			return "\n".join(lst)
-		else:
-			return ""
-
-		assert False, "not possible to reach this line."
-		return ""
-
-	def s_tree(self):
-		level = self.level()
-		tup = (level, self.s_name(), self.__class__.__name__)
-		s = "%2d) %s (instance of %s)" % tup
-		lst = []
-		lst.append(s)
-		for element in self.elements:
-			s = element.s_tree()
-			lst.append(s)
-		return "\n".join(lst)
-
-	def _s_tag(self, tfc):
-		return self.s_contents(tfc)
-
-
-
-
-class NestElement(Nest,CoreElement):
-	"""
-	An element that can have other elements nested inside it.
-
-	Attributes:
-		attr
-		elements: a list of other elements nested inside this one.
-	"""
-	def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]):
-		CoreElement.__init__(self, tag_name, def_attr, def_attr_value,
-				attr_names)
-		self.lock = False
-		self.elements = []
-		self.lock = True
-
-	def is_element(self):
-		return True
-
-	def __nonzero__(self):
-		return CoreElement.__nonzero__(self)
-
-	def _s_tag(self, tfc):
-		return CoreElement._s_tag(self, tfc)
-
-
-
-class Element(NestElement,TextElement):
-	"""
-	A class to represent an arbitrary XML tag.  Can either have other XML
-	elements nested inside it, or else can have a text string value, but
-	never both at the same time.
-
-	This is intended for user-defined XML tags.  The user can just use
-	"Element" for all custom tags.
-
-	PyAtom doesn't use this; PyAtom uses TextElement for tags with a text
-	string value, and NestElement for tags that nest other elements.  Users
-	can do the same, or can just use Element, as they like.
-
-	Attributes:
-		attr
-		elements:	a list of other elements nested inside, if any
-		text:	a text string value, if any
-
-	Note: if text is set, elements will be empty, and vice-versa.  If you
-	have elements nested inside and try to set the .text, this will raise
-	an exception, and vice-versa.
-	"""
-	# A Element can have other elements nested inside it, or it can have
-	# a single ".text" string value.  But never both at the same time.
-	# Once you nest another element, you can no longer use the .text.
-	def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]):
-		NestElement.__init__(self, tag_name, def_attr, def_attr_value,
-				attr_names)
-		self.lock = False
-		self.text = ""
-		self.lock = True
-
-	def nest_check(self):
-		if self.text:
-			raise TypeError, "Element has text contents so cannot nest"
-
-	def text_check(self):
-		if len(self.elements) > 0:
-			raise TypeError, "Element has nested elements so cannot assign text"
-
-	def has_contents(self):
-		return NestElement.has_contents(self) or TextElement.has_contents(self)
-
-	def multiline_contents(self):
-		return NestElement.has_contents(self) or self.text.find("\n") >= 0
-
-	def s_contents(self, tfc):
-		if len(self.elements) > 0:
-			return NestElement.s_contents(self, tfc)
-		elif self.text:
-			return TextElement.s_contents(self, tfc)
-		else:
-			return ""
-		assert False, "not possible to reach this line."
-
-	def s_tree(self):
-		lst = []
-		if len(self.elements) > 0:
-			level = self.level()
-			tup = (level, self.s_name(), self.__class__.__name__)
-			s = "%2d) %s (instance of %s)" % tup
-			lst.append(s)
-			for element in self.elements:
-				s = element.s_tree()
-				lst.append(s)
-			return "\n".join(lst)
-		elif self.text:
-			return XMLItem.s_tree(self)
-		else:
-			level = self.level()
-			tfc = TFC(level)
-			s = "%2d) %s %s" % (level, self.s_name(), "empty Element...")
-			return s
-		assert False, "not possible to reach this line."
-
-
-
-class Collection(XMLItem):
-	"""
-	A Collection contains 0 or more Elements, but isn't an XML element.
-	Use where a run of 0 or more Elements of the same type is legal.
-
-	When you init your Collection, you specify what class of Element it will
-	contain.  Attempts to append an Element of a different class will raise
-	an exception.  Note, however, that the various Element classes all
-	inherit from base classes, and you can specify a class from higher up in
-	the inheritance tree.  You could, if you wanted, make a Collection
-	containing "XMLItem" and then any item defined in PyAtom would be legal
-	in that collection.  (See XMLDoc, which contains two collections of
-	DocItem.)
-
-	Attributes:
-		contains:	the class of element this Collection will contain
-		elements:	a list of other elements nested inside, if any
-
-	Note: The string representation of a Collection is just the string
-	representations of the elements inside it.  However, a verbose string
-	reprentation may have an XML comment like this:
-
-	<!-- Collection of <class> with <n> elements -->
-
-	where <n> is the number of elements in the Collection and <class> is the
-	name of the class in this Collection.
-	"""
-	def __init__(self, element_class):
-		self.lock = False
-		self._parent = None
-		self._name = ""
-		self.elements = []
-		self.contains = element_class
-		self.lock = True
-	def __len__(self):
-		return len(self.elements)
-	def __getitem__(self, key):
-		return self.elements[key]
-	def __setitem__(self, key, value):
-		if not isinstance(value, self.contains):
-			raise TypeError, "object is the wrong type for this collection"
-		self.elements[key] = value
-	def __delitem__(self, key):
-		del(self.elements[key])
-
-	def __nonzero__(self):
-		# there are no attrs so if any element is nonzero, collection is too
-		for element in self.elements:
-			if element:
-				return True
-		return False
-
-	def is_element(self):
-		# A Collection is not really an Element
-		return False
-
-	def s_coll(self):
-		name = self.contains.__name__
-		n = len(self.elements)
-		if n == 1:
-			el = "element"
-		else:
-			el = "elements"
-		return "collection of %s with %d %s" % (name, n, el)
-
-	def append(self, element):
-		if not isinstance(element, self.contains):
-			print >> sys.stderr, "Error: attempted to insert", \
-					type(element).__name__, \
-					"into collection of", self.contains.__name__
-			raise TypeError, "object is the wrong type for this collection"
-		element._parent = self
-		self.elements.append(element)
-
-	def _s_tag(self, tfc):
-		# A collection exists only as a place to put real elements.
-		# There are no start or end tags...
-		# When tfc.b_print_all() is true, we do put an XML comment.
-
-		if not self.elements:
-			if not tfc.b_print_all():
-				return ""
-
-		lst = []
-
-		if tfc.b_print_verbose():
-			s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.s_coll(), " -->")
-			lst.append(s)
-			tfc = tfc.indent_by(1)
-
-		for element in self.elements:
-			s = element._s_tag(tfc)
-			if s:
-				lst.append(s)
-
-		return "\n".join(lst)
-
-	def s_tree(self):
-		level = self.level()
-		s = "%2d) %s %s" % (level, self.s_name(), self.s_coll())
-		lst = []
-		lst.append(s)
-		for element in self.elements:
-			s = element.s_tree()
-			lst.append(s)
-		return "\n".join(lst)
-
-
-
-class XMLDeclaration(XMLItem):
-	# REVIEW: should this print multi-line for multiple attrs?
-	def __init__(self):
-		self._parent = None
-		self._name = ""
-		self.attrs = {}
-		self.attrs[s_version] = "1.0"
-		self.attrs[s_encoding] = "utf-8"
-		self.attr_names = [s_version, s_encoding, s_standalone]
-
-	def _s_tag(self, tfc):
-		# An XMLDeclaration() instance is never empty, so always prints.
-
-		lst = []
-		s = "%s%s" % (tfc.s_indent(), "<?xml")
-		lst.append(s)
-		# 0) show all attrs in the order of attr_names
-		for attr in self.attr_names:
-			if attr in self.attrs.keys():
-				s_attr = ' %s="%s"' % (attr, self.attrs[attr])
-				lst.append(s_attr)
-		# 1) any attrs not in attr_names?  list them, too
-		for attr in self.attrs:
-			if not attr in self.attr_names:
-				s_attr = ' %s="%s"' % (attr, self.attrs[attr])
-				lst.append(s_attr)
-		lst.append("?>")
-
-		return "".join(lst)
-
-	def __nonzero__(self):
-		# Returns True because the XML Declaration is never empty.
-		return True
-
-	def is_element(self):
-		return True
-
-
-
-class XMLDoc(Nest):
-	"""
-	A data structure to represent an XML Document.  It will have the
-	following structure:
-
-	the XML Declaration item
-	0 or more document-level XML items
-	exactly one XML item (the "root tag")
-	0 or more document-level XML items
-
-	document level XML items are: Comment, PI, MarkupDecl
-
-
-	Attributes:
-		xml_decl:	the XMLDeclaration item
-		docitems_above:	a collection of DocItem (items above root_element)
-		root_element:	the XML tag containing your data
-		docitems_below:	a collection of DocItem (items below root_element)
-
-	Note: usually the root_element has lots of other XML items nested inside
-	it!
-	"""
-	def __init__(self, root_element=None):
-		Nest.__init__(self)
-
-		self._name = "XMLDoc"
-
-		self.xml_decl = XMLDeclaration()
-		self.docitems_above = Collection(DocItem)
-
-		if not root_element:
-			root_element = Comment("no root element yet")
-		self.root_element = root_element
-
-		self.docitems_below = Collection(DocItem)
-
-	def __setattr__(self, name, value):
-		# root_element may always be set to any ElementItem
-		if name == "root_element":
-			if not (isinstance(value, ElementItem)):
-				raise TypeError, "only ElementItem is permitted"
-
-			self.lock = False
-			# Item checks out, so assign it.  root_element should only
-			# ever have one element, and we always put the new element
-			# in the same slot in elements[].
-			if "i_root_element" in self.__dict__:
-				# Assign new root_element over old one in elements[]
-				assert self.elements[self.i_root_element] == self.root_element
-				self.elements[self.i_root_element] = value
-			else:
-				# This is the first time root_element was ever set.
-				self.i_root_element = len(self.elements)
-				self.elements.append(value)
-
-			value._parent = self
-			value._name = name
-			self.__dict__[name] = value
-			self.lock = True
-		else:
-			# for all other, fall through to inherited behavior
-			Nest.__setattr__(self, name, value)
-
-	def Validate(self):
-		# XMLDoc never has parent.  Never change this!
-		assert self._parent == None
-		return True
-
-
-
-def local_time_from_utc_time(t):
-	return t - time.timezone
-
-def utc_time_from_local_time(t):
-	return t + time.timezone
-
-def local_time():
-	return time.time() - time.timezone
-
-def utc_time():
-	return time.time()
-
-
-class TimeSeq(object):
-	"""
-	A class to generate a sequence of timestamps.
-
-	Atom feed validators complain if multiple timestamps have the same
-	value, so this provides a convenient way to set a bunch of timestamps
-	all at least one second different from each other.
-	"""
-	def __init__(self, init_time=0):
-		if init_time == 0:
-			self.time = local_time()
-		else:
-			self.time = float(init_time)
-	def next(self):
-		t = self.time
-		self.time += 1.0
-		return t
-
-format_RFC3339 = "%Y-%m-%dT%H:%M:%S"
-
-def parse_time_offset(s):
-	s = s.lstrip().rstrip()
-
-	if (s == '' or s == 'Z' or s == 'z'):
-		return 0
-
-	m = pat_time_offset.search(s)
-	sign = m.group(1)
-	offset_hour = int(m.group(2))
-	offset_min = int(m.group(3))
-	offset = offset_hour * 3600 + offset_min * 60
-	if sign == "-":
-		offset *= -1
-	return offset
-
-def s_timestamp(utc_time, time_offset="Z"):
-	"""
-	Format a time and offset into a string.
-
-	utc_time
-		a floating-point value, time in the UTC time zone
-	s_time_offset
-		a string specifying an offset from UTC.  Examples:
-			z or Z -- offset is 0 ("Zulu" time, UTC, aka GMT)
-			-08:00 -- 8 hours earlier than UTC (Pacific time zone)
-			"" -- empty string is technically not legal, but may work
-
-	Notes:
-		Returned string complies with RFC3339; uses ISO8601 date format.
-		Example: 2003-12-13T18:30:02Z
-		Example: 2003-12-13T18:30:02+02:00
-	"""
-
-	if not utc_time:
-		return ""
-
-	utc_time += parse_time_offset(time_offset)
-
-	try:
-		s = time.strftime(format_RFC3339, time.localtime(utc_time))
-	except:
-		return ""
-
-	return s + time_offset
-
-
-
-pat_RFC3339 = re.compile("(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(.*)")
-pat_time_offset = re.compile("([+-])(\d\d):(\d\d)")
-
-def utc_time_from_s_timestamp(s_date_time_stamp):
-	# parse RFC3339-compatible times that use ISO8601 date format
-	# date time stamp example: 2003-12-13T18:30:02Z
-	# date time stamp example: 2003-12-13T18:30:02+02:00
-	# leaving off the suffix is technically not legal, but allowed
-
-	s_date_time_stamp = s_date_time_stamp.lstrip().rstrip()
-
-	try:
-		m = pat_RFC3339.search(s_date_time_stamp)
-		year = int(m.group(1))
-		mon = int(m.group(2))
-		mday = int(m.group(3))
-		hour = int(m.group(4))
-		min = int(m.group(5))
-		sec = int(m.group(6))
-		tup = (year, mon, mday, hour, min, sec, -1, -1, -1)
-		t = time.mktime(tup)
-
-		s = m.group(7)
-		t += parse_time_offset(s)
-
-		return t
-
-	except:
-		return 0.0
-
-	assert False, "impossible to reach this line"
-
-
-def s_time_offset():
-	"""
-	Return a string with local offset from UTC in RFC3339 format.
-	"""
-
-	# If t is set to local time in seconds since the epoch, then...
-	# ...offset is the value you add to t to get UTC.  This is the
-	# reverse of time.timezone.
-
-	offset = -(time.timezone)
-
-	if offset > 0:
-		sign = "+"
-	else:
-		sign = "-"
-		offset = abs(offset)
-
-	offset_hour = offset // (60 * 60)
-	offset_min = (offset // 60) % 60
-	return "%s%02d:%02d" % (sign, offset_hour, offset_min)
-
-s_offset_local = s_time_offset()
-
-s_offset_default = s_offset_local
-
-def set_default_time_offset(s):
-	global s_offset_default
-	s_offset_default = s
-
-
-class Timestamp(CoreElement):
-	def __init__(self, tag_name, time=0.0):
-		CoreElement.__init__(self, tag_name, None, None)
-		self.lock = False
-		self.time = time
-		self.time_offset = s_offset_default
-		self.lock = True
-
-	def __delattr__(self, name):
-		CoreElement.__delattr_(self, name)
-
-	def __getattr__(self, name):
-		if name == "text":
-			return s_timestamp(self.time, self.time_offset)
-		return CoreElement.__getattr_(self, name)
-
-	def __setattr__(self, name, value):
-		if name == "text":
-			if type(value) != type(""):
-				raise TypeError, "can only assign a string to .text"
-			t = utc_time_from_s_timestamp(value)
-			if t:
-				self.time = utc_time_from_s_timestamp(value)
-			else:
-				raise ValueError, "value must be a valid timestamp string"
-			return
-		CoreElement.__setattr__(self, name, value)
-
-	def has_contents(self):
-		return self.time != 0
-
-	def multiline_contents(self):
-		return False
-
-	def s_contents(self, tfc):
-		return s_timestamp(self.time, self.time_offset)
-
-	def update(self):
-		self.time = local_time()
-		return self
-
-
-
-
-# Below are all the classes to implement Atom using the above tools.
-
-
-
-class AtomText(TextElement):
-	def __init__(self, tag_name):
-		attr_names = [ s_type ]
-		# legal values of type: "text", "html", "xhtml"
-		TextElement.__init__(self, tag_name, None, None, attr_names)
-
-class Title(AtomText):
-	def __init__(self, text=""):
-		AtomText.__init__(self, "title")
-		self.text = text
-		
-class Subtitle(AtomText):
-	def __init__(self, text=""):
-		AtomText.__init__(self, "subtitle")
-		self.text = text
-		
-class Content(AtomText):
-	def __init__(self, text=""):
-		AtomText.__init__(self, "content")
-		self.text = text
-		
-class Summary(AtomText):
-	def __init__(self, text=""):
-		AtomText.__init__(self, "summary")
-		self.text = text
-		
-class Rights(AtomText):
-	def __init__(self, text=""):
-		AtomText.__init__(self, "rights")
-		self.text = text
-		
-class Id(TextElement):
-	def __init__(self, text=""):
-		TextElement.__init__(self, "id", None, None)
-		self.text = text
-		
-class Generator(TextElement):
-	def __init__(self):
-		attr_names = [ "uri", "version" ]
-		TextElement.__init__(self, "generator", None, None, attr_names)
-		
-class Category(TextElement):
-	def __init__(self, term_val=""):
-		attr_names = [s_term, "scheme", "label"]
-		TextElement.__init__(self, "category", s_term, term_val, attr_names)
-
-class Link(TextElement):
-	def __init__(self, href_val=""):
-		attr_names = [
-				s_href, "rel", "type", "hreflang", "title", "length", s_lang]
-		TextElement.__init__(self, "link", s_href, href_val, attr_names)
-
-class Icon(TextElement):
-	def __init__(self):
-		TextElement.__init__(self, "icon", None, None)
-
-class Logo(TextElement):
-	def __init__(self):
-		TextElement.__init__(self, "logo", None, None)
-
-class Name(TextElement):
-	def __init__(self, text=""):
-		TextElement.__init__(self, "name", None, None)
-		self.text = text
-
-class Email(TextElement):
-	def __init__(self):
-		TextElement.__init__(self, "email", None, None)
-
-class Uri(TextElement):
-	def __init__(self):
-		TextElement.__init__(self, "uri", None, None)
-
-
-
-class BasicAuthor(NestElement):
-	def __init__(self, tag_name, name):
-		NestElement.__init__(self, tag_name, None, None)
-		self.name = Name(name)
-		self.email = Email()
-		self.uri = Uri()
-
-class Author(BasicAuthor):
-	def __init__(self, name=""):
-		BasicAuthor.__init__(self, "author", name)
-
-class Contributor(BasicAuthor):
-	def __init__(self, name=""):
-		BasicAuthor.__init__(self, "contributor", name)
-
-
-
-class Updated(Timestamp):
-	def __init__(self, time=0.0):
-		Timestamp.__init__(self, "updated", time)
-
-class Published(Timestamp):
-	def __init__(self, time=0.0):
-		Timestamp.__init__(self, "published", time)
-
-
-
-class FeedElement(NestElement):
-	def __init__(self, tag_name):
-		NestElement.__init__(self, tag_name, None, None)
-
-		self.title = Title("")
-		self.id = Id("")
-		self.updated = Updated()
-		self.authors = Collection(Author)
-		self.links = Collection(Link)
-
-		self.subtitle = Subtitle("")
-		self.categories = Collection(Category)
-		self.contributors = Collection(Contributor)
-		self.generator = Generator()
-		self.icon = Icon()
-		self.logo = Logo()
-		self.rights = Rights("")
-
-class Feed(FeedElement):
-	def __init__(self):
-		FeedElement.__init__(self, "feed")
-		self.attrs["xmlns"] = "http://www.w3.org/2005/Atom"
-		self.title.text = "Title of Feed Goes Here"
-		self.id.text = "ID of Feed Goes Here"
-		self.entries = Collection(Entry)
-
-class Source(FeedElement):
-	def __init__(self):
-		FeedElement.__init__(self, "source")
-
-
-
-class Entry(NestElement):
-	def __init__(self):
-		NestElement.__init__(self, "entry", None, None)
-		self.title = Title("Title of Entry Goes Here")
-		self.id = Id("ID of Entry Goes Here")
-		self.updated = Updated()
-		self.authors = Collection(Author)
-		self.links = Collection(Link)
-
-		self.content = Content("")
-		self.summary = Summary("")
-		self.categories = Collection(Category)
-		self.contributors = Collection(Contributor)
-		self.published = Published()
-		self.source = Source()
-		self.rights = Rights("")
-
-
-
-def diff(s0, name0, s1, name1):
-	from difflib import ndiff
-	lst0 = s0.split("\n")
-	lst1 = s1.split("\n")
-	report = '\n'.join(ndiff(lst0, lst1))
-	return report
-
-
-def run_test_cases():
-
-	# The default is to make time stamps in local time with appropriate
-	# offset; for our tests, we want a default "Z" offset instead.
-	set_default_time_offset("Z")
-
-	failed_tests = 0
-
-
-	# Test: convert current time into a timestamp string and back
-
-	now = local_time()
-	# timestamp format does not allow fractional seconds
-	now = float(int(now))	# truncate any fractional seconds
-	s = s_timestamp(now)
-	t = utc_time_from_s_timestamp(s)
-	if now != t:
-		failed_tests += 1
-		print "test case failed:"
-		print now, "-- original timestamp"
-		print t, "-- converted timestamp does not match"
-
-
-	# Test: convert a timestamp string to a time value and back
-
-	s_time = "2003-12-13T18:30:02Z"
-	t = utc_time_from_s_timestamp(s_time)
-	s = s_timestamp(t)
-	if s_time != s:
-		failed_tests += 1
-		print "test case failed:"
-		print s_time, "-- original timestamp"
-		print s, "-- converted timestamp does not match"
-
-
-	# Test: generate the "Atom-Powered Robots Run Amok" example
-	#
-	# Note: the original had some of the XML declarations in
-	# a different order than PyAtom puts them.  I swapped around
-	# the lines here so they would match the PyAtom order.  Other
-	# than that, this is the example from:
-	#
-	# http://www.atomenabled.org/developers/syndication/#sampleFeed
-
-	s_example = """\
-<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-	<title>Example Feed</title>
-	<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
-	<updated>2003-12-13T18:30:02Z</updated>
-	<author>
-		<name>John Doe</name>
-	</author>
-	<link href="http://example.org/"/>
-	<entry>
-		<title>Atom-Powered Robots Run Amok</title>
-		<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
-		<updated>2003-12-13T18:30:02Z</updated>
-		<link href="http://example.org/2003/12/13/atom03"/>
-		<summary>Some text.</summary>
-	</entry>
-</feed>"""
-
-	xmldoc = XMLDoc()
-	
-	feed = Feed()
-	xmldoc.root_element = feed
-
-	feed.title = "Example Feed"
-	feed.id = "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
-	feed.updated = "2003-12-13T18:30:02Z"
-
-	link = Link("http://example.org/")
-	feed.links.append(link)
-
-	author = Author("John Doe")
-	feed.authors.append(author)
-
-
-	entry = Entry()
-	feed.entries.append(entry)
-	entry.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
-	entry.title = "Atom-Powered Robots Run Amok"
-	entry.updated = "2003-12-13T18:30:02Z"
-	entry.summary = "Some text."
-
-	link = Link("http://example.org/2003/12/13/atom03")
-	entry.links.append(link)
-
-
-	s = str(xmldoc)
-	if s_example != s:
-		failed_tests += 1
-		print "test case failed:"
-		print "The generated XML doesn't match the example.  diff follows:"
-		print diff(s_example, "s_example", s, "s")
-
-
-	# Test: verify that xmldoc.Validate() succeeds
-
-	if not xmldoc.Validate():
-		failed_tests += 1
-		print "test case failed:"
-		print "xmldoc.Validate() failed."
-
-
-	# Test: does Element work both nested an non-nested?
-	s_test = """\
-<test>
-	<test:agent number="007">James Bond</test:agent>
-	<test:pet
-			nickname="Mei-Mei"
-			type="cat">Matrix</test:pet>
-</test>"""
-
-	class TestPet(Element):
-		def __init__(self, name=""):
-			Element.__init__(self, "test:pet", None, None)
-			self.text = name
-
-	class TestAgent(Element):
-		def __init__(self, name=""):
-			Element.__init__(self, "test:agent", None, None)
-			self.text = name
-
-	class Test(Element):
-		def __init__(self):
-			Element.__init__(self, "test", None, None)
-			self.test_agent = TestAgent()
-			self.test_pet = TestPet()
-
-	test = Test()
-	test.test_agent = "James Bond"
-	test.test_agent.attrs["number"] = "007"
-	test.test_pet = "Matrix"
-	test.test_pet.attrs["type"] = "cat"
-	test.test_pet.attrs["nickname"] = "Mei-Mei"
-
-	s = str(test)
-	if s_test != s:
-		failed_tests += 1
-		print "test case failed:"
-		print "test output doesn't match.  diff follows:"
-		print diff(s_test, "s_test", s, "s")
-
-
-	if failed_tests > 0:
-		print "self-test failed!"
-	else:
-		print "self-test successful."
-
-
-
-if __name__ == "__main__":
-	run_test_cases()
diff --git a/extra/modules/pyatom/readme.txt b/extra/modules/pyatom/readme.txt
deleted file mode 100644
index a311805..0000000
--- a/extra/modules/pyatom/readme.txt
+++ /dev/null
@@ -1,64 +0,0 @@
-PyAtom
-
-
-PyAtom is a Python library module I wrote to make it very easy to create
-an Atom syndication feed.
-
-http://atomenabled.org/developers/syndication/
-
-
-I have released PyAtom under The Academic Free License 2.1.  I intend to
-donate PyAtom to the Python Software Foundation.
-
-
-Notes on PyAtom:
-
-XML is best represented in a tree structure, and PyAtom is a set of
-classes that automatically manage the tree structure for the user.  The
-top level of an Atom feed is an XML "Feed" element with a "<feed>" tag;
-the Feed element has other elements nested inside it that describe the
-feed, and then it has 0 or more Entry elements, each of which has
-elements that describe the Entry.
-
-Take a look at RunPyAtomTestCases(), at the end of pyatom.py, for
-example code showing how to set up a feed with an entry.
-
-To create an XML document with a feed in it, the user does this:
-
-xmldoc = XMLDoc()
-feed = Feed()
-xmldoc.root_element = feed
-
-To assign an entry to a feed, the user just does this:
-
-feed.entries.append(entry)
-
-This adds "entry" to the internal list that keeps track of entries.
-"entry" is now nested inside "feed", which is nested inside "xmldoc".
-
-Later, when the user wants to save the XML in a file, the user can just
-do this:
-
-f = open("file.xml", "w")
-s = str(xmldoc)
-f.write(s)
-
-To make the string from xmldoc, the XMLDoc class walks through the XML
-elements nested inside xmldoc, asking each one to return its string.
-Each element that has other elements nested inside does the same thing.
-The whole tree is recursively walked, and the tags all return strings
-that are indented properly for their level in the tree.
-
-The classes that implement Atom in PyAtom just use the heck out of
-inheritance.  There are abstract base classes that implement broadly
-useful behavior, and lots of classes that just inherit and use this
-behavior; but there are plenty of places where the child classes
-overload the inherited behavior and do something different.  The way
-Python handles inheritance made this a joy to code up.
-
-
-
-If you have any questions about anything here, please contact me using
-this email address:
-
-pyatom@langri.com
diff --git a/extra/modules/root.py b/extra/modules/root.py
deleted file mode 100644
index da39a6d..0000000
--- a/extra/modules/root.py
+++ /dev/null
@@ -1,1312 +0,0 @@
-from quixote import get_publisher, get_response, get_request, redirect, get_session
-from quixote.directory import Directory
-from quixote.html import TemplateIO, htmltext
-
-from wcs.qommon.misc import get_variadic_url, simplify
-
-import os
-import re
-import string
-import urlparse
-
-try:
-    import lasso
-except ImportError:
-    pass
-
-import wcs
-import wcs.root
-import qommon
-from qommon import _
-from qommon import get_cfg, get_logger
-from qommon import template
-from qommon import errors
-from qommon.form import *
-from qommon import logger
-from wcs.roles import logged_users_role
-
-from qommon import emails
-from qommon.sms import SMS
-from wcs.categories import Category
-from wcs.formdef import FormDef
-from wcs.data_sources import NamedDataSource
-from qommon.tokens import Token
-from qommon.admin.emails import EmailsDirectory
-from qommon.admin.texts import TextsDirectory
-
-from links import Link
-from announces import Announce, AnnounceSubscription
-from myspace import MyspaceDirectory
-from agenda import AgendaDirectory
-from events import Event, get_default_event_tags
-from payments import PublicPaymentDirectory
-from payments_ui import InvoicesDirectory
-
-import admin
-
-import wcs.forms.root
-from wcs.workflows import Workflow
-from wcs.forms.preview import PreviewDirectory
-
-from saml2 import Saml2Directory
-
-OldRootDirectory = wcs.root.RootDirectory
-
-import qommon.ident.password
-import qommon.ident.idp
-
-
-def category_get_homepage_position(self):
-    if hasattr(self, 'homepage_position') and self.homepage_position:
-        return self.homepage_position
-    if self.url_name == 'consultations':
-        return '2nd'
-    return '1st'
-Category.get_homepage_position = category_get_homepage_position
-
-def category_get_limit(self):
-    if hasattr(self, 'limit') and self.limit is not None:
-        return self.limit
-    return 7
-Category.get_limit = category_get_limit
-
-Category.TEXT_ATTRIBUTES = ['name', 'url_name', 'description', 'homepage_position']
-Category.INT_ATTRIBUTES = ['position', 'limit']
-
-
-class FormsRootDirectory(wcs.forms.root.RootDirectory):
-
-    def _q_index(self, *args):
-        get_response().filter['is_index'] = True
-        return wcs.forms.root.RootDirectory._q_index(self, *args)
-
-    def user_forms(self, user_forms):
-        r = TemplateIO(html=True)
-        base_url = get_publisher().get_root_url()
-
-        draft = [x for x in user_forms if x.is_draft() and not x.formdef.is_disabled()]
-        if draft:
-            r += htmltext('<h4 id="drafts">%s</h4>') % _('My Current Drafts')
-            r += htmltext('<ul>')
-            for f in draft:
-                if f.formdef.category:
-                    category_url = '%s' % f.formdef.category.url_name
-                else:
-                    category_url = '.'
-                r += htmltext('<li><a href="%s%s/%s/%s">%s</a>, %s') % (base_url,
-                    category_url,
-                    f.formdef.url_name, f.id, f.formdef.name,
-                    misc.localstrftime(f.receipt_time))
-                r += htmltext(' (<a href="%s%s/%s/%s?remove-draft">%s</a>)') % (base_url,
-                    category_url,
-                    f.formdef.url_name, f.id, _('delete'))
-                r += htmltext('</li>')
-            r += htmltext('</ul>')
-
-        forms_by_status_name = {}
-        for f in user_forms:
-            if f.is_draft():
-                continue
-            status = f.get_visible_status()
-            if status:
-                status_name = status.name
-            else:
-                status_name = None
-            if status_name in forms_by_status_name:
-                forms_by_status_name[status_name].append(f)
-            else:
-                forms_by_status_name[status_name] = [f]
-        for status_name in forms_by_status_name:
-            if status_name:
-                r += htmltext('<h4>%s</h4>') % _('My forms with status "%s"') % status_name
-            else:
-                r += htmltext('<h4>%s</h4>') % _('My forms with an unknown status') % status_name
-            r += htmltext('<ul>')
-            forms_by_status_name[status_name].sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
-            for f in forms_by_status_name[status_name]:
-                if f.formdef.category_id:
-                    category_url = f.formdef.category.url_name
-                else:
-                    category_url = '.'
-                r += htmltext('<li><a href="%s%s/%s/%s/">%s</a>, %s</li>') % (
-                        base_url,
-                        category_url,
-                        f.formdef.url_name, f.id, f.formdef.name, 
-                        misc.localstrftime(f.receipt_time))
-            r += htmltext('</ul>')
-        return r.getvalue()
-
-
-class AnnounceDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete', 'email']
-
-    def __init__(self, announce):
-        self.announce = announce
-
-    def _q_index(self):
-        template.html_top(_('Announces to citizens'))
-        r = TemplateIO(html=True)
-
-        if self.announce.publication_time:
-            date_heading = '%s - ' % time.strftime(misc.date_format(), self.announce.publication_time)
-        else:
-            date_heading = ''
-
-        r += htmltext('<h3>%s%s</h3>') % (date_heading, self.announce.title)
-
-        r += htmltext('<p>')
-        r += self.announce.text
-        r += htmltext('</p>')
-
-        r += htmltext('<p>')
-        r += htmltext('<a href="../">%s</a>') % _('Back')
-        r += htmltext('</p>')
-        return r.getvalue()
-
-
-class AnnouncesDirectory(Directory):
-    _q_exports = ['', 'subscribe', 'email', 'atom', 'sms', 'emailconfirm',
-            'email_unsubscribe', 'sms_unsubscribe', 'smsconfirm', 'rawlist']
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append(('announces/', _('Announces')))
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        template.html_top(_('Announces to citizens'))
-        r = TemplateIO(html=True)
-        r += self.announces_list()
-        r += htmltext('<ul id="announces-links">')
-        r += htmltext('<li><a href="subscribe">%s</a></li>') % _('Receiving those Announces')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def _get_announce_subscription(self):
-        """ """
-        sub = None
-        if get_request().user:
-            subs = AnnounceSubscription.select(lambda x: x.user_id == get_request().user.id)
-            if subs:
-                sub = subs[0]
-        return sub
-
-    def rawlist(self):
-        get_response().filter = None
-        return self.announces_list()
-
-    def announces_list(self):
-        announces = Announce.get_published_announces()
-        if not announces:
-            raise errors.TraversalError()
-
-        # XXX: will need pagination someday
-        r = TemplateIO(html=True)
-        for item in announces:
-            r += htmltext('<div class="announce-item">\n')
-            r += htmltext('<h4>')
-            if item.publication_time:
-                r += time.strftime(misc.date_format(), item.publication_time)
-                r += ' - '
-            r += item.title
-            r += htmltext('</h4>\n')
-            r += htmltext('<p>\n')
-            r += item.text
-            r += htmltext('\n</p>\n')
-            r += htmltext('</div>\n')
-        return r.getvalue()
-
-
-    def sms(self):
-        sms_mode = get_cfg('sms', {}).get('mode', 'none')
-
-        if sms_mode == 'none':
-            raise errors.TraversalError()
-
-        get_response().breadcrumb.append(('sms', _('SMS')))
-        template.html_top(_('Receiving announces by SMS'))
-        r = TemplateIO(html=True)
-
-        if sms_mode == 'demo':
-            r += TextsDirectory.get_html_text('aq-sms-demo')
-        else:
-            announces_cfg = get_cfg('announces',{})
-            mobile_mask = announces_cfg.get('mobile_mask')
-            if mobile_mask:
-                mobile_mask = ' (' + mobile_mask + ')'
-            else:
-                mobile_mask = ''
-            form = Form(enctype='multipart/form-data')
-            form.add(StringWidget, 'mobile', title = _('Mobile number %s') % mobile_mask, size=12, required=True)
-            form.add_submit('submit', _('Subscribe'))
-            form.add_submit('cancel', _('Cancel'))
-
-            if form.get_submit() == 'cancel':
-                return redirect('subscribe')
-
-            if form.is_submitted() and not form.has_errors():
-                s = self.sms_submit(form)
-                if s == False:
-                    r += form.render()
-                else:
-                    return redirect("smsconfirm")
-            else:
-                r += form.render()
-        return r.getvalue()
-
-    def sms_submit(self, form):
-        mobile = form.get_widget("mobile").parse()
-        # clean the string, remove any extra character
-        mobile = re.sub('[^0-9+]','',mobile)
-        # if a mask was set, validate
-        announces_cfg = get_cfg('announces',{})
-        mobile_mask = announces_cfg.get('mobile_mask')
-        if mobile_mask:
-            mobile_regexp = re.sub('X','[0-9]', mobile_mask) + '$'
-            if not re.match(mobile_regexp, mobile):
-                form.set_error("mobile", _("Phone number invalid ! It must match ") + mobile_mask)
-                return False
-        if mobile.startswith('00'):
-            mobile = '+' + mobile[2:]
-        else:
-            # Default to france international prefix
-            if not mobile.startswith('+'):
-                mobile = re.sub("^0", "+33", mobile)
-        sub = self._get_announce_subscription()
-        if not sub:
-            sub = AnnounceSubscription()
-        if get_request().user:
-            sub.user_id = get_request().user.id
-
-        if mobile:
-            sub.sms = mobile
-
-        if not get_request().user:
-            sub.enabled = False
-
-        sub.store()
-
-        # Asking sms confirmation
-        token = Token(3 * 86400, 4, string.digits)
-        token.type = 'announces-subscription-confirmation'
-        token.subscription_id = sub.id
-        token.store()
-
-        message = _("Confirmation code : %s") % str(token.id)
-        sms_cfg = get_cfg('sms', {})
-        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
-        mode = sms_cfg.get('mode', 'none')
-        sms = SMS.get_sms_class(mode)
-        try:
-            sms.send(sender, [mobile], message)
-        except errors.SMSError, e:
-            get_logger().error(e)
-            form.set_error("mobile", _("Send SMS confirmation failed"))
-            sub.remove("sms")
-            return False
-
-    def smsconfirm(self):
-        template.html_top(_('Receiving announces by SMS confirmation'))
-        r = TemplateIO(html=True)
-        r += htmltext("<p>%s</p>") % _("You will receive a confirmation code by SMS.")
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'code', title = _('Confirmation code (4 characters)'), size=12, required=True)
-        form.add_submit('submit', _('Subscribe'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-
-        if form.is_submitted() and not form.has_errors():
-            token = None
-            id = form.get_widget("code").parse()
-            try:
-                token = Token.get(id)
-            except KeyError:
-                form.set_error("code",  _('Invalid confirmation code.'))
-            else:
-                if token.type != 'announces-subscription-confirmation':
-                    form.set_error("code",  _('Invalid confirmation code.'))
-                else:
-                    sub = AnnounceSubscription.get(token.subscription_id)
-                    token.remove_self()
-                    sub.enabled_sms = True
-                    sub.store()
-                    return redirect('.')
-            r += form.render()
-        else:
-            r += form.render()
-
-        return r.getvalue()
-
-    def sms_unsubscribe(self):
-        sub = self._get_announce_subscription()
-
-        form = Form(enctype='multipart/form-data')
-        if not sub:
-            return redirect('..')
-
-        form.add_submit('submit', _('Unsubscribe'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-
-        get_response().breadcrumb.append(('sms', _('SMS Unsubscription')))
-        template.html_top()
-        r = TemplateIO(html=True)
-
-        if form.is_submitted() and not form.has_errors():
-            if sub:
-                sub.remove("sms")
-
-            root_url = get_publisher().get_root_url()
-            r += htmltext('<p>')
-            r += _('You have been unsubscribed from announces')
-            r += htmltext('</p>')
-            if not get_response().iframe_mode:
-                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
-        else:
-            r += htmltext('<p>')
-            r += _('Do you want to stop receiving announces by sms ?')
-            r += htmltext('</p>')
-            r += form.render()
-
-        return r.getvalue()
-
-
-    def subscribe(self):
-        get_response().breadcrumb.append(('subscribe', _('Subscription')))
-        template.html_top(_('Receiving Announces'))
-        r = TemplateIO(html=True)
-
-        r += TextsDirectory.get_html_text('aq-announces-subscription')
-
-        sub = self._get_announce_subscription()
-
-        r += htmltext('<ul id="announce-modes">')
-        if sub and sub.email:
-            r += htmltext(' <li>')
-            r += htmltext('<span id="par_mail">%s</span>') % _('Email (currently subscribed)')
-            r += htmltext(' <a href="email_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
-        else:
-            r += htmltext(' <li><a href="email" id="par_mail" rel="popup">%s</a></li>') % _('Email')
-        if sub and sub.sms:
-            r += htmltext(' <li>')
-            if sub.enabled_sms:
-                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently subscribed)') % sub.sms
-            else:
-                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently not confirmed)') % sub.sms
-                r += htmltext(' <a href="smsconfirm" rel="popup">%s</a> ') % _('Confirmation')
-            r += htmltext(' <a href="sms_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
-        elif get_cfg('sms', {}).get('mode', 'none') != 'none':
-            r += htmltext(' <li><a href="sms" id="par_sms">%s</a></li>') % _('SMS')
-        r += htmltext(' <li><a class="feed-link" href="atom" id="par_rss">%s</a>') % _('Feed')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def email(self):
-        get_response().breadcrumb.append(('email', _('Email Subscription')))
-        template.html_top(_('Receiving Announces by email'))
-        r = TemplateIO(html=True)
-
-        form = Form(enctype='multipart/form-data')
-        if get_request().user:
-            if get_request().user.email:
-                r += htmltext('<p>')
-                r += _('You are logged in and your email is %s, ok to subscribe ?') % \
-                        get_request().user.email
-                r += htmltext('</p>')
-                form.add_submit('submit', _('Subscribe'))
-            else:
-                r += htmltext('<p>')
-                r += _("You are logged in but there is no email address in your profile.")
-                r += htmltext('</p>')
-                form.add(EmailWidget, 'email', title = _('Email'), required = True)
-                form.add_submit('submit', _('Subscribe'))
-                form.add_submit('submit-remember', _('Subscribe and add this email to my profile'))
-        else:
-            r += htmltext('<p>')
-            r += _('FIXME will only be used for this purpose etc.')
-            r += htmltext('</p>')
-            form.add(EmailWidget, 'email', title = _('Email'), required = True)
-            form.add_submit('submit', _('Subscribe'))
-
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('subscribe')
-
-        if form.is_submitted() and not form.has_errors():
-            s = self.email_submit(form)
-            if s is not False:
-                return s
-        else:
-            r += form.render()
-
-        return r.getvalue()
-
-    def email_submit(self, form):
-        sub = self._get_announce_subscription()
-        if not sub:
-            sub = AnnounceSubscription()
-
-        if get_request().user:
-            sub.user_id = get_request().user.id
-
-        if form.get_widget('email'):
-            sub.email = form.get_widget('email').parse()
-        elif get_request().user.email:
-            sub.email = get_request().user.email
-
-        if not get_request().user:
-            sub.enabled = False
-
-        sub.store()
-
-        if get_request().user:
-            r = TemplateIO(html=True)
-            root_url = get_publisher().get_root_url()
-            r += htmltext('<p>')
-            r += _('You have been subscribed to the announces.')
-            r += htmltext('</p>')
-            if not get_response().iframe_mode:
-                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
-            return r.getvalue()
-
-        # asking email confirmation before subscribing someone
-        token = Token(3 * 86400)
-        token.type = 'announces-subscription-confirmation'
-        token.subscription_id = sub.id
-        token.store()
-        data = {
-            'confirm_url': get_request().get_url() + 'confirm?t=%s&a=cfm' % token.id,
-            'cancel_url': get_request().get_url() + 'confirm?t=%s&a=cxl' % token.id,
-            'time': misc.localstrftime(time.localtime(token.expiration)),
-        }
-
-        emails.custom_ezt_email('announces-subscription-confirmation',
-                data, sub.email, exclude_current_user = False)
-
-        r = TemplateIO(html=True)
-        root_url = get_publisher().get_root_url()
-        r += htmltext('<p>')
-        r += _('You have been sent an email for confirmation')
-        r += htmltext('</p>')
-        if not get_response().iframe_mode:
-            r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
-        return r.getvalue()
-
-    def emailconfirm(self):
-        tokenv = get_request().form.get('t')
-        action = get_request().form.get('a')
-
-        root_url = get_publisher().get_root_url()
-
-        try:
-            token = Token.get(tokenv)
-        except KeyError:
-            return template.error_page(
-                    _('The token you submitted does not exist, has expired, or has been cancelled.'),
-                    continue_to = (root_url, _('home page')))
-
-        if token.type != 'announces-subscription-confirmation':
-            return template.error_page(
-                    _('The token you submitted is not appropriate for the requested task.'),
-                    continue_to = (root_url, _('home page')))
-
-        sub = AnnounceSubscription.get(token.subscription_id)
-
-        if action == 'cxl':
-            r = TemplateIO(html=True)
-            root_url = get_publisher().get_root_url()
-            template.html_top(_('Email Subscription'))
-            r += htmltext('<h1>%s</h1>') % _('Request Cancelled')
-            r += htmltext('<p>%s</p>') % _('The request for subscription has been cancelled.')
-            r += htmltext('<p>')
-            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
-            r += htmltext('</p>')
-            token.remove_self()
-            sub.remove_self()
-            return r.getvalue()
-
-        if action == 'cfm':
-            token.remove_self()
-            sub.enabled = True
-            sub.store()
-            r = TemplateIO(html=True)
-            root_url = get_publisher().get_root_url()
-            template.html_top(_('Email Subscription'))
-            r += htmltext('<h1>%s</h1>') % _('Subscription Confirmation')
-            r += htmltext('<p>%s</p>') % _('Your subscription to announces is now effective.')
-            r += htmltext('<p>')
-            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
-            r += htmltext('</p>')
-            return r.getvalue()
-
-    def atom(self):
-        response = get_response()
-        response.set_content_type('application/atom+xml')
-
-        from pyatom import pyatom
-        xmldoc = pyatom.XMLDoc()
-        feed = pyatom.Feed()
-        xmldoc.root_element = feed
-        feed.title = get_cfg('misc', {}).get('sitename') or 'Publik'
-        feed.id = get_request().get_url()
-
-        author_email = get_cfg('emails', {}).get('reply_to')
-        if not author_email:
-            author_email = get_cfg('emails', {}).get('from')
-        if author_email:
-            feed.authors.append(pyatom.Author(author_email))
-
-        announces = Announce.get_published_announces()
-
-        if announces and announces[0].modification_time:
-            feed.updated = misc.format_time(announces[0].modification_time,
-                        '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
-                        gmtime = True)
-        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
-
-        for item in announces:
-            entry = item.get_atom_entry()
-            if entry:
-                feed.entries.append(entry)
-
-        return str(feed)
-
-    def email_unsubscribe(self):
-        sub = self._get_announce_subscription()
-
-        form = Form(enctype='multipart/form-data')
-        if not sub:
-            form.add(EmailWidget, 'email', title = _('Email'), required = True)
-
-        form.add_submit('submit', _('Unsubscribe'))
-        form.add_submit('cancel', _('Cancel'))
-
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-
-        get_response().breadcrumb.append(('email', _('Email Unsubscription')))
-        template.html_top()
-        r = TemplateIO(html=True)
-
-        if form.is_submitted() and not form.has_errors():
-            if sub:
-                sub.remove("email")
-            else:
-                email = form.get_widget('email').parse()
-                for s in AnnounceSubscription.select():
-                    if s.email == email:
-                        s.remove("email")
-
-            root_url = get_publisher().get_root_url()
-            r += htmltext('<p>')
-            r += _('You have been unsubscribed from announces')
-            r += htmltext('</p>')
-            if not get_response().iframe_mode:
-                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
-
-        else:
-            r += htmltext('<p>')
-            r += _('Do you want to stop receiving announces by email?')
-            r += htmltext('</p>')
-            r += form.render()
-
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            announce = Announce.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-
-        if announce.hidden:
-            raise errors.TraversalError()
-
-        get_response().breadcrumb.append((str(announce.id), announce.title))
-        return AnnounceDirectory(announce)
-
-OldRegisterDirectory = wcs.root.RegisterDirectory
-
-class AlternateRegisterDirectory(OldRegisterDirectory):
-    def _q_traverse(self, path):
-        get_response().filter['bigdiv'] = 'new_member'
-        return OldRegisterDirectory._q_traverse(self, path)
-
-    def _q_index(self):
-        get_logger().info('register')
-        ident_methods = get_cfg('identification', {}).get('methods', [])
-
-        if len(ident_methods) == 0:
-            idps = get_cfg('idp', {})
-            if len(idps) == 0:
-                return template.error_page(_('Authentication subsystem is not yet configured.'))
-            ident_methods = ['idp'] # fallback to old behaviour; saml.
-
-        if len(ident_methods) == 1:
-            method = ident_methods[0]
-        else:
-            method = 'password'
-
-        return qommon.ident.register(method)
-
-OldLoginDirectory = wcs.root.LoginDirectory
-
-class AlternateLoginDirectory(OldLoginDirectory):
-    def _q_traverse(self, path):
-        get_response().filter['bigdiv'] = 'member'
-        return OldLoginDirectory._q_traverse(self, path)
-
-    def _q_index(self):
-        get_logger().info('login')
-        ident_methods = get_cfg('identification', {}).get('methods', [])
-
-        if get_request().form.get('ReturnUrl'):
-            get_request().form['next'] = get_request().form.pop('ReturnUrl')
-
-        if 'IsPassive' in get_request().form and 'idp' in ident_methods:
-            # if isPassive is given in query parameters, we restrict ourselves
-            # to saml login.
-            ident_methods = ['idp']
-
-        if len(ident_methods) > 1 and 'idp' in ident_methods:
-            # if there is more than one identification method, and there is a
-            # possibility of SSO, if we got there as a consequence of an access
-            # unauthorized url on admin/ or backoffice/, then idp auth method
-            # is chosen forcefully.
-            after_url = get_request().form.get('next')
-            if after_url:
-                root_url = get_publisher().get_root_url()
-                after_path = urlparse.urlparse(after_url)[2]
-                after_path = after_path[len(root_url):]
-                if after_path.startswith(str('admin')) or \
-                        after_path.startswith(str('backoffice')):
-                    ident_methods = ['idp']
-
-        # don't display authentication system choice
-        if len(ident_methods) == 1:
-            method = ident_methods[0]
-            try:
-                return qommon.ident.login(method)
-            except KeyError:
-                get_logger().error('failed to login with method %s' % method)
-                return errors.TraversalError()
-
-        if sorted(ident_methods) == ['idp', 'password']:
-            r = TemplateIO(html=True)
-            get_response().breadcrumb.append(('login', _('Login')))
-            identities_cfg = get_cfg('identities', {})
-            form = Form(enctype = 'multipart/form-data', id = 'login-form', use_tokens = False)
-            if identities_cfg.get('email-as-username', False):
-                form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
-            else:
-                form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
-            form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
-            form.add_submit('submit', _('Connect'))
-            if form.is_submitted() and not form.has_errors():
-                tmp = qommon.ident.password.MethodDirectory().login_submit(form)
-                if not form.has_errors():
-                    return tmp
-
-            r += htmltext('<div id="login-password">')
-            r += get_session().display_message()
-            r += form.render()
-
-            base_url = get_publisher().get_root_url()
-            r += htmltext('<p><a href="%sident/password/forgotten">%s</a></p>') % (
-                    base_url, _('Forgotten password ?'))
-
-            r += htmltext('</div>')
-
-            # XXX: this part only supports a single IdP
-            r += htmltext('<div id="login-sso">')
-            r += TextsDirectory.get_html_text('aq-sso-text')
-            form = Form(enctype='multipart/form-data',
-                    action = '%sident/idp/login' % base_url)
-            form.add_hidden('method', 'idp')
-            for kidp, idp in get_cfg('idp', {}).items():
-                p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
-                        misc.get_abs_path(idp['metadata']),
-                        misc.get_abs_path(idp.get('publickey')), None)
-                form.add_hidden('idp', p.providerId)
-                break
-            form.add_submit('submit', _('Connect'))
-
-            r += form.render()
-            r += htmltext('</div>')
-
-            get_request().environ['REQUEST_METHOD'] = 'GET'
-
-            r += htmltext("""<script type="text/javascript">
-              document.getElementById('login-form')['username'].focus();
-            </script>""")
-            return r.getvalue()
-        else:
-            return OldLoginDirectory._q_index(self)
-
-
-OldIdentDirectory = wcs.root.IdentDirectory
-class AlternateIdentDirectory(OldIdentDirectory):
-    def _q_traverse(self, path):
-        get_response().filter['bigdiv'] = 'member'
-        return OldIdentDirectory._q_traverse(self, path)
-
-
-class AlternatePreviewDirectory(PreviewDirectory):
-    def _q_traverse(self, path):
-        get_response().filter['bigdiv'] = 'rub_service'
-        return super(AlternatePreviewDirectory, self)._q_traverse(path)
-
-
-class AlternateRootDirectory(OldRootDirectory):
-    _q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout',
-            'saml', 'register', 'ident', 'afterjobs',
-            ('informations-editeur', 'informations_editeur'),
-            ('announces', 'announces_dir'),
-            'accessibility', 'contact', 'help',
-            'myspace', 'services', 'agenda', 'categories', 'user',
-            ('tmp-upload', 'tmp_upload'), 'json', '__version__',
-            'themes', 'pages', 'payment', 'invoices', 'roles',
-            'api', 'code', 'fargo', 'tryauth', 'auth', 'preview',
-            ('reload-top', 'reload_top'), 'static',
-            ('i18n.js', 'i18n_js')]
-
-    admin = admin.AdminRootDirectory()
-    announces_dir = AnnouncesDirectory()
-    register = AlternateRegisterDirectory()
-    login = AlternateLoginDirectory()
-    ident = AlternateIdentDirectory()
-    myspace = MyspaceDirectory()
-    agenda = AgendaDirectory()
-    saml = Saml2Directory()
-    payment = PublicPaymentDirectory()
-    invoices = InvoicesDirectory()
-    code = wcs.forms.root.TrackingCodesDirectory()
-    preview = AlternatePreviewDirectory()
-
-    def get_substitution_variables(self):
-        d = {}
-        def print_links(fd):
-            fd.write(str(self.links()))
-        d['links'] = print_links
-        return d
-
-    def _q_traverse(self, path):
-        self.feed_substitution_parts()
-
-        # set app_label to Publik if none was specified (this is used in
-        # backoffice header top line)
-        if not get_publisher().get_site_option('app_label'):
-            if not get_publisher().site_options.has_section('options'):
-                get_publisher().site_options.add_section('options')
-            get_publisher().site_options.set('options', 'app_label', 'Publik')
-
-        response = get_response()
-        if not hasattr(response, 'filter'):
-            response.filter = {}
-
-        response.filter['auquotidien'] = True
-        response.filter['gauche'] = self.box_side(path)
-        response.filter['keywords'] = template.get_current_theme().get('keywords')
-        get_publisher().substitutions.feed(self)
-
-        response.breadcrumb = [ ('', _('Home')) ]
-
-        if not self.admin:
-            self.admin = get_publisher().admin_directory_class()
-
-        if not self.backoffice:
-            self.backoffice = get_publisher().backoffice_directory_class()
-
-        try:
-            return Directory._q_traverse(self, path)
-        except errors.TraversalError, e:
-            try:
-                f = FormDef.get_by_urlname(path[0])
-            except KeyError:
-                pass
-            else:
-                base_url = get_publisher().get_root_url()
-
-                uri_rest = get_request().environ.get('REQUEST_URI')
-                if not uri_rest:
-                    # REQUEST_URI doesn't exist when using internal HTTP server
-                    # (--http)
-                    uri_rest = get_request().get_path()
-                    if get_request().get_query():
-                        uri_rest += '?' + get_request().get_query()
-                if uri_rest.startswith(base_url):
-                    uri_rest = uri_rest[len(base_url):]
-                if f.category:
-                    return redirect('%s%s/%s' % (base_url, f.category.url_name, uri_rest))
-
-            raise e
-
-
-    def _q_lookup(self, component):
-        # is this a category ?
-        try:
-            category = Category.get_by_urlname(component)
-        except KeyError:
-            pass
-        else:
-            return FormsRootDirectory(category)
-
-        # is this a formdef ?
-        try:
-            formdef = FormDef.get_by_urlname(component)
-        except KeyError:
-            pass
-        else:
-            # if there's no category, or the request is a POST, directly call
-            # into FormsRootDirectory.
-            if formdef.category_id is None or get_request().get_method() == 'POST':
-                get_response().filter['bigdiv'] = 'rub_service'
-                return FormsRootDirectory()._q_lookup(component)
-
-            # if there is category, let it fall back to raise TraversalError,
-            # it will get caught in _q_traverse that will redirect it to an
-            # URL embedding the category
-
-        return None
-
-    def json(self):
-        return FormsRootDirectory().json()
-
-    def categories(self):
-        return FormsRootDirectory().categories()
-
-    def _q_index(self):
-        if get_request().is_json():
-            return FormsRootDirectory().json()
-
-        root_url = get_publisher().get_root_url()
-        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
-            return redirect('%smyspace/new' % root_url)
-
-        redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
-        if redirect_url:
-            return redirect(misc.get_variadic_url(redirect_url,
-                get_publisher().substitutions.get_context_variables()))
-
-        if get_response().iframe_mode:
-            # never display home page in an iframe
-            return redirect('%sservices' % root_url)
-
-        template.html_top()
-        r = TemplateIO(html=True)
-        get_response().filter['is_index'] = True
-
-        if not 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
-            t = TextsDirectory.get_html_text('aq-home-page')
-            if not t:
-                if get_request().user:
-                    t = TextsDirectory.get_html_text('welcome-logged')
-                else:
-                    t = TextsDirectory.get_html_text('welcome-unlogged')
-            if t:
-                r += htmltext('<div id="home-page-intro">')
-                r += t
-                r += htmltext('</div>')
-
-        r += htmltext('<div id="centre">')
-        r += self.box_services(position='1st')
-        r += htmltext('</div>')
-        r += htmltext('<div id="droite">')
-        r += self.myspace_snippet()
-        r += self.box_services(position='2nd')
-        r += self.consultations()
-        r += self.announces()
-        r += htmltext('</div>')
-
-        user = get_request().user
-        if user and user.can_go_in_backoffice():
-            get_response().filter['backoffice'] = True
-
-        return r.getvalue()
-
-    def services(self):
-        template.html_top()
-        get_response().filter['bigdiv'] = 'rub_service'
-        return self.box_services(level = 2)
-
-    def box_services(self, level=3, position=None):
-        ## Services
-        if get_request().user and get_request().user.roles:
-            accepted_roles = get_request().user.roles
-        else:
-            accepted_roles = []
-
-        cats = Category.select(order_by = 'name')
-        cats = [x for x in cats if x.url_name != 'consultations']
-        Category.sort_by_position(cats)
-
-        all_formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
-                order_by = 'name')
-        if get_response().page_template_key == 'mobile':
-            # if we are in 'mobile' mode, and some formdefs have a 'mobile'
-            # keyword, we limit the display to those
-            if any((x for x in all_formdefs if x.keywords and 'mobile' in x.keywords)):
-                all_formdefs = [x for x in all_formdefs if x.keywords and 'mobile' in x.keywords]
-
-        if position:
-            t = self.display_list_of_formdefs(
-                            [x for x in cats if x.get_homepage_position() == position],
-                            all_formdefs, accepted_roles)
-        else:
-            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
-
-        if not t:
-            return
-
-        r = TemplateIO(html=True)
-
-        if position == '2nd':
-            r += htmltext('<div id="services-2nd">')
-        else:
-            r += htmltext('<div id="services">')
-        if level == 2:
-            r += htmltext('<h2>%s</h2>') % _('Services')
-        else:
-            r += htmltext('<h3>%s</h3>') % _('Services')
-
-        if get_response().iframe_mode:
-            if get_request().user:
-                message = TextsDirectory.get_html_text('welcome-logged')
-            else:
-                message = TextsDirectory.get_html_text('welcome-unlogged')
-
-            if message:
-                r += htmltext('<div id="welcome-message">')
-                r += message
-                r += htmltext('</div>')
-        elif 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
-            homepage_text = TextsDirectory.get_html_text('aq-home-page')
-            if homepage_text:
-                r += htmltext('<div id="home-page-intro">')
-                r += homepage_text
-                r += htmltext('</div>')
-
-        r += htmltext('<ul>')
-        r += t
-        r += htmltext('</ul>')
-
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
-        r = TemplateIO(html=True)
-        for category in cats:
-            if category.url_name == 'consultations':
-                self.consultations_category = category
-                continue
-            formdefs = [x for x in all_formdefs if str(x.category_id) == str(category.id)]
-            formdefs_advertise = []
-
-            for formdef in formdefs[:]:
-                if formdef.is_disabled(): # is a redirection
-                    continue
-                if not formdef.roles:
-                    continue
-                if not get_request().user:
-                    if formdef.always_advertise:
-                        formdefs_advertise.append(formdef)
-                    formdefs.remove(formdef)
-                    continue
-                if logged_users_role().id in formdef.roles:
-                    continue
-                for q in accepted_roles:
-                    if q in formdef.roles:
-                        break
-                else:
-                    if formdef.always_advertise:
-                        formdefs_advertise.append(formdef)
-                    formdefs.remove(formdef)
-
-            if not formdefs and not formdefs_advertise:
-                continue
-
-            keywords = {}
-            for formdef in formdefs:
-                for keyword in formdef.keywords_list:
-                    keywords[keyword] = True
-
-            r += htmltext('<li id="category-%s" data-keywords="%s">') % (
-                    category.url_name, ' '.join(keywords))
-            r += htmltext('<strong>')
-            r += htmltext('<a href="%s/">') % category.url_name
-            r += category.name
-            r += htmltext('</a></strong>\n')
-            r += category.get_description_html_text()
-            r += htmltext('<ul>')
-            limit = category.get_limit()
-            for formdef in formdefs[:limit]:
-                r += htmltext('<li data-keywords="%s">') % ' '.join(formdef.keywords_list)
-                classes = []
-                if formdef.is_disabled() and formdef.disabled_redirection:
-                    classes.append('redirection')
-                r += htmltext('<a class="%s" href="%s/%s/">%s</a>') % (
-                        ' '.join(classes), category.url_name, formdef.url_name, formdef.name)
-                r += htmltext('</li>\n')
-            if len(formdefs) < limit:
-                for formdef in formdefs_advertise[:limit-len(formdefs)]:
-                    r += htmltext('<li class="required-authentication">')
-                    r += htmltext('<a href="%s/%s/">%s</a>') % (category.url_name, formdef.url_name, formdef.name)
-                    r += htmltext('<span> (%s)</span>') % _('authentication required')
-                    r += htmltext('</li>\n')
-            if (len(formdefs)+len(formdefs_advertise)) > limit:
-                r += htmltext('<li class="all-forms"><a href="%s/" title="%s">%s</a></li>') % (category.url_name,
-                        _('Access to all forms of the "%s" category') % category.name,
-                        _('Access to all forms in this category'))
-            r += htmltext('</ul>')
-            r += htmltext('</li>\n')
-
-        return r.getvalue()
-
-    def consultations(self):
-        cats = [x for x in Category.select() if x.url_name == 'consultations']
-        if not cats:
-            return
-        consultations_category = cats[0]
-        formdefs = FormDef.select(lambda x: (
-                    str(x.category_id) == str(consultations_category.id) and
-                        (not x.is_disabled() or x.disabled_redirection)),
-                    order_by = 'name')
-        if not formdefs:
-            return
-        ## Consultations
-        r = TemplateIO(html=True)
-        r += htmltext('<div id="consultations">')
-        r += htmltext('<h3>%s</h3>') % _('Consultations')
-        r += consultations_category.get_description_html_text()
-        r += htmltext('<ul>')
-        for formdef in formdefs:
-            r += htmltext('<li>')
-            r += htmltext('<a href="%s/%s/">%s</a>') % (consultations_category.url_name,
-                formdef.url_name, formdef.name)
-            r += htmltext('</li>')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def box_side(self, path):
-        r = TemplateIO(html=True)
-        root_url = get_publisher().get_root_url()
-
-        if self.has_anonymous_access_codes() and path == [''] and (
-                'include-tracking-code-form' in get_response().filter.get('keywords', [])):
-            r += htmltext('<form id="follow-form" action="%scode/load">') % root_url
-            r += htmltext('<h3>%s</h3>') % _('Tracking code')
-            r += htmltext('<input size="12" name="code" placeholder="%s"/>') % _('ex: RPQDFVCD')
-            r += htmltext('<input type="submit" value="%s"/>') % _('Load')
-            r += htmltext('</form>')
-
-        r += self.links()
-
-        cats = Category.select(order_by = 'name')
-        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
-        Category.sort_by_position(cats)
-        if cats:
-            r += htmltext('<div id="side-services">')
-            r += htmltext('<h3>%s</h3>') % _('Services')
-            r += htmltext('<ul>')
-            for cat in cats:
-                r += htmltext('<li><a href="%s/">%s</a></li>') % (cat.url_name, cat.name)
-            r += htmltext('</ul>')
-            r += htmltext('</div>')
-
-        if Event.keys(): # if there are events, add a link to the agenda
-            tags = get_cfg('misc', {}).get('event_tags')
-            if not tags:
-                tags = get_default_event_tags()
-            r += htmltext('<h3 id="agenda-link"><a href="%sagenda/">%s</a></h3>') % (root_url, _('Agenda'))
-
-            if path and path[0] == 'agenda':
-                r += htmltext('<p class="tags">')
-                for tag in tags:
-                    r += htmltext('<a href="%sagenda/tag/%s">%s</a> ') % (root_url, tag, tag)
-                r += htmltext('</p>')
-                r += self.agenda.display_remote_calendars()
-
-                r += htmltext('<p>')
-                r += htmltext('  <a href="%sagenda/filter">%s</a>') % (root_url, _('Advanced Filter'))
-                r += htmltext('</p>')
-
-        v = r.getvalue()
-        if v:
-            r = TemplateIO(html=True)
-            r += htmltext('<div id="sidebox">')
-            r += v
-            r += htmltext('</div>')
-            return r.getvalue()
-
-        return None
-
-    def has_anonymous_access_codes(self):
-        return any((x for x in FormDef.select() if x.enable_tracking_codes))
-
-    def links(self):
-        links = Link.select()
-        if not links:
-            return ''
-
-        Link.sort_by_position(links)
-
-        r = TemplateIO(html=True)
-
-        r += htmltext('<div id="links">')
-        if links[0].url:
-            # first link has an URL, so it's not a title, so we display a
-            # generic title
-            r += htmltext('<h3>%s</h3>') % _('Useful links')
-        has_ul = False
-        vars = get_publisher().substitutions.get_context_variables()
-        for link in links:
-            if not link.url:
-                # acting title
-                if has_ul:
-                    r += htmltext('</ul>')
-                r += htmltext('<h3>%s</h3>') % link.title
-                r += htmltext('<ul>')
-                has_ul = True
-            else:
-                if not has_ul:
-                    r += htmltext('<ul>')
-                    has_ul = True
-                r += htmltext('<li class="link-%s"><a href="%s">%s</a></li>') % (
-                        simplify(link.title), get_variadic_url(link.url, vars), link.title)
-        if has_ul:
-            r += htmltext('</ul>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def announces(self):
-        announces = Announce.get_published_announces()
-        if not announces:
-            return
-
-        r = TemplateIO(html=True)
-        r += htmltext('<div id="announces">')
-        r += htmltext('<h3>%s</h3>') % _('Announces to citizens')
-        for item in announces[:3]:
-            r += htmltext('<div class="announce-item">')
-            r += htmltext('<h4>')
-            if item.publication_time:
-                r += time.strftime(misc.date_format(), item.publication_time)
-                r += ' - '
-            r += item.title
-            r += htmltext('</h4>')
-            r += htmltext('<p>')
-            r += item.text
-            r += htmltext('</p>')
-            r += htmltext('</div>')
-
-        r += htmltext('<ul id="announces-links">')
-        r += htmltext('<li><a href="announces/subscribe">%s</a></li>') % _('Receiving those Announces')
-        r += htmltext('<li><a href="announces/">%s</a></li>') % _('Previous Announces')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def myspace_snippet(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<div id="myspace">')
-        r += htmltext('<h3>%s</h3>') % _('My Space')
-        r += htmltext('<ul>')
-        if get_request().user and not get_request().user.anonymous:
-            r += htmltext('  <li><a href="myspace/" id="member">%s</a></li>') % _('Access to your personal space')
-            r += htmltext('  <li><a href="logout" id="logout">%s</a></li>') % _('Logout')
-        else:
-            r += htmltext('  <li><a href="register/" id="inscr">%s</a></li>') % _('Registration')
-            r += htmltext('  <li><a href="login/" id="login">%s</a></li>') % _('Login')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def page_view(self, key, title, urlname = None):
-        if not urlname:
-            urlname = key[3:].replace(str('_'), str('-'))
-        get_response().breadcrumb.append((urlname, title))
-        template.html_top(title)
-        r = TemplateIO(html=True)
-        r += htmltext('<div class="article">')
-        r += htmltext(TextsDirectory.get_html_text(key))
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def informations_editeur(self):
-        get_response().filter['bigdiv'] = 'info'
-        return self.page_view('aq-editor-info', _('Editor Informations'),
-                urlname = 'informations_editeur')
-
-    def accessibility(self):
-        get_response().filter['bigdiv'] = 'accessibility'
-        return self.page_view('aq-accessibility', _('Accessibility Statement'))
-
-    def contact(self):
-        get_response().filter['bigdiv'] = 'contact'
-        return self.page_view('aq-contact', _('Contact'))
-
-    def help(self):
-        get_response().filter['bigdiv'] = 'help'
-        return self.page_view('aq-help', _('Help'))
-
-
-from qommon.publisher import get_publisher_class
-get_publisher_class().root_directory_class = AlternateRootDirectory
-get_publisher_class().after_login_url = 'myspace/'
-get_publisher_class().use_sms_feature = True
-
-# help links
-get_publisher_class().backoffice_help_url = {
-    'fr': 'https://doc-publik.entrouvert.com/',
-}
-
-
-EmailsDirectory.register('announces-subscription-confirmation',
-        N_('Confirmation of Announces Subscription'),
-        N_('Available variables: change_url, cancel_url, time, sitename'),
-        default_subject = N_('Announce Subscription Request'),
-        default_body = N_("""\
-You have (or someone impersonating you has) requested to subscribe to
-announces from [sitename].  To confirm this request, visit the
-following link:
-
-[confirm_url]
-
-If you are not the person who made this request, or you wish to cancel
-this request, visit the following link:
-
-[cancel_url]
-
-If you do nothing, the request will lapse after 3 days (precisely on
-[time]).
-"""))
-
-
-TextsDirectory.register('aq-announces-subscription',
-        N_('Text on announces subscription page'),
-        default = N_('''\
-<p>
-FIXME
-'</p>'''))
-
-TextsDirectory.register('aq-sms-demo',
-        N_('Text when subscribing to announces SMS and configured as demo'),
-        default = N_('''
-<p>
-Receiving announces by SMS is not possible in this demo
-</p>'''))
-
-TextsDirectory.register('aq-editor-info', N_('Editor Informations'))
-TextsDirectory.register('aq-accessibility', N_('Accessibility Statement'))
-TextsDirectory.register('aq-contact', N_('Contact Information'))
-TextsDirectory.register('aq-help', N_('Help'))
-TextsDirectory.register('aq-sso-text',  N_('Connecting with Identity Provider'),
-        default = N_('''<h3>Connecting with Identity Provider</h3>
-<p>You can also use your identity provider to connect.
-</p>'''))
-
-TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg = True)
diff --git a/extra/modules/saml2.py b/extra/modules/saml2.py
deleted file mode 100644
index 477f17f..0000000
--- a/extra/modules/saml2.py
+++ /dev/null
@@ -1,89 +0,0 @@
-try:
-    import lasso
-except ImportError:
-    pass
-
-from qommon import get_cfg, get_logger
-import qommon.saml2
-
-
-class Saml2Directory(qommon.saml2.Saml2Directory):
-    def extract_attributes(self, session, login):
-        '''Separate attributes as two dictionaries: one for last value, one for
-           the list of values.'''
-        d = {}
-        m = {}
-
-        lasso_session = lasso.Session.newFromDump(session.lasso_session_dump)
-        try:
-            assertion = lasso_session.getAssertions(None)[0]
-        except:
-            get_logger().warn('failed to lookup assertion')
-            return d, m
-
-        try:
-            for attribute in assertion.attributeStatement[0].attribute:
-                try:
-                    d[attribute.name] = attribute.attributeValue[0].any[0].content
-                    for attribute_value in attribute.attributeValue:
-                        l = m.setdefault(attribute.name, [])
-                        l.append(attribute_value.any[0].content)
-                except IndexError:
-                    pass
-        except IndexError:
-            pass
-        return d, m
-
-    def fill_user_attributes(self, session, login, user):
-        qommon.saml2.Saml2Directory.fill_user_attributes(self, session, login, user)
-
-        idp = qommon.saml2.get_remote_provider_cfg(login)
-        if not idp.get('attribute-mapping'):
-            self.legacy_fill_user_attributes(session, login, user)
-
-    def legacy_fill_user_attributes(self, session, login, user):
-        '''Fill fields using a legacy attribute to field varname mapping'''
-        d, m = self.extract_attributes(session, login)
-        users_cfg = get_cfg('users', {}) or {}
-        get_logger().debug('using legacy attribute filling')
-
-        # standard attributes
-        user.name = d.get('cn')
-        user.email = d.get('mail')
-
-        # email field
-        field_email = users_cfg.get('field_email')
-        if field_email:
-            user.form_data[field_email] = d.get('mail') or d.get('email')
-
-        # name field, this only works if there's a single field for the name
-        field_name_values = users_cfg.get('field_name')
-        if field_name_values:
-            if type(field_name_values) is str: # it was a string in previous versions
-                field_name_values = [field_name_values]
-            if len(field_name_values) == 1:
-                user.form_data[field_name_values[0]] = d.get('cn')
-
-        # other fields, matching is done on known LDAP attribute names and
-        # common variable names
-        extra_field_mappings = [
-                ('gn', ('firstname', 'prenom')),
-                ('givenName', ('firstname', 'prenom')),
-                ('surname', ('surname', 'name', 'nom',)),
-                ('sn', ('surname', 'name', 'nom',)),
-                ('personalTitle', ('personalTitle', 'civilite',)),
-                ('l', ('location', 'commune', 'ville',)),
-                ('streetAddress', ('streetAddress', 'address', 'adresse', 'street',)),
-                ('street', ('streetAddress', 'address', 'adresse', 'street',)),
-                ('postalCode', ('postalCode', 'codepostal', 'cp',)),
-                ('telephoneNumber', ('telephoneNumber', 'telephonefixe', 'telephone',)),
-                ('mobile', ('mobile', 'telephonemobile',)),
-                ('faxNumber', ('faxNumber', 'fax')),
-        ]
-
-        for attribute_key, field_varnames in extra_field_mappings:
-            if not attribute_key in d:
-                continue
-            for field in user.get_formdef().fields:
-                if field.varname in field_varnames:
-                    user.form_data[field.id] = d.get(attribute_key)
diff --git a/extra/modules/strongbox.py b/extra/modules/strongbox.py
deleted file mode 100644
index 4d6ecde..0000000
--- a/extra/modules/strongbox.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import os
-import time
-
-from quixote import get_publisher
-from qommon.storage import StorableObject
-from qommon import misc
-
-class StrongboxType(StorableObject):
-    _names = 'strongbox-types'
-
-    id = None
-    label = None
-    validation_months = 0
-
-
-class StrongboxFile():
-    id = None
-    orig_filename = None
-    base_filename = None
-    content_type = None
-    charset = None
-
-    def __init__(self, id, f):
-        self.id = id
-        self.orig_filename = f.orig_filename
-        self.base_filename = f.base_filename
-        self.content_type = f.content_type
-        self.charset = f.charset
-        self.fp = f.fp
-
-    def __getstate__(self):
-        odict = self.__dict__.copy()
-        if not odict.has_key('fp'):
-            return odict
-
-        # XXX: add some locking
-        del odict['fp']
-        dirname = os.path.join(get_publisher().app_dir, 'strongbox-files')
-        if not os.path.exists(dirname):
-            os.mkdir(dirname)
-        odict['filename'] = os.path.join(dirname, str(self.id))
-        self.fp.seek(0)
-        fd = file(odict['filename'], 'w')
-        fd.write(self.fp.read())
-        fd.close()
-        return odict
-
-
-class StrongboxItem(StorableObject):
-    _names = 'strongbox-items'
-    _hashed_indexes = ['user_id']
-
-    user_id = None
-    description = None
-    file = None
-    type_id = None
-    expiration_time = None
-    proposed_by = None
-    proposed_time = None
-    validated_time = None
-
-    def get_display_name(self):
-        if self.description:
-            return self.description
-        return self.file.base_filename
-
-    def set_file(self, file):
-        self.file = StrongboxFile(self.id, file)
-
-    def remove_file(self):
-        os.unlink(self.file.filename)
-
-    def set_expiration_time_from_date(self, value):
-        if not value:
-            self.expiration_time = None
-            return
-        if not StrongboxType.has_key(self.type_id):
-            return
-
-        if not StrongboxType.get(self.type_id).validation_months:
-            self.expiration_time = None
-            return
-
-        date = time.strptime(value, misc.date_format())
-        year, month, day = date[:3]
-        month += StrongboxType.get(self.type_id).validation_months
-        while month > 12:
-           year += 1
-           month -= 12
-        while True:
-            try:
-                self.expiration_time = time.strptime(
-                       '%04d-%02d-%02d' % (year, month, day), '%Y-%m-%d')
-            except ValueError:
-                day -= 1
-                continue
-            break
-
diff --git a/extra/modules/strongbox_ui.py b/extra/modules/strongbox_ui.py
deleted file mode 100644
index 71456e2..0000000
--- a/extra/modules/strongbox_ui.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import time
-
-from quixote import get_request, get_response, get_session, redirect
-from quixote.directory import Directory, AccessControlled
-from quixote.html import TemplateIO, htmltext
-
-import wcs
-import wcs.admin.root
-
-from qommon import _
-from qommon import errors, misc
-from qommon.form import *
-from qommon.strftime import strftime
-from qommon.backoffice.menu import html_top
-from qommon import get_cfg
-
-from strongbox import StrongboxType, StrongboxItem
-
-
-
-class StrongboxTypeDirectory(Directory):
-    _q_exports = ['', 'edit', 'delete']
-
-    def __init__(self, strongboxtype):
-        self.strongboxtype = strongboxtype
-
-    def _q_index(self):
-        html_top('strongbox', title = _('Item Type: %s') % self.strongboxtype.label)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Item Type: %s') % self.strongboxtype.label
-        get_response().filter['sidebar'] = self.get_sidebar()
-        r += get_session().display_message()
-
-        if self.strongboxtype.validation_months:
-            r += htmltext('<div class="bo-block">')
-            r += htmltext('<ul>')
-            r += htmltext('<li>')
-            r += _('Number of months of validity:')
-            r += ' '
-            r += self.strongboxtype.validation_months
-            r += htmltext('</li>')
-            r += htmltext('</ul>')
-            r += htmltext('</div>')
-
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul>')
-        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
-        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def edit(self):
-        form = self.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            self.submit(form)
-            return redirect('..')
-
-        html_top('strongbox', title = _('Edit Item Type: %s') % self.strongboxtype.label)
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('Edit Item Type: %s') % self.strongboxtype.label
-        r += form.render()
-        return r.getvalue()
-
-    def form(self):
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'label', title = _('Label'), required = True,
-                value = self.strongboxtype.label)
-        form.add(IntWidget, 'validation_months', title=_('Number of months of validity'),
-                value=self.strongboxtype.validation_months,
-                hint=_('Use 0 if there is no expiration'))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        return form
-
-    def submit(self, form):
-        for k in ('label', 'validation_months'):
-            widget = form.get_widget(k)
-            if widget:
-                setattr(self.strongboxtype, k, widget.parse())
-        self.strongboxtype.store()
-
-    def delete(self):
-        form = Form(enctype='multipart/form-data')
-        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
-                        'You are about to irrevocably delete this item type.')))
-        form.add_submit('submit', _('Submit'))
-        form.add_submit('cancel', _('Cancel'))
-        if form.get_submit() == 'cancel':
-            return redirect('..')
-        if not form.is_submitted() or form.has_errors():
-            get_response().breadcrumb.append(('delete', _('Delete')))
-            html_top('strongbox', title = _('Delete Item Type'))
-            r = TemplateIO(html=True)
-            r += htmltext('<h2>%s</h2>') % _('Deleting Item Type: %s') % self.strongboxtype.label
-            r += form.render()
-            return r.getvalue()
-        else:
-            self.strongboxtype.remove_self()
-            return redirect('..')
-
-
-class StrongboxTypesDirectory(Directory):
-    _q_exports = ['', 'new']
-
-    def _q_traverse(self, path):
-        get_response().breadcrumb.append(('types/', _('Item Types')))
-        return Directory._q_traverse(self, path)
-
-    def _q_index(self):
-        return redirect('..')
-
-    def new(self):
-        type_ui = StrongboxTypeDirectory(StrongboxType())
-
-        form = type_ui.form()
-        if form.get_submit() == 'cancel':
-            return redirect('.')
-
-        if form.is_submitted() and not form.has_errors():
-            type_ui.submit(form)
-            return redirect('%s/' % type_ui.strongboxtype.id)
-
-        get_response().breadcrumb.append(('new', _('New Item Type')))
-        html_top('strongbox', title = _('New Item Type'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s</h2>') % _('New Item Type')
-        r += form.render()
-        return r.getvalue()
-
-    def _q_lookup(self, component):
-        try:
-            strongboxtype = StrongboxType.get(component)
-        except KeyError:
-            raise errors.TraversalError()
-        get_response().breadcrumb.append((str(strongboxtype.id), strongboxtype.label))
-        return StrongboxTypeDirectory(strongboxtype)
-
-
-class StrongboxDirectory(AccessControlled, Directory):
-    _q_exports = ['', 'types', 'add', 'add_to']
-    label = N_('Strongbox')
-
-    types = StrongboxTypesDirectory()
-
-    def is_accessible(self, user):
-        from .backoffice import check_visibility
-        return check_visibility('strongbox', user)
-
-    def _q_access(self):
-        user = get_request().user
-        if not user:
-            raise errors.AccessUnauthorizedError()
-
-        if not self.is_accessible(user):
-            raise errors.AccessForbiddenError(
-                    public_msg = _('You are not allowed to access Strongbox Management'),
-                    location_hint = 'backoffice')
-
-        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
-
-
-    def _q_index(self):
-        html_top('strongbox', _('Strongbox'))
-        r = TemplateIO(html=True)
-        get_response().filter['sidebar'] = self.get_sidebar()
-
-        r += get_session().display_message()
-
-        r += htmltext('<div class="splitcontent-left">')
-        r += htmltext('<div class="bo-block">')
-        r += htmltext('<h2>%s</h2>') % _('Propose a file to a user')
-        form = Form(enctype='multipart/form-data')
-        form.add(StringWidget, 'q', title = _('User'), required=True)
-        form.add_submit('search', _('Search'))
-        r += form.render()
-        if form.is_submitted() and not form.has_errors():
-            q = form.get_widget('q').parse()
-            users = self.search_for_users(q)
-            if users:
-                if len(users) == 1:
-                    return redirect('add_to?user_id=%s' % users[0].id)
-                if len(users) < 50:
-                    r += _('(first 50 users only)')
-                r += htmltext('<ul>')
-                for u in users:
-                    r += htmltext('<li><a href="add_to?user_id=%s">%s</a></li>') % (u.id, u.display_name)
-                r += htmltext('</ul>')
-            else:
-                r += _('No user found.')
-        r += htmltext('</div>')
-        r += htmltext('</div>')
-
-        r += htmltext('<div class="splitcontent-right">')
-        r += htmltext('<div class="bo-block">')
-        types = StrongboxType.select()
-        r += htmltext('<h2>%s</h2>') % _('Item Types')
-        if not types:
-            r += htmltext('<p>')
-            r += _('There is no item types defined at the moment.')
-            r += htmltext('</p>')
-
-        r += htmltext('<ul class="biglist" id="strongbox-list">')
-        for l in types:
-            type_id = l.id
-            r += htmltext('<li class="biglistitem" id="itemId_%s">') % type_id
-            r += htmltext('<strong class="label"><a href="types/%s/">%s</a></strong>') % (type_id, l.label)
-            r += htmltext('</li>')
-        r += htmltext('</ul>')
-        r += htmltext('</div>')
-        r += htmltext('</div>')
-        return r.getvalue()
-
-    def get_sidebar(self):
-        r = TemplateIO(html=True)
-        r += htmltext('<ul id="sidebar-actions">')
-        r += htmltext('  <li><a class="new-item" href="types/new">%s</a></li>') % _('New Item Type')
-        r += htmltext('</ul>')
-        return r.getvalue()
-
-    def search_for_users(self, q):
-        if hasattr(get_publisher().user_class, 'search'):
-            return get_publisher().user_class.search(q)
-        if q:
-            users = [x for x in get_publisher().user_class.select()
-                     if q in (x.name or '') or q in (x.email or '')]
-            return users
-        else:
-            return []
-
-    def get_form(self):
-        types = [(x.id, x.label) for x in StrongboxType.select()]
-        form = Form(action='add', enctype='multipart/form-data')
-        form.add(StringWidget, 'description', title=_('Description'), size=60)
-        form.add(FileWidget, 'file', title=_('File'), required=True)
-        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
-                 options = [(None, _('Not specified'))] + types)
-        form.add(DateWidget, 'date_time', title = _('Document Date'))
-        form.add_submit('submit', _('Upload'))
-        return form
-
-    def add(self):
-        form = self.get_form()
-        form.add(StringWidget, 'user_id', title=_('User'))
-        if not form.is_submitted():
-           return redirect('.')
-
-        sffile = StrongboxItem()
-        sffile.user_id = form.get_widget('user_id').parse()
-        sffile.description = form.get_widget('description').parse()
-        sffile.proposed_time = time.localtime()
-        sffile.proposed_id = get_request().user.id
-        sffile.type_id = form.get_widget('type_id').parse()
-        v = form.get_widget('date_time').parse()
-        sffile.set_expiration_time_from_date(v)
-        sffile.store()
-        sffile.set_file(form.get_widget('file').parse())
-        sffile.store()
-        return redirect('.')
-
-    def add_to(self):
-        form = Form(enctype='multipart/form-data', action='add_to')
-        form.add(StringWidget, 'user_id', title = _('User'), required=True)
-        try:
-            user_id = form.get_widget('user_id').parse()
-            user = get_publisher().user_class.get(user_id)
-        except:
-            return redirect('.')
-        if not user:
-            return redirect('.')
-        get_request().form = {}
-        get_request().environ['REQUEST_METHOD'] = 'GET'
-
-        html_top('strongbox', _('Strongbox'))
-        r = TemplateIO(html=True)
-        r += htmltext('<h2>%s %s</h2>') % (_('Propose a file to:'), user.display_name)
-        form = self.get_form()
-        form.add(HiddenWidget, 'user_id', title=_('User'), value=user.id)
-        r += form.render()
-        return r.getvalue()
diff --git a/extra/modules/template.py b/extra/modules/template.py
deleted file mode 100644
index 7048247..0000000
--- a/extra/modules/template.py
+++ /dev/null
@@ -1,82 +0,0 @@
-from quixote import get_request, get_publisher, get_response
-from qommon.publisher import get_publisher_class
-from quixote.html import htmltext
-
-from qommon import _
-from qommon import template
-from qommon.admin.texts import TextsDirectory
-from wcs.categories import Category
-
-wcs_error_page = template.error_page
-
-def render_response(publisher, body):
-    response = publisher.get_request().response
-    body = str(body)
-
-    for key in ('bigdiv', 'gauche'):
-        if not response.filter.has_key(key):
-            response.filter[key] = None
-
-    root_url = publisher.get_root_url()
-    wcs_path = publisher.get_request().get_path()[len(root_url):]
-    section = wcs_path.split('/')[0]
-
-    if section in ('backoffice', 'admin'):
-        return template.decorate(body, response)
-
-    section_title = ''
-    page_title = response.filter.get('title')
-    if 'error-page' in response.filter:
-        pass
-    elif section == 'consultations':
-        section_title = '<h2 id="consultations">%s</h2>\n' % _('Consultations')
-        response.filter['bigdiv'] = 'rub_consultation'
-    elif section == 'announces':
-        response.filter['bigdiv'] = 'rub_annonce'
-        section_title = '<h2 id="announces">%s</h2>\n' % _('Announces to citizens')
-        if page_title == _('Announces to citizens'):
-            page_title = ''
-    elif section == 'agenda':
-        response.filter['bigdiv'] = 'rub_agenda'
-        section_title = '<h2 id="agenda">%s</h2>\n' % _('Agenda')
-        if page_title == _('Agenda'):
-            page_title = ''
-    elif section and len(section) > 1:
-        # XXX: this works but is not efficient
-        if Category.has_urlname(section):
-            section_title = '<h2 id="services">%s</h2>\n' % _('Services')
-            response.filter['bigdiv'] = 'rub_service'
-
-    if not 'auquotidien-no-titles-in-section' in response.filter.get('keywords', []):
-        if page_title:
-            if section_title:
-                page_title = '<h3>%s</h3>' % page_title
-            else:
-                page_title = '<h2 id="info">%s</h2>' % page_title
-        else:
-            page_title = ''
-
-        body = '\n'.join((section_title, page_title, body))
-
-    if hasattr(response, 'breadcrumb') and response.breadcrumb:
-        if len(response.breadcrumb) == 1:
-            response.breadcrumb = None
-
-    return template.decorate(body, response)
-
-
-def error_page(*args, **kwargs):
-    get_response().filter = {}
-    get_response().filter['error-page'] = None
-    get_response().filter['keywords'] = template.get_current_theme().get('keywords')
-    get_response().filter['title'] = template.get_current_theme().get('keywords')
-    get_response().filter['gauche'] = TextsDirectory.get_html_text('aq-error-assistance')
-    error_page = wcs_error_page(*args, **kwargs)
-    title = get_response().filter['title']
-    get_response().filter['title'] = None
-    return htmltext('<div id="info"><h2>%s</h2>' % title) + error_page + htmltext('</div>')
-
-template.error_page = error_page
-get_publisher_class().render_response = render_response
-
-TextsDirectory.register('aq-error-assistance', N_('Assistance text next to errors'))
diff --git a/setup.py b/setup.py
index 70f6ccc..ed1679d 100644
--- a/setup.py
+++ b/setup.py
@@ -55,8 +55,8 @@ distutils.core.setup(
         version = get_version(),
         maintainer = 'Frederic Peters',
         maintainer_email = 'fpeters@entrouvert.com',
-        package_dir = { 'extra': 'extra' },
-        packages = ['extra', 'extra.modules', 'extra.modules.pyatom'],
+        package_dir = {'auquotidien': 'auquotidien'},
+        packages = ['auquotidien', 'auquotidien.modules', 'auquotidien.modules.pyatom'],
         cmdclass = {'build_py': qx_build_py,
                     'sdist': eo_sdist},
         data_files = data_tree('share/wcs/texts', 'texts') +\
diff --git a/tests/utilities.py b/tests/utilities.py
index 008eb2b..4e0a0c7 100644
--- a/tests/utilities.py
+++ b/tests/utilities.py
@@ -20,7 +20,7 @@ def create_temporary_pub():
     publisher.WcsPublisher.DATA_DIR = os.path.abspath(
             os.path.join(os.path.dirname(wcs.__file__), '..', 'data'))
     config.add_section('extra')
-    config.set('extra', 'auquotidien', os.path.join(os.path.dirname(__file__), '..', 'extra'))
+    config.set('extra', 'auquotidien', os.path.join(os.path.dirname(__file__), '..', 'auquotidien'))
     publisher.WcsPublisher.configure(config)
     pub = publisher.WcsPublisher.create_publisher()
     # allow saving the user
