Projet

Général

Profil

« Précédent | Suivant » 

Révision 8b02623d

Ajouté par Frédéric Péters il y a plus de 6 ans

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

Voir les différences:

MANIFEST.in
7 7
include po/*.pot
8 8
recursive-include apache-errors/ *.html
9 9
recursive-include data/themes/ *.css *.png *.jpeg '*.jpg *.xml *.html *.js *.ezt *.gif *.otf
10
recursive-include extra/ *.py
10
recursive-include auquotidien/ *.py
11 11
recursive-include static/ *.png *.jpeg '*.jpg *.js *.gif
12 12
recursive-include texts/ *.html
13 13
recursive-include theme/ *.css *.png *.jpeg '*.jpg *.xml *.html *.js *.ezt *.gif
auquotidien/auquotidien.py
1
from quixote import get_publisher
2

  
3
from qommon import _
4
from qommon.publisher import get_publisher_class, get_request
5
from qommon.misc import get_cfg
6

  
7
import modules.admin
8
import modules.backoffice
9
import modules.links_ui
10
import modules.announces_ui
11
import modules.categories_admin
12
import modules.events_ui
13
import modules.payments_ui
14
import modules.strongbox_ui
15
import modules.formpage
16
import modules.template
17
import modules.root
18
import modules.payments
19
import modules.connectors
20
import modules.abelium_domino_ui
21
import modules.abelium_domino_vars
22
import modules.abelium_domino_synchro
23

  
24
get_publisher_class().register_translation_domain('auquotidien')
25
get_publisher_class().default_configuration_path = 'au-quotidien-wcs-settings.xml'
26

  
27
rdb = get_publisher_class().backoffice_directory_class
28

  
29
rdb.items = []
30

  
31
rdb.register_directory('announces', modules.announces_ui.AnnouncesDirectory())
32
rdb.register_menu_item('announces/', _('Announces'))
33

  
34
rdb.register_directory('links', modules.links_ui.LinksDirectory())
35
rdb.register_menu_item('links/', _('Links'))
36

  
37
rdb.register_directory('events', modules.events_ui.EventsDirectory())
38
rdb.register_menu_item('events/', _('Events'))
39

  
40
rdb.register_directory('payments', modules.payments_ui.PaymentsDirectory())
41
rdb.register_menu_item('payments/', _('Payments'))
42

  
43
rdb.register_directory('strongbox', modules.strongbox_ui.StrongboxDirectory())
44
rdb.register_menu_item('strongbox/', _('Strongbox'))
45

  
46
rdb.register_directory('settings', modules.admin.SettingsDirectory())
47
rdb.register_directory('categories', modules.categories_admin.CategoriesDirectory())
48

  
49
import wcs.admin.settings
50
wcs.admin.settings.SettingsDirectory.domino = modules.abelium_domino_ui.AbeliumDominoDirectory()
51
wcs.admin.settings.SettingsDirectory._q_exports.append('domino')
52

  
53
import wcs.categories
54
wcs.categories.Category.XML_NODES = [('name', 'str'), ('url_name', 'str'),
55
        ('description', 'str'), ('position', 'int'),
56
        ('homepage_position', 'str'), ('redirect_url', 'str'), ('limit', 'int')]
auquotidien/modules/abelium_domino_synchro.py
1
import sys
2
from datetime import datetime
3
import collections
4
from decimal import Decimal
5

  
6
from qommon import _
7
from qommon.cron import CronJob
8
from qommon.publisher import get_publisher_class
9
from qommon import get_logger
10

  
11
from wcs.users import User
12

  
13
from abelium_domino_ui import (get_client, is_activated, get_invoice_regie,
14
        abelium_domino_ws)
15
from payments import Invoice, Transaction
16

  
17
DOMINO_ID_PREFIX = 'DOMINO-'
18

  
19
def synchronize_domino(publisher):
20
    regie = get_invoice_regie(publisher)
21
    logger = get_logger()
22
    if not is_activated(publisher) or not regie:
23
        return
24
    client = get_client(publisher)
25
    if client is None:
26
        logger.warning('Unable to create a DominoWS object')
27
        return
28
    client.clear_cache()
29
    users = User.values()
30
    users_by_mail = dict(((user.email, user) for user in users))
31
    users_by_code_interne = {}
32
    for user in users:
33
        if hasattr(user, 'abelium_domino_code_famille'):
34
            users_by_code_interne[user.abelium_domino_code_famille] = user
35
    try:
36
        invoices = client.invoices
37
    except abelium_domino_ws.DominoException, e:
38
        publisher.notify_of_exception(sys.exc_info(), context='[DOMINO]')
39
        logger.error('domino cron: failure to retrieve invoice list from domino '
40
                'for synchronization [error:%s]', e)
41
        return
42
    # import new invoices
43
    logger.info('domino cron: retrieved %i invoices', len(invoices))
44
    for invoice_id, invoice in invoices.iteritems():
45
        user = None
46
        if invoice.family.code_interne in users_by_code_interne:
47
            user = users_by_code_interne[invoice.family.code_interne]
48
        if user is None:
49
            for email in (invoice.family.email_pere, invoice.family.email_mere,
50
                    invoice.family.adresse_internet):
51
                user = users_by_mail.get(email)
52
                if user:
53
                    break
54
            else:
55
                continue
56
        external_id = '%s%s' % (DOMINO_ID_PREFIX, invoice.id)
57
        payment_invoice = Invoice.get_on_index(external_id, 'external_id', ignore_errors=True)
58
        if payment_invoice:
59
            continue
60
        if invoice.reste_du == Decimal(0) or invoice.reste_du < Decimal(0):
61
            continue
62
        payment_invoice = Invoice()
63
        payment_invoice.user_id = user.id
64
        payment_invoice.external_id = external_id
65
        payment_invoice.regie_id = regie.id
66
        payment_invoice.formdef_id = None
67
        payment_invoice.formdata_id = None
68
        payment_invoice.amount = invoice.reste_du
69
        payment_invoice.date = invoice.creation
70
        payment_invoice.domino_synchro_date = datetime.now()
71
        if 'etablissement' in invoice._detail:
72
            etablissement = invoice._detail['etablissement'].encode('utf-8')
73
            payment_invoice.subject = _('%s - Childcare services') % etablissement
74
        else:
75
            payment_invoice.subject = _('Childcare services')
76
        if invoice._detail.get('lignes'):
77
            details = []
78
            details.append('<table class="invoice-details"><thead>')
79
            tpl = '''<tr>
80
    <td>%(designation)s</td>
81
    <td>%(quantite)s</td>
82
    <td>%(prix)s</td>
83
    <td>%(montant)s</td>
84
</tr>'''
85
            captions = {
86
                    'designation': _('Caption'),
87
                    'quantite': _('Quantity'),
88
                    'prix': _('Price'),
89
                    'amount': _('Amount')
90
            }
91
            details.append(tpl % captions)
92
            details.append('</thead>')
93
            details.append('<tbody>')
94
            for ligne in invoice._detail['lignes']:
95
                def encode(x):
96
                    a, b = x
97
                    b = b.encode('utf-8')
98
                    return (a,b)
99
                ligne = map(encode, ligne)
100
                ligne = dict(ligne)
101
                base = collections.defaultdict(lambda:'')
102
                base.update(ligne)
103
                details.append(tpl % base)
104
            details.append('</tbody></table>')
105
            payment_invoice.details = '\n'.join(details)
106
        payment_invoice.store()
107
        logger.info('domino cron: remote invoice %s for family %s added to user %s invoices with id %s',
108
                invoice.id, invoice.family.id, user.id, payment_invoice.id)
109

  
110
    # update invoices
111
    invoices_ids = dict(invoices.iteritems())
112
    for payment_invoice in Invoice.values():
113
        if payment_invoice.external_id is None or not payment_invoice.external_id.startswith(DOMINO_ID_PREFIX):
114
            continue # not a payment related to domino we skip
115
        i = payment_invoice.external_id[len(DOMINO_ID_PREFIX):]
116
        i = int(i)
117
        invoice = invoices_ids.get(i)
118
        if payment_invoice.paid:
119
            if not invoice: 
120
                # invoice has been paid (locally or not) but remote invoice has
121
                # been deleted do, we do nothing.
122
                continue
123
            if getattr(payment_invoice, 'domino_knows_its_paid', None) or getattr(payment_invoice, 'paid_by_domino', None):
124
                # synchronization of payment already done, skip
125
                continue
126
            transactions = Transaction.get_with_indexed_value('invoice_ids', payment_invoice.id)
127
            if not transactions:
128
                logger.warning("domino cron: invoice %s is marked paid but does "
129
                        "not have any linked transaction.", payment_invoice.id)
130
                details = '' # no details about the payment, problem
131
            else:
132
                details = repr(transactions[0].__dict__)
133
            if invoice.montant != payment_invoice.amount:
134
                pass # add warning logs
135
            try:
136
                client.pay_invoice([invoice], invoice.montant, details,
137
                        payment_invoice.paid_date)
138
            except abelium_domino_ws.DominoException, e:
139
                logger.error('domino cron: invoice %s has been paid, but the remote system '
140
                        'is unreachable, notification will be done again '
141
                        'later [error: %s]', invoice.id, e)
142
            else:
143
                # memorize the date of synchronization
144
                payment_invoice.domino_knows_its_paid = datetime.now()
145
                payment_invoice.store()
146
                logger.info('domino cron: domino: invoice %s has been paid; remote system has been '
147
                        'notified', payment_invoice.id)
148
        else: # unpaid
149
            if not invoice:
150
                logger.info('domino cron: remote invoice %s disapearred, so its '
151
                        'still-unpaid local counterpart invoice %s was deleted.',
152
                        i, payment_invoice.id)
153
                payment_invoice.remove_self()
154
            elif invoice.paid():
155
                payment_invoice.paid_by_domino = True
156
                payment_invoice.pay()
157
                logger.info('domino cron: remote invoice %s has beend paid, '
158
                        'local invoice %s of user %s is now marked as paid.',
159
                        invoice.id, payment_invoice.id, payment_invoice.user_id)
160
            else: # not invoice.paid()
161
                pass # still waiting for the payment
162

  
163
get_publisher_class().register_cronjob(CronJob(function=synchronize_domino,
164
    hours=range(0, 24), minutes=range(0, 60, 30)))
auquotidien/modules/abelium_domino_ui.py
1
from quixote import get_publisher, redirect, get_request
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import TemplateIO, htmltext
4

  
5
from qommon import _
6
from qommon import get_cfg, get_logger
7
from qommon.form import Form, StringWidget, CheckboxWidget, SingleSelectWidget
8
from qommon.backoffice.menu import html_top
9
from quixote.html import htmltext
10

  
11
from payments import Regie
12

  
13

  
14
# constants
15
ABELIUM_DOMINO = 'abelium_domino'
16
ACTIVATED = 'activated'
17
WSDL_URL = 'wsdl_url'
18
SERVICE_URL = 'service_url'
19
DOMAIN = 'domain'
20
LOGIN = 'login'
21
PASSWORD = 'password'
22
INVOICE_REGIE = 'invoice_regie'
23

  
24
try:
25
    import abelium_domino_ws
26
except ImportError, e:
27
    abelium_domino_ws = None
28
    import_error = e
29

  
30
def get_abelium_cfg(publisher):
31
    if not publisher:
32
        publisher = get_publisher()
33
    return publisher.cfg.get(ABELIUM_DOMINO, {})
34

  
35
def is_activated(publisher=None):
36
    cfg = get_abelium_cfg(publisher)
37
    return cfg.get(ACTIVATED, False) and abelium_domino_ws is not None
38

  
39
def get_client(publisher=None):
40
    publisher = publisher or get_publisher()
41

  
42
    cfg = get_abelium_cfg(publisher)
43
    try:
44
        publisher._ws_cache = abelium_domino_ws.DominoWs(
45
            url=cfg.get(WSDL_URL, ''),
46
            domain=cfg.get(DOMAIN,''),
47
            login=cfg.get(LOGIN, ''),
48
            password=cfg.get(PASSWORD, ''),
49
            location=cfg.get(SERVICE_URL),
50
            logger=get_logger())
51
    except IOError:
52
        return None
53
    return publisher._ws_cache
54

  
55
def get_family(user, publisher=None):
56
    family = None
57
    if user is None:
58
        return None
59
    client = get_client(publisher)
60
    if not client:
61
        return None
62
    if hasattr(user, 'abelium_domino_code_famille'):
63
        family = client.get_family_by_code_interne(
64
            user.abelium_domino_code_famille)
65
    if family is None and user.email:
66
        family = client.get_family_by_mail(user.email)
67
    return family
68

  
69
def get_invoice_regie(publisher=None):
70
    cfg = get_abelium_cfg(publisher)
71
    regie_id = cfg.get(INVOICE_REGIE)
72
    if not regie_id:
73
        return None
74
    return Regie.get(regie_id, ignore_errors=True)
75

  
76
class AbeliumDominoDirectory(Directory):
77
    _q_exports = [ '' , 'debug' ]
78
    label = N_('Domino')
79

  
80
    def debug(self):
81
        from abelium_domino_vars import SESSION_CACHE
82
        html_top(ABELIUM_DOMINO)
83
        r = TemplateIO(html=True)
84
        r += htmltext('<form method="post"><button>Lancer le cron</button></form>')
85
        if get_request().get_method() == 'POST':
86
            try:
87
                from abelium_domino_synchro import synchronize_domino
88
                synchronize_domino(get_publisher())
89
            except Exception, e:
90
                r += htmltext('<pre>%s</pre>') % repr(e)
91
        r += htmltext('<p>code interne: %s</p>') % getattr(get_request().user, str('abelium_domino_code_famille'), None)
92
        r += htmltext('<dl>')
93
        context = get_publisher().substitutions.get_context_variables()
94
        for var in sorted(context.keys()):
95
            value = context[var]
96
            if value:
97
                r += htmltext('<dt>%s</dt>') % var
98
                r += htmltext('<dd>%s</dt>') % value
99
        r += htmltext('</dl>')
100
        delattr(get_request().session, SESSION_CACHE)
101

  
102
    def _q_index(self):
103
        publisher = get_publisher()
104
        cfg = get_cfg(ABELIUM_DOMINO, {})
105
        form = self.form(cfg)
106

  
107
        title = _('Abelium Domino')
108
        html_top(ABELIUM_DOMINO, title = title)
109
        r = TemplateIO(html=True)
110
        r += htmltext('<h2>%s</h2>') % title
111

  
112
        if form.is_submitted() and not form.has_errors():
113
            if form.get_widget('cancel').parse():
114
                return redirect('..')
115
            if form.get_widget('submit').parse():
116
                for name in [f[0] for f in self.form_desc] + [INVOICE_REGIE]:
117
                    widget = form.get_widget(name)
118
                    if widget:
119
                        cfg[name] = widget.parse()
120
                publisher.cfg[ABELIUM_DOMINO] = cfg
121
                publisher.write_cfg()
122
                return redirect('.')
123

  
124
        if abelium_domino_ws:
125
            r += form.render()
126
        else:
127
            message = _('The Abelium Domino module is not '
128
                    'activated because of this error when '
129
                    'loading it: %r') % import_error
130
            r += htmltext('<p class="errornotice">%s</p>') % message
131
        r += htmltext('<dl style="display: none">')
132
        context = get_publisher().substitutions.get_context_variables()
133
        for var in sorted(context.keys()):
134
            value = context[var]
135
            if value:
136
                r += htmltext('<dt>%s</dt>') % var
137
                r += htmltext('<dd>%s</dt>') % value
138
        r += htmltext('</dl>')
139
        return r.getvalue()
140

  
141
    form_desc = (
142
            # name, required, title, kind
143
            (ACTIVATED, False, _('Activated'), bool),
144
            (WSDL_URL, True, _('WSDL URL'), str),
145
            (SERVICE_URL, False, _('Service URL'), str),
146
            (DOMAIN, True, _('Domain'), str),
147
            (LOGIN, True, _('Login'), str),
148
            (PASSWORD, True, _('Password'), str),
149
    )
150

  
151

  
152
    def form(self, initial_value={}):
153
        form = Form(enctype='multipart/form-data')
154
        kinds = { str: StringWidget, bool: CheckboxWidget }
155
        for name, required, title, kind in self.form_desc:
156
            widget = kinds[kind]
157
            form.add(widget, name, required=required, title=title,
158
                    value=initial_value.get(name, ''))
159
        options = [(regie.id, regie.label) \
160
                        for regie in Regie.values()]
161
        options.insert(0, (None, _('None')))
162
        form.add(SingleSelectWidget, INVOICE_REGIE,
163
                title=_('Regie which will receive payments'),
164
                value=initial_value.get(INVOICE_REGIE),
165
                options=options)
166

  
167
        form.add_submit('submit', _('Submit'))
168
        form.add_submit('cancel', _('Cancel'))
169

  
170
        return form
auquotidien/modules/abelium_domino_vars.py
1
from decimal import Decimal
2
import logging
3

  
4
from quixote.publish import get_publisher
5

  
6
from qommon import _
7
from qommon.substitution import Substitutions
8
from wcs.publisher import WcsPublisher
9

  
10
from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
11

  
12
SESSION_CACHE = 'abelium_domino_variable_cache'
13

  
14
class DominoVariables(object):
15
    VARIABLE_TEMPLATE = 'domino_var_%s'
16
    CHILD_VARIABLE_TEMPLATE = 'domino_var_%s_enfant%s'
17

  
18
    CHILD_COLUMNS = abelium_domino_ws.Child.COLUMNS
19
    FAMILY_COLUMNS = abelium_domino_ws.Family.COLUMNS \
20
        + abelium_domino_ws.Family.MORE_COLUMNS
21
    def __init__(self, publisher=None, request=None):
22
        self.publisher = publisher
23
        self.request = request
24

  
25
    def get_substitution_variables(self):
26
        vars = {}
27
        if not is_activated() or not self.request or not self.request.user \
28
                or not getattr(self.request.user, 'email'):
29
            return vars
30

  
31
        # test cache
32
        cache = getattr(self.request.session, SESSION_CACHE, None)
33
        if cache is not None:
34
            return cache
35
        # call the web service
36
        try:
37
            charset = get_publisher().site_charset
38
            family = get_family(self.request.user)
39
            if family:
40
                family.complete()
41
                for i, child in enumerate(family.children):
42
                    for remote_name, name, converter, desc in self.CHILD_COLUMNS:
43
                        v = getattr(child, name, None)
44
                        if v is None:
45
                            continue
46
                        if hasattr(v, 'encode'):
47
                            v = v.encode(charset)
48
                        vars[self.CHILD_VARIABLE_TEMPLATE % (name, i+1)] = v
49
                vars[self.VARIABLE_TEMPLATE % 'nombre_enfants'] = len(family.children)
50
                for remote_name, name, converted, desc in self.FAMILY_COLUMNS:
51
                    if hasattr(family, name):
52
                        v = getattr(family, name)
53
                        if v is None:
54
                            continue
55
                        if hasattr(v, 'encode'):
56
                            v = v.encode(charset)
57
                        vars[self.VARIABLE_TEMPLATE % name] = v
58
                amount = Decimal(0)
59
                for invoice in family.invoices:
60
                    amount += invoice.reste_du
61
                if amount:
62
                    vars['user_famille_reste_du'] = str(amount)
63
        except abelium_domino_ws.DominoException:
64
            logging.exception('unable to call the domino ws for user %s', self.request.user.id)
65
        setattr(self.request.session, SESSION_CACHE, vars)
66
        self.request.session.store()
67
        return vars
68

  
69
    def get_substitution_variables_list(cls):
70
        if not is_activated():
71
            return ()
72
        vars = []
73
        for remote_name, name, converted, desc in cls.FAMILY_COLUMNS:
74
            vars.append((_('Domino'), cls.VARIABLE_TEMPLATE % name, desc))
75
        for remote_name, name, converted, desc in cls.CHILD_COLUMNS:
76
            vars.append((_('Domino'), cls.CHILD_VARIABLE_TEMPLATE % (name, '{0,1,2,..}'), desc))
77
        return vars
78
    get_substitution_variables_list = classmethod(get_substitution_variables_list)
79

  
80
Substitutions.register_dynamic_source(DominoVariables)
81
WcsPublisher.register_extra_source(DominoVariables)
auquotidien/modules/abelium_domino_workflow.py
1
import re
2
import time
3

  
4
from quixote import get_request, get_publisher, get_session
5
from quixote.directory import Directory
6

  
7
from qommon import _
8
from qommon.substitution import Substitutions
9
from qommon.form import Form, ValidatedStringWidget
10
import qommon.misc
11
from qommon import get_logger
12

  
13
from wcs.workflows import Workflow, WorkflowStatusJumpItem, register_item_class
14
from wcs.forms.common import FormStatusPage
15

  
16
from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
17
import abelium_domino_ws
18

  
19
class InternalCodeStringWidget(ValidatedStringWidget):
20
    regex = '\d*'
21

  
22
class AbeliumDominoRegisterFamilyWorkflowStatusItem(WorkflowStatusJumpItem):
23
    status = None
24
    description = N_('Abelium Domino: Register a Family')
25
    key = 'abelium-domino-register-family'
26
    category = ('aq-abelium', N_('Abelium'))
27
    label = None
28

  
29
    def render_as_line(self):
30
        return _('Register a Family into Abelium Domino')
31

  
32
    def get_family(self, formdata):
33
        try:
34
            user = formdata.get_user()
35
            if user:
36
                family = get_family(user)
37
                if family:
38
                    family.complete()
39
                return family
40
        except abelium_domino_ws.DominoException:
41
            pass
42
        return None
43

  
44
    def fill_form(self, form, formdata, user):
45
        family = self.get_family(formdata)
46
        if 'family_id' not in form._names:
47
            form.add(InternalCodeStringWidget, 'family_id',
48
                     title=_('Family internal code'),
49
                     value=family and family.code_interne.encode('utf8'),
50
                     hint=_('If a family internal code is present, the '
51
                            'family is updated, if not it is created'))
52
            form.add_submit('create_update_button%s' % self.id,
53
                    _('Create or update the family'))
54

  
55
    def update(self, form, formdata, user, evo):
56
        fid_widget = form.get_widget('family_id')
57
        code_interne = fid_widget.parse()
58
        try:
59
            code_interne = int(code_interne)
60
        except ValueError:
61
            raise ValueError('Le code interne est invalide')
62
        code_interne = '%05d' % code_interne
63
        family = get_client().get_family_by_code_interne(code_interne)
64
        if not family:
65
            raise ValueError('Le code interne est invalide')
66
        family.complete()
67
        self.extract_family(form, formdata, user, evo, family)
68
        family.save()
69
        return family
70

  
71
    def create(self, form, formdata, user, evo):
72
        family = abelium_domino_ws.Family(client=get_client())
73
        self.extract_family(form, formdata, user, evo, family)
74
        return family
75

  
76
    def extract_family(self, form, formdata, user, evo, family):
77
        formdef = formdata.formdef
78
        children = [abelium_domino_ws.Child() for i in range(5)]
79
        max_i = 0
80
        for field in formdef.fields:
81
            value = formdata.data.get(field.id)
82
            if value in (None, ''):
83
                continue
84
            if hasattr(field, 'date_in_the_past'):
85
                value = time.strftime('%Y%m%d', value)
86
            value = unicode(value, 'utf8')
87
            if field.prefill and \
88
               field.prefill.get('type') == 'formula':
89
                v = field.prefill.get('value', '').strip()
90
                i = None
91
                name = None
92
                m = re.search('domino_var_([^ ]*)_enfant([0-9]*)', v)
93
                m2 = re.search('domino_var_([^ ]*)', v)
94
                if m:
95
                    name, i = m.groups()
96
                    try:
97
                        i = int(i)
98
                    except ValueError:
99
                        continue
100
                    max_i = max(i, max_i)
101
                    print 'enfant', name, i-1, value
102
                    setattr(children[i-1], name, value)
103
                elif m2:
104
                    name = m2.group(1)
105
                    print 'family', name, value
106
                    setattr(family, name, value)
107
        for child1, child2 in zip(family.children, children[:max_i]):
108
            child1.__dict__.update(child2.__dict__)
109
        family.save()
110
        if max_i > len(family.children): # add new children
111
            for child in children[len(family.children):max_i]:
112
                family.add_child(child)
113

  
114
    def submit_form(self, form, formdata, user, evo):
115
        logger = get_logger()
116
        if form.get_submit() != 'create_update_button%s' % self.id:
117
            return
118
        try:
119
            if form.get_widget('family_id').parse():
120
                family = self.update(form, formdata, user, evo)
121
                msg = _('Sucessfully updated the family %s')
122
                log_msg = _('Sucessfully updated the family %(code)s of %(user)s')
123
            else:
124
                family = self.create(form, formdata, user, evo)
125
                msg = _('Sucessfully created the family %s')
126
                log_msg = _('Sucessfully created the family %(code)s of %(user)s')
127
            code_interne = family.code_interne.encode('utf8')
128
            msg = msg % code_interne
129
            logger.info(log_msg, {'code': code_interne, 'user': formdata.get_user()})
130
            form_user = formdata.get_user()
131
            form_user.abelium_domino_code_famille = code_interne
132
            form_user.store()
133
        except Exception, e:
134
            if form.get_widget('family_id').parse():
135
                msg = _('Unable to update family: %s') % str(e)
136
            else:
137
                msg = _('Unable to create family: %s') % str(e)
138
            evo.comment = msg
139
            logger.exception(msg  % formdata.get_user())
140
        else:
141
            evo.comment = msg
142
            wf_status = self.get_target_status()
143
            if wf_status:
144
                evo.status = 'wf-%s' % wf_status[0].id
145
        return False
146

  
147
    def is_available(self, workflow=None):
148
        return get_publisher().has_site_option('domino')
149
    is_available = classmethod(is_available)
150

  
151
register_item_class(AbeliumDominoRegisterFamilyWorkflowStatusItem)
auquotidien/modules/abelium_domino_ws.py
1
# -*- coding: utf-8 -*-
2
from decimal import Decimal
3
import time
4
import datetime
5
from xml.etree import ElementTree as etree
6
import logging
7

  
8
try:
9
    from suds.client import Client
10
    from suds.bindings.binding import Binding
11
    # Webdev is bugged and using an HTML generator to produce XML content, 
12
    Binding.replyfilter = lambda self, x: x.replace('&nbsp;', '&#160;')
13
except ImportError:
14
    Client = None
15
    Binding = None
16

  
17
logger = logging.getLogger(__name__)
18

  
19
# cleaning and parsing functions
20

  
21
LINE_SEPARATOR = '\n'
22
COLUMN_SEPARATOR = '\t'
23

  
24
def unicode_and_strip(x):
25
    return unicode(x).strip()
26

  
27
def strip_and_int(x):
28
    try:
29
        return int(x.strip())
30
    except ValueError:
31
        return None
32

  
33
def strip_and_date(x):
34
    try:
35
        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
36
    except ValueError:
37
        return None
38

  
39
def parse_date(date_string):
40
    if date_string:
41
        return datetime.datetime.strptime(date_string, "%Y%m%d")
42
    else:
43
        None
44

  
45
class DominoException(Exception):
46
    pass
47

  
48
def object_cached(function):
49
    '''Decorate an object method so that its results is cached on the object
50
       instance after the first call.
51
    '''
52
    def decorated_function(self, *args, **kwargs):
53
        cache_name = '__%s_cache' % function.__name__
54
        if not hasattr(self, cache_name):
55
            setattr(self, cache_name, (time.time(), {}))
56
        t, d = getattr(self, cache_name)
57
        if time.time() - t > 80:
58
            setattr(self, cache_name, (time.time(), {}))
59
            t, d = getattr(self, cache_name)
60
        k = tuple(*args) + tuple(sorted(kwargs.items()))
61
        if not k in d:
62
            d[k] = function(self, *args, **kwargs)
63
        return d[k]
64
    return decorated_function
65

  
66
# Data model
67
class SimpleObject(object):
68
    '''Base class for object returned by the web service'''
69

  
70
    '''Describe basic columns'''
71
    COLUMNS = ()
72
    '''Describe extended object columns'''
73
    MORE_COLUMNS = ()
74

  
75
    def __init__(self, **kwargs):
76
        self.__dict__.update(kwargs)
77

  
78
    def __repr__(self):
79
        c = {}
80
        for remote_name, name, converter, desc in self.COLUMNS:
81
            if hasattr(self, name):
82
                c[name] = getattr(self, name)
83
        return str(c)
84

  
85
    def serialize(self):
86
        l = []
87
        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
88
            if local_name == 'id':
89
                continue
90
            v = getattr(self, local_name, None)
91
            if v is None:
92
                continue
93
            if isinstance(v, (datetime.date, datetime.datetime)):
94
                v = v.strftime('%Y%m%d')
95
            if remote_name.endswith('_DA') and '-' in v:
96
                v = v.replace('-', '')
97
            l.append(u'{0}: "{1}"'.format(remote_name, v))
98
        return u','.join(l)
99

  
100
    def debug(self):
101
        '''Output a debugging view of this object'''
102
        res = ''
103
        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
104
            if hasattr(self, name):
105
                res += name + ':' + repr(getattr(self, name)) + '\n'
106
        return res
107

  
108
    def __int__(self):
109
        '''Return the object id'''
110
        return self.id
111

  
112
class UrgentContact(SimpleObject):
113
    COLUMNS = (
114
            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
115
            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
116
            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
117
            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
118
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
119
            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
120
            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
121
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
122
            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
123
            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
124
            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
125
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
126
            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
127
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
128
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
129
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
130
    )
131

  
132
class Child(SimpleObject):
133
    COLUMNS = (
134
            ('IDENFANTS', 'id', strip_and_int, 'Identifiant de ENFANTS'),
135
            ('NOM_CH', 'nom', unicode_and_strip, 'Nom'),
136
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'Prénom'),
137
            ('NAISSANCE_DA', 'date_naissance', strip_and_date, 'Date de Naissance'),
138
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'Commentaires / Notes'),
139
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
140
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'Code Postal'),
141
            ('VILLE_CH', 'ville', unicode_and_strip, 'Ville'),
142
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'Code Interne'),
143
            ('LIEUNAISSANCE_CH', 'lieu_naissance', unicode_and_strip, 'Lieu de Naissance'),
144
            ('DEPNAISSANCE_CH', 'departement_naissance', unicode_and_strip, 'Département Naissance'),
145
            ('NUMSECU_CH', 'num_securite_sociale', unicode_and_strip, 'N° de SECU'),
146
            ('NATIONALITE_CH', 'nationalite', unicode_and_strip, 'Nationalité'),
147
            ('PRENOM2_CH', 'prenom2', unicode_and_strip, 'Prénom 2'),
148
            ('SEXE_CH', 'sexe', unicode_and_strip, 'Sexe'),
149
            ('IDTABLELIBRE1', 'IDTABLELIBRE1', unicode_and_strip, 'IDTABLELIBRE1'),
150
            ('IDTABLELIBRE2', 'IDTABLELIBRE2', unicode_and_strip, 'IDTABLELIBRE2'),
151
            ('IDTABLELIBRE3', 'IDTABLELIBRE3', unicode_and_strip, 'IDTABLELIBRE3'),
152
            ('IDTABLELIBRE4', 'IDTABLELIBRE4', unicode_and_strip, 'IDTABLELIBRE4'),
153
            ('CHAMPLIBRE1_CH', 'CHAMPLIBRE1_CH', unicode_and_strip, 'Valeur Champ Libre 1'),
154
            ('CHAMPLIBRE2_CH', 'CHAMPLIBRE2_CH', unicode_and_strip, 'Valeur Champ Libre 2'),
155
            ('CHAMPCALCULE1_CH', 'CHAMPCALCULE1_CH', unicode_and_strip, 'Valeur Champ Calculé 1'),
156
            ('CHAMPCALCULE2_CH', 'CHAMPCALCULE2_CH', unicode_and_strip, 'Valeur Champ Calculé 2'),
157
            ('SOMMEIL_ME', 'sommeil', unicode_and_strip, 'Sommeil'),
158
            ('ACTIVITE_ME', 'activite', unicode_and_strip, 'Activités'),
159
            ('HABITUDE_ME', 'habitude', unicode_and_strip, 'Habitudes'),
160
            ('PHOTO_CH', 'photographie', unicode_and_strip, 'Photographie'),
161
            ('NUMCOMPTE_CH', 'numcompte', unicode_and_strip, 'N° Compte Comptable'),
162
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'Téléphone'),
163
            ('IDFAMILLES2', 'id_famille2', unicode_and_strip, 'Identifiant famille 2'),
164
            ('PERE_CH', 'pere', unicode_and_strip, 'Nom du père'),
165
            ('MERE_CH', 'mere', unicode_and_strip, 'Nom de la mère'),
166
            ('AUTOPARENTALEMERE_IN', 'autorisation_parentale_mere', unicode_and_strip, 'Autorisation Parentale Mère'),
167
            ('AUTOPARENTALEPERE_IN', 'autorisation_parentale_pere', unicode_and_strip, 'Autorisation Parentale de Père'),
168
            ('IDPORTAIL_ENFANTS', 'id_portail_enfants', unicode_and_strip, 'Identifiant de PORTAIL_ENFANTS'),
169
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'Adresse Internet'),
170
    )
171

  
172
    def save(self):
173
        if hasattr(self, 'id'):
174
            self.client.update_child(self)
175
        else:
176
            self.id = self.client.add_child(self)
177
        self.client.clear_cache()
178

  
179
class Family(SimpleObject):
180
    COLUMNS = (
181
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
182
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
183
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
184
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
185
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
186
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
187
    )
188

  
189
    MORE_COLUMNS = (
190
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
191
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
192
            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
193
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
194
            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
195
            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
196
            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
197
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
198
            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
199
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
200
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
201
            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
202
            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
203
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
204
            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
205
            ('REVENUMENSUEL_MO', 'revenu_mensuel', unicode_and_strip, 'revenu mensuel de la famille'),
206
            ('REVENUANNUEL_MO', 'revenu_annuel', unicode_and_strip, 'revenu annuel de la famille'),
207
            ('QUOTIENTFAMILIAL_MO', 'quotient_familial', unicode_and_strip, 'quotient familial'),
208
            ('NBTOTALENFANTS_EN', 'nb_total_enfants', unicode_and_strip, 'nombre total d\'enfants'),
209
            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge', unicode_and_strip, 'nombre d\'enfants à charge'),
210
            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
211
            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
212
            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere', unicode_and_strip, 'autorisation parentale de père'),
213
            ('DATENAISPERE_DA', 'date_naissance_pere', strip_and_date, 'date de naisance du père'),
214
            ('DEPNAISPERE_EN', 'departement_naissance_pere', unicode_and_strip, 'département de naissance du père'),
215
            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
216
            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
217
            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
218
            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
219
            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
220
            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
221
            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
222
            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
223
            ('TELPERE_LR_IN', 'tel_pere_liste_rouge', unicode_and_strip, 'téléphone liste rouge père'),
224
            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge père'),
225
            ('TEL_LR_IN', 'tel_liste_rourge', unicode_and_strip, 'téléphone liste rouge'),
226
            ('TEL2_LR_IN', 'tel2_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge'),
227
            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
228
            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
229
            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere', unicode_and_strip, 'autorisation parentale mère'),
230
            ('DATENAISMERE_DA', 'date_naissance_mere', strip_and_date, 'date de naissance de la mère'),
231
            ('DEPNAISMERE_EN', 'departement_naissance_mere', unicode_and_strip, 'département de naissance de la mère'),
232
            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
233
            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
234
            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere', unicode_and_strip, 'revenu mensuel du père'),
235
            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
236
            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
237
            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
238
            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
239
            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere', unicode_and_strip, 'revenu mensuel mère'),
240
            ('REVANNUELPERE_MO', 'revenu_annuel_pere', unicode_and_strip, 'revenu annuel père'),
241
            ('REVANNUELMERE_MO', 'revenu_annuel_mere', unicode_and_strip, 'revenu annuel mère'),
242
            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
243
            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
244
            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge', unicode_and_strip, 'téléphone liste rouge mère'),
245
            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge mère'),
246
            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
247
            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
248
            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
249
            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
250
            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
251
            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
252
            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
253
            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
254
            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
255
            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
256
            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
257
            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
258
            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
259
            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
260
            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
261
            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
262
            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
263
            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
264
            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
265
            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
266
            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
267
            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
268
            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
269
            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
270
            ('TOTALDU_MO', 'total_du', unicode_and_strip, 'total dû'),
271
            ('TOTALREGLE_MO', 'total_regle', unicode_and_strip, 'total réglé'),
272
            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
273
            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
274
            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
275
            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
276
            ('RIVOLI_EN', 'code_rivoli', unicode_and_strip, 'identifiant code rivoli'),
277
            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
278
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
279
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
280
            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
281
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'commentaires / notes'),
282
            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
283
            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
284
            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
285
            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
286
            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
287
            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
288
            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
289
            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
290
            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
291
            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
292
            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
293
            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
294
            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
295
            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
296
            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
297
            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
298
            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
299
            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
300
            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
301
            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
302
            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
303
            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
304
            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
305
            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
306
            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
307
            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
308
            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
309
#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
310
#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
311
            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
312
            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
313
            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
314
            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
315
            ('ECHEANCEASSURANCE_DA', 'echeance_assurance', unicode_and_strip, 'date echéance assurance'),
316
            ('RM_MIKADO_MO', 'rm_mikado', unicode_and_strip, 'revenus mensuels mikado'),
317
            ('RA_MIKADO_MO', 'ra_mikado', unicode_and_strip, 'revenus annuels mikado'),
318
            ('QF_MIKADO_MO', 'qf_mikado', unicode_and_strip, 'quotient familial mikado'),
319
            ('RM_DIABOLO_MO', 'rm_diabolo', unicode_and_strip, 'revenus mensuels diabolo'),
320
            ('RA_DIABOLO_MO', 'ra_diabolo', unicode_and_strip, 'revenus annuels diabolo'),
321
            ('QF_DIABOLO_MO', 'qf_diabolo', unicode_and_strip, 'quotient familial diabolo'),
322
            ('RM_OLIGO_MO', 'rm_oligo', unicode_and_strip, 'revenus mensuels oligo'),
323
            ('RA_OLIGO_MO', 'ra_oligo', unicode_and_strip, 'revenus annuels oligo'),
324
            ('QF_OLIGO_MO', 'qf_oligo', unicode_and_strip, 'quotient familial oligo'),
325
            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado', unicode_and_strip, 'date d\'application des revenus de mikado'),
326
            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo', unicode_and_strip, 'date d\'application des revenus de diabolo'),
327
            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo', unicode_and_strip, 'date d\'application des revenus de oligo'),
328
    )
329

  
330
    def __init__(self, *args, **kwargs):
331
        self.children = []
332
        super(Family, self).__init__(*args, **kwargs)
333

  
334
    def complete(self):
335
        k = [a for a,b,c,d in self.MORE_COLUMNS]
336
        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
337
                columns=self.MORE_COLUMNS, instances=(self,)))
338
        l = self.client.get_children(self.id).values()
339
        self.children = sorted(l, key=lambda c: c.id)
340
        return self
341

  
342
    @property
343
    def invoices(self):
344
        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
345

  
346
    def add_child(self, child):
347
        if hasattr(self, 'id'):
348
            child.id_famille = self.id
349
            child.client = self.client
350
        self.children.append(child)
351

  
352
    def save(self):
353
        if hasattr(self, 'id'):
354
            self.client.update_family(self)
355
        else:
356
            self.code_interne = self.client.new_code_interne()
357
            self.id = self.client.add_family(self)
358
        for child in self.children:
359
            child.id_famille = self.id
360
            child.save()
361
        self.client.clear_cache()
362

  
363
class Invoice(SimpleObject):
364
    COLUMNS = (
365
        ('', 'id_famille', int, ''),
366
        ('', 'id', int, ''),
367
        ('', 'numero', str, ''),
368
        ('', 'debut_periode', parse_date, ''),
369
        ('', 'fin_periode', parse_date, ''),
370
        ('', 'creation', parse_date, ''),
371
        ('', 'echeance', parse_date, ''),
372
        ('', 'montant', Decimal, ''),
373
        ('', 'reste_du', Decimal, ''),
374
    )
375
    _detail = {}
376

  
377
    def detail(self):
378
        if not self._detail:
379
            self.client.factures_detail([self])
380
        return self._detail
381

  
382
    @property
383
    def family(self):
384
        return self.client.families[self.id_famille]
385

  
386
    def paid(self):
387
        return self.reste_du == Decimal(0)
388

  
389
class DominoWs(object):
390
    '''Interface to the WebService exposed by Abelium Domino.
391

  
392
       It allows to retrieve family and invoices.
393

  
394
       Beware that it does a lot of caching to limit call to the webservice, so
395
       if you need fresh data, call clear_cache()
396

  
397
       All family are in the families dictionnary and all invoices in the
398
       invoices dictionnary.
399
    '''
400

  
401
    def __init__(self, url, domain, login, password, location=None,
402
            logger=logger):
403
        if not Client:
404
            raise ValueError('You need python suds')
405
        self.logger = logger
406
        self.logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)',
407
                url, domain, login, password, location)
408
        self.url = url
409
        self.domain = domain
410
        self.login = login
411
        self.password = password
412
        self.client = Client(url, location=location, timeout=60)
413
        self.client.options.cache.setduration(seconds=60)
414

  
415
    def clear_cache(self):
416
        '''Remove cached attributes from the instance.'''
417

  
418
        for key, value in self.__dict__.items():
419
            if key.startswith('__') and key.endswith('_cache'):
420
                del self.__dict__[key]
421

  
422
    def call(self, function_name, *args):
423
        '''Call SOAP method named function_name passing args list as parameters.
424

  
425
           Any error is converted into the DominoException class.'''
426
        print 'call', function_name, args
427

  
428
        try:
429
            self.logger.debug(('soap call to %s(%s)' % (function_name, args)).encode('utf-8'))
430
            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
431
            self.logger.debug((u'result: %s' % data).encode('utf-8'))
432
            self.data = data
433
        except IOError, e:
434
            raise DominoException('Erreur IO', e)
435
        if data is None:
436
           data = ''
437
        if data.startswith('ERREUR'):
438
            raise DominoException(data[9:].encode('utf8'))
439
        return data
440

  
441
    def parse_tabular_data(self, data):
442
        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
443
           Empty lines (ignoring spaces) are ignored.
444
        '''
445

  
446
        rows = data.split(LINE_SEPARATOR)
447
        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
448
        return rows
449

  
450
    def __call__(self, function_name, cls=None, args=[], instances=None, columns=None):
451
        '''Call SOAP method named function_name, splitlines, map tab separated
452
        values to _map keys in a dictionnary, and use this dictionnary to
453
        initialize an object of class cls.
454

  
455
        - If instances is present, the given instances are updated with the
456
          returned content, in order, row by row.
457
        - If cls is not None and instances is None, a new instance of the class
458
          cls is instancied for every row and initialized with the content of
459
          the row.
460
        - If cls and instances are None, the raw data returned by the SOAP call
461
          is returned.
462
        '''
463

  
464
        data = self.call(function_name, *args)
465
        if cls or instances:
466
            rows = self.parse_tabular_data(data)
467
            kwargs = {}
468
            if instances:
469
                rows = zip(rows, instances)
470
            for row in rows:
471
                if instances:
472
                    row, instance = row
473
                if not row[0]:
474
                    continue
475
                for a, b in zip(columns or cls.COLUMNS, row):
476
                    x, name, converter, desc = a
477
                    kwargs[name] = converter(b.strip())
478
                if instances:
479
                    instance.__dict__.update(kwargs)
480
                    yield instance
481
                else:
482
                    yield cls(client=self, **kwargs)
483
        else:
484
            yield data
485

  
486
    def add_family(self, family):
487
        result = self.call('AJOUTER_FAMILLE', family.serialize())
488
        return int(result.strip())
489

  
490
    def update_family(self, family):
491
        if not hasattr(family, 'id'):
492
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
493
        result = self.call('MODIFIER_FAMILLE', unicode(family.id), family.serialize())
494
        return result.strip() == 'OK'
495

  
496
    def add_child(self, child):
497
        result = self.call('AJOUTER_ENFANT', child.serialize())
498
        return int(result.strip())
499

  
500
    def update_child(self, child):
501
        if not hasattr(child, 'id'):
502
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
503
        result = self.call('MODIFIER_ENFANT', unicode(child.id), child.serialize())
504
        return result.strip() == 'OK'
505

  
506
    @property
507
    @object_cached
508
    def families(self):
509
        '''Dictionary of all families indexed by their id.
510

  
511
           After the first use, the value is cached. Use clear_cache() to reset
512
           it.
513
        '''
514

  
515
        return self.get_families()
516

  
517
    def get_families(self, id_famille=0, full=False):
518
        '''Get families informations.
519
           There is no caching.
520

  
521
           id_famille - if not 0, the family with this id is retrieved. If 0
522
           all families are retrieved. Default to 0.
523
           full - If True return all the columns of the family table. Default
524
           to False.
525
        '''
526
        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
527
        families = self('LISTER_FAMILLES',
528
                Family,
529
                args=(','.join([x[0] for x in columns]), id_famille))
530
        return dict([(int(x), x) for x in families])
531

  
532
    def get_children(self, id_famille=0):
533
        columns = Child.COLUMNS
534
        if id_famille == 0:
535
            children = self('LISTER_ENFANTS',
536
                Child,
537
                args=((','.join([x[0] for x in columns])),))
538
        else:
539
            children = self('LISTER_ENFANTS_FAMILLE',
540
                Child,
541
                args=(id_famille, (','.join([x[0] for x in columns]))))
542
        return dict([(int(x), x) for x in children])
543

  
544
    def get_urgent_contacts(self, id_enfant):
545
        columns = UrgentContact.COLUMNS
546
        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
547
                UrgentContact,
548
                args=((id_enfant, ','.join([x[0] for x in columns]))))
549
        return dict([(int(x), x) for x in urgent_contacts])
550

  
551
    @property
552
    @object_cached
553
    def invoices(self):
554
        '''Dictionnary of all invoices indexed by their id.
555

  
556
           After the first use, the value is cached. Use clear_cache() to reset
557
           it.
558
        '''
559
        invoices = self.get_invoices()
560
        for invoice in invoices.values():
561
            invoice.famille = self.families[invoice.id_famille]
562
        return invoices
563

  
564
    def new_code_interne(self):
565
        max_ci = 0
566
        for family in self.families.values():
567
            try:
568
                max_ci = max(max_ci, int(family.code_interne))
569
            except:
570
                pass
571
        return '%05d' % (max_ci+1)
572

  
573
    def get_invoices(self, id_famille=0, state='TOUTES'):
574
        '''Get invoices informations.
575

  
576
           id_famille - If value is not 0, only invoice for the family with
577
           this id are retrieved. If value is 0, invoices for all families are
578
           retrieved. Default to 0.
579
           etat - state of the invoices to return, possible values are
580
           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
581
        '''
582
        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
583
            args=(id_famille, state))
584
        invoices = list(invoices)
585
        for invoice in invoices:
586
            invoice.famille = self.families[invoice.id_famille]
587
        return dict(((int(x), x) for x in invoices))
588

  
589
    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
590
    def factures_detail(self, invoices):
591
        '''Retrieve details of some invoice'''
592
        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
593
        try:
594
            tree = etree.fromstring(data.encode('utf8'))
595
            for invoice, facture_node in zip(invoices, tree.findall('facture')):
596
                rows = []
597
                for ligne in facture_node.findall('detail_facture/ligne'):
598
                    row = []
599
                    rows.append(row)
600
                    for header in self.FACTURE_DETAIL_HEADERS:
601
                        if header in ligne.attrib:
602
                            row.append((header, ligne.attrib[header]))
603
                etablissement = facture_node.find('detail_etablissements/etablissement')
604
                if etablissement is not None:
605
                    nom = etablissement.get('nom').strip()
606
                else:
607
                    nom = ''
608
                d = { 'etablissement': nom, 'lignes': rows }
609
                invoice._detail = d
610
        except Exception, e:
611
            raise DominoException('Exception when retrieving invoice details', e)
612

  
613
    def get_family_by_mail(self, email):
614
        '''Return the first whose one email attribute matches the given email'''
615
        for famille in self.families.values():
616
            if email in (famille.email_pere, famille.email_mere,
617
                    famille.adresse_internet):
618
                return famille
619
        return None
620

  
621
    def get_family_by_code_interne(self, code_interne):
622
        '''Return the first whose one email attribute matches the given email'''
623
        for famille in self.families.values():
624
            if getattr(famille, 'code_interne', None) == code_interne:
625
                return famille
626
        return None
627

  
628
    def pay_invoice(self, id_invoices, amount, other_information, date=None):
629
        '''Notify Domino of the payment of some invoices.
630

  
631
           id_invoices - integer if of the invoice or Invoice instances
632
           amount - amount as a Decimal object
633
           other_information - free content to attach to the payment, like a
634
           bank transaction number for correlation.
635
           date - date of the payment, must be a datetime object. If None,
636
           now() is used. Default to None.
637
        '''
638

  
639
        if not date:
640
            date = datetime.datetime.now()
641
        due = sum([self.invoices[int(id_invoice)].reste_du
642
                 for id_invoice in id_invoices])
643
        if Decimal(amount) == Decimal(due):
644
            return self('SOLDER_FACTURE', None, args=(str(amount), 
645
                    ''.join([ '%s;' % int(x) for x in id_invoices]),
646
                    date.strftime('%Y-%m-%d'), other_information))
647
        else:
648
            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
auquotidien/modules/admin.py
1
import os
2

  
3
from quixote import get_publisher, redirect
4
from quixote.directory import Directory
5
from quixote.html import htmltext, TemplateIO
6

  
7
import wcs.admin.root
8
import wcs.root
9
from wcs.roles import get_user_roles
10

  
11
from qommon import _
12
from qommon import errors, get_cfg
13
from qommon.form import *
14

  
15
import wcs.admin.settings
16
from wcs.formdef import FormDef
17
from wcs.categories import Category
18
from qommon.backoffice.menu import html_top
19

  
20
from events import get_default_event_tags
21
import re
22
from abelium_domino_ui import AbeliumDominoDirectory
23

  
24
class AdminRootDirectory(wcs.admin.root.RootDirectory):
25
    def __init__(self):
26
        self._q_exports = wcs.admin.root.RootDirectory._q_exports
27
        self.menu_items[-1] = ('/', N_('Publik'))
28

  
29
    def get_intro_text(self):
30
        return _('Welcome on Publik administration interface')
31

  
32

  
33
class PanelDirectory(Directory):
34
    _q_exports = ['', 'update', 'announces', 'permissions', 'event_keywords',
35
            'announce_themes', 'strongbox', 'clicrdv', 'domino']
36
    label = N_('Control Panel')
37

  
38
    domino = AbeliumDominoDirectory()
39

  
40
    def _verify_mask(self, form):
41
        if form.is_submitted():
42
            if not re.match("[0-9Xx]*$", form.get('mobile_mask') or ''):
43
                form.set_error('mobile_mask', _('Invalid value'))
44
            else:
45
                string_value = form.get_widget('mobile_mask').value
46
                if string_value:
47
                    form.get_widget('mobile_mask').value = string_value.upper()
48

  
49
    def announces(self):
50
        announces_cfg = get_cfg('announces', {})
51
        sms_cfg = get_cfg('sms', {})
52
        form = Form(enctype='multipart/form-data')
53
        hint = ""
54
        if sms_cfg.get('mode','') in ("none",""):
55
            hint = htmltext(_('You must also <a href="%s">configure your SMS provider</a>') % "../settings/sms")
56

  
57
        form.add(CheckboxWidget, 'sms_support', title = _('SMS support'),
58
                hint = hint, value = announces_cfg.get('sms_support', 0))
59
        form.add(StringWidget, 'mobile_mask', title = _('Mask for mobile numbers'),
60
                hint = _('example: 06XXXXXXXX'),
61
                value = announces_cfg.get('mobile_mask',''))
62
        form.add_submit('submit', _('Submit'))
63
        form.add_submit('cancel', _('Cancel'))
64

  
65
        self._verify_mask(form)
66

  
67
        if form.get_widget('cancel').parse():
68
            return redirect('..')
69

  
70
        if not form.is_submitted() or form.has_errors():
71
            get_response().breadcrumb.append(('aq/announces', _('Announces Options')))
72
            html_top('settings', _('Announces Options'))
73
            r = TemplateIO(html=True)
74
            r += htmltext('<h2>%s</h2>') % _('Announces Options')
75
            r += form.render()
76
            return r.getvalue()
77
        else:
78
            from wcs.admin.settings import cfg_submit
79
            cfg_submit(form, 'announces', ('sms_support','mobile_mask'))
80
            return redirect('..')
81

  
82
    def permissions(self):
83
        permissions_cfg = get_cfg('aq-permissions', {})
84
        form = Form(enctype='multipart/form-data')
85
        form.add(SingleSelectWidget, 'forms', title = _('Admin role for forms'),
86
                value = permissions_cfg.get('forms', None),
87
                options = [(None, _('Nobody'), None)] + get_user_roles())
88
        form.add(SingleSelectWidget, 'events', title = _('Admin role for events'),
89
                value = permissions_cfg.get('events', None),
90
                options = [(None, _('Nobody'), None)] + get_user_roles())
91
        form.add(SingleSelectWidget, 'links', title = _('Admin role for links'),
92
                value = permissions_cfg.get('links', None),
93
                options = [(None, _('Nobody'), None)] + get_user_roles())
94
        form.add(SingleSelectWidget, 'announces', title = _('Admin role for announces'),
95
                value = permissions_cfg.get('announces', None),
96
                options = [(None, _('Nobody'), None)] + get_user_roles())
97
        form.add(SingleSelectWidget, 'payments', title = _('Admin role for payments'),
98
                value = permissions_cfg.get('payments', None),
99
                options = [(None, _('Nobody'), None)] + get_user_roles())
100
        form.add(SingleSelectWidget, 'strongbox', title = _('Admin role for strongbox'),
101
                value = permissions_cfg.get('strongbox', None),
102
                options = [(None, _('Nobody'), None)] + get_user_roles())
103
        form.add_submit('submit', _('Submit'))
104
        form.add_submit('cancel', _('Cancel'))
105

  
106
        if form.get_widget('cancel').parse():
107
            return redirect('..')
108

  
109
        if not form.is_submitted() or form.has_errors():
110
            get_response().breadcrumb.append(('aq/permissions', _('Permissions')))
111
            html_top('settings', _('Permissions'))
112
            r = TemplateIO(html=True)
113
            r += htmltext('<h2>%s</h2>') % _('Permissions')
114
            r += form.render()
115
            return r.getvalue()
116
        else:
117
            from wcs.admin.settings import cfg_submit
118
            cfg_submit(form, 'aq-permissions',
119
                        ('forms', 'events', 'links', 'announces', 'payments', 'strongbox'))
120
            return redirect('..')
121

  
122
    def event_keywords(self):
123
        misc_cfg = get_cfg('misc', {})
124
        form = Form(enctype='multipart/form-data')
125
        form.add(WidgetList, 'event_tags', title = _('Event Keywords'),
126
                value = misc_cfg.get('event_tags', get_default_event_tags()),
127
                elemnt_type = StringWidget,
128
                add_element_label = _('Add Keyword'),
129
                element_kwargs = {str('render_br'): False, str('size'): 30})
130

  
131
        form.add_submit('submit', _('Submit'))
132
        form.add_submit('cancel', _('Cancel'))
133

  
134
        if form.get_widget('cancel').parse():
135
            return redirect('..')
136

  
137
        if not form.is_submitted() or form.has_errors():
138
            get_response().breadcrumb.append(('aq/event_keywords', _('Event Keywords')))
139
            html_top('settings', _('Event Keywords'))
140
            r = TemplateIO(html=True)
141
            r += htmltext('<h2>%s</h2>') % _('Event Keywords')
142
            r += form.render()
143
            return r.getvalue()
144
        else:
145
            from wcs.admin.settings import cfg_submit
146
            cfg_submit(form, 'misc', ('event_tags',))
147
            return redirect('..')
148

  
149
    def announce_themes(self):
150
        misc_cfg = get_cfg('misc', {})
151
        form = Form(enctype='multipart/form-data')
152
        form.add(WidgetList, 'announce_themes', title = _('Announce Themes'),
153
                value = misc_cfg.get('announce_themes', []),
154
                elemnt_type = StringWidget,
155
                add_element_label = _('Add Theme'),
156
                element_kwargs = {str('render_br'): False, str('size'): 30})
157

  
158
        form.add_submit('submit', _('Submit'))
159
        form.add_submit('cancel', _('Cancel'))
160

  
161
        if form.get_widget('cancel').parse():
162
            return redirect('..')
163

  
164
        if not form.is_submitted() or form.has_errors():
165
            get_response().breadcrumb.append(('aq/announce_themes', _('Announce Themes')))
166
            html_top('settings', _('Announce Themes'))
167
            r = TemplateIO(html=True)
168
            r += htmltext('<h2>%s</h2>') % _('Announce Themes')
169
            r += form.render()
170
            return r.getvalue()
171
        else:
172
            from wcs.admin.settings import cfg_submit
173
            cfg_submit(form, 'misc', ('announce_themes',))
174
            return redirect('..')
175

  
176
    def strongbox(self):
177
        if not get_publisher().has_site_option('strongbox'):
178
            raise errors.TraversalError()
179
        misc_cfg = get_cfg('misc', {})
180
        form = Form(enctype='multipart/form-data')
181
        form.add(CheckboxWidget, 'aq-strongbox', title=_('Strongbox Support'),
182
                value=misc_cfg.get('aq-strongbox'), required=False)
183

  
184
        form.add_submit('submit', _('Submit'))
185
        form.add_submit('cancel', _('Cancel'))
186

  
187
        if form.get_widget('cancel').parse():
188
            return redirect('..')
189

  
190
        if not form.is_submitted() or form.has_errors():
191
            get_response().breadcrumb.append(('aq/strongbox', _('Strongbox Support')))
192
            html_top('settings', _('Strongbox Support'))
193
            r = TemplateIO(html=True)
194
            r += htmltext('<h2>%s</h2>') % _('Strongbox Support')
195
            r += form.render()
196
            return r.getvalue()
197
        else:
198
            from wcs.admin.settings import cfg_submit
199
            cfg_submit(form, 'misc', ('aq-strongbox',))
200
            return redirect('..')
201

  
202
    def clicrdv(self):
203
        if not get_publisher().has_site_option('clicrdv'):
204
            raise errors.TraversalError()
205
        misc_cfg = get_cfg('misc', {})
206
        form = Form(enctype='multipart/form-data')
207
        form.add(SingleSelectWidget, 'aq-clicrdv-server', title=_('ClicRDV Server'),
208
                value=misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), required=True,
209
                options=[(str('www.clicrdv.com'), _('Production Server')),
210
                         (str('sandbox.clicrdv.com'), _('Sandbox Server'))])
211
        form.add(StringWidget, 'aq-clicrdv-api-key', title=_('API Key'),
212
                value=misc_cfg.get('aq-clicrdv-api-key'), required=False,
213
                size=40, hint=_('Empty to disable ClicRDV support'))
214
        form.add(StringWidget, 'aq-clicrdv-api-username', title=_('Username'),
215
                value=misc_cfg.get('aq-clicrdv-api-username'), required=False)
216
        form.add(StringWidget, 'aq-clicrdv-api-password', title=_('Password'),
217
                value=misc_cfg.get('aq-clicrdv-api-password'), required=False)
218

  
219
        form.add_submit('submit', _('Submit'))
220
        form.add_submit('cancel', _('Cancel'))
221

  
222
        if form.get_widget('cancel').parse():
223
            return redirect('..')
224

  
225
        if not form.is_submitted() or form.has_errors():
226
            get_response().breadcrumb.append(('aq/clicrdv', _('ClicRDV Integration')))
227
            html_top('settings', _('ClicRDV Integration'))
228
            r = TemplateIO(html=True)
229
            r += htmltext('<h2>%s</h2>') % _('ClicRDV Integration')
230
            r += form.render()
231
            r += htmltext('<p>%s</p>') % _('Available Interventions: ')
232
            try:
233
                from clicrdv import get_all_intervention_sets
234
                intervention_sets = get_all_intervention_sets()
235
                r += htmltext('<ul>')
236
                for s in intervention_sets:
237
                    r += htmltext('<li><strong>clicrdv_get_interventions_in_set(%s)</strong> - %s') % (
238
                            s['id'], s['name'])
239
                    r += htmltext('<ul>')
240
                    for n, intervention in s['interventions']:
241
                        r += htmltext('<li>%s (id: %s)</li>') % (intervention, n)
242
                    r += htmltext('</ul></li>')
243
                r += htmltext('</ul>')
244
            except Exception, e:
245
                r += htmltext('<p>%s (%s)</p>') % (
246
                        _('Cannot access to ClicRDV service'), str(e))
247
            return r.getvalue()
248
        else:
249
            from wcs.admin.settings import cfg_submit
250
            cfg_submit(form, 'misc', ('aq-clicrdv-server',
251
                                      'aq-clicrdv-api-key',
252
                                      'aq-clicrdv-api-username',
253
                                      'aq-clicrdv-api-password'))
254
            return redirect('..')
255

  
256

  
257
class SettingsDirectory(wcs.admin.settings.SettingsDirectory):
258
    def _q_index(self):
259
        r = TemplateIO(html=True)
260
        r += htmltext(super(SettingsDirectory, self)._q_index())
261
        r += htmltext('<div class="splitcontent-right">')
262
        r += htmltext('<div class="bo-block">')
263
        r += htmltext('<h2>%s</h2>') % _('Extra Options')
264
        r += htmltext('<ul>')
265
        r += htmltext('<li><a href="aq/announces">%s</a></li>') % _('Announces Options')
266
        r += htmltext('<li><a href="aq/permissions">%s</a></li>') % _('Permissions')
267
        r += htmltext('<li><a href="aq/event_keywords">%s</a></li>') % _('Event Keywords')
268
        r += htmltext('<li><a href="aq/announce_themes">%s</a></li>') % _('Announce Themes')
269
        if get_publisher().has_site_option('strongbox'):
270
            r += htmltext('<li><a href="aq/strongbox">%s</a></li>') % _('Strongbox Support')
271
        if get_publisher().has_site_option('clicrdv'):
272
            r += htmltext('<li><a href="aq/clicrdv">%s</a></li>') % _('ClicRDV Integration')
273
        if get_publisher().has_site_option('domino'):
274
            r += htmltext('<li><a href="aq/domino">%s</a></li>') % _('Abelium Domino Integration')
275
        r += htmltext('</ul>')
276
        r += htmltext('</div>')
277
        r += htmltext('</div>')
278
        return r.getvalue()
279

  
280
    def _q_lookup(self, component):
281
        if component == 'aq':
282
            return PanelDirectory()
283
        return super(SettingsDirectory, self)._q_lookup(component)
284

  
285
import categories_admin
auquotidien/modules/agenda.py
1
import time
2
import datetime
3
from sets import Set
4

  
5
from quixote.directory import Directory
6
from quixote import get_publisher, get_request, redirect, get_session, get_response
7
from quixote.html import htmltext, TemplateIO
8

  
9
from qommon import _
10
from qommon import misc, template, errors, get_cfg
11
from qommon.form import *
12

  
13
from events import Event, RemoteCalendar, get_default_event_tags
14

  
15

  
16
class TagDirectory(Directory):
17
    def _q_lookup(self, component):
18
        events = Event.select()
19
        for remote_calendar in RemoteCalendar.select():
20
            if remote_calendar.events:
21
                events.extend(remote_calendar.events)
22
        self.events = [x for x in events if component in (x.keywords or [])]
23
        self.events.sort(lambda x,y: cmp(x.date_start, y.date_start))
24
        self.tag = component
25
        return self.display_events()
26

  
27
    def display_events(self):
28
        template.html_top(_('Agenda'))
29
        r = TemplateIO(html=True)
30
        if len(self.events) > 1:
31
            r += htmltext('<p id="nb-events">')
32
            r += _('%(nb)d events with %(keyword)s keyword') % {
33
                    'nb': len(self.events),
34
                    'keyword': self.tag
35
            }
36
            r += htmltext('</p>')
37

  
38
        if self.events:
39
            r += htmltext('<dl id="events">')
40
            for ev in self.events:
41
                r += htmltext(ev.as_html_dt_dd())
42
            r += htmltext('</dl>')
43
        else:
44
            r += htmltext('<p id="nb-events">')
45
            r += _('No event registered with the %s keyword.') % self.tag
46
            r += htmltext('</p>')
47
        return r.getvalue()
48

  
49

  
50
class AgendaDirectory(Directory):
51
    _q_exports = ['', 'icalendar', 'tag', 'atom', 'filter']
52

  
53
    year = None
54
    month = None
55

  
56
    tag = TagDirectory()
57

  
58
    def _q_traverse(self, path):
59
        get_response().breadcrumb.append(('agenda/', _('Agenda')))
60
        self.year, self.month = time.localtime()[:2]
61
        if len(path) >= 1 and path[0].isdigit():
62
            self.year, self.month = (None, None)
63
            self.year = int(path[0])
64
            get_response().breadcrumb.append(('%s/' % self.year, self.year))
65
            path = path[1:]
66
            if len(path) >= 1 and path[0] in [str(x) for x in range(1, 13)]:
67
                self.month = int(path[0])
68
                get_response().breadcrumb.append(('%s/' % self.month,
69
                            misc.get_month_name(self.month)))
70
                path = path[1:]
71
            if len(path) == 0:
72
                return redirect(get_request().get_path() + '/')
73
        return Directory._q_traverse(self, path)
74

  
75
    def _q_index(self):
76
        if self.month:
77
            r = TemplateIO(html=True)
78
            r += htmltext(self.display_month_links())
79
            r += htmltext(self.display_month())
80
            return r.getvalue()
81
        else:
82
            return redirect('..')
83

  
84
    def display_month(self):
85
        template.html_top(_('Agenda'))
86
        events = Event.select()
87
        remote_cal = get_request().form.get('cal')
88
        if remote_cal != 'local':
89
            if remote_cal:
90
                try:
91
                    events = RemoteCalendar.get(remote_cal).events
92
                except KeyError:
93
                    raise errors.TraversalError()
94
                if not events:
95
                    events = []
96
            else:
97
                for remote_calendar in RemoteCalendar.select():
98
                    if remote_calendar.events:
99
                        events.extend(remote_calendar.events)
100
        events = [x for x in events if x.in_month(self.year, self.month)]
101
        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
102

  
103
        r = TemplateIO(html=True)
104
        if events:
105
            if len(events) > 1:
106
                r += htmltext('<p id="nb-events">')
107
                r += _('%(nb)d events for %(month_name)s %(year)s') % {
108
                        'nb': len(events),
109
                        'month_name': misc.get_month_name(self.month),
110
                        'year': self.year}
111
                r += htmltext('</p>')
112

  
113
            r += htmltext('<dl id="events">')
114
            for ev in events:
115
                r += htmltext(ev.as_html_dt_dd())
116
            r += htmltext('</dl>')
117
        else:
118
            r += htmltext('<p id="nb-events">')
119
            r += _('No event registered for the month of %s.') % '%s %s' % (
120
                    misc.get_month_name(self.month), self.year)
121
            r += htmltext('</p>')
122

  
123
        root_url = get_publisher().get_root_url()
124
        r += htmltext('<div id="agenda-subs">')
125
        r += htmltext('<p>')
126
        r += _('You can subscribe to this calendar:')
127
        r += htmltext('</p>')
128
        r += htmltext('<ul>')
129
        r += htmltext('  <li><a href="%sagenda/icalendar" id="par_ical">%s</a></li>') % (
130
                root_url, _('iCalendar'))
131
        r += htmltext('  <li><a href="%sagenda/atom" id="par_rss">%s</a></li>') % (
132
                root_url, _('Feed'))
133
        r += htmltext('</ul>')
134
        r += htmltext('</div>')
135
        return r.getvalue()
136

  
137
    def display_month_links(self):
138
        today = datetime.date(*(time.localtime()[:2] + (1,)))
139
        r = TemplateIO(html=True)
140
        r += htmltext('<ul id="month-links">')
141
        for i in range(12):
142
            r += htmltext('<li>')
143
            if (today.year, today.month) == (self.year, self.month):
144
                r += htmltext('<strong>')
145
                r += '%s %s' % (misc.get_month_name(today.month), today.year)
146
                r += htmltext('</strong>')
147
            else:
148
                root_url = get_publisher().get_root_url()
149
                r += htmltext('<a href="%sagenda/%s/%s/">') % (root_url, today.year, today.month)
150
                r += '%s %s' % (misc.get_month_name(today.month), today.year)
151
                r += htmltext('</a>')
152
            r += htmltext('</li>')
153
            today += datetime.timedelta(31)
154
        r += htmltext('</ul>')
155
        return r.getvalue()
156

  
157
    def display_remote_calendars(self):
158
        r = TemplateIO(html=True)
159
        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
160
        if not remote_calendars:
161
            return
162
        remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
163
        r += htmltext('<p class="tags">')
164
        remote_cal = get_request().form.get('cal')
165
        agenda_root_url = get_publisher().get_root_url() + 'agenda/'
166
        if remote_cal:
167
            r += htmltext('<a href="%s">%s</a> ') % (agenda_root_url, _('All'))
168
        else:
169
            r += htmltext('<strong><a href="%s">%s</a></strong> ') % (agenda_root_url, _('All'))
170
        if remote_cal != 'local':
171
            r += htmltext('<a href="%s?cal=local">%s</a> ') % (agenda_root_url, _('Local'))
172
        else:
173
            r += htmltext('<strong><a href="%s?cal=local">%s</a></strong> ') % (agenda_root_url, _('Local'))
174
        for cal in remote_calendars:
175
            if remote_cal == str(cal.id):
176
                r += htmltext('<strong><a href="%s?cal=%s">%s</a></strong> ') % (
177
                        agenda_root_url, cal.id, cal.label)
178
            else:
179
                r += htmltext('<a href="%s?cal=%s">%s</a> ') % (agenda_root_url, cal.id, cal.label)
180
        r += htmltext('</p>')
181
        return r.getvalue()
182

  
183
    def icalendar(self):
184
        if not Event.keys():
185
            raise errors.TraversalError()
186
        response = get_response()
187
        response.set_content_type('text/calendar', 'utf-8')
188
        vcal = Event.as_vcalendar()
189
        if type(vcal) is unicode:
190
            return vcal.encode('utf-8')
191
        else:
192
            return vcal
193

  
194
    def atom(self):
195
        response = get_response()
196
        response.set_content_type('application/atom+xml')
197

  
198
        from pyatom import pyatom
199
        xmldoc = pyatom.XMLDoc()
200
        feed = pyatom.Feed()
201
        xmldoc.root_element = feed
202
        feed.title = get_cfg('misc', {}).get('sitename', 'Publik') + ' - ' + _('Agenda')
203
        feed.id = get_request().get_url()
204

  
205
        author_email = get_cfg('emails', {}).get('reply_to')
206
        if not author_email:
207
            author_email = get_cfg('emails', {}).get('from')
208
        if author_email:
209
            feed.authors.append(pyatom.Author(author_email))
210

  
211
        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
212

  
213
        year, month = time.localtime()[:2]
214
        nyear, nmonth = year, month+1
215
        if nmonth > 12:
216
            nyear, nmonth = nyear+1, 1
217

  
218
        events = [x for x in Event.select() if x.in_month(year, month) or x.in_month(nyear, nmonth)]
219
        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
220
        events.reverse()
221

  
222
        for item in events:
223
            entry = item.get_atom_entry()
224
            if entry is not None:
225
                feed.entries.append(entry)
226

  
227
        return str(feed)
228

  
229
    def filter(self, no_event=False):
230
        template.html_top(_('Agenda'))
231
        tags = get_cfg('misc', {}).get('event_tags')
232
        if not tags:
233
            tags = get_default_event_tags()
234
        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
235

  
236
        form = Form(enctype='multipart/form-data')
237
        if tags and remote_calendars:
238
            form.widgets.append(HtmlWidget('<table id="agenda-filter"><tr><td>'))
239
        if tags:
240
            form.add(CheckboxesWidget, 'tags', title=_('Tags'),
241
                    options=[(x,x) for x in tags],
242
                    inline=False)
243
        if tags and remote_calendars:
244
            form.widgets.append(HtmlWidget('</td><td>'))
245
        if remote_calendars:
246
            remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
247
            form.add(CheckboxesWidget, 'calendars', title=_('Calendars'),
248
                    options=[('local', _('Local'))] + [(x.id, x.label) for x in remote_calendars],
249
                    inline=False)
250
        if tags and remote_calendars:
251
            form.widgets.append(HtmlWidget('</td></tr></table>'))
252

  
253
        form.add_submit('submit', _('Submit'))
254
        form.add_submit('cancel', _('Cancel'))
255
        if form.get_widget('cancel').parse():
256
            return redirect('.')
257

  
258
        if no_event or not form.is_submitted():
259
            r = TemplateIO(html=True)
260
            if no_event:
261
                r += htmltext('<p id="nb-events">')
262
                r += _('No events matching the filter.')
263
                r += htmltext('</p>')
264
            r += form.render()
265
            return r.getvalue()
266
        else:
267
            return self.filter_submitted(form, tags, remote_calendars)
268

  
269
    def filter_submitted(self, form, tags, remote_calendars):
270
        if remote_calendars:
271
            selected_remote_calendars = form.get_widget('calendars').parse()
272
            events = []
273
            for remote_calendar in selected_remote_calendars:
274
                if remote_calendar == 'local':
275
                    events.extend(Event.select())
276
                else:
277
                    try:
278
                        events.extend(RemoteCalendar.get(remote_calendar).events)
279
                    except KeyError:
280
                        pass
281
        else:
282
            events = Event.select()
283

  
284
        events = [x for x in events if x.after_today()]
285

  
286
        if tags:
287
            selected_tags = Set(form.get_widget('tags').parse())
288
            if selected_tags and len(selected_tags) != len(tags):
289
                events = [x for x in events if Set(x.keywords).intersection(selected_tags)]
290

  
291
        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
292

  
293
        r = TemplateIO(html=True)
294

  
295
        if len(events) > 1:
296
            r += htmltext('<p id="nb-events">')
297
            r += htmltext(_('%(nb)d events')) % {'nb': len(events)}
298
            r += htmltext('</p>')
299

  
300
        if events:
301
            r += htmltext('<dl id="events">')
302
            for ev in events:
303
                r += htmltext(ev.as_html_dt_dd())
304
            r += htmltext('</dl>')
305
            return r.getvalue()
306
        else:
307
            return self.filter(no_event=True)
auquotidien/modules/announces.py
1
import time
2

  
3
from quixote import get_publisher
4

  
5
from quixote.html import htmlescape
6

  
7
from qommon import _
8
from qommon.storage import StorableObject
9
from qommon import get_cfg, get_logger
10
from qommon import errors
11
from qommon import misc
12

  
13
from qommon import emails
14
from qommon.sms import SMS
15
from qommon.admin.emails import EmailsDirectory
16

  
17
class AnnounceSubscription(StorableObject):
18
    _names = 'announce-subscriptions'
19
    _indexes = ['user_id']
20

  
21
    user_id = None
22
    email = None
23
    sms = None
24
    enabled = True
25
    enabled_sms = False
26
    enabled_themes = None
27

  
28
    def remove(self, type=None):
29
        """ type (string) : email or sms """
30
        if type == "email":
31
            self.email = None
32
        elif type == "sms":
33
            self.sms = None
34
            self.enabled_sms = False
35
        if not type or (not self.sms and not self.email):
36
            self.remove_self()
37
        else:
38
            self.store()
39
        
40
    def get_user(self):
41
        if self.user_id:
42
            try:
43
                return get_publisher().user_class.get(self.user_id)
44
            except KeyError:
45
                return None
46
        return None
47
    user = property(get_user)
48

  
49

  
50
class Announce(StorableObject):
51
    _names = 'announces'
52

  
53
    title = None
54
    text = None
55

  
56
    hidden = False
57

  
58
    publication_time = None
59
    modification_time = None
60
    expiration_time = None
61
    sent_by_email_time = None
62
    sent_by_sms_time = None
63
    theme = None
64

  
65
    position = None
66

  
67
    def sort_by_position(cls, links):
68
        def cmp_position(x, y):
69
            if x.position == y.position:
70
                return 0
71
            if x.position is None:
72
                return 1
73
            if y.position is None:
74
                return -1
75
            return cmp(x.position, y.position)
76
        links.sort(cmp_position)
77
    sort_by_position = classmethod(sort_by_position)
78

  
79
    def get_atom_entry(self):
80
        from pyatom import pyatom
81
        entry = pyatom.Entry()
82
        entry.id = self.get_url()
83
        entry.title = self.title
84

  
85
        entry.content.attrs['type'] = 'html'
86
        entry.content.text = str('<p>' + htmlescape(
87
                    unicode(self.text, get_publisher().site_charset).encode('utf-8')) + '</p>')
88

  
89
        link = pyatom.Link(self.get_url())
90
        entry.links.append(link)
91

  
92
        if self.publication_time:
93
            entry.published = misc.format_time(self.publication_time,
94
                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
95
                    gmtime = True)
96

  
97
        if self.modification_time:
98
            entry.updated = misc.format_time(self.modification_time,
99
                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
100
                    gmtime = True)
101

  
102
        return entry
103

  
104
    def get_url(self):
105
        return '%s/announces/%s/' % (get_publisher().get_frontoffice_url(), self.id)
106

  
107
    def store(self):
108
        self.modification_time = time.gmtime()
109
        StorableObject.store(self)
110

  
111
    def email(self, job=None):
112
        self.sent_by_email_time = time.gmtime()
113
        StorableObject.store(self)
114

  
115
        data = {
116
            'title': self.title,
117
            'text': self.text
118
        }
119

  
120
        subscribers = AnnounceSubscription.select(lambda x: x.enabled)
121

  
122
        rcpts = []
123
        for l in subscribers:
124
            if self.theme:
125
                if l.enabled_themes is not None:
126
                    if self.theme not in l.enabled_themes:
127
                        continue
128
            if l.user and l.user.email:
129
                rcpts.append(l.user.email)
130
            elif l.email:
131
                rcpts.append(l.email)
132

  
133
        emails.custom_ezt_email('aq-announce', data, email_rcpt = rcpts, hide_recipients = True)
134

  
135
    def sms(self, job=None):
136
        self.sent_by_sms_time = time.gmtime()
137
        StorableObject.store(self)
138

  
139
        subscribers = AnnounceSubscription.select(lambda x: x.enabled_sms)
140

  
141
        rcpts = []
142
        for sub in subscribers:
143
            if self.theme:
144
                if sub.enabled_themes is not None:
145
                    if self.theme not in sub.enabled_themes:
146
                        continue
147
            if sub.sms:
148
                rcpts.append(sub.sms)
149

  
150
        sms_cfg = get_cfg('sms', {})
151
        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
152
        message = "%s: %s" % (self.title, self.text)
153
        mode = sms_cfg.get('mode', 'none')
154
        sms = SMS.get_sms_class(mode)
155
        try:
156
            sms.send(sender, rcpts, message[:160])
157
        except errors.SMSError, e:
158
            get_logger().error(e)
159

  
160
    def get_published_announces(cls):
161
        announces = cls.select(lambda x: not x.hidden)
162
        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
163
                    y.publication_time or y.modification_time))
164
        announces = [x for x in announces if x.publication_time < time.gmtime()
165
                     and (x.expiration_time is None or x.expiration_time > time.gmtime())]
166
        announces.reverse()
167
        return announces
168
    get_published_announces = classmethod(get_published_announces)
169

  
170

  
171
EmailsDirectory.register('aq-announce',
172
        N_('Publication of announce to subscriber'),
173
        N_('Available variables: title, text'),
174
        default_subject = N_('Announce: [title]'),
175
        default_body = N_("""\
176
[text]
177

  
178
-- 
179
This is an announce sent to you by your city, you can opt to not receive
180
those messages anymore on the city website.
181
"""))
182

  
auquotidien/modules/announces_ui.py
1
from quixote import get_request, get_response, get_session, redirect
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import htmltext, TemplateIO
4

  
5
import wcs
6

  
7
from qommon import _
8
from qommon.backoffice.menu import html_top
9
from qommon.admin.menu import command_icon
10
from qommon import get_cfg
11
from qommon import errors
12
from qommon.form import *
13
from qommon.afterjobs import AfterJob
14

  
15
from announces import Announce, AnnounceSubscription
16

  
17

  
18
class SubscriptionDirectory(Directory):
19
    _q_exports = ['delete_email', "delete_sms"]
20

  
21
    def __init__(self, subscription):
22
        self.subscription = subscription
23

  
24
    def delete_email(self):
25
        form = Form(enctype='multipart/form-data')
26
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
27
                        'You are about to delete this subscription.')))
28
        form.add_submit('submit', _('Submit'))
29
        form.add_submit('cancel', _('Cancel'))
30
        if form.get_submit() == 'cancel':
31
            return redirect('..')
32
        if not form.is_submitted() or form.has_errors():
33
            get_response().breadcrumb.append(('delete', _('Delete')))
34
            html_top('announces', title = _('Delete Subscription'))
35
            r = TemplateIO(html=True)
36
            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
37
            r += form.render()
38
            return r.getvalue()
39
        else:
40
            self.subscription.remove("email")
41
            return redirect('..')
42

  
43
    def delete_sms(self):
44
        form = Form(enctype='multipart/form-data')
45
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
46
                        'You are about to delete this subscription.')))
47
        form.add_submit('submit', _('Submit'))
48
        form.add_submit('cancel', _('Cancel'))
49
        if form.get_submit() == 'cancel':
50
            return redirect('..')
51
        if not form.is_submitted() or form.has_errors():
52
            get_response().breadcrumb.append(('delete', _('Delete')))
53
            html_top('announces', title = _('Delete Subscription'))
54
            r = TemplateIO(html=True)
55
            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
56
            r += form.render()
57
            return r.getvalue()
58
        else:
59
            self.subscription.remove("sms")
60
            return redirect('..')
61

  
62

  
63
class SubscriptionsDirectory(Directory):
64
    _q_exports = ['']
65

  
66
    def _q_traverse(self, path):
67
        get_response().breadcrumb.append(('subscriptions', _('Subscriptions')))
68
        return Directory._q_traverse(self, path)
69

  
70
    def _q_index(self):
71
        html_top('announces', _('Announces Subscribers'))
72
        r = TemplateIO(html=True)
73

  
74
        r += htmltext('<h2>%s</h2>') % _('Announces Subscribers')
75

  
76
        subscribers = AnnounceSubscription.select()
77
        r += htmltext('<ul class="biglist" id="subscribers-list">')
78
        for l in subscribers:
79
            if l.email:
80
                if l.enabled is False:
81
                    r += htmltext('<li class="disabled">')
82
                else:
83
                    r += htmltext('<li>')
84
                r += htmltext('<strong class="label">')
85
                if l.user:
86
                    r += l.user.display_name
87
                elif l.email:
88
                    r += l.email
89
                r += htmltext('</strong>')
90
                r += htmltext('<p class="details">')
91
                if l.user:
92
                    r += l.user.email
93
                r += htmltext('</p>')
94
                r += htmltext('<p class="commands">')
95
                r += command_icon('%s/delete_email' % l.id, 'remove', popup = True)
96
                r += htmltext('</p></li>')
97
                r += htmltext('</li>')
98
            if l.sms:
99
                if l.enabled_sms is False:
100
                    r += htmltext('<li class="disabled">')
101
                else:
102
                    r += htmltext('<li>')
103
                r += htmltext('<strong class="label">')
104
                if l.user:
105
                    r += l.user.display_name
106
                elif l.email:
107
                    r += l.email
108
                r += htmltext('</strong>')
109
                r += htmltext('<p class="details">')
110
                r += l.sms
111
                r += htmltext('</p>')
112
                r += htmltext('<p class="commands">')
113
                r += command_icon('%s/delete_sms' % l.id, 'remove', popup = True)
114
                r += htmltext('</p></li>')
115
                r += htmltext('</li>')
116
        r += htmltext('</ul>')
117
        return r.getvalue()
118

  
119
    def _q_lookup(self, component):
120
        try:
121
            sub = AnnounceSubscription.get(component)
122
        except KeyError:
123
            raise errors.TraversalError()
124
        get_response().breadcrumb.append((str(sub.id), str(sub.id)))
125
        return SubscriptionDirectory(sub)
126

  
127
    def listing(self):
128
        return redirect('.')
129

  
130
class AnnounceDirectory(Directory):
131
    _q_exports = ['', 'edit', 'delete', 'email', 'sms']
132

  
133
    def __init__(self, announce):
134
        self.announce = announce
135

  
136
    def _q_index(self):
137
        form = Form(enctype='multipart/form-data')
138
        get_response().filter['sidebar'] = self.get_sidebar()
139

  
140
        if self.announce.sent_by_email_time is None:
141
            form.add_submit('email', _('Send email'))
142

  
143
        announces_cfg = get_cfg('announces', {})
144
        if announces_cfg.get('sms_support', 0) and self.announce.sent_by_sms_time is None:
145
            form.add_submit('sms', _('Send SMS'))
146

  
147
        if form.get_submit() == 'edit':
148
            return redirect('edit')
149
        if form.get_submit() == 'delete':
150
            return redirect('delete')
151
        if form.get_submit() == 'email':
152
            return redirect('email')
153
        if form.get_submit() == 'sms':
154
            return redirect('sms')
155

  
156
        html_top('announces', title = _('Announce: %s') % self.announce.title)
157
        r = TemplateIO(html=True)
158
        r += htmltext('<h2>%s</h2>') % _('Announce: %s') % self.announce.title
159
        r += htmltext('<div class="bo-block">')
160
        r += htmltext('<p>')
161
        r += self.announce.text
162
        r += htmltext('</p>')
163
        r += htmltext('</div>')
164

  
165
        if form.get_submit_widgets():
166
            r += form.render()
167

  
168
        return r.getvalue()
169

  
170
    def get_sidebar(self):
171
        r = TemplateIO(html=True)
172
        r += htmltext('<ul>')
173
        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
174
        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
175
        r += htmltext('</ul>')
176
        return r.getvalue()
177

  
178
    def email(self):
179
        if get_request().form.get('job'):
180
            try:
181
                job = AfterJob.get(get_request().form.get('job'))
182
            except KeyError:
183
                return redirect('..')
184
            html_top('announces', title = _('Announce: %s') % self.announce.title)
185
            r = TemplateIO(html=True)
186
            get_response().add_javascript(['jquery.js', 'afterjob.js'])
187
            r += htmltext('<dl class="job-status">')
188
            r += htmltext('<dt>')
189
            r += _(job.label)
190
            r += htmltext('</dt>')
191
            r += htmltext('<dd>')
192
            r += htmltext('<span class="afterjob" id="%s">') % job.id
193
            r += _(job.status)
194
            r += htmltext('</span>')
195
            r += htmltext('</dd>')
196
            r += htmltext('</dl>')
197

  
198
            r += htmltext('<div class="done">')
199
            r += htmltext('<a href="../">%s</a>') % _('Back')
200
            r += htmltext('</div>')
201

  
202
            return r.getvalue()
203
        else:
204
            job = get_response().add_after_job(
205
                    str(N_('Sending emails for announce')),
206
                    self.announce.email)
207
            return redirect('email?job=%s' % job.id)
208

  
209
    def sms(self):
210
        if get_request().form.get('job'):
211
            try:
212
                job = AfterJob.get(get_request().form.get('job'))
213
            except KeyError:
214
                return redirect('..')
215
            html_top('announces', title = _('Announce: %s') % self.announce.title)
216
            get_response().add_javascript(['jquery.js', 'afterjob.js'])
217
            r = TemplateIO(html=True)
218
            r += htmltext('<dl class="job-status">')
219
            r += htmltext('<dt>')
220
            r += _(job.label)
221
            r += htmltext('</dt>')
222
            r += htmltext('<dd>')
223
            r += htmltext('<span class="afterjob" id="%s">') % job.id
224
            r += _(job.status)
225
            r += htmltext('</span>')
226
            r += htmltext('</dd>')
227
            r += htmltext('</dl>')
228

  
229
            r += htmltext('<div class="done">')
230
            r += htmltext('<a href="../">%s</a>') % _('Back')
231
            r += htmltext('</div>')
232

  
233
            return r.getvalue()
234
        else:
235
            job = get_response().add_after_job(
236
                    str(N_('Sending sms for announce')),
237
                    self.announce.sms)
238
            return redirect('sms?job=%s' % job.id)
239

  
240
    def edit(self):
241
        form = self.form()
242
        if form.get_submit() == 'cancel':
243
            return redirect('.')
244

  
245
        if form.is_submitted() and not form.has_errors():
246
            self.submit(form)
247
            return redirect('..')
248

  
249
        html_top('announces', title = _('Edit Announce: %s') % self.announce.title)
250
        r = TemplateIO(html=True)
251
        r += htmltext('<h2>%s</h2>') % _('Edit Announce: %s') % self.announce.title
252
        r += form.render()
253
        return r.getvalue()
254

  
255
    def form(self):
256
        form = Form(enctype='multipart/form-data')
257
        form.add(StringWidget, 'title', title = _('Title'), required = True,
258
                value = self.announce.title)
259
        if self.announce.publication_time:
260
            pub_time = time.strftime(misc.date_format(), self.announce.publication_time)
261
        else:
262
            pub_time = None
263
        form.add(DateWidget, 'publication_time', title = _('Publication Time'),
264
                value = pub_time)
265
        if self.announce.expiration_time:
266
            exp_time = time.strftime(misc.date_format(), self.announce.expiration_time)
267
        else:
268
            exp_time = None
269
        form.add(DateWidget, 'expiration_time', title = _('Expiration Time'),
270
                value = exp_time)
271
        form.add(TextWidget, 'text', title = _('Text'), required = True,
272
                value = self.announce.text, rows = 10, cols = 70)
273
        if get_cfg('misc', {}).get('announce_themes'):
274
            form.add(SingleSelectWidget, 'theme', title = _('Announce Theme'),
275
                    value = self.announce.theme,
276
                    options = get_cfg('misc', {}).get('announce_themes'))
277
        form.add(CheckboxWidget, 'hidden', title = _('Hidden'),
278
                value = self.announce.hidden)
279
        form.add_submit('submit', _('Submit'))
280
        form.add_submit('cancel', _('Cancel'))
281
        return form
282

  
283
    def submit(self, form):
284
        for k in ('title', 'text', 'hidden', 'theme'):
285
            widget = form.get_widget(k)
286
            if widget:
287
                setattr(self.announce, k, widget.parse())
288
        for k in ('publication_time', 'expiration_time'):
289
            widget = form.get_widget(k)
290
            if widget:
291
                wid_time = widget.parse()
292
                if wid_time:
293
                    setattr(self.announce, k, time.strptime(wid_time, misc.date_format()))
294
                else:
295
                    setattr(self.announce, k, None)
296
        self.announce.store()
297

  
298
    def delete(self):
299
        form = Form(enctype='multipart/form-data')
300
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
301
                        'You are about to irrevocably delete this announce.')))
302
        form.add_submit('submit', _('Submit'))
303
        form.add_submit('cancel', _('Cancel'))
304
        if form.get_submit() == 'cancel':
305
            return redirect('..')
306
        if not form.is_submitted() or form.has_errors():
307
            get_response().breadcrumb.append(('delete', _('Delete')))
308
            html_top('announces', title = _('Delete Announce'))
309
            r = TemplateIO(html=True)
310
            r += htmltext('<h2>%s</h2>') % _('Deleting Announce: %s') % self.announce.title
311
            r += form.render()
312
            return r.getvalue()
313
        else:
314
            self.announce.remove_self()
315
            return redirect('..')
316

  
317

  
318
class AnnouncesDirectory(AccessControlled, Directory):
319
    _q_exports = ['', 'new', 'listing', 'subscriptions', 'update_order', 'log']
320
    label = N_('Announces')
321

  
322
    subscriptions = SubscriptionsDirectory()
323

  
324
    def is_accessible(self, user):
325
        from .backoffice import check_visibility
326
        return check_visibility('announces', user)
327

  
328
    def _q_access(self):
329
        user = get_request().user
330
        if not user:
331
            raise errors.AccessUnauthorizedError()
332

  
333
        if not self.is_accessible(user):
334
            raise errors.AccessForbiddenError(
335
                    public_msg = _('You are not allowed to access Announces Management'),
336
                    location_hint = 'backoffice')
337

  
338
        get_response().breadcrumb.append(('announces/', _('Announces')))
339

  
340
    def _q_index(self):
341
        html_top('announces', _('Announces'))
342
        r = TemplateIO(html=True)
343

  
344
        get_response().filter['sidebar'] = self.get_sidebar()
345

  
346
        announces = Announce.select()
347
        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
348
                    y.publication_time or y.modification_time))
349
        announces.reverse()
350

  
351
        r += htmltext('<ul class="biglist" id="announces-list">')
352
        for l in announces:
353
            announce_id = l.id
354
            if l.hidden:
355
                r += htmltext('<li class="disabled" class="biglistitem" id="itemId_%s">') % announce_id
356
            else:
357
                r += htmltext('<li class="biglistitem" id="itemId_%s">') % announce_id
358
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (l.id, l.title)
359
            if l.publication_time:
360
                r += htmltext('<p class="details">')
361
                r += time.strftime(misc.date_format(), l.publication_time)
362
                r += htmltext('</p>')
363
            r += htmltext('</li>')
364
        r += htmltext('</ul>')
365
        return r.getvalue()
366

  
367
    def get_sidebar(self):
368
        r = TemplateIO(html=True)
369
        r += htmltext('<ul id="sidebar-actions">')
370
        r += htmltext(' <li><a class="new-item" href="new">%s</a></li>') % _('New')
371
        r += htmltext(' <li><a href="subscriptions/">%s</a></li>') % _('Subscriptions')
372
        r += htmltext(' <li><a href="log">%s</a></li>') % _('Log')
373
        r += htmltext('</ul>')
374
        return r.getvalue()
375

  
376
    def log(self):
377
        announces = Announce.select()
378
        log = []
379
        for l in announces:
380
            if l.publication_time:
381
                log.append((l.publication_time, _('Publication'), l))
382
            if l.sent_by_email_time:
383
                log.append((l.sent_by_email_time, _('Email'), l))
384
            if l.sent_by_sms_time:
385
                log.append((l.sent_by_sms_time, _('SMS'), l))
386
        log.sort()
387

  
388
        get_response().breadcrumb.append(('log', _('Log')))
389
        html_top('announces', title = _('Log'))
390
        r = TemplateIO(html=True)
391

  
392
        r += htmltext('<table>')
393
        r += htmltext('<thead>')
394
        r += htmltext('<tr>')
395
        r += htmltext('<th>%s</th>') % _('Time')
396
        r += htmltext('<th>%s</th>') % _('Type')
397
        r += htmltext('<td></td>')
398
        r += htmltext('</tr>')
399
        r += htmltext('</thead>')
400
        r += htmltext('<tbody>')
401
        for log_time, log_type, log_announce in log:
402
            r += htmltext('<tr>')
403
            r += htmltext('<td>')
404
            r += misc.localstrftime(log_time)
405
            r += htmltext('</td>')
406
            r += htmltext('<td>')
407
            r += log_type
408
            r += htmltext('</td>')
409
            r += htmltext('<td>')
410
            r += htmltext('<a href="%s">%s</a>') % (log_announce.id, log_announce.title)
411
            r += htmltext('</td>')
412
            r += htmltext('</tr>')
413
        r += htmltext('</tbody>')
414
        r += htmltext('</table>')
415
        return r.getvalue()
416

  
417
    def update_order(self):
418
        request = get_request()
419
        new_order = request.form['order'].strip(';').split(';')
420
        announces = Announce.select()
421
        dict = {}
422
        for l in announces:
423
            dict[str(l.id)] = l
424
        for i, o in enumerate(new_order):
425
            dict[o].position = i + 1
426
            dict[o].store()
427
        return 'ok'
428

  
429
    def new(self):
430
        announce = Announce()
431
        announce.publication_time = time.gmtime()
432
        announce_ui = AnnounceDirectory(announce)
433

  
434
        form = announce_ui.form()
435
        if form.get_submit() == 'cancel':
436
            return redirect('.')
437

  
438
        if form.is_submitted() and not form.has_errors():
439
            announce_ui.submit(form)
440
            return redirect('%s/' % announce_ui.announce.id)
441

  
442
        get_response().breadcrumb.append(('new', _('New Announce')))
443
        html_top('announces', title = _('New Announce'))
444
        r = TemplateIO(html=True)
445
        r += htmltext('<h2>%s</h2>') % _('New Announce')
446
        r += form.render()
447
        return r.getvalue()
448

  
449
    def _q_lookup(self, component):
450
        try:
451
            announce = Announce.get(component)
452
        except KeyError:
453
            raise errors.TraversalError()
454
        get_response().breadcrumb.append((str(announce.id), announce.title))
455
        return AnnounceDirectory(announce)
456

  
457
    def listing(self):
458
        return redirect('.')
459

  
auquotidien/modules/backoffice.py
1
import os
2

  
3
from quixote import get_publisher, redirect
4
from quixote.directory import Directory
5
from quixote.html import TemplateIO, htmltext
6

  
7
from qommon import _
8
from qommon.publisher import get_publisher_class
9

  
10
import wcs.backoffice.management
11
import wcs.root
12
from wcs.categories import Category
13
from wcs.formdef import FormDef
14

  
15
from qommon import get_cfg, errors
16
from qommon.form import *
17

  
18
CURRENT_USER = object()
19

  
20
def check_visibility(target, user=CURRENT_USER):
21
    if user is CURRENT_USER:
22
        user = get_request().user
23
    if not user:
24
        return False
25
    target = target.strip('/')
26
    if target == 'management':
27
        target = 'forms'
28
    if target == 'strongbox':
29
        if not get_publisher().has_site_option(target):
30
            # strongbox disabled in site-options.cfg
31
            return False
32
        if not get_cfg('misc', {}).get('aq-strongbox'):
33
            # strongbox disabled in settings panel
34
            return False
35
    admin_role = get_cfg('aq-permissions', {}).get(target, None)
36
    if not admin_role:
37
        return False
38
    if not (user.is_admin or admin_role in (user.roles or [])):
39
        return False
40
    return True
41

  
42

  
43
class BackofficeRootDirectory(wcs.backoffice.root.RootDirectory):
44
    def get_intro_text(self):
45
        return _('Welcome on Publik back office interface')
46

  
47
    def _q_index(self):
48
        if len(self.get_menu_items()) == 1:
49
            return redirect(self.get_menu_items()[0]['url'])
50
        return wcs.backoffice.root.RootDirectory._q_index(self)
51

  
52
    def home(self):
53
        return redirect('management/')
54

  
55
get_publisher_class().backoffice_directory_class = BackofficeRootDirectory
auquotidien/modules/categories_admin.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2010  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from quixote import redirect
18
from quixote.directory import Directory
19
from quixote.html import TemplateIO, htmltext
20

  
21
from qommon import _
22
from qommon import misc
23
from wcs.categories import Category
24
from qommon.form import *
25
from qommon.backoffice.menu import html_top
26
from qommon.admin.menu import command_icon, error_page
27
import wcs.admin.categories
28

  
29
class CategoryUI:
30
    def __init__(self, category):
31
        self.category = category
32
        if self.category is None:
33
            self.category = Category()
34

  
35
    def get_form(self):
36
        form = Form(enctype='multipart/form-data')
37
        form.add(StringWidget, 'name', title = _('Category Name'), required = True, size=30,
38
                value = self.category.name)
39
        form.add(TextWidget, 'description', title = _('Description'), cols = 80, rows = 10,
40
                value = self.category.description)
41

  
42
        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
43
        if not homepage_redirect_url:
44
            form.add(SingleSelectWidget, 'homepage_position',
45
                        title=_('Position on homepage'),
46
                        value=self.category.get_homepage_position(),
47
                        options = [('1st', _('First Column')),
48
                                   ('2nd', _('Second Column')),
49
                                   ('side', _('Sidebar')),
50
                                   ('none', _('None'))])
51
            form.add(IntWidget, 'limit',
52
                        title=_('Limit number of items displayed on homepage'),
53
                        value=self.category.get_limit())
54

  
55
        form.add(StringWidget, 'redirect_url', size=32,
56
                title=_('URL Redirection'),
57
                hint=_('If set, redirect the site category page to the given URL.'),
58
                value=self.category.redirect_url)
59

  
60
        form.add_submit('submit', _('Submit'))
61
        form.add_submit('cancel', _('Cancel'))
62
        return form
63

  
64
    def submit_form(self, form):
65
        if self.category.id:
66
            self.category.name = form.get_widget('name').parse()
67
            category = self.category
68
        else:
69
            category = Category(name = form.get_widget('name').parse())
70

  
71
        name = form.get_widget('name').parse()
72
        category_names = [x.name for x in Category.select() if x.id != category.id]
73
        if name in category_names:
74
            form.get_widget('name').set_error(_('This name is already used'))
75
            raise ValueError()
76

  
77
        category.description = form.get_widget('description').parse()
78
        category.redirect_url = form.get_widget('redirect_url').parse()
79

  
80
        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
81
        if not homepage_redirect_url:
82
            category.homepage_position = form.get_widget('homepage_position').parse()
83
            category.limit = form.get_widget('limit').parse()
84

  
85
        category.store()
86

  
87

  
88

  
89
class CategoryPage(wcs.admin.categories.CategoryPage):
90
    def __init__(self, component):
91
        self.category = Category.get(component)
92
        self.category_ui = CategoryUI(self.category)
93
        get_response().breadcrumb.append((component + '/', self.category.name))
94

  
95

  
96
class CategoriesDirectory(wcs.admin.categories.CategoriesDirectory):
97
    label = N_('Categories')
98

  
99
    def new(self):
100
        get_response().breadcrumb.append( ('categories/', _('Categories')) )
101
        get_response().breadcrumb.append( ('new', _('New')) )
102
        category_ui = CategoryUI(None)
103
        form = category_ui.get_form()
104
        if form.get_widget('cancel').parse():
105
            return redirect('.')
106

  
107
        if form.is_submitted() and not form.has_errors():
108
            try:
109
                category_ui.submit_form(form)
110
            except ValueError:
111
                pass
112
            else:
113
                return redirect('.')
114

  
115
        html_top('categories', title = _('New Category'))
116
        r = TemplateIO(html=True)
117
        r += htmltext('<h2>%s</h2>') % _('New Category')
118
        r += form.render()
119
        return r.getvalue()
120

  
121
    def _q_lookup(self, component):
122
        get_response().breadcrumb.append( ('categories/', _('Categories')) )
123
        return CategoryPage(component)
auquotidien/modules/clicrdv.py
1
import base64
2
import datetime
3
import urllib2
4

  
5
try:
6
    import json
7
except ImportError:
8
    import simplejson as json
9

  
10
import time
11
import vobject
12

  
13
from qommon import _
14
from qommon import get_cfg
15
from qommon.misc import format_time
16
from qommon.form import *
17

  
18
from wcs.data_sources import register_data_source_function
19
from wcs.formdata import Evolution
20
from wcs.forms.common import FormStatusPage
21
from wcs.workflows import Workflow, WorkflowStatusItem, register_item_class
22

  
23
def get_clicrdv_req(url):
24
    misc_cfg = get_cfg('misc', {})
25

  
26
    url = 'https://%s/api/v1/%s' % (
27
                misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), url)
28
    if '?' in url:
29
       url = url + '&apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
30
    else:
31
       url = url + '?apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
32

  
33
    req = urllib2.Request(url)
34
    username = misc_cfg.get('aq-clicrdv-api-username')
35
    password = misc_cfg.get('aq-clicrdv-api-password')
36
    authheader = 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
37
    req.add_header('Authorization', authheader)
38
    return req
39

  
40
def get_json(url):
41
    return json.load(urllib2.urlopen(get_clicrdv_req(url)))
42

  
43
def as_str(s):
44
    if type(s) is unicode:
45
        return s.encode(get_publisher().site_charset)
46
    return s
47

  
48
def get_all_intervention_sets():
49
    interventions_set = []
50
    for interventionset in sorted(get_json('interventionsets').get('records'), 
51
            lambda x,y: cmp(x['sort'],y['sort'])):
52
        interventions = []
53
        for intervention in sorted(get_json('interventionsets/%s/interventions' % interventionset.get('id')).get('records'),
54
                lambda x,y: cmp(x['sort'], y['sort'])):
55
            if intervention.get('deleted') == True:
56
                continue
57
            name = '%s' % as_str(intervention.get('publicname'))
58
            if not name:
59
                name = '%s' % as_str(intervention.get('name'))
60
            interventions.append((intervention.get('id'), as_str(name)))
61
        interventions_set.append({
62
                'id': interventionset.get('id'),
63
                'group_id': interventionset.get('group_id'),
64
                'name': as_str(interventionset.get('name')),
65
                'publicname': as_str(interventionset.get('publicname')) or '',
66
                'description': as_str(interventionset.get('description')) or '',
67
                'interventions': interventions
68
                })
69
    return interventions_set
70

  
71
def get_all_interventions():
72
    interventions = []
73
    for s in get_all_intervention_sets():
74
        for i, publicname in s['interventions']:
75
            intervention_label = '%s - %s' % (s['publicname'], publicname)
76
            interventions.append((i, as_str(intervention_label)))
77
    return interventions
78

  
79
def get_interventions_in_set(interventionset_id):
80
    interventions = []
81
    interventions_json = get_json('interventionsets/%s/interventions' % interventionset_id)
82
    for intervention in interventions_json.get('records'):
83
        if intervention.get('deleted') != True:
84
            name = '%s' % as_str(intervention.get('publicname'))
85
            if not name:
86
                name = '%s' % as_str(intervention.get('name'))
87
            interventions.append((intervention.get('id'), name))
88
    return interventions
89

  
90
def get_available_timeslots(intervention, date_start=None, date_end=None):
91
    timeslots = []
92
    iid = intervention
93
    gid = get_json('interventions/%s' % iid).get('group_id')
94
    request_url = 'availabletimeslots?intervention_ids[]=%s&group_id=%s' % (iid, gid)
95
    if date_start is None:
96
        date_start = datetime.datetime.today().strftime('%Y-%m-%d')
97
    if date_end is None:
98
        date_end = (datetime.datetime.today() + datetime.timedelta(366)).strftime('%Y-%m-%d')
99
    if date_start:
100
        request_url = request_url + '&start=%s' % urllib2.quote(date_start)
101
    if date_end:
102
        request_url = request_url + '&end=%s' % urllib2.quote(date_end)
103
    for timeslot in get_json(request_url).get('availabletimeslots'):
104
        timeslots.append(timeslot.get('start'))
105
    timeslots.sort()
106
    return timeslots
107

  
108
def get_available_dates(intervention):
109
    dates = []
110
    for timeslot in get_available_timeslots(intervention):
111
        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
112
        date_tuple = (time.strftime('%Y-%m-%d', parsed),
113
                      format_time(parsed, '%(weekday_name)s %(day)0.2d/%(month)0.2d/%(year)s'))
114
        if date_tuple in dates:
115
            continue
116
        dates.append(date_tuple)
117
    return dates
118

  
119
def get_available_times(intervention, date):
120
    times = []
121
    timeslots = get_available_timeslots(intervention,
122
                    date_start='%s 00:00:00' % date,
123
                    date_end='%s 23:59:59' % date)
124
    for timeslot in timeslots:
125
        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
126
        time_tuple = (time.strftime('%H:%M:%S', parsed),
127
                      time.strftime('%Hh%M', parsed))
128
        times.append(time_tuple)
129
    times.sort()
130
    return times
131

  
132
register_data_source_function(get_all_interventions, 'clicrdv_get_all_interventions')
133
register_data_source_function(get_interventions_in_set, 'clicrdv_get_interventions_in_set')
134
register_data_source_function(get_available_dates, 'clicrdv_get_available_dates')
135
register_data_source_function(get_available_times, 'clicrdv_get_available_times')
136

  
137
def form_download_event(self):
138
    self.check_receiver()
139

  
140
    found = False
141
    for evo in self.filled.evolution:
142
        if evo.parts:
143
            for p in evo.parts:
144
                if not isinstance(p, AppointmentPart):
145
                    continue
146
                cal = vobject.iCalendar()
147
                cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
148
                vevent = vobject.newFromBehavior('vevent')
149
                vevent.add('uid').value = 'clicrdv-%s' % p.id
150
                vevent.add('summary').value = p.json_dict.get('group_name')
151
                vevent.add('dtstart').value = datetime.datetime.strptime(
152
                                p.json_dict.get('start'), '%Y-%m-%d %H:%M:%S')
153
                vevent.add('dtend').value = datetime.datetime.strptime(
154
                                p.json_dict.get('end'), '%Y-%m-%d %H:%M:%S')
155
                vevent.add('location').value = p.json_dict.get('location')
156
                cal.add(vevent)
157

  
158
                response = get_response()
159
                response.set_content_type('text/calendar')
160
                return cal.serialize()
161

  
162
    raise TraversalError()
163

  
164

  
165
class AppointmentPart(object):
166
    def __init__(self, json_dict):
167
        self.id = json_dict.get('id')
168
        self.json_dict = json_dict
169

  
170
    def view(self):
171
        return htmltext('<p class="appointment"><a href="clicrdvevent">%s</a></p>' % (
172
                                _('Download Appointment')))
173

  
174

  
175
class AppointmentErrorPart(object):
176
    def __init__(self, msg):
177
        self.msg = msg
178

  
179
    def view(self):
180
        return htmltext('<p class="appointment-error">%s</p>' % str(self.msg))
181

  
182

  
183
class ClicRdvCreateAppointment(WorkflowStatusItem):
184
    description = N_('Create a ClicRDV Appointment')
185
    key = 'clicrdv-create'
186
    category = ('aq-clicrdv', N_('ClicRDV'))
187

  
188
    endpoint = False
189

  
190
    var_firstname = None
191
    var_lastname = None
192
    var_email = None
193
    var_firstphone = None
194
    var_secondphone = None
195
    var_datetime = None
196
    var_intervention_id = None
197
    status_on_success = None
198
    status_on_failure = None
199

  
200
    def init(cls):
201
        FormStatusPage._q_extra_exports.append('clicrdvevent')
202
        FormStatusPage.clicrdvevent = form_download_event
203
    init = classmethod(init)
204

  
205
    def is_available(self, workflow=None):
206
        return get_publisher().has_site_option('clicrdv')
207
    is_available = classmethod(is_available)
208

  
209
    def render_as_line(self):
210
        return _('Create an appointment in ClicRDV')
211

  
212
    def get_parameters(self):
213
        return ('var_firstname', 'var_lastname', 'var_email', 'var_firstphone',
214
                'var_secondphone', 'var_datetime', 'var_intervention_id',
215
                'status_on_success', 'status_on_failure')
216

  
217
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
218
        parameter_labels = {
219
            'var_firstname': N_('First Name'),
220
            'var_lastname': N_('Last Name'),
221
            'var_email': N_('Email'),
222
            'var_firstphone': N_('Phone (1st)'),
223
            'var_secondphone':  N_('Phone (2nd)'),
224
            'var_datetime': N_('Date/time'),
225
            'var_intervention_id': N_('Intervention Id'),
226
        }
227
        for parameter in self.get_parameters():
228
            if not parameter in parameter_labels:
229
                continue
230
            if parameter in parameters:
231
                form.add(StringWidget, '%s%s' % (prefix, parameter),
232
                         title=_(parameter_labels.get(parameter)),
233
                         value=getattr(self, parameter),
234
                         required=False)
235
        if 'status_on_success' in parameters:
236
            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
237
                    title=_('Status On Success'), value=self.status_on_success,
238
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
239
        if 'status_on_failure' in parameters:
240
            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
241
                    title=_('Status On Failure'), value=self.status_on_failure,
242
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
243

  
244
    def perform(self, formdata):
245
        args = {}
246
        for parameter in self.get_parameters():
247
            args[parameter] = self.compute(getattr(self, parameter))
248
            if not args.get(parameter):
249
                del args[parameter]
250
        message = {'appointment':
251
                      {'fiche': {'firstname': args.get('var_firstname', '-'),
252
                                 'lastname': args.get('var_lastname', '-'),
253
                                 'email': args.get('var_email'),
254
                                 'firstphone': args.get('var_firstphone'),
255
                                 'secondphone': args.get('var_secondphone'),
256
                                },
257
                       'date': args.get('var_datetime'),
258
                       'intervention_ids': [int(args.get('var_intervention_id'))],
259
                       # 'comments': '-',
260
                       'websource': 'Publik'}
261
                  }
262

  
263
        req = get_clicrdv_req('appointments')
264
        req.add_data(json.dumps(message))
265
        req.add_header('Content-Type', 'application/json')
266

  
267
        try:
268
            fd = urllib2.urlopen(req)
269
        except urllib2.HTTPError, e:
270
            success = False
271
            try:
272
                msg = json.load(e.fp)[0].get('error')
273
            except:
274
                msg = _('unknown error')
275

  
276
            if formdata.evolution:
277
                evo = formdata.evolution[-1]
278
            else:
279
                formdata.evolution = []
280
                evo = Evolution()
281
                evo.time = time.localtime()
282
                evo.status = formdata.status
283
                formdata.evolution.append(evo)
284
            evo.add_part(AppointmentErrorPart(msg))
285
        else:
286
            success = True
287
            response = json.load(fd)
288
            appointment_id = response.get('records')[0].get('id')
289

  
290
            # add a message in formdata.evolution
291
            if formdata.evolution:
292
                evo = formdata.evolution[-1]
293
            else:
294
                formdata.evolution = []
295
                evo = Evolution()
296
                evo.time = time.localtime()
297
                evo.status = formdata.status
298
                formdata.evolution.append(evo)
299
            evo.add_part(AppointmentPart(response.get('records')[0]))
300

  
301
        formdata.store()
302

  
303
        if (success and self.status_on_success) or (success is False and self.status_on_failure):
304
            if success:
305
                formdata.status = 'wf-%s' % self.status_on_success
306
            else:
307
                formdata.status = 'wf-%s' % self.status_on_failure
308

  
309
register_item_class(ClicRdvCreateAppointment)
310

  
311

  
312
class ClicRdvCancelAppointment(WorkflowStatusItem):
313
    description = N_('Cancel a ClicRDV Appointment')
314
    key = 'clicrdv-cancel'
315
    category = ('aq-clicrdv', N_('ClicRDV'))
316

  
317
    endpoint = False
318

  
319
    status_on_success = None
320
    status_on_failure = None
321

  
322
    def get_parameters(self):
323
        return ('status_on_success', 'status_on_failure')
324

  
325
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
326
        if 'status_on_success' in parameters:
327
            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
328
                    title=_('Status On Success'), value=self.status_on_success,
329
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
330
        if 'status_on_failure' in parameters:
331
            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
332
                    title=_('Status On Failure'), value=self.status_on_failure,
333
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
334

  
335
    def is_available(self, workflow=None):
336
        return get_publisher().has_site_option('clicrdv')
337
    is_available = classmethod(is_available)
338

  
339
    def render_as_line(self):
340
        return _('Cancel an appointment in ClicRDV')
341

  
342
    def perform(self, formdata):
343
        success = True
344

  
345
        for evo in [evo for evo in formdata.evolution if evo.parts]:
346
            for part in [part for part in evo.parts if isinstance(part, AppointmentPart)]:
347
                appointment_id = part.id
348
                try:
349
                    req = get_clicrdv_req('appointments/%s' % appointment_id)
350
                    req.get_method = (lambda: 'DELETE')
351
                    fd = urllib2.urlopen(req)
352
                    none = fd.read()
353
                except urllib2.URLError:
354
                    # clicrdv will return a "Bad Request" (HTTP 400) response
355
                    # when it's not possible to remove an appointment
356
                    # (for example because it's too late)
357
                    success = False
358

  
359
        if (success and self.status_on_success) or (success is False and self.status_on_failure):
360
            if success:
361
                formdata.status = 'wf-%s' % self.status_on_success
362
            else:
363
                formdata.status = 'wf-%s' % self.status_on_failure
364

  
365
register_item_class(ClicRdvCancelAppointment)
auquotidien/modules/connectors.py
1
import clicrdv
2
import abelium_domino_workflow
auquotidien/modules/events.py
1
import time
2
import datetime
3
import urllib2
4
import vobject
5

  
6
from quixote import get_request, get_publisher, get_response
7
from quixote.html import htmltext, TemplateIO, htmlescape
8

  
9
from qommon import _
10
from qommon.publisher import get_publisher_class
11
from qommon.storage import StorableObject
12
from qommon.cron import CronJob
13
from qommon import misc
14

  
15
class Event(StorableObject):
16
    _names = 'events'
17

  
18
    title = None
19
    description = None
20
    url = None
21
    date_start = None
22
    date_end = None
23
    location = None
24
    organizer = None
25
    more_infos = None
26
    keywords = None
27

  
28
    def in_month(self, year, month):
29
        if not self.date_end: # a single date
30
            return tuple(self.date_start[:2]) == (year, month)
31
        else:
32
            # an interval
33
            if tuple(self.date_start[:2]) > (year, month): # start later
34
                return False
35
            if tuple(self.date_end[:2]) < (year, month): # ended before
36
                return False
37
        return True
38

  
39
    def after_today(self):
40
        today = time.localtime()[:3]
41
        if not self.date_end:
42
            return tuple(self.date_start[:3]) > today
43
        return tuple(self.date_end[:3]) > today
44

  
45
    def format_date(self):
46
        d = {
47
            'year_start': self.date_start[0],
48
            'month_start': misc.get_month_name(self.date_start[1]),
49
            'day_start': self.date_start[2]
50
            }
51
        if self.date_end and self.date_start[:3] != self.date_end[:3]:
52
            d.update({
53
                'year_end': self.date_end[0],
54
                'month_end': misc.get_month_name(self.date_end[1]),
55
                'day_end': self.date_end[2]
56
                })
57
            d2 = datetime.date(*self.date_start[:3]) + datetime.timedelta(days=1)
58
            if tuple(self.date_end[:3]) == (d2.year, d2.month, d2.day):
59
                # two consecutive days
60
                if self.date_start[1] == self.date_end[1]:
61
                    return _('On %(month_start)s %(day_start)s and %(day_end)s') % d
62
                else:
63
                    return _('On %(month_start)s %(day_start)s and %(month_end)s %(day_end)s') % d
64
            else:
65
                if self.date_start[0] == self.date_end[0]: # same year
66
                    if self.date_start[1] == self.date_end[1]: # same month
67
                        return _('From %(month_start)s %(day_start)s to %(day_end)s') % d
68
                    else:
69
                        return _('From %(month_start)s %(day_start)s '
70
                                 'to %(month_end)s %(day_end)s') % d
71
                else:
72
                    return _('From %(month_start)s %(day_start)s %(year_start)s '
73
                             'to %(month_end)s %(day_end)s %(year_end)s') % d
74
        else:
75
            return _('On %(month_start)s %(day_start)s') % d
76

  
77
    def as_vevent(self):
78
        vevent = vobject.newFromBehavior('vevent')
79
        site_charset = get_publisher().site_charset
80
        vevent.add('uid').value = '%04d%02d%02d-%s@%s' % (self.date_start[:3] + (self.id,
81
            get_request().get_server().lower().split(':')[0].rstrip('.')))
82
        vevent.add('summary').value = unicode(self.title, site_charset)
83
        vevent.add('dtstart').value = datetime.date(*self.date_start[:3])
84
        vevent.dtstart.value_param = 'DATE'
85
        if self.date_end:
86
            vevent.add('dtend').value = datetime.date(*self.date_end[:3])
87
            vevent.dtend.value_param = 'DATE'
88
        if self.description:
89
            vevent.add('description').value = unicode(self.description.strip(), site_charset)
90
        if self.url:
91
            vevent.add('url').value = unicode(self.url, site_charset)
92
        if self.location:
93
            vevent.add('location').value = unicode(self.location, site_charset)
94
        if self.organizer:
95
            vevent.add('organizer').value = unicode(self.organizer, site_charset)
96
        if self.keywords:
97
            vevent.add('categories').value = [unicode(x, site_charset) for x in self.keywords]
98
        vevent.add('class').value = 'PUBLIC'
99
        return vevent
100

  
101
    def as_vcalendar(cls):
102
        cal = vobject.iCalendar()
103
        cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
104
        for x in cls.select():
105
            cal.add(x.as_vevent())
106
        return cal.serialize()
107
    as_vcalendar = classmethod(as_vcalendar)
108
    
109
    def as_html_dt_dd(self):
110
        root_url = get_publisher().get_root_url()
111
        r = TemplateIO(html=True)
112
        r += htmltext('<dt>')
113
        r += self.format_date()
114
        r += htmltext('</dt>')
115
        r += htmltext('<p><dd><strong>%s</strong>') % self.title
116
        if self.description:
117
            r += ' - ' + self.description
118
        r += htmltext('</p>')
119
        if (self.location or self.organizer or self.more_infos or self.keywords):
120
            r += htmltext('<ul>')
121
            if self.location:
122
                r += htmltext('<li>%s: %s</li>') % (_('Location'), self.location)
123
            if self.organizer:
124
                r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.organizer)
125
            if self.more_infos:
126
                r += htmltext('<li>%s</li>') % self.more_infos
127
            if self.keywords:
128
                r += htmltext('<li>')
129
                for k in self.keywords:
130
                    r += htmltext('<a class="tag" href="%sagenda/tag/%s">%s</a> ') % (root_url, k, k)
131
                r += htmltext('</li>')
132
            r += htmltext('</ul>')
133

  
134
        if self.url:
135
            if get_response().iframe_mode:
136
                r += htmltext('<a class="external" href="%s" target="_top">%s</a>') % (
137
                        self.url, _('More information'))
138
            else:
139
                r += htmltext('<a class="external" href="%s">%s</a>') % (
140
                        self.url, _('More information'))
141
        r += htmltext('</dd>')
142
        return r.getvalue()
143

  
144
    def get_url(self):
145
        return '%s/agenda/events/%s/' % (get_publisher().get_frontoffice_url(), self.id)
146

  
147
    def get_atom_entry(self):
148
        from pyatom import pyatom
149
        entry = pyatom.Entry()
150
        entry.id = self.get_url()
151
        entry.title = self.title
152

  
153
        entry.content.attrs['type'] = 'html'
154
        entry.content.text = str('<p>' + htmlescape(
155
                    unicode(self.description, get_publisher().site_charset).encode('utf-8')) + '</p>')
156

  
157
        return entry
158

  
159

  
160
class RemoteCalendar(StorableObject):
161
    _names = 'remote_calendars'
162

  
163
    label = None
164
    url = None
165
    content = None
166
    events = None
167
    error = None  # (time, string, params)
168

  
169
    def download_and_parse(self, job=None):
170
        old_content = self.content
171

  
172
        try:
173
            self.content = urllib2.urlopen(self.url).read()
174
        except urllib2.HTTPError, e:
175
            self.error = (time.localtime(), N_('HTTP Error %s on download'), (e.code,))
176
            self.store()
177
            return
178
        except urllib2.URLError, e:
179
            self.error = (time.localtime(), N_('Error on download'), ())
180
            self.store()
181
            return
182

  
183
        if self.error:
184
            self.error = None
185
            self.store()
186

  
187
        if self.content == old_content:
188
            return
189

  
190
        self.events = []
191
        try:
192
            parsed_cal = vobject.readOne(self.content)
193
        except vobject.base.ParseError:
194
            self.error = (time.localtime(), N_('Failed to parse file'), ())
195
            self.store()
196
            return
197

  
198
        site_charset = get_publisher().site_charset
199
        for vevent in parsed_cal.vevent_list:
200
            ev = Event()
201
            ev.title = vevent.summary.value.encode(site_charset, 'replace')
202
            try:
203
                ev.url = vevent.url.value.encode(site_charset, 'replace')
204
            except AttributeError:
205
                pass
206
            ev.date_start = vevent.dtstart.value.timetuple()
207
            try:
208
                ev.date_end = vevent.dtend.value.timetuple()
209
            except AttributeError:
210
                pass
211
            try:
212
                ev.description = vevent.description.value.encode(site_charset, 'replace')
213
            except AttributeError:
214
                pass
215
            try:
216
                ev.keywords = [x.encode(site_charset) for x in vevent.categories.value]
217
            except AttributeError:
218
                pass
219
            self.events.append(ev)
220
        self.store()
221

  
222
        
223
    def get_error_message(self):
224
        if not self.error:
225
            return None
226
        return '(%s) %s' % (misc.localstrftime(self.error[0]),
227
                _(self.error[1]) % self.error[2])
228

  
229

  
230
def update_remote_calendars(publisher):
231
    for source in RemoteCalendar.select():
232
        source.download_and_parse()
233

  
234
def get_default_event_tags():
235
    return [_('All Public'), _('Adults'), _('Children'), _('Free')]
236

  
237
get_publisher_class().register_cronjob(CronJob(update_remote_calendars, minutes = [0]))
238

  
auquotidien/modules/events_ui.py
1
import time
2

  
3
from quixote import get_request, get_response, get_session, redirect
4
from quixote.directory import Directory, AccessControlled
5
from quixote.html import TemplateIO, htmltext
6

  
7
import wcs
8
import wcs.admin.root
9

  
10
from qommon import _
11
from qommon.backoffice.menu import html_top
12
from qommon.admin.menu import command_icon
13
from qommon import get_cfg
14
from qommon import errors, misc
15
from qommon.form import *
16
from qommon.strftime import strftime
17

  
18
from events import Event, RemoteCalendar, get_default_event_tags
19

  
20

  
21

  
22
class RemoteCalendarDirectory(Directory):
23
    _q_exports = ['', 'edit', 'delete', 'update']
24

  
25
    def __init__(self, calendar):
26
        self.calendar = calendar
27

  
28
    def _q_index(self):
29
        form = Form(enctype='multipart/form-data')
30
        form.add_submit('edit', _('Edit'))
31
        form.add_submit('delete', _('Delete'))
32
        form.add_submit('update', _('Update'))
33
        form.add_submit('back', _('Back'))
34

  
35
        if form.get_submit() == 'edit':
36
            return redirect('edit')
37
        if form.get_submit() == 'update':
38
            return redirect('update')
39
        if form.get_submit() == 'delete':
40
            return redirect('delete')
41
        if form.get_submit() == 'back':
42
            return redirect('..')
43

  
44
        html_top('events', title = _('Remote Calendar: %s') % self.calendar.label)
45
        r = TemplateIO(html=True)
46
        r += htmltext('<h2>%s</h2>') % _('Remote Calendar: %s') % self.calendar.label
47

  
48
        r += get_session().display_message()
49

  
50
        r += htmltext('<p>')
51
        self.calendar.url
52
        if self.calendar.error:
53
            r += htmltext(' - <span class="error-message">%s</span>') % self.calendar.get_error_message()
54
        r += htmltext('</p>')
55

  
56
        if not self.calendar.content:
57
            r += htmltext('<p>')
58
            r += _('No content has been retrieved yet.')
59
            r += htmltext('</p>')
60
        else:
61
            r += htmltext('<ul>')
62
            for ev in sorted(self.calendar.events, lambda x,y: cmp(x.date_start, y.date_start)):
63
                r += htmltext('<li>')
64
                if ev.date_start:
65
                    r += strftime(misc.date_format(), ev.date_start)
66
                if ev.date_end and ev.date_start[:3] != ev.date_end[:3]:
67
                    r += ' - '
68
                    r += strftime(misc.date_format(), ev.date_start)
69

  
70
                r += ' : '
71
                if ev.url:
72
                    r += htmltext('<a href="%s">%s</a>') % (ev.url, ev.title)
73
                else:
74
                    r += ev.title
75
                r += htmltext('</li>')
76
            r += htmltext('</ul>')
77

  
78
        r += form.render()
79
        return r.getvalue()
80

  
81
    def edit(self):
82
        form = self.form()
83
        if form.get_submit() == 'cancel':
84
            return redirect('.')
85

  
86
        if form.is_submitted() and not form.has_errors():
87
            self.submit(form)
88
            return redirect('..')
89

  
90
        html_top('events', title = _('Edit Remote Calendar: %s') % self.calendar.label)
91
        r = TemplateIO(html=True)
92
        r += htmltext('<h2>%s</h2>') % _('Edit Remote Calendar: %s') % self.calendar.label
93
        r += form.render()
94
        return r.getvalue()
95

  
96
    def form(self):
97
        form = Form(enctype='multipart/form-data')
98
        form.add(StringWidget, 'label', title = _('Label'), required = True,
99
                value = self.calendar.label)
100
        form.add(StringWidget, 'url', title = _('URL'), required = True,
101
                value = self.calendar.url, size = 40)
102
        form.add_submit('submit', _('Submit'))
103
        form.add_submit('cancel', _('Cancel'))
104
        return form
105

  
106
    def submit(self, form):
107
        for k in ('label', 'url'):
108
            widget = form.get_widget(k)
109
            if widget:
110
                setattr(self.calendar, k, widget.parse())
111
        self.calendar.store()
112

  
113
    def delete(self):
114
        form = Form(enctype='multipart/form-data')
115
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
116
                        'You are about to irrevocably delete this remote calendar.')))
117
        form.add_submit('submit', _('Submit'))
118
        form.add_submit('cancel', _('Cancel'))
119
        if form.get_submit() == 'cancel':
120
            return redirect('..')
121
        if not form.is_submitted() or form.has_errors():
122
            get_response().breadcrumb.append(('delete', _('Delete')))
123
            html_top('events', title = _('Delete Remote Calendar'))
124
            r = TemplateIO(html=True)
125
            r += htmltext('<h2>%s</h2>') % _('Deleting Remote Calendar: %s') % self.calendar.label
126
            r += form.render()
127
            return r.getvalue()
128
        else:
129
            self.calendar.remove_self()
130
            return redirect('..')
131

  
132
    def update(self):
133
        get_session().message = ('info',
134
                _('Calendar update has been requested, reload in a few moments'))
135
        get_response().add_after_job('updating remote calendar',
136
                self.calendar.download_and_parse,
137
                fire_and_forget = True)
138
        return redirect('.')
139

  
140

  
141

  
142
class RemoteCalendarsDirectory(Directory):
143
    _q_exports = ['', 'new']
144

  
145
    def _q_traverse(self, path):
146
        get_response().breadcrumb.append(('remote/', _('Remote Calendars')))
147
        return Directory._q_traverse(self, path)
148

  
149
    def _q_index(self):
150
        return redirect('..')
151

  
152
    def new(self):
153
        calendar_ui = RemoteCalendarDirectory(RemoteCalendar())
154

  
155
        form = calendar_ui.form()
156
        if form.get_submit() == 'cancel':
157
            return redirect('.')
158

  
159
        if form.is_submitted() and not form.has_errors():
160
            calendar_ui.submit(form)
161
            return redirect('%s/' % calendar_ui.calendar.id)
162

  
163
        get_response().breadcrumb.append(('new', _('New Remote Calendar')))
164
        html_top('events', title = _('New Remote Calendar'))
165
        r = TemplateIO(html=True)
166
        r += htmltext('<h2>%s</h2>') % _('New Remote Calendar')
167
        r += form.render()
168
        return r.getvalue()
169

  
170
    def _q_lookup(self, component):
171
        try:
172
            event = RemoteCalendar.get(component)
173
        except KeyError:
174
            raise errors.TraversalError()
175
        get_response().breadcrumb.append((str(event.id), event.label))
176
        return RemoteCalendarDirectory(event)
177

  
178

  
179
class EventDirectory(Directory):
180
    _q_exports = ['', 'edit', 'delete']
181

  
182
    def __init__(self, event):
183
        self.event = event
184

  
185
    def _q_index(self):
186
        form = Form(enctype='multipart/form-data')
187
        form.add_submit('edit', _('Edit'))
188
        form.add_submit('delete', _('Delete'))
189
        form.add_submit('back', _('Back'))
190

  
191
        if form.get_submit() == 'edit':
192
            return redirect('edit')
193
        if form.get_submit() == 'delete':
194
            return redirect('delete')
195
        if form.get_submit() == 'back':
196
            return redirect('..')
197

  
198
        html_top('events', title = _('Event: %s') % self.event.title)
199
        r = TemplateIO(html=True)
200
        r += htmltext('<h2>%s</h2>') % _('Event: %s') % self.event.title
201
        r += htmltext('<p>')
202
        r += self.event.description
203
        r += htmltext('</p>')
204
        r += htmltext('<ul>')
205
        if self.event.location:
206
            r += htmltext('<li>%s: %s</li>') % (_('Location'), self.event.location)
207
        if self.event.organizer:
208
            r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.event.organizer)
209
        if self.event.url:
210
            r += htmltext('<li>%s: <a href="%s">%s</a></li>') % (_('URL'), self.event.url, self.event.url)
211
        r += htmltext('</ul>')
212

  
213
        if self.event.more_infos:
214
            r += htmltext('<p>')
215
            r += self.event.more_infos
216
            r += htmltext('</p>')
217

  
218
        r += form.render()
219
        return r.getvalue()
220

  
221
    def edit(self):
222
        form = self.form()
223
        if form.get_submit() == 'cancel':
224
            return redirect('.')
225

  
226
        if form.is_submitted() and not form.has_errors():
227
            self.submit(form)
228
            return redirect('..')
229

  
230
        html_top('events', title = _('Edit Event: %s') % self.event.title)
231
        r = TemplateIO(html=True)
232
        r += htmltext('<h2>%s</h2>') % _('Edit Event: %s') % self.event.title
233
        r += form.render()
234
        return r.getvalue()
235

  
236
    def form(self):
237
        form = Form(enctype='multipart/form-data')
238
        form.add(StringWidget, 'title', title = _('Title'), required = True,
239
                value = self.event.title)
240
        form.add(TextWidget, 'description', title = _('Description'),
241
                cols = 70, rows = 10,
242
                required = True, value = self.event.description)
243
        form.add(StringWidget, 'url', title = _('URL'), required = False,
244
                value = self.event.url, size = 40)
245
        form.add(DateWidget, 'date_start', title = _('Start Date'), required = True,
246
                value = strftime(misc.date_format(), self.event.date_start))
247
        form.add(DateWidget, 'date_end', title = _('End Date'), required = False,
248
                value = strftime(misc.date_format(), self.event.date_end))
249
        form.add(TextWidget, 'location', title = _('Location'),
250
                cols = 70, rows = 4,
251
                required = False, value = self.event.location)
252
        form.add(StringWidget, 'organizer', title = _('Organizer'), required = False,
253
                value = self.event.organizer, size = 40)
254
        form.add(TextWidget, 'more_infos', title = _('More informations'),
255
                cols = 70, rows = 10,
256
                required = False, value = self.event.more_infos)
257
        form.add(TagsWidget, 'keywords', title = _('Keywords'),
258
                value = self.event.keywords, size = 50,
259
                known_tags = get_cfg('misc', {}).get('event_tags', get_default_event_tags()))
260
        form.add_submit('submit', _('Submit'))
261
        form.add_submit('cancel', _('Cancel'))
262
        return form
263

  
264
    def submit(self, form):
265
        for k in ('title', 'description', 'url', 'date_start', 'date_end',
266
                    'organizer', 'location', 'more_infos', 'keywords'):
267
            widget = form.get_widget(k)
268
            if widget:
269
                if k in ('date_start', 'date_end'):
270
                    # convert dates to 9-item tuples
271
                    v = widget.parse()
272
                    if v:
273
                        setattr(self.event, k, time.strptime(v, misc.date_format()))
274
                    else:
275
                        setattr(self.event, k, None)
276
                else:
277
                    setattr(self.event, k, widget.parse())
278
        self.event.store()
279

  
280
    def delete(self):
281
        form = Form(enctype='multipart/form-data')
282
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
283
                        'You are about to irrevocably delete this event.')))
284
        form.add_submit('submit', _('Submit'))
285
        form.add_submit('cancel', _('Cancel'))
286
        if form.get_submit() == 'cancel':
287
            return redirect('..')
288
        if not form.is_submitted() or form.has_errors():
289
            get_response().breadcrumb.append(('delete', _('Delete')))
290
            html_top('events', title = _('Delete Event'))
291
            r = TemplateIO(html=True)
292
            r += htmltext('<h2>%s</h2>') % _('Deleting Event: %s') % self.event.title
293
            r += form.render()
294
            return r.getvalue()
295
        else:
296
            self.event.remove_self()
297
            return redirect('..')
298

  
299

  
300

  
301

  
302
class EventsDirectory(AccessControlled, Directory):
303
    _q_exports = ['', 'new', 'listing', 'remote']
304
    label = N_('Events')
305

  
306
    remote = RemoteCalendarsDirectory()
307

  
308
    def is_accessible(self, user):
309
        from .backoffice import check_visibility
310
        return check_visibility('events', user)
311

  
312
    def _q_access(self):
313
        user = get_request().user
314
        if not user:
315
            raise errors.AccessUnauthorizedError()
316

  
317
        if not self.is_accessible(user):
318
            raise errors.AccessForbiddenError(
319
                    public_msg = _('You are not allowed to access Events Management'),
320
                    location_hint = 'backoffice')
321

  
322
        get_response().breadcrumb.append(('events/', _('Events')))
323

  
324

  
325
    def _q_index(self):
326
        html_top('events', _('Events'))
327
        r = TemplateIO(html=True)
328

  
329
        get_response().filter['sidebar'] = self.get_sidebar()
330

  
331
        r += htmltext('<div class="splitcontent-left">')
332

  
333
        r += htmltext('<div class="bo-block">')
334
        events = Event.select()
335
        r += htmltext('<h2>%s</h2>') % _('Events')
336
        if not events:
337
            r += htmltext('<p>')
338
            r += _('There is no event defined at the moment.')
339
            r += htmltext('</p>')
340
        r += htmltext('<ul class="biglist" id="events-list">')
341
        for l in events:
342
            event_id = l.id
343
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % event_id
344
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (event_id, l.title)
345
            r += ' - '
346
            r += l.format_date()
347
            r += htmltext('<p class="commands">')
348
            r += command_icon('%s/edit' % event_id, 'edit')
349
            r += command_icon('%s/delete' % event_id, 'remove')
350
            r += htmltext('</p></li>')
351
        r += htmltext('</ul>')
352
        r += htmltext('</div>')
353
        r += htmltext('</div>')
354

  
355
        r += htmltext('<div class="splitcontent-right">')
356
        r += htmltext('<div class="bo-block">')
357
        rcalendars = RemoteCalendar.select()
358
        r += htmltext('<h2>%s</h2>') % _('Remote Calendars')
359
        if not rcalendars:
360
            r += htmltext('<p>')
361
            r += _('There is no remote calendars defined at the moment.')
362
            r += htmltext('</p>')
363

  
364
        r += htmltext('<ul class="biglist" id="events-list">')
365
        for l in rcalendars:
366
            rcal_id = l.id
367
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % rcal_id
368
            r += htmltext('<strong class="label"><a href="remote/%s/">%s</a></strong>') % (rcal_id, l.label)
369
            r += htmltext('<p class="details">')
370
            r += l.url
371
            if l.error:
372
                r += htmltext('<br /><span class="error-message">%s</span>') % l.get_error_message()
373
            r += htmltext('</p>')
374
            r += htmltext('<p class="commands">')
375
            r += command_icon('remote/%s/edit' % rcal_id, 'edit')
376
            r += command_icon('remote/%s/delete' % rcal_id, 'remove')
377
            r += htmltext('</p></li>')
378
        r += htmltext('</ul>')
379
        r += htmltext('</div>')
380
        r += htmltext('</div>')
381
        return r.getvalue()
382

  
383
    def get_sidebar(self):
384
        r = TemplateIO(html=True)
385
        r += htmltext('<ul id="sidebar-actions">')
386
        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Event')
387
        r += htmltext('  <li><a class="new-item" href="remote/new">%s</a></li>') % _('New Remote Calendar')
388
        r += htmltext('</ul>')
389
        return r.getvalue()
390

  
391
    def new(self):
392
        event_ui = EventDirectory(Event())
393

  
394
        form = event_ui.form()
395
        if form.get_submit() == 'cancel':
396
            return redirect('.')
397

  
398
        if form.is_submitted() and not form.has_errors():
399
            event_ui.submit(form)
400
            return redirect('%s/' % event_ui.event.id)
401

  
402
        get_response().breadcrumb.append(('new', _('New Event')))
403
        html_top('events', title = _('New Event'))
404
        r = TemplateIO(html=True)
405
        r += htmltext('<h2>%s</h2>') % _('New Event')
406
        r += form.render()
407
        return r.getvalue()
408

  
409
    def _q_lookup(self, component):
410
        try:
411
            event = Event.get(component)
412
        except KeyError:
413
            raise errors.TraversalError()
414
        get_response().breadcrumb.append((str(event.id), event.title))
415
        return EventDirectory(event)
416

  
417
    def listing(self):
418
        return redirect('.')
auquotidien/modules/formpage.py
1
from quixote import get_publisher, get_request, redirect
2
from quixote.directory import Directory
3
from quixote.html import htmltext
4

  
5
import os
6

  
7
import wcs
8
import wcs.forms.root
9
import wcs.forms.preview
10
from qommon import _
11
from qommon import template
12
from qommon import errors
13
from qommon.form import *
14
from wcs.roles import logged_users_role
15

  
16
from qommon import emails
17

  
18
OldFormPage = wcs.forms.root.FormPage
19

  
20
class AlternateFormPage(OldFormPage):
21
    def form_side(self, *args, **kwargs):
22
        form_side_html = OldFormPage.form_side(self, *args, **kwargs)
23
        # add a 'Steps' title
24
        form_side_html = str(form_side_html).replace('<ol>', '<h2>%s</h2>\n<ol>' % _('Steps'))
25
        get_response().filter['gauche'] = form_side_html
26
        get_response().filter['steps'] = form_side_html
27
        return
28

  
29
wcs.forms.root.FormPage = AlternateFormPage
30
wcs.forms.root.PublicFormStatusPage.form_page_class = AlternateFormPage
31
wcs.forms.preview.PreviewFormPage.__bases__ = (AlternateFormPage,)
32

  
33

  
34
OldFormsRootDirectory = wcs.forms.root.RootDirectory
35

  
36
class AlternateFormsRootDirectory(OldFormsRootDirectory):
37
    def form_list(self, *args, **kwargs):
38
        form_list = OldFormsRootDirectory.form_list(self, *args, **kwargs)
39
        return htmltext(str(form_list).replace('h2>', 'h3>'))
40

  
41
wcs.forms.root.RootDirectory = AlternateFormsRootDirectory
auquotidien/modules/links.py
1
from qommon.storage import StorableObject
2

  
3
class Link(StorableObject):
4
    _names = 'links'
5

  
6
    title = None
7
    url = None
8
    position = None
9

  
10
    def sort_by_position(cls, links):
11
        def cmp_position(x, y):
12
            if x.position == y.position:
13
                return 0
14
            if x.position is None:
15
                return 1
16
            if y.position is None:
17
                return -1
18
            return cmp(x.position, y.position)
19
        links.sort(cmp_position)
20
    sort_by_position = classmethod(sort_by_position)
21

  
auquotidien/modules/links_ui.py
1
from quixote import get_request, get_response, get_session, redirect
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import TemplateIO, htmltext
4

  
5
import wcs
6
import wcs.admin.root
7

  
8
from qommon import _
9
from qommon import errors
10
from qommon.form import *
11
from qommon.backoffice.menu import html_top
12
from qommon.admin.menu import command_icon
13
from qommon import get_cfg
14

  
15
from links import Link
16

  
17

  
18
class LinkDirectory(Directory):
19
    _q_exports = ['', 'edit', 'delete']
20

  
21
    def __init__(self, link):
22
        self.link = link
23

  
24
    def _q_index(self):
25
        form = Form(enctype='multipart/form-data')
26
        form.add_submit('edit', _('Edit'))
27
        form.add_submit('delete', _('Delete'))
28
        form.add_submit('back', _('Back'))
29

  
30
        if form.get_submit() == 'edit':
31
            return redirect('edit')
32
        if form.get_submit() == 'delete':
33
            return redirect('delete')
34
        if form.get_submit() == 'back':
35
            return redirect('..')
36

  
37
        html_top('links', title = _('Link: %s') % self.link.title)
38
        r = TemplateIO(html=True)
39
        r += htmltext('<h2>%s</h2>') % _('Link: %s') % self.link.title
40
        r += htmltext('<p>')
41
        r += self.link.url
42
        r += htmltext('</p>')
43

  
44
        r += form.render()
45
        return r.getvalue()
46

  
47
    def edit(self):
48
        form = self.form()
49
        if form.get_submit() == 'cancel':
50
            return redirect('.')
51

  
52
        if form.is_submitted() and not form.has_errors():
53
            self.submit(form)
54
            return redirect('..')
55

  
56
        html_top('links', title = _('Edit Link: %s') % self.link.title)
57
        r = TemplateIO(html=True)
58
        r += htmltext('<h2>%s</h2>') % _('Edit Link: %s') % self.link.title
59
        r += form.render()
60
        return r.getvalue()
61

  
62

  
63
    def form(self):
64
        form = Form(enctype='multipart/form-data')
65
        form.add(StringWidget, 'title', title = _('Title'), required = True,
66
                value = self.link.title)
67
        form.add(StringWidget, 'url', title = _('URL'), required=False,
68
                value = self.link.url,
69
                hint=_('Leave empty to create a title'))
70
        form.add_submit('submit', _('Submit'))
71
        form.add_submit('cancel', _('Cancel'))
72
        return form
73

  
74
    def submit(self, form):
75
        for k in ('title', 'url'):
76
            widget = form.get_widget(k)
77
            if widget:
78
                setattr(self.link, k, widget.parse())
79
        self.link.store()
80

  
81
    def delete(self):
82
        form = Form(enctype='multipart/form-data')
83
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
84
                        'You are about to irrevocably delete this link.')))
85
        form.add_submit('submit', _('Submit'))
86
        form.add_submit('cancel', _('Cancel'))
87
        if form.get_submit() == 'cancel':
88
            return redirect('..')
89
        if not form.is_submitted() or form.has_errors():
90
            get_response().breadcrumb.append(('delete', _('Delete')))
91
            html_top('links', title = _('Delete Link'))
92
            r = TemplateIO(html=True)
93
            r += htmltext('<h2>%s</h2>') % _('Deleting Link: %s') % self.link.title
94
            r += form.render()
95
            return r.getvalue()
96
        else:
97
            self.link.remove_self()
98
            return redirect('..')
99

  
100

  
101
class LinksDirectory(AccessControlled, Directory):
102
    _q_exports = ['', 'new', 'listing', 'update_order']
103
    label = N_('Links')
104

  
105
    def is_accessible(self, user):
106
        from .backoffice import check_visibility
107
        return check_visibility('links', user)
108

  
109
    def _q_access(self):
110
        user = get_request().user
111
        if not user:
112
            raise errors.AccessUnauthorizedError()
113

  
114
        if not self.is_accessible(user):
115
            raise errors.AccessForbiddenError(
116
                    public_msg = _('You are not allowed to access Links Management'),
117
                    location_hint = 'backoffice')
118

  
119
        get_response().breadcrumb.append(('links/', _('Links')))
120

  
121

  
122
    def _q_index(self):
123
        html_top('links', _('Links'))
124
        r = TemplateIO(html=True)
125
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js'])
126
        get_response().filter['sidebar'] = self.get_sidebar()
127

  
128
        links = Link.select()
129
        Link.sort_by_position(links)
130

  
131
        r += htmltext('<ul class="biglist sortable" id="links-list">')
132
        for l in links:
133
            link_id = l.id
134
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % link_id
135
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (link_id, l.title)
136
            r += htmltext('<p class="details">')
137
            r += l.url
138
            r += htmltext('</p>')
139
            r += htmltext('<p class="commands">')
140
            r += command_icon('%s/edit' % link_id, 'edit')
141
            r += command_icon('%s/delete' % link_id, 'remove')
142
            r += htmltext('</p></li>')
143
        r += htmltext('</ul>')
144
        return r.getvalue()
145

  
146
    def get_sidebar(self):
147
        r = TemplateIO(html=True)
148
        r += htmltext('<ul id="sidebar-actions">')
149
        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Link')
150
        r += htmltext('</ul>')
151
        return r.getvalue()
152

  
153
    def update_order(self):
154
        request = get_request()
155
        new_order = request.form['order'].strip(';').split(';')
156
        links = Link.select()
157
        dict = {}
158
        for l in links:
159
            dict[str(l.id)] = l
160
        for i, o in enumerate(new_order):
161
            dict[o].position = i + 1
162
            dict[o].store()
163
        return 'ok'
164

  
165

  
166
    def new(self):
167
        link_ui = LinkDirectory(Link())
168

  
169
        form = link_ui.form()
170
        if form.get_submit() == 'cancel':
171
            return redirect('.')
172

  
173
        if form.is_submitted() and not form.has_errors():
174
            link_ui.submit(form)
175
            return redirect('%s/' % link_ui.link.id)
176

  
177
        get_response().breadcrumb.append(('new', _('New Link')))
178
        html_top('links', title = _('New Link'))
179
        r = TemplateIO(html=True)
180
        r += htmltext('<h2>%s</h2>') % _('New Link')
181
        r += form.render()
182
        return r.getvalue()
183

  
184
    def _q_lookup(self, component):
185
        try:
186
            link = Link.get(component)
187
        except KeyError:
188
            raise errors.TraversalError()
189
        get_response().breadcrumb.append((str(link.id), link.title))
190
        return LinkDirectory(link)
191

  
192
    def listing(self):
193
        return redirect('.')
194

  
auquotidien/modules/myspace.py
1
try:
2
    import lasso
3
except ImportError:
4
    pass
5

  
6
import json
7

  
8
from quixote import get_publisher, get_request, redirect, get_response, get_session_manager, get_session
9
from quixote.directory import AccessControlled, Directory
10
from quixote.html import TemplateIO, htmltext
11
from quixote.util import StaticFile, FileStream
12

  
13
from qommon import _
14
from qommon import template
15
from qommon.form import *
16
from qommon import get_cfg, get_logger
17
from qommon import errors
18
from wcs.api import get_user_from_api_query_string
19

  
20
import qommon.ident.password
21
from qommon.ident.password_accounts import PasswordAccount
22

  
23
from qommon.admin.texts import TextsDirectory
24

  
25
from wcs.formdef import FormDef
26
import wcs.myspace
27
import root
28

  
29
from announces import AnnounceSubscription
30
from strongbox import StrongboxItem, StrongboxType
31
from payments import Invoice, Regie, is_payment_supported
32

  
33
class MyInvoicesDirectory(Directory):
34
    _q_exports = ['']
35

  
36
    def _q_traverse(self, path):
37
        if not is_payment_supported():
38
            raise errors.TraversalError()
39
        get_response().breadcrumb.append(('invoices/', _('Invoices')))
40
        return Directory._q_traverse(self, path)
41

  
42
    def _q_index(self):
43
        user = get_request().user
44
        if not user or user.anonymous:
45
            raise errors.AccessUnauthorizedError()
46

  
47
        template.html_top(_('Invoices'))
48
        r = TemplateIO(html=True)
49
        r += TextsDirectory.get_html_text('aq-myspace-invoice')
50

  
51
        r += get_session().display_message()
52

  
53
        invoices = []
54
        invoices.extend(Invoice.get_with_indexed_value(
55
            str('user_id'), str(user.id)))
56

  
57
        def cmp_invoice(a, b):
58
            t = cmp(a.regie_id, b.regie_id)
59
            if t != 0:
60
                return t
61
            return -cmp(a.date, b.date)
62

  
63
        invoices.sort(cmp_invoice)
64

  
65
        last_regie_id = None
66
        unpaid = False
67
        for invoice in invoices:
68
            if invoice.regie_id != last_regie_id:
69
                if last_regie_id:
70
                    r += htmltext('</ul>')
71
                    if unpaid:
72
                        r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
73
                    r += htmltext('</form>')
74
                last_regie_id = invoice.regie_id
75
                r += htmltext('<h3>%s</h3>') % Regie.get(last_regie_id).label
76
                unpaid = False
77
                r += htmltext('<form action="%s/invoices/multiple">' % get_publisher().get_frontoffice_url())
78
                r += htmltext('<ul>')
79

  
80
            r += htmltext('<li>')
81
            if not (invoice.paid or invoice.canceled):
82
                r += htmltext('<input type="checkbox" name="invoice" value="%s"/>' % invoice.id)
83
                unpaid = True
84
            r += misc.localstrftime(invoice.date)
85
            r += ' - '
86
            r += '%s' % invoice.subject
87
            r += ' - '
88
            r += '%s' % invoice.amount
89
            r += htmltext(' &euro;')
90
            r += ' - '
91
            button = '<span class="paybutton">%s</span>' % _('Pay')
92
            if invoice.canceled:
93
                r += _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
94
                r += ' - '
95
                button = _('Details')
96
            if invoice.paid:
97
                r += _('paid on %s') % misc.localstrftime(invoice.paid_date)
98
                r += ' - '
99
                button = _('Details')
100
            r += htmltext('<a href="%s/invoices/%s">%s</a>' % (get_publisher().get_frontoffice_url(),
101
                    invoice.id, button))
102
            r += htmltext('</li>')
103

  
104
        if last_regie_id:
105
            r += htmltext('</ul>')
106
            if unpaid:
107
                r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
108
            r += htmltext('</form>')
109

  
110
        return r.getvalue()
111

  
112

  
113
class StrongboxDirectory(Directory):
114
    _q_exports = ['', 'add', 'download', 'remove', 'pick', 'validate']
115

  
116
    def _q_traverse(self, path):
117
        if not get_cfg('misc', {}).get('aq-strongbox'):
118
            raise errors.TraversalError()
119
        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
120
        return Directory._q_traverse(self, path)
121

  
122
    def get_form(self):
123
        types = [(x.id, x.label) for x in StrongboxType.select()]
124
        form = Form(action='add', enctype='multipart/form-data')
125
        form.add(StringWidget, 'description', title=_('Description'), size=60)
126
        form.add(FileWidget, 'file', title=_('File'), required=True)
127
        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
128
                 options = [(None, _('Not specified'))] + types)
129
        form.add(DateWidget, 'date_time', title = _('Document Date'))
130
        form.add_submit('submit', _('Upload'))
131
        return form
132

  
133
    def _q_index(self):
134
        template.html_top(_('Strongbox'))
135
        r = TemplateIO(html=True)
136

  
137
        # TODO: a paragraph of explanations here could be useful
138

  
139
        sffiles = StrongboxItem.get_with_indexed_value(
140
                        str('user_id'), str(get_request().user.id))
141
        if sffiles:
142
            r += htmltext('<table id="strongbox-items">')
143
            r += htmltext('<tr><th></th><th>%s</th><th>%s</th><th></th></tr>') % (
144
                _('Type'), _('Expiration'))
145
        else:
146
            r += htmltext('<p>')
147
            r += _('There is currently nothing in your strongbox.')
148
            r += htmltext('</p>')
149
        has_items_to_validate = False
150
        for i, sffile in enumerate(sffiles):
151
            expired = False
152
            if not sffile.validated_time:
153
                has_items_to_validate = True
154
                continue
155
            if sffile.expiration_time and sffile.expiration_time < time.localtime():
156
                expired = True
157
            if i%2:
158
                classnames = ['odd']
159
            else:
160
                classnames = ['even']
161
            if expired:
162
                classnames.append('expired')
163
            r += htmltext('<tr class="%s">') % ' '.join(classnames)
164
            r += htmltext('<td class="label">')
165
            r += sffile.get_display_name()
166
            r += htmltext('</td>')
167
            if sffile.type_id:
168
                r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
169
            else:
170
                r += htmltext('<td class="type">-</td>')
171
            if sffile.expiration_time:
172
                r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
173
                if expired:
174
                    r += ' (%s)' % _('expired')
175
                r += htmltext('</td>')
176
            else:
177
                r += htmltext('<td class="expiration">-</td>')
178
            r += htmltext('<td class="actions">')
179
            r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
180
            r += htmltext('[<a rel="popup" href="remove?id=%s">%s</a>] ') % (sffile.id, _('remove'))
181
            r += htmltext('</td>')
182
            r += htmltext('</tr>')
183

  
184
        if has_items_to_validate:
185
            r += htmltext('<tr><td colspan="4"><h3>%s</h3></td></tr>') % _('Proposed Items')
186
            for sffile in sffiles:
187
                if sffile.validated_time:
188
                    continue
189
                if sffile.expiration_time and sffile.expiration_time < time.localtime():
190
                    expired = True
191
                if i%2:
192
                    classnames = ['odd']
193
                else:
194
                    classnames = ['even']
195
                if expired:
196
                    classnames.append('expired')
197
                r += htmltext('<tr class="%s">') % ' '.join(classnames)
198

  
199
                r += htmltext('<td class="label">')
200
                r += sffile.get_display_name()
201
                r += htmltext('</td>')
202
                if sffile.type_id:
203
                    r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
204
                else:
205
                    r += htmltext('<td class="type">-</td>')
206

  
207
                if sffile.expiration_time:
208
                    r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
209
                    if expired:
210
                        r += ' (%s)' % _('expired')
211
                    r += htmltext('</td>')
212
                else:
213
                    r += htmltext('<td class="expiration">-</td>')
214
                r += htmltext('<td class="actions">')
215
                r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
216
                r += htmltext(' [<a href="validate?id=%s">%s</a>] ') % (sffile.id, _('validate'))
217
                r += htmltext(' [<a href="remove?id=%s">%s</a>] ') % (sffile.id, _('reject'))
218
                r += htmltext('</td>')
219
                r += htmltext('</tr>')
220
        if sffiles:
221
            r += htmltext('</table>')
222

  
223
        r += htmltext('<h3>%s</h3>') % _('Add a file to the strongbox')
224
        form = self.get_form()
225
        r += form.render()
226
        return r.getvalue()
227

  
228
    def add(self):
229
        form = self.get_form()
230
        if not form.is_submitted():
231
            if get_request().form.get('mode') == 'pick':
232
                return redirect('pick')
233
            else:
234
                return redirect('.')
235

  
236
        sffile = StrongboxItem()
237
        sffile.user_id = get_request().user.id
238
        sffile.description = form.get_widget('description').parse()
239
        sffile.validated_time = time.localtime()
240
        sffile.type_id = form.get_widget('type_id').parse()
241
        v = form.get_widget('date_time').parse()
242
        sffile.set_expiration_time_from_date(v)
243
        sffile.store()
244
        sffile.set_file(form.get_widget('file').parse())
245
        sffile.store()
246
        if get_request().form.get('mode') == 'pick':
247
            return redirect('pick')
248
        else:
249
            return redirect('.')
250

  
251
    def download(self):
252
        id = get_request().form.get('id')
253
        if not id:
254
            raise errors.TraversalError()
255
        try:
256
            sffile = StrongboxItem.get(id)
257
        except KeyError:
258
            raise errors.TraversalError()
259
        if str(sffile.user_id) != str(get_request().user.id):
260
            raise errors.TraversalError()
261

  
262
        filename = sffile.file.filename
263
        fd = file(filename)
264
        size = os.path.getsize(filename)
265
        response = get_response()
266
        response.set_content_type('application/octet-stream')
267
        response.set_header('content-disposition', 'attachment; filename="%s"' % sffile.file.base_filename)
268
        return FileStream(fd, size)
269

  
270
    def validate(self):
271
        id = get_request().form.get('id')
272
        if not id:
273
            raise errors.TraversalError()
274
        try:
275
            sffile = StrongboxItem.get(id)
276
        except KeyError:
277
            raise errors.TraversalError()
278
        if str(sffile.user_id) != str(get_request().user.id):
279
            raise errors.TraversalError()
280
        sffile.validated_time = time.time()
281
        sffile.store()
282
        return redirect('.')
283

  
284
    def remove(self):
285
        id = get_request().form.get('id')
286
        if not id:
287
            raise errors.TraversalError()
288
        try:
289
            sffile = StrongboxItem.get(id)
290
        except KeyError:
291
            raise errors.TraversalError()
292
        if str(sffile.user_id) != str(get_request().user.id):
293
            raise errors.TraversalError()
294

  
295
        r = TemplateIO(html=True)
296
        form = Form(enctype='multipart/form-data')
297
        form.add_hidden('id', get_request().form.get('id'))
298
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
299
                        'You are about to irrevocably delete this item from your strongbox.')))
300
        form.add_submit('submit', _('Submit'))
301
        form.add_submit('cancel', _('Cancel'))
302
        if form.get_submit() == 'cancel':
303
            return redirect('.')
304
        if not form.is_submitted() or form.has_errors():
305
            if sffile.type_id:
306
                r += htmltext('<h2>%s</h2>') % _('Deleting %(filetype)s: %(filename)s') % {
307
                        'filetype': StrongboxType.get(sffile.type_id).label,
308
                        'filename': sffile.get_display_name()
309
                    }
310
            else:
311
                r += htmltext('<h2>%s</h2>') % _('Deleting %(filename)s') % {'filename': sffile.get_display_name()}
312
            r += form.render()
313
            return r.getvalue()
314
        else:
315
            sffile.remove_self()
316
            sffile.remove_file()
317
            return redirect('.')
318

  
319
    def picked_file(self):
320
        get_response().set_content_type('application/json')
321
        sffile = StrongboxItem.get(get_request().form.get('val'))
322
        sffile.file.fp = file(sffile.file.filename)
323
        if sffile.user_id != get_request().user.id:
324
            raise errors.TraversalError()
325
        # XXX: this will copy the file, it would be quite nice if it was
326
        # possible to just make it a symlink to the sffile
327
        token = get_session().add_tempfile(sffile.file)
328
        return json.dumps({'token': token, 'filename': sffile.file.base_filename})
329

  
330
    def pick(self):
331
        if get_request().form.get('select') == 'true':
332
            return self.picked_file()
333
        r = TemplateIO(html=True)
334
        root_url = get_publisher().get_root_url()
335
        sffiles = StrongboxItem.get_with_indexed_value(
336
                        str('user_id'), str(get_request().user.id))
337
        r += htmltext('<h2>%s</h2>') % _('Pick a file')
338

  
339
        if not sffiles:
340
            r += htmltext('<p>')
341
            r += _('You do not have any file in your strongbox at the moment.')
342
            r += htmltext('</p>')
343
            r += htmltext('<div class="buttons">')
344
            r += htmltext('<a href="%smyspace/strongbox/" target="_blank">%s</a>') % (root_url,
345
                _('Open Strongbox Management'))
346
            r += htmltext('</div>')
347
        else:
348
            r += htmltext('<form id="strongbox-pick">')
349
            r += htmltext('<ul>')
350
            for sffile in sffiles:
351
                r += htmltext('<li><label><input type="radio" name="file" value="%s"/>%s</label>') % (
352
                                sffile.id, sffile.get_display_name())
353
                r += htmltext(' [<a href="%smyspace/strongbox/download?id=%s">%s</a>] ') % (
354
                                root_url, sffile.id, _('view'))
355
                r += htmltext('</li>')
356
            r += htmltext('</ul>')
357

  
358
            r += htmltext('<div class="buttons">')
359
            r += htmltext('<input name="cancel" type="button" value="%s"/>') % _('Cancel')
360
            r += ' '
361
            r += htmltext('<input name="pick" type="button" value="%s"/>') % _('Pick')
362
            r += htmltext('</div>')
363
            r += htmltext('</form>')
364
        return r.getvalue()
365

  
366

  
367
class JsonDirectory(Directory):
368
    '''Export of several lists in json, related to the current user or the
369
       SAMLv2 NameID we'd get in the URL'''
370

  
371
    _q_exports = ['forms']
372

  
373
    user = None
374

  
375
    def _q_traverse(self, path):
376
        self.user = get_user_from_api_query_string() or get_request().user
377
        if not self.user:
378
            raise errors.AccessUnauthorizedError()
379
        return Directory._q_traverse(self, path)
380

  
381
    def forms(self):
382
        formdefs = FormDef.select(order_by='name', ignore_errors=True)
383
        user_forms = []
384
        for formdef in formdefs:
385
            user_forms.extend(formdef.data_class().get_with_indexed_value(
386
                        'user_id', self.user.id))
387
        user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
388

  
389
        get_response().set_content_type('application/json')
390

  
391

  
392
        forms_output = []
393
        for form in user_forms:
394
            visible_status = form.get_visible_status(user=self.user)
395
            # skip drafts and hidden forms
396
            if not visible_status:
397
                continue
398
            name = form.formdef.name
399
            id = form.get_display_id()
400
            status = visible_status.name
401
            title = _('%(name)s #%(id)s (%(status)s)') % {
402
                    'name': name,
403
                    'id': id,
404
                    'status': status
405
            }
406
            url = form.get_url()
407
            d = { 'title': title, 'url': url }
408
            d.update(form.get_substitution_variables(minimal=True))
409
            forms_output.append(d)
410
        return json.dumps(forms_output, cls=misc.JSONEncoder)
411

  
412

  
413
class MyspaceDirectory(wcs.myspace.MyspaceDirectory):
414
    _q_exports = ['', 'profile', 'new', 'password', 'remove', 'drafts', 'forms',
415
            'announces', 'strongbox', 'invoices', 'json']
416

  
417
    strongbox = StrongboxDirectory()
418
    invoices = MyInvoicesDirectory()
419
    json = JsonDirectory()
420

  
421
    def _q_traverse(self, path):
422
        get_response().filter['bigdiv'] = 'profile'
423
        get_response().breadcrumb.append(('myspace/', _('My Space')))
424

  
425
        # Migrate custom text settings
426
        texts_cfg = get_cfg('texts', {})
427
        if 'text-aq-top-of-profile' in texts_cfg and (
428
                        not 'text-top-of-profile' in texts_cfg):
429
            texts_cfg['text-top-of-profile'] = texts_cfg['text-aq-top-of-profile']
430
            del texts_cfg['text-aq-top-of-profile']
431
            get_publisher().write_cfg()
432

  
433
        return Directory._q_traverse(self, path)
434

  
435

  
436
    def _q_index(self):
437
        user = get_request().user
438
        if not user:
439
            raise errors.AccessUnauthorizedError()
440
        template.html_top(_('My Space'))
441
        r = TemplateIO(html=True)
442
        if user.anonymous:
443
            return redirect('new')
444

  
445
        user_formdef = user.get_formdef()
446

  
447
        user_forms = []
448
        if user:
449
            formdefs = FormDef.select(order_by='name', ignore_errors=True)
450
            user_forms = []
451
            for formdef in formdefs:
452
                user_forms.extend(formdef.data_class().get_with_indexed_value(
453
                            'user_id', user.id))
454
            user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
455

  
456
        profile_links = []
457
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
458
            if user_formdef:
459
                profile_links.append('<a href="#my-profile">%s</a>' % _('My Profile'))
460
        if user_forms:
461
            profile_links.append('<a href="#my-forms">%s</a>' % _('My Forms'))
462
        if get_cfg('misc', {}).get('aq-strongbox'):
463
            profile_links.append('<a href="strongbox/">%s</a>' % _('My Strongbox'))
464
        if is_payment_supported():
465
            profile_links.append('<a href="invoices/">%s</a>' % _('My Invoices'))
466

  
467
        root_url = get_publisher().get_root_url()
468
        if user.can_go_in_backoffice():
469
            profile_links.append('<a href="%sbackoffice/">%s</a>' % (root_url, _('Back office')))
470
        if user.is_admin:
471
            profile_links.append('<a href="%sadmin/">%s</a>' % (root_url, _('Admin')))
472

  
473
        if profile_links:
474
            r += htmltext('<p id="profile-links">')
475
            r += htmltext(' - '.join(profile_links))
476
            r += htmltext('</p>')
477

  
478
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
479
            if user_formdef:
480
                r += self._my_profile(user_formdef, user)
481

  
482
            r += self._index_buttons(user_formdef)
483

  
484
            try:
485
                x = PasswordAccount.get_on_index(get_request().user.id, str('user_id'))
486
            except KeyError:
487
                pass
488
            else:
489
                r += htmltext('<p>')
490
                r += _('You can delete your account freely from the services portal. '
491
                       'This action is irreversible; it will destruct your personal '
492
                       'datas and destruct the access to your request history.')
493
                r += htmltext(' <strong><a href="remove" rel="popup">%s</a></strong>.') % _('Delete My Account')
494
                r += htmltext('</p>')
495

  
496
        options = get_cfg('misc', {}).get('announce_themes')
497
        if options:
498
            try:
499
                subscription = AnnounceSubscription.get_on_index(
500
                        get_request().user.id, str('user_id'))
501
            except KeyError:
502
                pass
503
            else:
504
                r += htmltext('<p class="command"><a href="announces">%s</a></p>') % _(
505
                        'Edit my Subscription to Announces')
506

  
507
        if user_forms:
508
            r += htmltext('<h3 id="my-forms">%s</h3>') % _('My Forms')
509
            r += root.FormsRootDirectory().user_forms(user_forms)
510

  
511
        return r.getvalue()
512

  
513
    def _my_profile(self, user_formdef, user):
514
        r = TemplateIO(html=True)
515
        r += htmltext('<h3 id="my-profile">%s</h3>') % _('My Profile')
516

  
517
        r += TextsDirectory.get_html_text('top-of-profile')
518

  
519
        if user.form_data:
520
            r += htmltext('<ul>')
521
            for field in user_formdef.fields:
522
                if not hasattr(field, str('get_view_value')):
523
                    continue
524
                value = user.form_data.get(field.id)
525
                r += htmltext('<li>')
526
                r += field.label
527
                r += ' : '
528
                if value:
529
                    r += field.get_view_value(value)
530
                r += htmltext('</li>')
531
            r += htmltext('</ul>')
532
        else:
533
            r += htmltext('<p>%s</p>') % _('Empty profile')
534
        return r.getvalue()
535

  
536
    def _index_buttons(self, form_data):
537
        r = TemplateIO(html=True)
538
        passwords_cfg = get_cfg('passwords', {})
539
        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
540
        if get_session().lasso_session_dump:
541
            ident_method = 'idp'
542

  
543
        if form_data and ident_method != 'idp':
544
            r += htmltext('<p class="command"><a href="profile" rel="popup">%s</a></p>') % _('Edit My Profile')
545

  
546
        if ident_method == 'password' and passwords_cfg.get('can_change', False):
547
            r += htmltext('<p class="command"><a href="password" rel="popup">%s</a></p>') % _('Change My Password')
548

  
549
        return r.getvalue()
550

  
551
    def profile(self):
552
        user = get_request().user
553
        if not user or user.anonymous:
554
            raise errors.AccessUnauthorizedError()
555

  
556
        form = Form(enctype = 'multipart/form-data')
557
        formdef = user.get_formdef()
558
        formdef.add_fields_to_form(form, form_data = user.form_data)
559

  
560
        form.add_submit('submit', _('Apply Changes'))
561
        form.add_submit('cancel', _('Cancel'))
562

  
563
        if form.get_submit() == 'cancel':
564
            return redirect('.')
565

  
566
        if form.is_submitted() and not form.has_errors():
567
            self.profile_submit(form, formdef)
568
            return redirect('.')
569

  
570
        template.html_top(_('Edit Profile'))
571
        return form.render()
572

  
573
    def profile_submit(self, form, formdef):
574
        user = get_request().user
575
        data = formdef.get_data(form)
576

  
577
        user.set_attributes_from_formdata(data)
578
        user.form_data = data
579

  
580
        user.store()
581

  
582
    def password(self):
583
        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
584
        if ident_method != 'password':
585
            raise errors.TraversalError()
586

  
587
        user = get_request().user
588
        if not user or user.anonymous:
589
            raise errors.AccessUnauthorizedError()
590

  
591
        form = Form(enctype = 'multipart/form-data')
592
        form.add(PasswordWidget, 'new_password', title = _('New Password'),
593
                required=True)
594
        form.add(PasswordWidget, 'new2_password', title = _('New Password (confirm)'),
595
                required=True) 
596

  
597
        form.add_submit('submit', _('Change Password'))
598
        form.add_submit('cancel', _('Cancel'))
599

  
600
        if form.get_submit() == 'cancel':
601
            return redirect('.')
602

  
603
        if form.is_submitted() and not form.has_errors():
604
            qommon.ident.password.check_password(form, 'new_password')
605
            new_password = form.get_widget('new_password').parse()
606
            new2_password = form.get_widget('new2_password').parse()
607
            if new_password != new2_password:
608
                form.set_error('new2_password', _('Passwords do not match'))
609

  
610
        if form.is_submitted() and not form.has_errors():
611
            self.submit_password(new_password)
612
            return redirect('.')
613

  
614
        template.html_top(_('Change Password'))
615
        return form.render()
616

  
617
    def submit_password(self, new_password):
618
        passwords_cfg = get_cfg('passwords', {})
619
        account = PasswordAccount.get(get_session().username)
620
        account.hashing_algo = passwords_cfg.get('hashing_algo')
621
        account.set_password(new_password)
622
        account.store()
623

  
624
    def new(self):
625
        if not get_request().user or not get_request().user.anonymous:
626
            raise errors.AccessUnauthorizedError()
627

  
628
        form = Form(enctype = 'multipart/form-data')
629
        formdef = get_publisher().user_class.get_formdef()
630
        if formdef:
631
            formdef.add_fields_to_form(form)
632
        else:
633
            get_logger().error('missing user formdef (in myspace/new)')
634

  
635
        form.add_submit('submit', _('Register'))
636

  
637
        if form.is_submitted() and not form.has_errors():
638
            user = get_publisher().user_class()
639
            data = formdef.get_data(form)
640
            user.set_attributes_from_formdata(data)
641
            user.name_identifiers = get_request().user.name_identifiers
642
            user.lasso_dump = get_request().user.lasso_dump
643
            user.set_attributes_from_formdata(data)
644
            user.form_data = data
645
            user.store()
646
            get_session().set_user(user.id)
647
            root_url = get_publisher().get_root_url()
648
            return redirect('%smyspace' % root_url)
649

  
650
        template.html_top(_('Welcome'))
651
        return form.render()
652

  
653

  
654
    def remove(self):
655
        user = get_request().user
656
        if not user or user.anonymous:
657
            raise errors.AccessUnauthorizedError()
658

  
659
        form = Form(enctype = 'multipart/form-data')
660
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
661
                        'Are you really sure you want to remove your account?')))
662
        form.add_submit('submit', _('Remove my account'))
663
        form.add_submit('cancel', _('Cancel'))
664

  
665
        if form.get_submit() == 'cancel':
666
            return redirect('.')
667

  
668
        if form.is_submitted() and not form.has_errors():
669
            user = get_request().user
670
            account = PasswordAccount.get_on_index(user.id, str('user_id'))
671
            get_session_manager().expire_session() 
672
            account.remove_self()
673
            return redirect(get_publisher().get_root_url())
674

  
675
        template.html_top(_('Removing Account'))
676
        return form.render()
677

  
678
    def announces(self):
679
        options = get_cfg('misc', {}).get('announce_themes')
680
        if not options:
681
            raise errors.TraversalError()
682
        user = get_request().user
683
        if not user or user.anonymous:
684
            raise errors.AccessUnauthorizedError()
685
        subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id'))
686
        if not subscription:
687
            raise errors.TraversalError()
688

  
689
        if subscription.enabled_themes is None:
690
            enabled_themes = options
691
        else:
692
            enabled_themes = subscription.enabled_themes
693

  
694
        form = Form(enctype = 'multipart/form-data')
695
        form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'),
696
                value=enabled_themes, options=[(x, x, x) for x in options],
697
                inline=False, required=False)
698

  
699
        form.add_submit('submit', _('Apply Changes'))
700
        form.add_submit('cancel', _('Cancel'))
701

  
702
        if form.get_submit() == 'cancel':
703
            return redirect('.')
704

  
705
        if form.is_submitted() and not form.has_errors():
706
            chosen_themes = form.get_widget('themes').parse()
707
            if chosen_themes == options:
708
                chosen_themes = None
709
            subscription.enabled_themes = chosen_themes
710
            subscription.store()
711
            return redirect('.')
712

  
713
        template.html_top()
714
        get_response().breadcrumb.append(('announces', _('Announce Subscription')))
715
        return form.render()
716

  
717

  
718
TextsDirectory.register('aq-myspace-invoice',
719
        N_('Message on top of invoices page'),
720
        category = N_('Invoices'))
721

  
auquotidien/modules/payments.py
1
import random
2
import string
3
from datetime import datetime as dt
4
import hashlib
5
import time
6
import urllib
7

  
8
from decimal import Decimal
9

  
10
from quixote import (redirect, get_publisher, get_request, get_session,
11
        get_response)
12
from quixote.directory import Directory
13
from quixote.html import TemplateIO, htmltext
14

  
15
if not set:
16
    from sets import Set as set
17

  
18
eopayment = None
19
try:
20
    import eopayment
21
except ImportError:
22
    pass
23

  
24
from qommon import _
25
from qommon import errors, get_logger, get_cfg, emails
26
from qommon.storage import StorableObject
27
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
28
    WidgetDict
29
from qommon.misc import simplify
30

  
31
from wcs.formdef import FormDef
32
from wcs.formdata import Evolution
33
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
34
from wcs.users import User
35

  
36
def is_payment_supported():
37
    if not eopayment:
38
        return False
39
    return get_cfg('aq-permissions', {}).get('payments', None) is not None
40

  
41

  
42
class Regie(StorableObject):
43
    _names = 'regies'
44

  
45
    label = None
46
    description = None
47
    service = None
48
    service_options = None
49

  
50
    def get_payment_object(self):
51
        return eopayment.Payment(kind=self.service,
52
                                 options=self.service_options)
53

  
54

  
55
class Invoice(StorableObject):
56
    _names = 'invoices'
57
    _hashed_indexes = ['user_id', 'regie_id']
58
    _indexes = ['external_id']
59

  
60
    user_id = None
61
    regie_id = None
62
    formdef_id = None
63
    formdata_id = None
64
    subject = None
65
    details = None
66
    amount = None
67
    date = None
68
    paid = False
69
    paid_date = None
70
    canceled = False
71
    canceled_date = None
72
    canceled_reason = None
73
    next_status = None
74
    external_id = None
75
    request_kwargs = {}
76

  
77
    def __init__(self, id=None, regie_id=None, formdef_id=None):
78
        self.id = id
79
        self.regie_id = regie_id
80
        self.formdef_id = formdef_id
81
        if get_publisher() and not self.id:
82
            self.id = self.get_new_id()
83

  
84
    def get_user(self):
85
        if self.user_id:
86
            return User.get(self.user_id, ignore_errors=True)
87
        return None
88

  
89
    @property
90
    def username(self):
91
        user = self.get_user()
92
        return user.name if user else ''
93

  
94
    def get_new_id(self, create=False):
95
        # format : date-regie-formdef-alea-check
96
        r = random.SystemRandom()
97
        self.fresh = True
98
        while True:
99
            id = '-'.join([
100
                dt.now().strftime('%Y%m%d'),
101
                'r%s' % (self.regie_id or 'x'),
102
                'f%s' % (self.formdef_id or 'x'),
103
                ''.join([r.choice(string.digits) for x in range(5)])
104
                ])
105
            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
106
            id = id + '-' + crc
107
            if not self.has_key(id):
108
                return id
109

  
110
    def store(self, *args, **kwargs):
111
        if getattr(self, 'fresh', None) is True:
112
            del self.fresh
113
            notify_new_invoice(self)
114
        return super(Invoice, self).store(*args, **kwargs)
115

  
116
    def check_crc(cls, id):
117
        try:
118
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
119
        except:
120
            return False
121
    check_crc = classmethod(check_crc)
122

  
123
    def pay(self):
124
        self.paid = True
125
        self.paid_date = dt.now()
126
        self.store()
127
        get_logger().info(_('invoice %s paid'), self.id)
128
        notify_paid_invoice(self)
129

  
130
    def unpay(self):
131
        self.paid = False
132
        self.paid_date = None
133
        self.store()
134
        get_logger().info(_('invoice %s unpaid'), self.id)
135

  
136
    def cancel(self, reason=None):
137
        self.canceled = True
138
        self.canceled_date = dt.now()
139
        if reason:
140
            self.canceled_reason = reason
141
        self.store()
142
        notify_canceled_invoice(self)
143
        get_logger().info(_('invoice %s canceled'), self.id)
144

  
145
    def payment_url(self):
146
        base_url = get_publisher().get_frontoffice_url()
147
        return '%s/invoices/%s' % (base_url, self.id)
148

  
149

  
150
INVOICE_EVO_VIEW = {
151
    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
152
    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
153
    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
154
    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
155
}
156

  
157
class InvoiceEvolutionPart:
158
    action = None
159
    id = None
160
    subject = None
161
    amount = None
162
    transaction = None
163

  
164
    def __init__(self, action, invoice, transaction=None):
165
        self.action = action
166
        self.id = invoice.id
167
        self.subject = invoice.subject
168
        self.amount = invoice.amount
169
        self.transaction = transaction
170

  
171
    def view(self):
172
        vars = {
173
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
174
            'id': self.id,
175
            'subject': self.subject,
176
            'amount': self.amount,
177
        }
178
        if self.transaction:
179
            vars['transaction_order_id'] = self.transaction.order_id
180
        return htmltext('<p class="invoice-%s">' % self.action + \
181
                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
182

  
183

  
184
class Transaction(StorableObject):
185
    _names = 'transactions'
186
    _hashed_indexes = ['invoice_ids']
187
    _indexes = ['order_id']
188

  
189
    invoice_ids = None
190

  
191
    order_id = None
192
    start = None
193
    end = None
194
    bank_data = None
195

  
196
    def __init__(self, *args, **kwargs):
197
        self.invoice_ids = list()
198
        StorableObject.__init__(self, *args, **kwargs)
199

  
200
    def get_new_id(cls, create=False):
201
        r = random.SystemRandom()
202
        while True:
203
            id = ''.join([r.choice(string.digits) for x in range(16)])
204
            if not cls.has_key(id):
205
                return id
206
    get_new_id = classmethod(get_new_id)
207

  
208
class PaymentWorkflowStatusItem(WorkflowStatusItem):
209
    description = N_('Payment Creation')
210
    key = 'payment'
211
    endpoint = False
212
    category = ('aq-payment', N_('Payment'))
213
    support_substitution_variables = True
214

  
215
    subject = None
216
    details = None
217
    amount = None
218
    regie_id = None
219
    next_status = None
220
    request_kwargs = {}
221

  
222
    def is_available(self, workflow=None):
223
        return is_payment_supported()
224
    is_available = classmethod(is_available)
225

  
226
    def render_as_line(self):
227
        if self.regie_id:
228
            try:
229
                return _('Payable to %s' % Regie.get(self.regie_id).label)
230
            except KeyError:
231
                return _('Payable (not completed)')
232
        else:
233
            return _('Payable (not completed)')
234

  
235
    def get_parameters(self):
236
        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
237
                'request_kwargs')
238

  
239
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
240
        if 'subject' in parameters:
241
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
242
                     value=self.subject, size=40)
243
        if 'details' in parameters:
244
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
245
                     value=self.details, cols=80, rows=10)
246
        if 'amount' in parameters:
247
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
248
        if 'regie_id' in parameters:
249
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
250
                title=_('Regie'), value=self.regie_id,
251
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
252
        if 'next_status' in parameters:
253
            form.add(SingleSelectWidget, '%snext_status' % prefix,
254
                title=_('Status after validation'), value = self.next_status,
255
                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
256
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
257
        if 'request_kwargs' in parameters:
258
            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
259
            hint = ''
260
            hint +=_('If the value starts by = it will be '
261
                'interpreted as a Python expression.')
262
            hint += ' '
263
            hint += _('Standard keys are: %s.') % (', '.join(keys))
264
            form.add(WidgetDict, 'request_kwargs',
265
                title=_('Parameters for the payment system'),
266
                hint=hint,
267
                value = self.request_kwargs)
268

  
269
    def perform(self, formdata):
270
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
271
        invoice.user_id = formdata.user_id
272
        invoice.formdata_id = formdata.id
273
        invoice.next_status = self.next_status
274
        if self.subject:
275
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
276
        else:
277
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
278
                    'form_name': formdata.formdef.name,
279
                    'formdata_id': formdata.id }
280
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
281
        invoice.amount = Decimal(self.compute(self.amount))
282
        invoice.date = dt.now()
283
        invoice.request_kwargs = {}
284
        if self.request_kwargs:
285
            for key, value in self.request_kwargs.iteritems():
286
                invoice.request_kwargs[key] = self.compute(value)
287
        invoice.store()
288
        # add a message in formdata.evolution
289
        evo = Evolution()
290
        evo.time = time.localtime()
291
        evo.status = formdata.status
292
        evo.add_part(InvoiceEvolutionPart('create', invoice))
293
        if not formdata.evolution:
294
            formdata.evolution = []
295
        formdata.evolution.append(evo)
296
        formdata.store()
297
        # redirect the user to "my invoices"
298
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
299

  
300
register_item_class(PaymentWorkflowStatusItem)
301

  
302
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
303
    description = N_('Payment Cancel')
304
    key = 'payment-cancel'
305
    endpoint = False
306
    category = ('aq-payment', N_('Payment'))
307

  
308
    reason = None
309
    regie_id = None
310

  
311
    def is_available(self, workflow=None):
312
        return is_payment_supported()
313
    is_available = classmethod(is_available)
314

  
315
    def render_as_line(self):
316
        if self.regie_id:
317
            if self.regie_id == '_all':
318
                return _('Cancel all Payments')
319
            else:
320
                try:
321
                    return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
322
                except KeyError:
323
                    return _('Cancel Payments (non completed)')
324
        else:
325
            return _('Cancel Payments (non completed)')
326

  
327
    def get_parameters(self):
328
        return ('reason', 'regie_id')
329

  
330
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
331
        if 'reason' in parameters:
332
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
333
                     value=self.reason, size=40)
334
        if 'regie_id' in parameters:
335
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
336
                title=_('Regie'), value=self.regie_id,
337
                options = [(None, '---'), ('_all', _('All Regies'))] + \
338
                            [(x.id, x.label) for x in Regie.select()])
339

  
340
    def perform(self, formdata):
341
        invoices_id = []
342
        # get all invoices for the formdata and the selected regie
343
        for evo in [evo for evo in formdata.evolution if evo.parts]:
344
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
345
                if part.action == 'create':
346
                    invoices_id.append(part.id)
347
                elif part.id in invoices_id:
348
                    invoices_id.remove(part.id)
349
        invoices = [Invoice.get(id) for id in invoices_id]
350
        # select invoices for the selected regie (if not "all regies")
351
        if self.regie_id != '_all':
352
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
353
        # security filter: check user
354
        invoices = [i for i in invoices if i.user_id == formdata.user_id]
355
        # security filter: check formdata & formdef
356
        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
357
                and (i.formdef_id == formdata.formdef.id)]
358
        evo = Evolution()
359
        evo.time = time.localtime()
360
        for invoice in invoices:
361
            if not (invoice.paid or invoice.canceled):
362
                invoice.cancel(self.reason)
363
                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
364
        if not formdata.evolution:
365
            formdata.evolution = []
366
        formdata.evolution.append(evo)
367
        formdata.store()
368
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
369

  
370
register_item_class(PaymentCancelWorkflowStatusItem)
371

  
372

  
373
def request_payment(invoice_ids, url, add_regie=True):
374
    for invoice_id in invoice_ids:
375
        if not Invoice.check_crc(invoice_id):
376
            raise KeyError()
377
    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
378
    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
379
    regie_ids = set([invoice.regie_id for invoice in invoices])
380
    # Do not apply if more than one regie is used or no invoice is not paid or canceled
381
    if len(invoices) == 0 or len(regie_ids) != 1:
382
        url = get_publisher().get_frontoffice_url()
383
        if get_session().user:
384
            # FIXME: add error messages
385
            url += '/myspace/invoices/'
386
        return redirect(url)
387
    if add_regie:
388
        url = '%s%s' % (url, list(regie_ids)[0])
389

  
390
    transaction = Transaction()
391
    transaction.store()
392
    transaction.invoice_ids = invoice_ids
393
    transaction.start = dt.now()
394

  
395
    amount = Decimal(0)
396
    for invoice in invoices:
397
        amount += Decimal(invoice.amount)
398

  
399
    regie = Regie.get(invoice.regie_id)
400
    payment = regie.get_payment_object()
401
    # initialize request_kwargs using informations from the first invoice
402
    # and update using current user informations
403
    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
404
    request = get_request()
405
    if request.user and request.user.email:
406
        request_kwargs['email'] = request.user.email
407
    if request.user and request.user.display_name:
408
        request_kwargs['name'] = simplify(request.user.display_name)
409
    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
410
    transaction.order_id = order_id
411
    transaction.store()
412

  
413
    for invoice in invoices:
414
        if invoice.formdef_id and invoice.formdata_id:
415
            formdef = FormDef.get(invoice.formdef_id)
416
            formdata = formdef.data_class().get(invoice.formdata_id)
417
            evo = Evolution()
418
            evo.time = time.localtime()
419
            evo.status = formdata.status
420
            evo.add_part(InvoiceEvolutionPart('try', invoice,
421
                transaction=transaction))
422
            if not formdata.evolution:
423
                formdata.evolution = []
424
            formdata.evolution.append(evo)
425
            formdata.store()
426

  
427
    if kind == eopayment.URL:
428
        return redirect(data)
429
    elif kind == eopayment.FORM:
430
        return return_eopayment_form(data)
431
    else:
432
        raise NotImplementedError()
433

  
434
def return_eopayment_form(form):
435
    r = TemplateIO(html=True)
436
    r += htmltext('<html><body onload="document.payform.submit()">')
437
    r += htmltext('<form action="%s" method="%s" name="payform">') % (form.url, form.method)
438
    for field in form.fields:
439
        r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
440
                field['type'],
441
                field['name'],
442
                field['value'])
443
    r += htmltext('<input type="submit" name="submit" value="%s"/>') % _('Pay')
444
    r += htmltext('</body></html>')
445
    return r.getvalue()
446

  
447

  
448
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
449
    description = N_('Payment Validation')
450
    key = 'payment-validation'
451
    endpoint = False
452
    category = ('aq-payment', N_('Payment'))
453

  
454
    next_status = None
455

  
456
    def is_available(self, workflow=None):
457
        return is_payment_supported()
458
    is_available = classmethod(is_available)
459

  
460
    def render_as_line(self):
461
        return _('Wait for payment validation')
462

  
463
    def get_parameters(self):
464
        return ('next_status',)
465

  
466
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
467
        if 'next_status' in parameters:
468
            form.add(SingleSelectWidget, '%snext_status' % prefix,
469
                title=_('Status once validated'), value = self.next_status,
470
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
471

  
472
register_item_class(PaymentValidationWorkflowStatusItem)
473

  
474

  
475
class PublicPaymentRegieBackDirectory(Directory):
476
    def __init__(self, asynchronous):
477
        self.asynchronous = asynchronous
478

  
479
    def _q_lookup(self, component):
480
        logger = get_logger()
481
        request = get_request()
482
        query_string = get_request().get_query()
483
        if request.get_method() == 'POST' and query_string == '':
484
            query_string = urllib.urlencode(request.form)
485
        try:
486
            regie = Regie.get(component)
487
        except KeyError:
488
            raise errors.TraversalError()
489
        if self.asynchronous:
490
            logger.debug('received asynchronous notification %r' % query_string)
491
        payment = regie.get_payment_object()
492
        payment_response = payment.response(query_string)
493
        logger.debug('payment response %r', payment_response)
494
        order_id = payment_response.order_id
495
        bank_data = payment_response.bank_data
496

  
497
        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
498
        if transaction is None:
499
            raise errors.TraversalError()
500
        commit = False
501
        if not transaction.end:
502
            commit = True
503
            transaction.end = dt.now()
504
            transaction.bank_data = bank_data
505
            transaction.store()
506
        if payment_response.signed and payment_response.is_paid() and commit:
507
            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
508
                                    order_id, payment_response.transaction_id, bank_data))
509

  
510
            for invoice_id in transaction.invoice_ids:
511
                # all invoices are now paid
512
                invoice = Invoice.get(invoice_id)
513
                invoice.pay()
514

  
515
                # workflow for each related formdata
516
                if invoice.formdef_id and invoice.formdata_id:
517
                    next_status = invoice.next_status
518
                    formdef = FormDef.get(invoice.formdef_id)
519
                    formdata = formdef.data_class().get(invoice.formdata_id)
520
                    wf_status = formdata.get_status()
521
                    for item in wf_status.items:
522
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
523
                            next_status = item.next_status
524
                            break
525
                    if next_status is not None:
526
                        formdata.status = 'wf-%s' % next_status
527
                        evo = Evolution()
528
                        evo.time = time.localtime()
529
                        evo.status = formdata.status
530
                        evo.add_part(InvoiceEvolutionPart('pay', invoice,
531
                            transaction=transaction))
532
                        if not formdata.evolution:
533
                            formdata.evolution = []
534
                        formdata.evolution.append(evo)
535
                        formdata.store()
536
                        # performs the items of the new status
537
                        formdata.perform_workflow()
538

  
539
        elif payment_response.is_error() and commit:
540
            logger.error('transaction %s finished with failure, bank_data:%s' % (
541
                                    order_id, bank_data))
542
        elif commit:
543
            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
544
                                    order_id, bank_data))
545
        if payment_response.return_content != None and self.asynchronous:
546
            get_response().set_content_type('text/plain')
547
            return payment_response.return_content
548
        else:
549
            if payment_response.is_error():
550
                # TODO: here return failure message
551
                get_session().message = ('info', _('Payment failed'))
552
            else:
553
                # TODO: Here return success message
554
                get_session().message = ('error', _('Payment succeeded'))
555
            url = get_publisher().get_frontoffice_url()
556
            if get_session().user:
557
                url += '/myspace/invoices/'
558
            return redirect(url)
559

  
560
class PublicPaymentDirectory(Directory):
561
    _q_exports = ['', 'init', 'back', 'back_asynchronous']
562

  
563
    back = PublicPaymentRegieBackDirectory(False)
564
    back_asynchronous = PublicPaymentRegieBackDirectory(True)
565

  
566

  
567
    def init(self):
568
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
569

  
570
        for invoice_id in invoice_ids:
571
            if not Invoice.check_crc(invoice_id):
572
                raise KeyError()
573

  
574
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
575

  
576
        return request_payment(invoice_ids, url)
577

  
578
def notify_new_invoice(invoice):
579
    notify_invoice(invoice, 'payment-new-invoice-email')
580

  
581
def notify_paid_invoice(invoice):
582
    notify_invoice(invoice, 'payment-invoice-paid-email')
583

  
584
def notify_canceled_invoice(invoice):
585
    notify_invoice(invoice, 'payment-invoice-canceled-email')
586

  
587
def notify_invoice(invoice, template):
588
    user = invoice.get_user()
589
    assert user is not None
590
    regie = Regie.get(id=invoice.regie_id)
591
    emails.custom_ezt_email(template, { 
592
                'user': user,
593
                'invoice': invoice,
594
                'regie': regie, 
595
                'invoice_url': invoice.payment_url() 
596
            }, user.email, fire_and_forget = True)
597

  
auquotidien/modules/payments_ui.py
1
import time
2
import pprint
3
import locale
4
import decimal
5
import datetime
6

  
7
from quixote import get_request, get_response, get_session, redirect
8
from quixote.directory import Directory, AccessControlled
9
from quixote.html import TemplateIO, htmltext
10

  
11
import wcs
12
import wcs.admin.root
13
from wcs.formdef import FormDef
14

  
15
from qommon import _
16
from qommon import errors, misc, template, get_logger
17
from qommon.form import *
18
from qommon.strftime import strftime
19
from qommon.admin.emails import EmailsDirectory
20
from qommon.backoffice.menu import html_top
21
from qommon import get_cfg
22

  
23
from payments import (eopayment, Regie, is_payment_supported, Invoice,
24
        Transaction, notify_paid_invoice)
25

  
26
from qommon.admin.texts import TextsDirectory
27

  
28
if not set:
29
    from sets import Set as set
30

  
31
def invoice_as_html(invoice):
32
    r = TemplateIO(html=True)
33
    r += htmltext('<div id="invoice">')
34
    r += htmltext('<h2>%s</h2>') % _('Invoice: %s') % invoice.subject
35
    r += htmltext('<h3>%s') % _('Amount: %s') % invoice.amount
36
    r += htmltext(' &euro;</h3>')
37
    r += htmltext('<!-- DEBUG \n')
38
    r += 'Invoice:\n'
39
    r += pprint.pformat(invoice.__dict__)
40
    for transaction in Transaction.get_with_indexed_value('invoice_ids', invoice.id):
41
        r += '\nTransaction:\n'
42
        r += pprint.pformat(transaction.__dict__)
43
    r += htmltext('\n-->')
44
    if invoice.formdef_id and invoice.formdata_id and \
45
            get_session().user == invoice.user_id:
46
        formdef = FormDef.get(invoice.formdef_id)
47
        if formdef:
48
            formdata = formdef.data_class().get(invoice.formdata_id, ignore_errors=True)
49
            if formdata:
50
                name = _('%(form_name)s #%(formdata_id)s') % {
51
                        'form_name': formdata.formdef.name,
52
                        'formdata_id': formdata.id }
53
                r += htmltext('<p class="from">%s <a href="%s">%s</a></p>') % (_('From:'), formdata.get_url(), name)
54
    r += htmltext('<p class="regie">%s</p>') % _('Regie: %s') % Regie.get(invoice.regie_id).label
55
    r += htmltext('<p class="date">%s</p>') % _('Created on: %s') % misc.localstrftime(invoice.date)
56
    if invoice.details:
57
        r += htmltext('<p class="details">%s</p>') % _('Details:')
58
        r += htmltext('<div class="details">')
59
        r += htmltext(invoice.details)
60
        r += htmltext('</div>')
61
    if invoice.canceled:
62
        r += htmltext('<p class="canceled">')
63
        r += '%s' % _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
64
        if invoice.canceled_reason:
65
            r += ' (%s)' % invoice.canceled_reason
66
        r += htmltext('</p>')
67
    if invoice.paid:
68
        r += htmltext('<p class="paid">%s</p>') % _('paid on %s') % misc.localstrftime(invoice.paid_date)
69
    r += htmltext('</div>')
70
    return r.getvalue()
71

  
72

  
73
class InvoicesDirectory(Directory):
74
    _q_exports = ['', 'multiple']
75

  
76
    def _q_traverse(self, path):
77
        if not is_payment_supported():
78
            raise errors.TraversalError()
79
        get_response().filter['bigdiv'] = 'profile'
80
        if get_session().user:
81
            # fake breadcrumb
82
            get_response().breadcrumb.append(('myspace/', _('My Space')))
83
            get_response().breadcrumb.append(('invoices/', _('Invoices')))
84
        return Directory._q_traverse(self, path)
85

  
86
    def multiple(self):
87
        invoice_ids = get_request().form.get('invoice')
88
        if type(invoice_ids) is not list:
89
            return redirect('%s' % invoice_ids)
90
        return redirect('+'.join(invoice_ids))
91

  
92
    def _q_lookup(self, component):
93
        if str('+') in component:
94
            invoice_ids = component.split(str('+'))
95
        else:
96
            invoice_ids = [component]
97
        for invoice_id in invoice_ids:
98
            if not Invoice.check_crc(invoice_id):
99
                raise errors.TraversalError()
100

  
101
        template.html_top(_('Invoices'))
102
        r = TemplateIO(html=True)
103
        r += TextsDirectory.get_html_text('aq-invoice')
104

  
105
        regies_id = set()
106
        for invoice_id in invoice_ids:
107
            try:
108
                invoice = Invoice.get(invoice_id)
109
            except KeyError:
110
                raise errors.TraversalError()
111
            r += invoice_as_html(invoice)
112
            if not (invoice.paid or invoice.canceled):
113
                regies_id.add(invoice.regie_id)
114

  
115
        if len(regies_id) == 1:
116
            r += htmltext('<p class="command">')
117
            r += htmltext('<a href="%s/payment/init?invoice_ids=%s">') % (
118
                    get_publisher().get_frontoffice_url(), component)
119
            if len(invoice_ids) > 1:
120
                r += _('Pay Selected Invoices')
121
            else:
122
                r += _('Pay')
123
            r += htmltext('</a></p>')
124
        if len(regies_id) > 1:
125
            r += _('You can not pay to different regies.')
126

  
127
        return r.getvalue()
128

  
129
    def _q_index(self):
130
        return redirect('..')
131

  
132

  
133
class RegieDirectory(Directory):
134
    _q_exports = ['', 'edit', 'delete', 'options']
135

  
136
    def __init__(self, regie):
137
        self.regie = regie
138

  
139
    def _q_index(self):
140
        html_top('payments', title = _('Regie: %s') % self.regie.label)
141
        r = TemplateIO(html=True)
142
        get_response().filter['sidebar'] = self.get_sidebar()
143
        r += htmltext('<h2>%s</h2>') % _('Regie: %s') % self.regie.label
144

  
145
        r += get_session().display_message()
146

  
147
        if self.regie.description:
148
            r += htmltext('<div class="bo-block">')
149
            r += htmltext('<p>')
150
            r += self.regie.description
151
            r += htmltext('</p>')
152
            r += htmltext('</div>')
153

  
154
        if self.regie.service:
155
            r += htmltext('<div class="bo-block">')
156
            url = get_publisher().get_frontoffice_url() + '/payment/back_asynchronous/'
157
            url += str(self.regie.id)
158
            r += htmltext('<p>')
159
            r += '%s %s' % (_('Banking Service:'), self.regie.service)
160
            r += htmltext(' (<a href="options">%s</a>)') % _('options')
161
            r += htmltext('</p>')
162
            r += htmltext('<p>')
163
            r += '%s %s' % (_('Payment notification URL:'), url)
164
            r += htmltext('</div>')
165

  
166
        r += self.invoice_listing()
167
        return r.getvalue()
168

  
169
    def get_sidebar(self):
170
        r = TemplateIO(html=True)
171
        r += htmltext('<ul>')
172
        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
173
        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
174
        r += htmltext('</ul>')
175
        return r.getvalue()
176

  
177
    def edit(self):
178
        form = self.form()
179
        if form.get_submit() == 'cancel':
180
            return redirect('.')
181

  
182
        if form.is_submitted() and not form.has_errors():
183
            self.submit(form)
184
            return redirect('..')
185

  
186
        html_top('payments', title = _('Edit Regie: %s') % self.regie.label)
187
        r = TemplateIO(html=True)
188
        r += htmltext('<h2>%s</h2>') % _('Edit Regie: %s') % self.regie.label
189
        r += form.render()
190
        return r.getvalue()
191

  
192

  
193
    def form(self):
194
        form = Form(enctype='multipart/form-data')
195
        form.add(StringWidget, 'label', title=_('Label'), required=True,
196
                value=self.regie.label)
197
        form.add(TextWidget, 'description', title=_('Description'),
198
                value=self.regie.description, rows=5, cols=60)
199
        form.add(SingleSelectWidget, 'service', title=_('Banking Service'),
200
                value=self.regie.service, required=True,
201
                options = [
202
                        ('dummy', _('Dummy (for tests)')),
203
                        ('sips', 'SIPS'),
204
                        ('systempayv2', 'systempay (Banque Populaire)'),
205
                        ('spplus', _('SP+ (Caisse d\'epargne)'))])
206
        form.add_submit('submit', _('Submit'))
207
        form.add_submit('cancel', _('Cancel'))
208
        return form
209

  
210
    def submit(self, form):
211
        for k in ('label', 'description', 'service'):
212
            widget = form.get_widget(k)
213
            if widget:
214
                setattr(self.regie, k, widget.parse())
215
        self.regie.store()
216

  
217
    def delete(self):
218
        form = Form(enctype='multipart/form-data')
219
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
220
                        'You are about to irrevocably delete this regie.')))
221
        form.add_submit('submit', _('Submit'))
222
        form.add_submit('cancel', _('Cancel'))
223
        if form.get_submit() == 'cancel':
224
            return redirect('..')
225
        if not form.is_submitted() or form.has_errors():
226
            get_response().breadcrumb.append(('delete', _('Delete')))
227
            r = TemplateIO(html=True)
228
            html_top('payments', title = _('Delete Regie'))
229
            r += htmltext('<h2>%s</h2>') % _('Deleting Regie: %s') % self.regie.label
230
            r += form.render()
231
            return r.getvalue()
232
        else:
233
            self.regie.remove_self()
234
            return redirect('..')
235

  
236
    def option_form(self):
237
        form = Form(enctype='multipart/form-data')
238
        module = eopayment.get_backend(self.regie.service)
239
        service_options = {}
240
        for infos in module.description['parameters']:
241
            if 'default' in infos:
242
                service_options[infos['name']] = infos['default']
243
        service_options.update(self.regie.service_options or {})
244

  
245
        banking_titles = {
246
            ('dummy', 'direct_notification_url'): N_('Direct Notification URL'),
247
            ('dummy', 'siret'): N_('Dummy SIRET'),
248
        }
249

  
250
        for infos in module.description['parameters']:
251
            name = infos['name']
252
            caption = infos.get('caption', name).encode(get_publisher().site_charset)
253
            title = banking_titles.get((self.regie.service, name), caption)
254
            kwargs = {}
255
            widget = StringWidget
256
            if infos.get('help_text') is not None:
257
                kwargs['hint'] = _(infos['help_text'].encode(get_publisher().site_charset))
258
            if infos.get('required', False):
259
                kwargs['required'] = True
260
            if infos.get('max_length') is not None:
261
                kwargs['size'] = infos['max_length']
262
            elif infos.get('length') is not None:
263
                kwargs['size'] = infos['length']
264
            else:
265
                kwargs['size'] = 80
266
            if kwargs['size'] > 100:
267
                widget = TextWidget
268
                kwargs['cols'] = 80
269
                kwargs['rows'] = 5
270
            if 'type' not in infos or infos['type'] is str:
271
                form.add(widget, name, title=_(title),
272
                         value=service_options.get(name), **kwargs)
273
            elif infos['type'] is bool:
274
                form.add(CheckboxWidget, name, title=title,
275
                         value=service_options.get(name), **kwargs)
276
        form.add_submit('submit', _('Submit'))
277
        form.add_submit('cancel', _('Cancel'))
278
        return form
279

  
280
    def options(self):
281
        r = TemplateIO(html=True)
282
        form = self.option_form()
283

  
284
        module = eopayment.get_backend(self.regie.service)
285
        try:
286
            r += htmltext('<!-- Payment backend description: \n')
287
            r += pprint.pformat(module.description)
288
            r += htmltext('-->')
289
        except:
290
            return template.error_page(_('Payment backend do not list its options'))
291
            raise errors.TraversalError()
292
        r += htmltext('<!-- \n')
293
        r += 'Service options\n'
294
        r += pprint.pformat(self.regie.service_options)
295
        r += htmltext('-->')
296

  
297
        if form.get_submit() == 'cancel':
298
            return redirect('.')
299

  
300
        if form.is_submitted() and not form.has_errors():
301
             if self.submit_options(form, module):
302
                return redirect('..')
303

  
304
        html_top('payments', title=_('Edit Service Options'))
305
        r += htmltext('<h2>%s</h2>') % _('Edit Service Options')
306
        r += form.render()
307
        return r.getvalue()
308

  
309
    def submit_options(self, form, module):
310
        # extra validation
311
        error = False
312
        for infos in module.description['parameters']:
313
            widget = form.get_widget(infos['name'])
314
            value = widget.parse()
315
            if value and 'validation' in infos:
316
                try:
317
                    if not infos['validation'](value):
318
                        widget.set_error(_('Valeur invalide'))
319
                        error = True
320
                except ValueError, e:
321
                    widget.set_error(_(e.message))
322
                    error = True
323
        if error:
324
            return False
325
        if not self.regie.service_options:
326
            self.regie.service_options = {}
327
        for infos in module.description['parameters']:
328
            name = infos['name']
329
            value = form.get_widget(name).parse()
330
            if value is None:
331
                value = ''
332
            if hasattr(value, 'strip'):
333
                value = value.strip()
334
            if infos.get('default') is not None:
335
                if value == infos['default']:
336
                    self.regie.service_options.pop(name, None)
337
                else:
338
                    self.regie.service_options[name] = form.get_widget(name).parse()
339
            elif not value:
340
                self.regie.service_options.pop(name, None)
341
            else:
342
                self.regie.service_options[name] = form.get_widget(name).parse()
343
        self.regie.store()
344
        return True
345

  
346
    PAGINATION = 50
347

  
348
    def monetary_amount(self, val):
349
        if not val:
350
            return ''
351
        if isinstance(val, basestring):
352
            val = val.replace(',', '.')
353
        return '%.2f' % decimal.Decimal(val)
354

  
355
    def get_sort_by(self):
356
        request = get_request()
357
        sort_by = request.form.get('sort_by')
358
        if sort_by not in ('date', 'paid_date', 'username'):
359
            sort_by = 'date'
360
        return sort_by
361

  
362
    def get_invoices(self):
363
        sort_by = self.get_sort_by()
364
        invoices = Invoice.get_with_indexed_value('regie_id', self.regie.id,
365
                ignore_errors=True)
366
        if 'date' in sort_by:
367
            reverse = True
368
            key = lambda i: getattr(i, sort_by) or datetime.datetime.now()
369
        else:
370
            reverse = False
371
            key = lambda i: getattr(i, sort_by) or ''
372
        invoices.sort(reverse=reverse, key=key)
373
        return invoices
374

  
375
    def unpay(self, request, invoice):
376
        get_logger().info(_('manually set unpaid invoice %(invoice_id)s in regie %(regie)s')
377
            % dict(invoice_id=invoice.id, regie=self.regie.id))
378
        transaction = Transaction()
379
        transaction.invoice_ids = [ invoice.id ]
380
        transaction.order_id = 'Manual action'
381
        transaction.start = datetime.datetime.now()
382
        transaction.end = transaction.start
383
        transaction.bank_data = { 
384
            'action': 'Set unpaid', 
385
            'by': request.user.get_display_name() + ' (%s)' % request.user.id
386
        }
387
        transaction.store()
388
        invoice.unpay()
389

  
390
    def pay(self, request, invoice):
391
        get_logger().info(_('manually set paid invoice %(invoice_id)s in regie %(regie)s')
392
            % dict(invoice_id=invoice.id, regie=self.regie.id))
393
        transaction = Transaction()
394
        transaction.invoice_ids = [ invoice.id ]
395
        transaction.order_id = 'Manual action'
396
        transaction.start = datetime.datetime.now()
397
        transaction.end = transaction.start
398
        transaction.bank_data = { 
399
            'action': 'Set paid', 
400
            'by': request.user.get_display_name() + ' (%s)' % request.user.id
401
        }
402
        transaction.store()
403
        invoice.pay()
404

  
405
    def invoice_listing(self):
406
        request = get_request()
407
        get_response().add_css_include('../../themes/auquotidien/admin.css')
408
        if request.get_method() == 'POST':
409
            invoice_id = request.form.get('id')
410
            invoice = Invoice.get(invoice_id, ignore_errors=True)
411
            if invoice:
412
                if 'unpay' in request.form:
413
                    self.unpay(request, invoice)
414
                elif 'pay' in request.form:
415
                    self.pay(request, invoice)
416
                return redirect('')
417
        try:
418
            offset = int(request.form.get('offset', 0))
419
        except ValueError:
420
            offset = 0
421
        r = TemplateIO(html=True)
422
        r += htmltext('<table id="invoice-listing" borderspacing="0">')
423
        r += htmltext('<thead>')
424
        r += htmltext('<tr>')
425
        r += htmltext('<td><a href="?sort_by=date&offset=%d">Creation</a></td>') % offset
426
        r += htmltext('<td>Amount</td>')
427
        r += htmltext('<td><a href="?sort_by=paid_date&offset=%d">Paid</a></td>') % offset
428
        r += htmltext('<td><a href="?sort_by=username&offset=%d">User</a></td>') % offset
429
        r += htmltext('<td>Titre</td>')
430
        r += htmltext('<td></td>')
431
        r += htmltext('</tr>')
432
        r += htmltext('</thead>')
433
        invoices = self.get_invoices()
434
        for invoice in invoices[offset:offset+self.PAGINATION]:
435
            r += htmltext('<tbody class="invoice-rows">')
436
            r += htmltext('<tr class="invoice-row"><td>')
437
            r += misc.localstrftime(invoice.date)
438
            r += htmltext('</td><td class="amount">')
439
            r += self.monetary_amount(invoice.amount)
440
            r += htmltext('</td><td>')
441
            if invoice.paid:
442
                r += misc.localstrftime(invoice.paid_date)
443
            else:
444
                r += ''
445
            r += htmltext('</td><td>')
446
            user = invoice.get_user()
447
            if user:
448
                r += user.name
449
            r += htmltext('</td><td class="subject">%s</td>') % (invoice.subject or '')
450
            r += htmltext('<td>')
451
            r += htmltext('<form method="post">')
452
            r += htmltext('<input type="hidden" name="id" value="%s"/> ') % invoice.id
453
            if invoice.paid:
454
                r += htmltext('<input type="submit" name="unpay" value="%s"/>') % _('Set unpaid')
455
            else:
456
                r += htmltext('<input type="submit" name="pay" value="%s"/>') % _('Set paid')
457
            r += htmltext('</form>')
458

  
459
            r += htmltext('</td></tr>')
460
            transactions = Transaction.get_with_indexed_value('invoice_ids',
461
                    invoice.id)
462
            for transaction in sorted(transactions, key=lambda x: x.start):
463
                r += htmltext('<tr>')
464
                r += htmltext('<td></td>')
465
                r += htmltext('<td colspan="5">')
466
                r += 'OrderID: %s' % transaction.order_id
467
                r += ' Start: %s' % transaction.start
468
                if transaction.end:
469
                    r += ' End: %s' % transaction.end
470
                if transaction.bank_data:
471
                    r += ' Bank data: %r' % transaction.bank_data
472
                r += htmltext('</td>')
473
                r += htmltext('</tr>')
474
            r += htmltext('</tbody>')
475
        r += htmltext('</tbody></table>')
476
        if offset != 0:
477
            r += htmltext('<a href="?offset=%d>%s</a> ') % (
478
                 max(0, offset-self.PAGINATION), _('Previous'))
479
        if offset + self.PAGINATION < len(invoices):
480
            r += htmltext('<a href="?offset=%d>%s</a> ') % (
481
                 max(0, offset-self.PAGINATION), _('Previous'))
482
        return r.getvalue()
483

  
484

  
485
class RegiesDirectory(Directory):
486
    _q_exports = ['', 'new']
487

  
488
    def _q_traverse(self, path):
489
        get_response().breadcrumb.append(('regie/', _('Regies')))
490
        return Directory._q_traverse(self, path)
491

  
492
    def _q_index(self):
493
        return redirect('..')
494

  
495
    def new(self):
496
        regie_ui = RegieDirectory(Regie())
497

  
498
        form = regie_ui.form()
499
        if form.get_submit() == 'cancel':
500
            return redirect('.')
501

  
502
        if form.is_submitted() and not form.has_errors():
503
            regie_ui.submit(form)
504
            return redirect('%s/' % regie_ui.regie.id)
505

  
506
        get_response().breadcrumb.append(('new', _('New Regie')))
507
        html_top('payments', title = _('New Regie'))
508
        r = TemplateIO(html=True)
509
        r += htmltext('<h2>%s</h2>') % _('New Regie')
510
        r += form.render()
511
        return r.getvalue()
512

  
513
    def _q_lookup(self, component):
514
        try:
515
            regie = Regie.get(component)
516
        except KeyError:
517
            raise errors.TraversalError()
518
        get_response().breadcrumb.append((str(regie.id), regie.label))
519
        return RegieDirectory(regie)
520

  
521

  
522
class PaymentsDirectory(AccessControlled, Directory):
523
    _q_exports = ['', 'regie']
524
    label = N_('Payments')
525

  
526
    regie = RegiesDirectory()
527

  
528
    def is_accessible(self, user):
529
        from .backoffice import check_visibility
530
        return check_visibility('payments', user)
531

  
532
    def _q_access(self):
533
        user = get_request().user
534
        if not user:
535
            raise errors.AccessUnauthorizedError()
536

  
537
        if not self.is_accessible(user):
538
            raise errors.AccessForbiddenError(
539
                    public_msg = _('You are not allowed to access Payments Management'),
540
                    location_hint = 'backoffice')
541

  
542
        get_response().breadcrumb.append(('payments/', _('Payments')))
543

  
544
    def _q_index(self):
545
        html_top('payments', _('Payments'))
546
        get_response().filter['sidebar'] = self.get_sidebar()
547
        r = TemplateIO(html=True)
548

  
549
        if not is_payment_supported:
550
            r += htmltext('<p class="infonotice">')
551
            r += _('Payment is not supported.')
552
            r += htmltext('</p>')
553

  
554
        regies = Regie.select()
555
        r += htmltext('<h2>%s</h2>') % _('Regies')
556
        if not regies:
557
            r += htmltext('<p>')
558
            r += _('There are no regies defined at the moment.')
559
            r += htmltext('</p>')
560
        r += htmltext('<ul class="biglist" id="regies-list">')
561
        for l in regies:
562
            regie_id = l.id
563
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % regie_id
564
            r += htmltext('<strong class="label"><a href="regie/%s/">%s</a></strong>') % (regie_id, l.label)
565
            r += htmltext('</li>')
566
        r += htmltext('</ul>')
567
        return r.getvalue()
568

  
569
    def get_sidebar(self):
570
        r = TemplateIO(html=True)
571
        r += htmltext('<ul id="sidebar-actions">')
572
        r += htmltext('  <li><a class="new-item" href="regie/new">%s</a></li>') % _('New Regie')
573
        r += htmltext('</ul>')
574
        return r.getvalue()
575

  
576

  
577
TextsDirectory.register('aq-invoice',
578
        N_('Message on top of an invoice'),
579
        category = N_('Invoices'))
580

  
581
EmailsDirectory.register('payment-new-invoice-email',
582
        N_('New invoice'),
583
        N_('Available variables: user, regie, invoice, invoice_url'),
584
        category = N_('Invoices'),
585
        default_subject = N_('New invoice'),
586
        default_body = N_('''
587
A new invoice is available at [invoice_url].
588
'''))
589

  
590
EmailsDirectory.register('payment-invoice-paid-email',
591
        N_('Paid invoice'),
592
        N_('Available variables: user, regie, invoice, invoice_url'),
593
        category = N_('Invoices'),
594
        default_subject = N_('Paid invoice'),
595
        default_body = N_('''
596
The invoice [invoice_url] has been paid.
597
'''))
598

  
599
EmailsDirectory.register('payment-invoice-canceled-email',
600
        N_('Canceled invoice'),
601
        N_('Available variables: user, regie, invoice, invoice_url'),
602
        category = N_('Invoices'),
603
        default_subject = N_('Canceled invoice'),
604
        default_body = N_('''
605
The invoice [invoice.id] has been canceled.
606
'''))
auquotidien/modules/pyatom/afl-2.1.txt
1
# Larry Rosen has ceased to use or recommend any version
2
# of the Academic Free License below version 2.1
3

  
4
The Academic Free License
5
v. 2.1
6

  
7
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:
8

  
9
Licensed under the Academic Free License version 2.1
10

  
11
1) Grant of Copyright License. Licensor hereby grants You a world-wide, royalty-free, non-exclusive, perpetual, sublicenseable license to do the following:
12

  
13
a) to reproduce the Original Work in copies;
14

  
15
b) to prepare derivative works ("Derivative Works") based upon the Original Work;
16

  
17
c) to distribute copies of the Original Work and Derivative Works to the public;
18

  
19
d) to perform the Original Work publicly; and
20

  
21
e) to display the Original Work publicly.
22

  
23
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.
24

  
25
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.
26

  
27
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.
28

  
29
5) This section intentionally omitted.
30

  
31
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.
32

  
33
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.
34

  
35
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.
36

  
37
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.
38

  
39
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.
40

  
41
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.
42

  
43
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.
44

  
45
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.
46

  
47
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.
48

  
49
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.
50

  
51
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.
52

  
53
 
54

  
auquotidien/modules/pyatom/pyatom.py
1
# pyatom.py -- PyAtom library module
2

  
3
"""
4
PyAtom
5

  
6
Module to make it really easy to create Atom syndication feeds.
7

  
8
This module is Copyright (C) 2006 Steve R. Hastings.
9
Licensed under the Academic Free License version 2.1
10

  
11
You might want to start with the test cases at the end; see how they
12
work, and then go back and look at the code in the module.
13

  
14
I hope you find this useful!
15

  
16
Steve R. Hastings
17

  
18
Please send your questions or comments to this email address:
19

  
20
pyatom@langri.com
21
"""
22

  
23

  
24

  
25
import re
26
import sys
27
import time
28

  
29
s_pyatom_name = "PyAtom"
30
s_pyatom_ver = "0.3.9"
31
s_pyatom_name_ver = "%s version %s" % (s_pyatom_name, s_pyatom_ver)
32

  
33
# string constants
34
# These string values are used in more than one place.
35

  
36
s_version = "version"
37
s_encoding = "encoding"
38
s_standalone = "standalone"
39

  
40
s_href = "href"
41
s_lang = "xml:lang"
42
s_link = "link"
43
s_term = "term"
44
s_type = "type"
45

  
46

  
47

  
48
def set_s_indent(s):
49
	"""
50
	Set up the globals PyAtom uses to indent its output:
51
	s_indent, and s_indent_big
52

  
53
	s_indent is the string to indent one level; default is \\t.
54

  
55
	s_indent_big is s_indent concatenated many times.  PyAtom uses slice
56
	copies to get indent strings from s_indent_big.
57
	"""
58
	global s_indent
59
	global s_indent_big
60
	s_indent = s
61
	s_indent_big = s*256
62

  
63
set_s_indent("\t")
64

  
65

  
66

  
67
class TFC(object):
68
	"""
69
	class TFC: Tag Format Control.
70
	Controls how tags are converted to strings.
71

  
72
	Arguments to __init__():
73
		level	Specifies what indent level to start at for output.  Default 0.
74
		mode	Specifies how to format the output:
75
				mode_terse -- minimal output (no indenting)
76
				mode_normal -- default
77
				mode_verbose -- normal, plus some XML comments
78

  
79
	Normally, if an XML item has no data, nothing is printed, but with
80
	mode_verbose you may get a comment like "Collection with 0 entries".
81

  
82
	Methods:
83
		b_print_all()
84
			Return True if TFC set for full printing.
85
		b_print_terse()
86
			Return True if TFC set for terse printing.
87
		b_print_verbose()
88
			Return True if TFC set for verbose printing.
89

  
90
		indent_by(incr)
91
			Return a TFC instance that indents by incr columns.
92
		s_indent(extra_indent=0)
93
			Return an indent string.
94
	"""
95
	mode_terse, mode_normal, mode_verbose = range(3)
96

  
97
	def __init__(self, level=0, mode=mode_normal):
98
		"""
99
Arguments:
100
	level	Specifies what indent level to start at for output.  Default 0.
101
	mode	Specifies how to format the output:
102
			mode_terse -- minimal output (no indenting)
103
			mode_normal -- default
104
			mode_verbose -- normal, plus some XML comments
105

  
106
Normally, if an XML item has no data, nothing is printed, but with
107
mode_verbose you may get a comment like "Collection with 0 entries".
108
		"""
109
		self.level = level
110
		self.mode = mode
111

  
112
	def b_print_all(self):
113
		"""
114
		Return True if TFC set for full printing.
115

  
116
		Some optional things are usually suppressed, but will be printed
117
		if the current level is 0.  And everything gets printed when
118
		mode_verbose is set.
119
		"""
120
		return self.level == 0 or self.mode == TFC.mode_verbose
121

  
122
	def b_print_terse(self):
123
		"""
124
		Return True if TFC set for terse printing.
125
		"""
126
		return self.mode == TFC.mode_terse
127

  
128
	def b_print_verbose(self):
129
		"""
130
		Return True if TFC set for verbose printing.
131
		"""
132
		return self.mode == TFC.mode_verbose
133

  
134
	def indent_by(self, incr):
135
		"""
136
		Return a TFC instance that indents by incr columns.
137

  
138
		Pass this to a function that takes a TFC to get a temporary indent.
139
		"""
140
		return TFC(self.level + incr, self.mode)
141
	def s_indent(self, extra_indent=0):
142
		"""
143
		Return an indent string.
144

  
145
		Return a string of white space that indents correctly for the 
146
		current TFC settings.  If specified, extra_indent will be added
147
		to the current indent level.
148
		"""
149
		if self.mode == TFC.mode_terse:
150
			return ""
151
		level = self.level + extra_indent
152
		return s_indent_big[0:level]
153

  
154

  
155

  
156
pat_nbsp = re.compile(r'&nbsp;')
157
def s_entities_to_ws(s):
158
	"""
159
	Return a copy of s with HTML whitespace entities replaced by a space.
160

  
161
	Currently just gets rid of HTML non-breaking spaces ("&nbsp;").
162
	"""
163
	if not s:
164
		return s
165

  
166
	s = re.sub(pat_nbsp, " ", s)
167
	return s
168

  
169
def s_normalize_ws(s):
170
	"""
171
	Return a copy of string s with each run of whitespace replaced by one space.
172
	>>> s = "and    now\n\n\nfor \t  something\v   completely\r\n  different"
173
	>>> print s_normalize_ws(s)
174
	and now for something completely different
175
	>>>
176
	"""
177
	lst = s.split()
178
	s = " ".join(lst)
179
	return s
180
	
181

  
182
def s_escape_html(s):
183
	"""
184
	Return a copy of string s with HTML codes escaped.
185

  
186
	This is useful when you want HTML tags printed literally, rather than
187
	interpreted.
188

  
189
	>>> print s_escape_html("<head>")
190
	&lt;head&gt;
191
	>>> print s_escape_html("&nbsp;")
192
	&amp;nbsp;
193
	"""
194
	s = s.replace("&", "&amp;")
195
	s = s.replace("<", "&lt;")
196
	s = s.replace(">", "&gt;")
197
	return s
198

  
199
def s_create_atom_id(t, domain_name, uri=""):
200
	"""
201
	Create ID using Mark Pilgrim's algorithm.
202

  
203
	Algorithm taken from here:
204
	http://diveintomark.org/archives/2004/05/28/howto-atom-id
205
	"""
206

  
207
	# ymd (year-month-day) example: 2003-12-13
208
	ymd = time.strftime("%Y-%m-%d", t)
209

  
210
	if uri == "":
211
		# mush (all mushed together) example: 20031213083000
212
		mush = time.strftime("%Y%m%d%H%M%S", t)
213
		uri = "/weblog/" + mush
214

  
215
	# s = "tag:" + domain_name + "," + ymd + ":" + uri
216
	s = "tag:%s,%s:%s" % (domain_name, ymd, uri)
217

  
218
	s = s.replace("#", "/")
219

  
220
	return s
221

  
222
s_copyright_multiyear = "Copyright %s %d-%d by %s."
223
s_copyright_oneyear = "Copyright %s %d by %s."
224
def s_copyright(s_owner, s_csym="(C)", end_year=None, start_year=None):
225
	"""
226
	Return a string with a copyright notice.
227

  
228
	s_owner
229
		string with copyright owner's name.
230
	s_csym
231
		string with copyright symbol. (An HTML entity might be good here.)
232
	end_year
233
		last year of the copyright.  Default is the current year.
234
	start_year
235
		first year of the copyright.
236

  
237
	If only end_year is specified, only print one year; if both end_year and
238
	start_year are specified, print a range.
239

  
240
	To localize the entire copyright message into another language, change
241
	the global variables with the copyright template:
242
		s_copyright_multiyear: for a year range
243
		s_copyright_oneyear: for a single year
244
	"""
245
	if not end_year:
246
		end_year = time.localtime().tm_year
247

  
248
	if start_year:
249
		return s_copyright_multiyear % (s_csym, start_year, end_year, s_owner)
250

  
251
	return s_copyright_oneyear % (s_csym, end_year, s_owner)
252

  
253

  
254

  
255
# Here are all of the possible XML items.
256
#
257
# Supported by PyAtom:
258
# XML Declaration: <?xml ... ?>
259
# Comments: <!-- ... -->
260
# Elements: <tag_name>...</tag_name>
261
#
262
# Minimal support:
263
# Markup Declarations: <!KEYWORD ... >
264
# Processing Instructions (PIs): <?KEYWORD ... ?>
265
#
266
# Not currently supported:
267
# INCLUDE and IGNORE directives: <!KEYWORD[ ... ]]>
268
# CDATA sections: <![CDATA[ ... ]]>
269
#
270

  
271
class XMLItem(object):
272
	"""
273
	All PyAtom classes inherit from this class.  All it does is provide a
274
	few default methods, and be a root for the inheritance tree.
275

  
276
	An XMLItem has several methods that return an XML tag representation of
277
	its contents.  Each XMLItem knows how to make a tag for itself.  An
278
	XMLItem that contains other XMLItems will ask each one to make a tag;
279
	so asking the top-level XMLItem for a tag will cause the entire tree
280
	of XMLItems to recursively make tags, and you get a full XML
281
	representation with tags appropriately nested and indented.
282
	"""
283
	def _s_tag(self, tfc):
284
		"""
285
		A stub which must always be overridden by child classes.
286
		"""
287
		assert False, "XMLItem instance is too abstract to print."
288

  
289
	def s_tag(self, level):
290
		"""
291
		Return the item as a string containing an XML tag declaration.
292

  
293
		The XML tag will be indented.
294
		Will return an empty string if the item is empty.
295
		"""
296
		tfc = TFC(level, TFC.mode_normal)
297
		return self._s_tag(tfc)
298

  
299
	def s_tag_verbose(self, level):
300
		"""
301
		Return the item as a string containing an XML tag declaration.
302

  
303
		The XML tag will be indented.
304
		May return an XML Comment if the item is empty.
305
		"""
306
		tfc = TFC(level, TFC.mode_verbose)
307
		return self._s_tag(tfc)
308

  
309
	def s_tag_terse(self, level):
310
		"""
311
		Return the item as a string containing an XML tag declaration.
312

  
313
		The XML tag will not be indented.
314
		Will return an empty string if the item is empty.
315
		"""
316
		tfc = TFC(level, TFC.mode_terse)
317
		return self._s_tag(tfc)
318

  
319
	def __str__(self):
320
		return self.s_tag(0)
321

  
322
	def level(self):
323
		"""
324
		Return an integer describing what level this tag is.
325

  
326
		The root tag of an XML document is level 0; document-level comments
327
		or other document-level declarations are also level 0.  Tags nested
328
		inside the root tag are level 1, tags nested inside those tags are
329
		level 2, and so on.
330

  
331
		This is currently only used by the s_tree() functions.  When
332
		printing tags normally, the code that walks the tree keeps track of
333
		what level is current.
334
		"""
335
		level = 0
336
		while self._parent != None:
337
			self = self._parent
338
			if self.is_element():
339
				level += 1
340
		return level
341

  
342
	def s_name(self):
343
		"""
344
		Return a name for the current item.
345

  
346
		Used only by the s_tree() functions.
347
		"""
348
		if self._name:
349
			return self._name
350
		return "unnamed_instance_of_" + type(self).__name__
351

  
352
	def s_tree(self):
353
		"""
354
		Return a verbose tree showing the current tag and its children.
355

  
356
		This is for debugging; it's not valid XML syntax.
357
		"""
358
		level = self.level()
359
		return "%2d) %s\t%s" % (level, self.s_name(), str(self))
360

  
361

  
362

  
363
class DocItem(XMLItem):
364
	"""
365
	A document-level XML item (appearing above root element).
366

  
367
	Items that can be document-level inherit from this class.
368
	"""
369
	pass
370

  
371

  
372

  
373
class ElementItem(XMLItem):
374
	"""
375
	An item that may be nested inside an element.
376

  
377
	Items that can be nested inside other elements inherit from this class.
378
	"""
379
	pass
380

  
381

  
382

  
383
class Comment(DocItem,ElementItem):
384
	"""
385
	An XML comment.
386

  
387
	Attributes:
388
		text
389
			set the text of the comment
390
	"""
391
	def __init__(self, text=""):
392
		"""
393
		text: set the text of the comment
394
		"""
395
		self._parent = None
396
		self._name = ""
397
		self.tag_name = "comment"
398
		self.text = text
399

  
400
	def _s_tag(self, tfc):
401
		if not self:
402
			if tfc.b_print_all():
403
				return tfc.s_indent() + "<!-- -->"
404
			else:
405
				return ""
406
		else:
407
			if self.text.find("\n") >= 0:
408
				lst = []
409
				lst.append(tfc.s_indent() + "<!--")
410
				lst.append(self.text)
411
				lst.append(tfc.s_indent() + "-->")
412
				return "\n".join(lst)
413
			else:
414
				s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.text, " -->")
415
				return s
416

  
417
		assert False, "not possible to reach this line."
418

  
419
	def __nonzero__(self):
420
		# Returns True if there is any comment text.
421
		# Returns False otherwise.
422
		return not not self.text
423

  
424
	def is_element(self):
425
		return True
426

  
427

  
428

  
429
# REVIEW: can a PI be an ElementItem?
430
class PI(DocItem):
431
	"""
432
	XML Processing Instruction (PI).
433

  
434
	Attributes:
435
		keyword
436
		text
437
	"""
438
	def __init__(self):
439
		self._parent = None
440
		self._name = ""
441
		self.keyword = ""
442
		self.text = ""
443

  
444
	def _s_tag(self, tfc):
445
		if not self:
446
			return ""
447
		else:
448
			if self.text.find("\n") >= 0:
449
				lst = []
450
				lst.append("%s%s%s" % (tfc.s_indent(), "<?", self.keyword))
451
				lst.append(self.text)
452
				lst.append("%s%s" % (tfc.s_indent(), "?>"))
453
				return "\n".join(lst)
454
			else:
455
				s = "%s%s%s %s%s"% \
456
						(tfc.s_indent(), "<?", self.keyword, self.text, "?>")
457
				return s
458

  
459
		assert False, "not possible to reach this line."
460

  
461
	def __nonzero__(self):
462
		# Returns True if there is any keyword.
463
		# Returns False otherwise.
464
		return not not self.keyword
465

  
466

  
467

  
468
# REVIEW: can a MarkupDecl be an ElementItem?
469
class MarkupDecl(DocItem):
470
	"""
471
	XML Markup Declaration.
472

  
473
	Attributes:
474
		keyword
475
		text
476
	"""
477
	def __init__(self):
478
		self._parent = None
479
		self._name = ""
480
		self.keyword = ""
481
		self.text = ""
482

  
483
	def _s_tag(self, tfc):
484
		if not self:
485
			return ""
486
		else:
487
			if self.text.find("\n") >= 0:
488
				lst = []
489
				lst.append("%s%s%s" % (tfc.s_indent(), "<!", self.keyword))
490
				lst.append(self.text)
491
				lst.append("%s%s" % (tfc.s_indent(), ">"))
492
				return "\n".join(lst)
493
			else:
494
				s = "%s%s%s %s%s" % \
495
						(tfc.s_indent(), "<!", self.keyword, self.text, ">")
496
				return s
497

  
498
		assert False, "not possible to reach this line."
499

  
500
	def __nonzero__(self):
501
		# Returns True if there is any keyword.
502
		# Returns False otherwise.
503
		return not not self.keyword
504

  
505

  
506

  
507
class CoreElement(ElementItem):
508
	"""
509
	This is an abstract class.
510

  
511
	All of the XML element classes inherit from this.
512
	"""
513
	def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []):
514
		# dictionary of attributes and their values
515
		self.lock = False
516
		self._parent = None
517
		self._name = ""
518
		self.tag_name = tag_name
519
		self.def_attr = def_attr
520
		self.attrs = {}
521
		if def_attr and def_attr_value:
522
			self.attrs[def_attr] = def_attr_value
523
		self.attr_names = attr_names
524
		self.lock = True
525

  
526
	def __nonzero__(self):
527
		# Returns True if any attrs are set or there are any contents.
528
		# Returns False otherwise.
529
		return not not self.attrs or self.has_contents()
530

  
531
	def text_check(self):
532
		"""
533
		Raise an exception, unless element has text contents.
534

  
535
		Child classes that have text must override this to do nothing.
536
		"""
537
		raise TypeError, "element does not have text contents"
538

  
539
	def nest_check(self):
540
		"""
541
		Raise an exception, unless element can nest other elements.
542

  
543
		Child classes that can nest must override this to do nothing.
544
		"""
545
		raise TypeError, "element cannot nest other elements"
546

  
547
	def __delattr__(self, name):
548
		# REVIEW: this should be made to work!
549
		raise TypeError, "cannot delete elements"
550

  
551
	def __getattr__(self, name):
552
		if name == "lock":
553
			# If the "lock" hasn't been created yet, we always want it
554
			# to be False, i.e. we are not locked.
555
			return False
556
		else:
557
			raise AttributeError, name
558

  
559
	def __setattr__(self, name, value):
560
		# Here's how this works:
561
		#
562
		# 0) "self.lock" is a boolean, set to False during __init__()
563
		# but turned True afterwards.  When it's False, you can add new
564
		# members to the class instance without any sort of checks; once
565
		# it's set True, __setattr__() starts checking assignments.
566
		# By default, when lock is True, you cannot add a new member to
567
		# the class instance, and any assignment to an old member has to
568
		# be of matching type.  So if you say "a.text = string", the
569
		# .text member has to exist and be a string member.
570
		#
571
		# This is the default __setattr__() for all element types.  It
572
		# gets overloaded by the __setattr__() in NestElement, because
573
		# for nested elments, it makes sense to be able to add new
574
		# elements nested inside.
575
		#
576
		# This is moderately nice.  But later in NestElement there is a
577
		# version of __setattr__() that is *very* nice; check it out.
578
		#
579
		# 1) This checks assignments to _parent, and makes sure they are
580
		# plausible (either an XMLItem, or None).
581

  
582
		try:
583
			lock = self.lock
584
		except AttributeError:
585
			lock = False
586

  
587
		if not lock:
588
			self.__dict__[name] = value
589
			return
590

  
591
		dict = self.__dict__
592
		if not name in dict:
593
			# brand-new item
594
			if lock:
595
				raise TypeError, "element cannot nest other elements"
596

  
597
		if name == "_parent":
598
			if not (isinstance(value, XMLItem) or value is None):
599
				raise TypeError, "only XMLItem or None is permitted"
600
			self.__dict__[name] = value
601
			return
602

  
603
		# locked item so do checks
604
		if not type(self.__dict__[name]) is type(value):
605
			raise TypeError, "value is not the same type"
606

  
607
		self.__dict__[name] = value
608
		
609

  
610
	def has_contents(self):
611
		return False
612

  
613
	def multiline_contents(self):
614
		return False
615

  
616
	def s_contents(self, tfc):
617
		assert False, "CoreElement is an abstract class; it has no contents."
618

  
619
	def _s_start_tag_name_attrs(self, tfc):
620
		"""
621
		Return a string with the start tag name, and any attributes.
622

  
623
		Wrap this in correct punctuation to get a start tag.
624
		"""
625
		def attr_newline(tfc):
626
			if tfc.b_print_terse():
627
				return " "
628
			else:
629
				return "\n" + tfc.s_indent(2)
630

  
631
		lst = []
632
		lst.append(self.tag_name)
633

  
634
		if len(self.attrs) == 1:
635
			# just one attr so do on one line
636
			attr = self.attrs.keys()[0]
637
			s_attr = '%s="%s"' % (attr, self.attrs[attr])
638
			lst.append(" " + s_attr)
639
		elif len(self.attrs) > 1:
640
			# more than one attr so do a nice nested tag
641
			# 0) show all attrs in the order of attr_names
642
			for attr in self.attr_names:
643
				if attr in self.attrs.keys():
644
					s_attr = '%s="%s"' % (attr, self.attrs[attr])
645
					lst.append(attr_newline(tfc) + s_attr)
646
			# 1) any attrs not in attr_names?  list them, too
647
			for attr in self.attrs:
648
				if not attr in self.attr_names:
649
					s_attr = '%s="%s"' % (attr, self.attrs[attr])
650
					lst.append(attr_newline(tfc) + s_attr)
651

  
652
		return "".join(lst)
653

  
654
	def _s_tag(self, tfc):
655
		if not self:
656
			if not tfc.b_print_all():
657
				return ""
658

  
659
		lst = []
660

  
661
		lst.append(tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc))
662

  
663
		if not self.has_contents():
664
			lst.append("/>")
665
		else:
666
			lst.append(">")
667
			if self.multiline_contents():
668
				s = "\n%s\n" % self.s_contents(tfc.indent_by(1))
669
				lst.append(s + tfc.s_indent())
670
			else:
671
				lst.append(self.s_contents(tfc))
672
			lst.append("</" + self.tag_name + ">")
673

  
674
		return "".join(lst)
675

  
676
	def s_start_tag(self, tfc):
677
		return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + ">"
678

  
679
	def s_end_tag(self):
680
		return "</" + self.tag_name + ">"
681

  
682
	def s_compact_tag(self, tfc):
683
		return tfc.s_indent() + "<" + self._s_start_tag_name_attrs(tfc) + "/>"
684

  
685
	def is_element(self):
686
		return True
687

  
688

  
689

  
690
class TextElement(CoreElement):
691
	"""
692
	An element that cannot have other elements nested inside it.
693

  
694
	Attributes:
695
		attr
696
		text
697
	"""
698
	def __init__(self, tag_name, def_attr, def_attr_value, attr_names = []):
699
		CoreElement.__init__(self, tag_name, def_attr, def_attr_value,
700
				attr_names)
701
		self.lock = False
702
		self.text = ""
703
		self.lock = True
704

  
705
	def text_check(self):
706
		pass
707

  
708
	def has_contents(self):
709
		return not not self.text
710

  
711
	def multiline_contents(self):
712
		return self.text.find("\n") >= 0
713

  
714
	def s_contents(self, tfc):
715
		return self.text
716

  
717

  
718

  
719
class Nest(ElementItem):
720
	"""
721
	A data structure that can store Elements, nested inside it.
722

  
723
	Note: this is not, itself, an Element!  Because it is not an XML
724
	element, it has no tags.  Its string representation is the
725
	representations of the elements nested inside it.
726

  
727
	NestElement and XMLDoc inherit from this.
728
	"""
729
	def __init__(self):
730
		self.lock = False
731
		self._parent = None
732
		self._name = ""
733
		self.elements = []
734
		self.lock = True
735
	def __len__(self):
736
		return len(self.elements)
737
	def __getitem__(self, key):
738
		return self.elements[key]
739
	def __setitem__(self, key, value):
740
		self.elements[key] = value
741
	def __delitem__(self, key):
742
		del(self.elements[key])
743

  
744
	def _do_setattr(self, name, value):
745
		if isinstance(value, XMLItem):
746
			value._parent = self
747
			value._name = name
748
			self.elements.append(value)
749
		self.__dict__[name] = value
750

  
751
	def __setattr__(self, name, value):
752
		# Lots of magic here!  This is important stuff.  Here's how it works:
753
		#
754
		# 0) self.lock is a boolean, set to False initially and then set
755
		# to True at the end of __init__().  When it's False, you can add new
756
		# members to the class instance without any sort of checks; once
757
		# it's set True, __setattr__() starts checking assignments.  By
758
		# default, when lock is True, any assignment to an old member
759
		# has to be of matching type.  You can add a new member to the
760
		# class instance, but __setattr__() checks to ensure that the
761
		# new member is an XMLItem.
762
		#
763
		# 1) Whether self.lock is set or not, if the value is an XMLitem,
764
		# then this will properly add the XMLItem into the tree
765
		# structure.  The XMLItem will have _parent set to the parent,
766
		# will have _name set to its name in the parent, and will be
767
		# added to the parent's elements list.  This is handled by
768
		# _do_setattr().
769
		#
770
		# 2) As a convenience for the user, if the user is assigning a
771
		# string, and self is an XMLItem that has a .text value, this
772
		# will assign the string to the .text value.  This allows usages
773
		# like "e.title = string", which is very nice.  Before I added
774
		# this, I frequently wrote that instead of "e.title.text =
775
		# string" so I wanted it to just work.  Likewise the user can
776
		# assign a time value directly into Timestamp elements.
777
		#
778
		# 3) This checks assignments to _parent, and makes sure they are
779
		# plausible (either an XMLItem, or None).
780

  
781
		try:
782
			lock = self.lock
783
		except AttributeError:
784
			lock = False
785

  
786
		if not lock:
787
			self._do_setattr(name, value)
788
			return
789

  
790
		dict = self.__dict__
791
		if not name in dict:
792
			# brand-new item
793
			if lock:
794
				self.nest_check()
795
				if not isinstance(value, XMLItem):
796
					raise TypeError, "only XMLItem is permitted"
797
			self._do_setattr(name, value)
798
			return
799

  
800
		if name == "_parent" or name == "root_element":
801
			if not (isinstance(value, XMLItem) or value is None):
802
				raise TypeError, "only XMLItem or None is permitted"
803
			self.__dict__[name] = value
804
			return
805

  
806
		if name == "_name" and type(value) == type(""):
807
			self.__dict__[name] = value
808
			return
809

  
810
		# for Timestamp elements, allow this:  element = time
811
		# (where "time" is a float value, since uses float for times)
812
		# Also allow valid timestamp strings.
813
		if isinstance(self.__dict__[name], Timestamp):
814
			if type(value) == type(1.0):
815
				self.__dict__[name].time = value
816
				return
817
			elif type(value) == type(""):
818
				t = utc_time_from_s_timestamp(value)
819
				if t:
820
					self.__dict__[name].time = t
821
				else:
822
					raise ValueError, "value must be a valid timestamp string"
823
				return
824

  
825
		# Allow string assignment to go to the .text attribute, for
826
		# elements that allow it.  All TextElements allow it;
827
		# Elements will allow it if they do not nave nested elements.
828
		# text_check() raises an error if it's not allowed.
829
		if isinstance(self.__dict__[name], CoreElement) and \
830
				type(value) == type(""):
831
			self.__dict__[name].text_check()
832
			self.__dict__[name].text = value
833
			return
834

  
835
		# locked item so do checks
836
		if not type(self.__dict__[name]) is type(value):
837
			raise TypeError, "value is not the same type"
838

  
839
		self.__dict__[name] = value
840
		
841
	def __delattr__(self, name):
842
		# This won't be used often, if ever, but if anyone tries it, it
843
		# should work.
844
		if isinstance(self.name, XMLItem):
845
			o = self.__dict__[name]
846
			self.elements.remove(o)
847
			del(self.__dict__[name])
848
		else:
849
			# REVIEW: what error should this raise?
850
			raise TypeError, "cannot delete that item"
851

  
852
	def nest_check(self):
853
		pass
854

  
855
	def is_element(self):
856
		# a Nest is not really an element
857
		return False
858

  
859
	def has_contents(self):
860
		for element in self.elements:
861
			if element:
862
				return True
863
		# empty iff all of the elements were empty
864
		return False
865

  
866
	def __nonzero__(self):
867
		return self.has_contents()
868

  
869
	def multiline_contents(self):
870
		# if there are any contents, we want multiline for nested tags
871
		return self.has_contents()
872

  
873
	def s_contents(self, tfc):
874
		if len(self.elements) > 0:
875
			# if any nested elements exist, we show those
876
			lst = []
877

  
878
			for element in self.elements:
879
				s = element._s_tag(tfc)
880
				if s:
881
					lst.append(s)
882

  
883
			return "\n".join(lst)
884
		else:
885
			return ""
886

  
887
		assert False, "not possible to reach this line."
888
		return ""
889

  
890
	def s_tree(self):
891
		level = self.level()
892
		tup = (level, self.s_name(), self.__class__.__name__)
893
		s = "%2d) %s (instance of %s)" % tup
894
		lst = []
895
		lst.append(s)
896
		for element in self.elements:
897
			s = element.s_tree()
898
			lst.append(s)
899
		return "\n".join(lst)
900

  
901
	def _s_tag(self, tfc):
902
		return self.s_contents(tfc)
903

  
904

  
905

  
906

  
907
class NestElement(Nest,CoreElement):
908
	"""
909
	An element that can have other elements nested inside it.
910

  
911
	Attributes:
912
		attr
913
		elements: a list of other elements nested inside this one.
914
	"""
915
	def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]):
916
		CoreElement.__init__(self, tag_name, def_attr, def_attr_value,
917
				attr_names)
918
		self.lock = False
919
		self.elements = []
920
		self.lock = True
921

  
922
	def is_element(self):
923
		return True
924

  
925
	def __nonzero__(self):
926
		return CoreElement.__nonzero__(self)
927

  
928
	def _s_tag(self, tfc):
929
		return CoreElement._s_tag(self, tfc)
930

  
931

  
932

  
933
class Element(NestElement,TextElement):
934
	"""
935
	A class to represent an arbitrary XML tag.  Can either have other XML
936
	elements nested inside it, or else can have a text string value, but
937
	never both at the same time.
938

  
939
	This is intended for user-defined XML tags.  The user can just use
940
	"Element" for all custom tags.
941

  
942
	PyAtom doesn't use this; PyAtom uses TextElement for tags with a text
943
	string value, and NestElement for tags that nest other elements.  Users
944
	can do the same, or can just use Element, as they like.
945

  
946
	Attributes:
947
		attr
948
		elements:	a list of other elements nested inside, if any
949
		text:	a text string value, if any
950

  
951
	Note: if text is set, elements will be empty, and vice-versa.  If you
952
	have elements nested inside and try to set the .text, this will raise
953
	an exception, and vice-versa.
954
	"""
955
	# A Element can have other elements nested inside it, or it can have
956
	# a single ".text" string value.  But never both at the same time.
957
	# Once you nest another element, you can no longer use the .text.
958
	def __init__(self, tag_name, def_attr, def_attr_value, attr_names=[]):
959
		NestElement.__init__(self, tag_name, def_attr, def_attr_value,
960
				attr_names)
961
		self.lock = False
962
		self.text = ""
963
		self.lock = True
964

  
965
	def nest_check(self):
966
		if self.text:
967
			raise TypeError, "Element has text contents so cannot nest"
968

  
969
	def text_check(self):
970
		if len(self.elements) > 0:
971
			raise TypeError, "Element has nested elements so cannot assign text"
972

  
973
	def has_contents(self):
974
		return NestElement.has_contents(self) or TextElement.has_contents(self)
975

  
976
	def multiline_contents(self):
977
		return NestElement.has_contents(self) or self.text.find("\n") >= 0
978

  
979
	def s_contents(self, tfc):
980
		if len(self.elements) > 0:
981
			return NestElement.s_contents(self, tfc)
982
		elif self.text:
983
			return TextElement.s_contents(self, tfc)
984
		else:
985
			return ""
986
		assert False, "not possible to reach this line."
987

  
988
	def s_tree(self):
989
		lst = []
990
		if len(self.elements) > 0:
991
			level = self.level()
992
			tup = (level, self.s_name(), self.__class__.__name__)
993
			s = "%2d) %s (instance of %s)" % tup
994
			lst.append(s)
995
			for element in self.elements:
996
				s = element.s_tree()
997
				lst.append(s)
998
			return "\n".join(lst)
999
		elif self.text:
1000
			return XMLItem.s_tree(self)
1001
		else:
1002
			level = self.level()
1003
			tfc = TFC(level)
1004
			s = "%2d) %s %s" % (level, self.s_name(), "empty Element...")
1005
			return s
1006
		assert False, "not possible to reach this line."
1007

  
1008

  
1009

  
1010
class Collection(XMLItem):
1011
	"""
1012
	A Collection contains 0 or more Elements, but isn't an XML element.
1013
	Use where a run of 0 or more Elements of the same type is legal.
1014

  
1015
	When you init your Collection, you specify what class of Element it will
1016
	contain.  Attempts to append an Element of a different class will raise
1017
	an exception.  Note, however, that the various Element classes all
1018
	inherit from base classes, and you can specify a class from higher up in
1019
	the inheritance tree.  You could, if you wanted, make a Collection
1020
	containing "XMLItem" and then any item defined in PyAtom would be legal
1021
	in that collection.  (See XMLDoc, which contains two collections of
1022
	DocItem.)
1023

  
1024
	Attributes:
1025
		contains:	the class of element this Collection will contain
1026
		elements:	a list of other elements nested inside, if any
1027

  
1028
	Note: The string representation of a Collection is just the string
1029
	representations of the elements inside it.  However, a verbose string
1030
	reprentation may have an XML comment like this:
1031

  
1032
	<!-- Collection of <class> with <n> elements -->
1033

  
1034
	where <n> is the number of elements in the Collection and <class> is the
1035
	name of the class in this Collection.
1036
	"""
1037
	def __init__(self, element_class):
1038
		self.lock = False
1039
		self._parent = None
1040
		self._name = ""
1041
		self.elements = []
1042
		self.contains = element_class
1043
		self.lock = True
1044
	def __len__(self):
1045
		return len(self.elements)
1046
	def __getitem__(self, key):
1047
		return self.elements[key]
1048
	def __setitem__(self, key, value):
1049
		if not isinstance(value, self.contains):
1050
			raise TypeError, "object is the wrong type for this collection"
1051
		self.elements[key] = value
1052
	def __delitem__(self, key):
1053
		del(self.elements[key])
1054

  
1055
	def __nonzero__(self):
1056
		# there are no attrs so if any element is nonzero, collection is too
1057
		for element in self.elements:
1058
			if element:
1059
				return True
1060
		return False
1061

  
1062
	def is_element(self):
1063
		# A Collection is not really an Element
1064
		return False
1065

  
1066
	def s_coll(self):
1067
		name = self.contains.__name__
1068
		n = len(self.elements)
1069
		if n == 1:
1070
			el = "element"
1071
		else:
1072
			el = "elements"
1073
		return "collection of %s with %d %s" % (name, n, el)
1074

  
1075
	def append(self, element):
1076
		if not isinstance(element, self.contains):
1077
			print >> sys.stderr, "Error: attempted to insert", \
1078
					type(element).__name__, \
1079
					"into collection of", self.contains.__name__
1080
			raise TypeError, "object is the wrong type for this collection"
1081
		element._parent = self
1082
		self.elements.append(element)
1083

  
1084
	def _s_tag(self, tfc):
1085
		# A collection exists only as a place to put real elements.
1086
		# There are no start or end tags...
1087
		# When tfc.b_print_all() is true, we do put an XML comment.
1088

  
1089
		if not self.elements:
1090
			if not tfc.b_print_all():
1091
				return ""
1092

  
1093
		lst = []
1094

  
1095
		if tfc.b_print_verbose():
1096
			s = "%s%s%s%s" % (tfc.s_indent(), "<!-- ", self.s_coll(), " -->")
1097
			lst.append(s)
1098
			tfc = tfc.indent_by(1)
1099

  
1100
		for element in self.elements:
1101
			s = element._s_tag(tfc)
1102
			if s:
1103
				lst.append(s)
1104

  
1105
		return "\n".join(lst)
1106

  
1107
	def s_tree(self):
1108
		level = self.level()
1109
		s = "%2d) %s %s" % (level, self.s_name(), self.s_coll())
1110
		lst = []
1111
		lst.append(s)
1112
		for element in self.elements:
1113
			s = element.s_tree()
1114
			lst.append(s)
1115
		return "\n".join(lst)
1116

  
1117

  
1118

  
1119
class XMLDeclaration(XMLItem):
1120
	# REVIEW: should this print multi-line for multiple attrs?
1121
	def __init__(self):
1122
		self._parent = None
1123
		self._name = ""
1124
		self.attrs = {}
1125
		self.attrs[s_version] = "1.0"
1126
		self.attrs[s_encoding] = "utf-8"
1127
		self.attr_names = [s_version, s_encoding, s_standalone]
1128

  
1129
	def _s_tag(self, tfc):
1130
		# An XMLDeclaration() instance is never empty, so always prints.
1131

  
1132
		lst = []
1133
		s = "%s%s" % (tfc.s_indent(), "<?xml")
1134
		lst.append(s)
1135
		# 0) show all attrs in the order of attr_names
1136
		for attr in self.attr_names:
1137
			if attr in self.attrs.keys():
1138
				s_attr = ' %s="%s"' % (attr, self.attrs[attr])
1139
				lst.append(s_attr)
1140
		# 1) any attrs not in attr_names?  list them, too
1141
		for attr in self.attrs:
1142
			if not attr in self.attr_names:
1143
				s_attr = ' %s="%s"' % (attr, self.attrs[attr])
1144
				lst.append(s_attr)
1145
		lst.append("?>")
1146

  
1147
		return "".join(lst)
1148

  
1149
	def __nonzero__(self):
1150
		# Returns True because the XML Declaration is never empty.
1151
		return True
1152

  
1153
	def is_element(self):
1154
		return True
1155

  
1156

  
1157

  
1158
class XMLDoc(Nest):
1159
	"""
1160
	A data structure to represent an XML Document.  It will have the
1161
	following structure:
1162

  
1163
	the XML Declaration item
1164
	0 or more document-level XML items
1165
	exactly one XML item (the "root tag")
1166
	0 or more document-level XML items
1167

  
1168
	document level XML items are: Comment, PI, MarkupDecl
1169

  
1170

  
1171
	Attributes:
1172
		xml_decl:	the XMLDeclaration item
1173
		docitems_above:	a collection of DocItem (items above root_element)
1174
		root_element:	the XML tag containing your data
1175
		docitems_below:	a collection of DocItem (items below root_element)
1176

  
1177
	Note: usually the root_element has lots of other XML items nested inside
1178
	it!
1179
	"""
1180
	def __init__(self, root_element=None):
1181
		Nest.__init__(self)
1182

  
1183
		self._name = "XMLDoc"
1184

  
1185
		self.xml_decl = XMLDeclaration()
1186
		self.docitems_above = Collection(DocItem)
1187

  
1188
		if not root_element:
1189
			root_element = Comment("no root element yet")
1190
		self.root_element = root_element
1191

  
1192
		self.docitems_below = Collection(DocItem)
1193

  
1194
	def __setattr__(self, name, value):
1195
		# root_element may always be set to any ElementItem
1196
		if name == "root_element":
1197
			if not (isinstance(value, ElementItem)):
1198
				raise TypeError, "only ElementItem is permitted"
1199

  
1200
			self.lock = False
1201
			# Item checks out, so assign it.  root_element should only
1202
			# ever have one element, and we always put the new element
1203
			# in the same slot in elements[].
1204
			if "i_root_element" in self.__dict__:
1205
				# Assign new root_element over old one in elements[]
1206
				assert self.elements[self.i_root_element] == self.root_element
1207
				self.elements[self.i_root_element] = value
1208
			else:
1209
				# This is the first time root_element was ever set.
1210
				self.i_root_element = len(self.elements)
1211
				self.elements.append(value)
1212

  
1213
			value._parent = self
1214
			value._name = name
1215
			self.__dict__[name] = value
1216
			self.lock = True
1217
		else:
1218
			# for all other, fall through to inherited behavior
1219
			Nest.__setattr__(self, name, value)
1220

  
1221
	def Validate(self):
1222
		# XMLDoc never has parent.  Never change this!
1223
		assert self._parent == None
1224
		return True
1225

  
1226

  
1227

  
1228
def local_time_from_utc_time(t):
1229
	return t - time.timezone
1230

  
1231
def utc_time_from_local_time(t):
1232
	return t + time.timezone
1233

  
1234
def local_time():
1235
	return time.time() - time.timezone
1236

  
1237
def utc_time():
1238
	return time.time()
1239

  
1240

  
1241
class TimeSeq(object):
1242
	"""
1243
	A class to generate a sequence of timestamps.
1244

  
1245
	Atom feed validators complain if multiple timestamps have the same
1246
	value, so this provides a convenient way to set a bunch of timestamps
1247
	all at least one second different from each other.
1248
	"""
1249
	def __init__(self, init_time=0):
1250
		if init_time == 0:
1251
			self.time = local_time()
1252
		else:
1253
			self.time = float(init_time)
1254
	def next(self):
1255
		t = self.time
1256
		self.time += 1.0
1257
		return t
1258

  
1259
format_RFC3339 = "%Y-%m-%dT%H:%M:%S"
1260

  
1261
def parse_time_offset(s):
1262
	s = s.lstrip().rstrip()
1263

  
1264
	if (s == '' or s == 'Z' or s == 'z'):
1265
		return 0
1266

  
1267
	m = pat_time_offset.search(s)
1268
	sign = m.group(1)
1269
	offset_hour = int(m.group(2))
1270
	offset_min = int(m.group(3))
1271
	offset = offset_hour * 3600 + offset_min * 60
1272
	if sign == "-":
1273
		offset *= -1
1274
	return offset
1275

  
1276
def s_timestamp(utc_time, time_offset="Z"):
1277
	"""
1278
	Format a time and offset into a string.
1279

  
1280
	utc_time
1281
		a floating-point value, time in the UTC time zone
1282
	s_time_offset
1283
		a string specifying an offset from UTC.  Examples:
1284
			z or Z -- offset is 0 ("Zulu" time, UTC, aka GMT)
1285
			-08:00 -- 8 hours earlier than UTC (Pacific time zone)
1286
			"" -- empty string is technically not legal, but may work
1287

  
1288
	Notes:
1289
		Returned string complies with RFC3339; uses ISO8601 date format.
1290
		Example: 2003-12-13T18:30:02Z
1291
		Example: 2003-12-13T18:30:02+02:00
1292
	"""
1293

  
1294
	if not utc_time:
1295
		return ""
1296

  
1297
	utc_time += parse_time_offset(time_offset)
1298

  
1299
	try:
1300
		s = time.strftime(format_RFC3339, time.localtime(utc_time))
1301
	except:
1302
		return ""
1303

  
1304
	return s + time_offset
1305

  
1306

  
1307

  
1308
pat_RFC3339 = re.compile("(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(.*)")
1309
pat_time_offset = re.compile("([+-])(\d\d):(\d\d)")
1310

  
1311
def utc_time_from_s_timestamp(s_date_time_stamp):
1312
	# parse RFC3339-compatible times that use ISO8601 date format
1313
	# date time stamp example: 2003-12-13T18:30:02Z
1314
	# date time stamp example: 2003-12-13T18:30:02+02:00
1315
	# leaving off the suffix is technically not legal, but allowed
1316

  
1317
	s_date_time_stamp = s_date_time_stamp.lstrip().rstrip()
1318

  
1319
	try:
1320
		m = pat_RFC3339.search(s_date_time_stamp)
1321
		year = int(m.group(1))
1322
		mon = int(m.group(2))
1323
		mday = int(m.group(3))
1324
		hour = int(m.group(4))
1325
		min = int(m.group(5))
1326
		sec = int(m.group(6))
1327
		tup = (year, mon, mday, hour, min, sec, -1, -1, -1)
1328
		t = time.mktime(tup)
1329

  
1330
		s = m.group(7)
1331
		t += parse_time_offset(s)
1332

  
1333
		return t
1334

  
1335
	except:
1336
		return 0.0
1337

  
1338
	assert False, "impossible to reach this line"
1339

  
1340

  
1341
def s_time_offset():
1342
	"""
1343
	Return a string with local offset from UTC in RFC3339 format.
1344
	"""
1345

  
1346
	# If t is set to local time in seconds since the epoch, then...
1347
	# ...offset is the value you add to t to get UTC.  This is the
1348
	# reverse of time.timezone.
1349

  
1350
	offset = -(time.timezone)
1351

  
1352
	if offset > 0:
1353
		sign = "+"
1354
	else:
1355
		sign = "-"
1356
		offset = abs(offset)
1357

  
1358
	offset_hour = offset // (60 * 60)
1359
	offset_min = (offset // 60) % 60
1360
	return "%s%02d:%02d" % (sign, offset_hour, offset_min)
1361

  
1362
s_offset_local = s_time_offset()
1363

  
1364
s_offset_default = s_offset_local
1365

  
1366
def set_default_time_offset(s):
1367
	global s_offset_default
1368
	s_offset_default = s
1369

  
1370

  
1371
class Timestamp(CoreElement):
1372
	def __init__(self, tag_name, time=0.0):
1373
		CoreElement.__init__(self, tag_name, None, None)
1374
		self.lock = False
1375
		self.time = time
1376
		self.time_offset = s_offset_default
1377
		self.lock = True
1378

  
1379
	def __delattr__(self, name):
1380
		CoreElement.__delattr_(self, name)
1381

  
1382
	def __getattr__(self, name):
1383
		if name == "text":
1384
			return s_timestamp(self.time, self.time_offset)
1385
		return CoreElement.__getattr_(self, name)
1386

  
1387
	def __setattr__(self, name, value):
1388
		if name == "text":
1389
			if type(value) != type(""):
1390
				raise TypeError, "can only assign a string to .text"
1391
			t = utc_time_from_s_timestamp(value)
1392
			if t:
1393
				self.time = utc_time_from_s_timestamp(value)
1394
			else:
1395
				raise ValueError, "value must be a valid timestamp string"
1396
			return
1397
		CoreElement.__setattr__(self, name, value)
1398

  
1399
	def has_contents(self):
1400
		return self.time != 0
1401

  
1402
	def multiline_contents(self):
1403
		return False
1404

  
1405
	def s_contents(self, tfc):
1406
		return s_timestamp(self.time, self.time_offset)
1407

  
1408
	def update(self):
1409
		self.time = local_time()
1410
		return self
1411

  
1412

  
1413

  
1414

  
1415
# Below are all the classes to implement Atom using the above tools.
1416

  
1417

  
1418

  
1419
class AtomText(TextElement):
1420
	def __init__(self, tag_name):
1421
		attr_names = [ s_type ]
1422
		# legal values of type: "text", "html", "xhtml"
1423
		TextElement.__init__(self, tag_name, None, None, attr_names)
1424

  
1425
class Title(AtomText):
1426
	def __init__(self, text=""):
1427
		AtomText.__init__(self, "title")
1428
		self.text = text
1429
		
1430
class Subtitle(AtomText):
1431
	def __init__(self, text=""):
1432
		AtomText.__init__(self, "subtitle")
1433
		self.text = text
1434
		
1435
class Content(AtomText):
1436
	def __init__(self, text=""):
1437
		AtomText.__init__(self, "content")
1438
		self.text = text
1439
		
1440
class Summary(AtomText):
1441
	def __init__(self, text=""):
1442
		AtomText.__init__(self, "summary")
1443
		self.text = text
1444
		
1445
class Rights(AtomText):
1446
	def __init__(self, text=""):
1447
		AtomText.__init__(self, "rights")
1448
		self.text = text
1449
		
1450
class Id(TextElement):
1451
	def __init__(self, text=""):
1452
		TextElement.__init__(self, "id", None, None)
1453
		self.text = text
1454
		
1455
class Generator(TextElement):
1456
	def __init__(self):
1457
		attr_names = [ "uri", "version" ]
1458
		TextElement.__init__(self, "generator", None, None, attr_names)
1459
		
1460
class Category(TextElement):
1461
	def __init__(self, term_val=""):
1462
		attr_names = [s_term, "scheme", "label"]
1463
		TextElement.__init__(self, "category", s_term, term_val, attr_names)
1464

  
1465
class Link(TextElement):
1466
	def __init__(self, href_val=""):
1467
		attr_names = [
1468
				s_href, "rel", "type", "hreflang", "title", "length", s_lang]
1469
		TextElement.__init__(self, "link", s_href, href_val, attr_names)
1470

  
1471
class Icon(TextElement):
1472
	def __init__(self):
1473
		TextElement.__init__(self, "icon", None, None)
1474

  
1475
class Logo(TextElement):
1476
	def __init__(self):
1477
		TextElement.__init__(self, "logo", None, None)
1478

  
1479
class Name(TextElement):
1480
	def __init__(self, text=""):
1481
		TextElement.__init__(self, "name", None, None)
1482
		self.text = text
1483

  
1484
class Email(TextElement):
1485
	def __init__(self):
1486
		TextElement.__init__(self, "email", None, None)
1487

  
1488
class Uri(TextElement):
1489
	def __init__(self):
1490
		TextElement.__init__(self, "uri", None, None)
1491

  
1492

  
1493

  
1494
class BasicAuthor(NestElement):
1495
	def __init__(self, tag_name, name):
1496
		NestElement.__init__(self, tag_name, None, None)
1497
		self.name = Name(name)
1498
		self.email = Email()
1499
		self.uri = Uri()
1500

  
1501
class Author(BasicAuthor):
1502
	def __init__(self, name=""):
1503
		BasicAuthor.__init__(self, "author", name)
1504

  
1505
class Contributor(BasicAuthor):
1506
	def __init__(self, name=""):
1507
		BasicAuthor.__init__(self, "contributor", name)
1508

  
1509

  
1510

  
1511
class Updated(Timestamp):
1512
	def __init__(self, time=0.0):
1513
		Timestamp.__init__(self, "updated", time)
1514

  
1515
class Published(Timestamp):
1516
	def __init__(self, time=0.0):
1517
		Timestamp.__init__(self, "published", time)
1518

  
1519

  
1520

  
1521
class FeedElement(NestElement):
1522
	def __init__(self, tag_name):
1523
		NestElement.__init__(self, tag_name, None, None)
1524

  
1525
		self.title = Title("")
1526
		self.id = Id("")
1527
		self.updated = Updated()
1528
		self.authors = Collection(Author)
1529
		self.links = Collection(Link)
1530

  
1531
		self.subtitle = Subtitle("")
1532
		self.categories = Collection(Category)
1533
		self.contributors = Collection(Contributor)
1534
		self.generator = Generator()
1535
		self.icon = Icon()
1536
		self.logo = Logo()
1537
		self.rights = Rights("")
1538

  
1539
class Feed(FeedElement):
1540
	def __init__(self):
1541
		FeedElement.__init__(self, "feed")
1542
		self.attrs["xmlns"] = "http://www.w3.org/2005/Atom"
1543
		self.title.text = "Title of Feed Goes Here"
1544
		self.id.text = "ID of Feed Goes Here"
1545
		self.entries = Collection(Entry)
1546

  
1547
class Source(FeedElement):
1548
	def __init__(self):
1549
		FeedElement.__init__(self, "source")
1550

  
1551

  
1552

  
1553
class Entry(NestElement):
1554
	def __init__(self):
1555
		NestElement.__init__(self, "entry", None, None)
1556
		self.title = Title("Title of Entry Goes Here")
1557
		self.id = Id("ID of Entry Goes Here")
1558
		self.updated = Updated()
1559
		self.authors = Collection(Author)
1560
		self.links = Collection(Link)
1561

  
1562
		self.content = Content("")
1563
		self.summary = Summary("")
1564
		self.categories = Collection(Category)
1565
		self.contributors = Collection(Contributor)
1566
		self.published = Published()
1567
		self.source = Source()
1568
		self.rights = Rights("")
1569

  
1570

  
1571

  
1572
def diff(s0, name0, s1, name1):
1573
	from difflib import ndiff
1574
	lst0 = s0.split("\n")
1575
	lst1 = s1.split("\n")
1576
	report = '\n'.join(ndiff(lst0, lst1))
1577
	return report
1578

  
1579

  
1580
def run_test_cases():
1581

  
1582
	# The default is to make time stamps in local time with appropriate
1583
	# offset; for our tests, we want a default "Z" offset instead.
1584
	set_default_time_offset("Z")
1585

  
1586
	failed_tests = 0
1587

  
1588

  
1589
	# Test: convert current time into a timestamp string and back
1590

  
1591
	now = local_time()
1592
	# timestamp format does not allow fractional seconds
1593
	now = float(int(now))	# truncate any fractional seconds
1594
	s = s_timestamp(now)
1595
	t = utc_time_from_s_timestamp(s)
1596
	if now != t:
1597
		failed_tests += 1
1598
		print "test case failed:"
1599
		print now, "-- original timestamp"
1600
		print t, "-- converted timestamp does not match"
1601

  
1602

  
1603
	# Test: convert a timestamp string to a time value and back
1604

  
1605
	s_time = "2003-12-13T18:30:02Z"
1606
	t = utc_time_from_s_timestamp(s_time)
1607
	s = s_timestamp(t)
1608
	if s_time != s:
1609
		failed_tests += 1
1610
		print "test case failed:"
1611
		print s_time, "-- original timestamp"
1612
		print s, "-- converted timestamp does not match"
1613

  
1614

  
1615
	# Test: generate the "Atom-Powered Robots Run Amok" example
1616
	#
1617
	# Note: the original had some of the XML declarations in
1618
	# a different order than PyAtom puts them.  I swapped around
1619
	# the lines here so they would match the PyAtom order.  Other
1620
	# than that, this is the example from:
1621
	#
1622
	# http://www.atomenabled.org/developers/syndication/#sampleFeed
1623

  
1624
	s_example = """\
1625
<?xml version="1.0" encoding="utf-8"?>
1626
<feed xmlns="http://www.w3.org/2005/Atom">
1627
	<title>Example Feed</title>
1628
	<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
1629
	<updated>2003-12-13T18:30:02Z</updated>
1630
	<author>
1631
		<name>John Doe</name>
1632
	</author>
1633
	<link href="http://example.org/"/>
1634
	<entry>
1635
		<title>Atom-Powered Robots Run Amok</title>
1636
		<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
1637
		<updated>2003-12-13T18:30:02Z</updated>
1638
		<link href="http://example.org/2003/12/13/atom03"/>
1639
		<summary>Some text.</summary>
1640
	</entry>
1641
</feed>"""
1642

  
1643
	xmldoc = XMLDoc()
1644
	
1645
	feed = Feed()
1646
	xmldoc.root_element = feed
1647

  
1648
	feed.title = "Example Feed"
1649
	feed.id = "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6"
1650
	feed.updated = "2003-12-13T18:30:02Z"
1651

  
1652
	link = Link("http://example.org/")
1653
	feed.links.append(link)
1654

  
1655
	author = Author("John Doe")
1656
	feed.authors.append(author)
1657

  
1658

  
1659
	entry = Entry()
1660
	feed.entries.append(entry)
1661
	entry.id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a"
1662
	entry.title = "Atom-Powered Robots Run Amok"
1663
	entry.updated = "2003-12-13T18:30:02Z"
1664
	entry.summary = "Some text."
1665

  
1666
	link = Link("http://example.org/2003/12/13/atom03")
1667
	entry.links.append(link)
1668

  
1669

  
1670
	s = str(xmldoc)
1671
	if s_example != s:
1672
		failed_tests += 1
1673
		print "test case failed:"
1674
		print "The generated XML doesn't match the example.  diff follows:"
1675
		print diff(s_example, "s_example", s, "s")
1676

  
1677

  
1678
	# Test: verify that xmldoc.Validate() succeeds
1679

  
1680
	if not xmldoc.Validate():
1681
		failed_tests += 1
1682
		print "test case failed:"
1683
		print "xmldoc.Validate() failed."
1684

  
1685

  
1686
	# Test: does Element work both nested an non-nested?
1687
	s_test = """\
1688
<test>
1689
	<test:agent number="007">James Bond</test:agent>
1690
	<test:pet
1691
			nickname="Mei-Mei"
1692
			type="cat">Matrix</test:pet>
1693
</test>"""
1694

  
1695
	class TestPet(Element):
1696
		def __init__(self, name=""):
1697
			Element.__init__(self, "test:pet", None, None)
1698
			self.text = name
1699

  
1700
	class TestAgent(Element):
1701
		def __init__(self, name=""):
1702
			Element.__init__(self, "test:agent", None, None)
1703
			self.text = name
1704

  
1705
	class Test(Element):
1706
		def __init__(self):
1707
			Element.__init__(self, "test", None, None)
1708
			self.test_agent = TestAgent()
1709
			self.test_pet = TestPet()
1710

  
1711
	test = Test()
1712
	test.test_agent = "James Bond"
1713
	test.test_agent.attrs["number"] = "007"
1714
	test.test_pet = "Matrix"
1715
	test.test_pet.attrs["type"] = "cat"
1716
	test.test_pet.attrs["nickname"] = "Mei-Mei"
1717

  
1718
	s = str(test)
1719
	if s_test != s:
1720
		failed_tests += 1
1721
		print "test case failed:"
1722
		print "test output doesn't match.  diff follows:"
1723
		print diff(s_test, "s_test", s, "s")
1724

  
1725

  
1726
	if failed_tests > 0:
1727
		print "self-test failed!"
1728
	else:
1729
		print "self-test successful."
1730

  
1731

  
1732

  
1733
if __name__ == "__main__":
1734
	run_test_cases()
auquotidien/modules/pyatom/readme.txt
1
PyAtom
2

  
3

  
4
PyAtom is a Python library module I wrote to make it very easy to create
5
an Atom syndication feed.
6

  
7
http://atomenabled.org/developers/syndication/
8

  
9

  
10
I have released PyAtom under The Academic Free License 2.1.  I intend to
11
donate PyAtom to the Python Software Foundation.
12

  
13

  
14
Notes on PyAtom:
15

  
16
XML is best represented in a tree structure, and PyAtom is a set of
17
classes that automatically manage the tree structure for the user.  The
18
top level of an Atom feed is an XML "Feed" element with a "<feed>" tag;
19
the Feed element has other elements nested inside it that describe the
20
feed, and then it has 0 or more Entry elements, each of which has
21
elements that describe the Entry.
22

  
23
Take a look at RunPyAtomTestCases(), at the end of pyatom.py, for
24
example code showing how to set up a feed with an entry.
25

  
26
To create an XML document with a feed in it, the user does this:
27

  
28
xmldoc = XMLDoc()
29
feed = Feed()
30
xmldoc.root_element = feed
31

  
32
To assign an entry to a feed, the user just does this:
33

  
34
feed.entries.append(entry)
35

  
36
This adds "entry" to the internal list that keeps track of entries.
37
"entry" is now nested inside "feed", which is nested inside "xmldoc".
38

  
39
Later, when the user wants to save the XML in a file, the user can just
40
do this:
41

  
42
f = open("file.xml", "w")
43
s = str(xmldoc)
44
f.write(s)
45

  
46
To make the string from xmldoc, the XMLDoc class walks through the XML
47
elements nested inside xmldoc, asking each one to return its string.
48
Each element that has other elements nested inside does the same thing.
49
The whole tree is recursively walked, and the tags all return strings
50
that are indented properly for their level in the tree.
51

  
52
The classes that implement Atom in PyAtom just use the heck out of
53
inheritance.  There are abstract base classes that implement broadly
54
useful behavior, and lots of classes that just inherit and use this
55
behavior; but there are plenty of places where the child classes
56
overload the inherited behavior and do something different.  The way
57
Python handles inheritance made this a joy to code up.
58

  
59

  
60

  
61
If you have any questions about anything here, please contact me using
62
this email address:
63

  
64
pyatom@langri.com
auquotidien/modules/root.py
1
from quixote import get_publisher, get_response, get_request, redirect, get_session
2
from quixote.directory import Directory
3
from quixote.html import TemplateIO, htmltext
4

  
5
from wcs.qommon.misc import get_variadic_url, simplify
6

  
7
import os
8
import re
9
import string
10
import urlparse
11

  
12
try:
13
    import lasso
14
except ImportError:
15
    pass
16

  
17
import wcs
18
import wcs.root
19
import qommon
20
from qommon import _
21
from qommon import get_cfg, get_logger
22
from qommon import template
23
from qommon import errors
24
from qommon.form import *
25
from qommon import logger
26
from wcs.roles import logged_users_role
27

  
28
from qommon import emails
29
from qommon.sms import SMS
30
from wcs.categories import Category
31
from wcs.formdef import FormDef
32
from wcs.data_sources import NamedDataSource
33
from qommon.tokens import Token
34
from qommon.admin.emails import EmailsDirectory
35
from qommon.admin.texts import TextsDirectory
36

  
37
from links import Link
38
from announces import Announce, AnnounceSubscription
39
from myspace import MyspaceDirectory
40
from agenda import AgendaDirectory
41
from events import Event, get_default_event_tags
42
from payments import PublicPaymentDirectory
43
from payments_ui import InvoicesDirectory
44

  
45
import admin
46

  
47
import wcs.forms.root
48
from wcs.workflows import Workflow
49
from wcs.forms.preview import PreviewDirectory
50

  
51
from saml2 import Saml2Directory
52

  
53
OldRootDirectory = wcs.root.RootDirectory
54

  
55
import qommon.ident.password
56
import qommon.ident.idp
57

  
58

  
59
def category_get_homepage_position(self):
60
    if hasattr(self, 'homepage_position') and self.homepage_position:
61
        return self.homepage_position
62
    if self.url_name == 'consultations':
63
        return '2nd'
64
    return '1st'
65
Category.get_homepage_position = category_get_homepage_position
66

  
67
def category_get_limit(self):
68
    if hasattr(self, 'limit') and self.limit is not None:
69
        return self.limit
70
    return 7
71
Category.get_limit = category_get_limit
72

  
73
Category.TEXT_ATTRIBUTES = ['name', 'url_name', 'description', 'homepage_position']
74
Category.INT_ATTRIBUTES = ['position', 'limit']
75

  
76

  
77
class FormsRootDirectory(wcs.forms.root.RootDirectory):
78

  
79
    def _q_index(self, *args):
80
        get_response().filter['is_index'] = True
81
        return wcs.forms.root.RootDirectory._q_index(self, *args)
82

  
83
    def user_forms(self, user_forms):
84
        r = TemplateIO(html=True)
85
        base_url = get_publisher().get_root_url()
86

  
87
        draft = [x for x in user_forms if x.is_draft() and not x.formdef.is_disabled()]
88
        if draft:
89
            r += htmltext('<h4 id="drafts">%s</h4>') % _('My Current Drafts')
90
            r += htmltext('<ul>')
91
            for f in draft:
92
                if f.formdef.category:
93
                    category_url = '%s' % f.formdef.category.url_name
94
                else:
95
                    category_url = '.'
96
                r += htmltext('<li><a href="%s%s/%s/%s">%s</a>, %s') % (base_url,
97
                    category_url,
98
                    f.formdef.url_name, f.id, f.formdef.name,
99
                    misc.localstrftime(f.receipt_time))
100
                r += htmltext(' (<a href="%s%s/%s/%s?remove-draft">%s</a>)') % (base_url,
101
                    category_url,
102
                    f.formdef.url_name, f.id, _('delete'))
103
                r += htmltext('</li>')
104
            r += htmltext('</ul>')
105

  
106
        forms_by_status_name = {}
107
        for f in user_forms:
108
            if f.is_draft():
109
                continue
110
            status = f.get_visible_status()
111
            if status:
112
                status_name = status.name
113
            else:
114
                status_name = None
115
            if status_name in forms_by_status_name:
116
                forms_by_status_name[status_name].append(f)
117
            else:
118
                forms_by_status_name[status_name] = [f]
119
        for status_name in forms_by_status_name:
120
            if status_name:
121
                r += htmltext('<h4>%s</h4>') % _('My forms with status "%s"') % status_name
122
            else:
123
                r += htmltext('<h4>%s</h4>') % _('My forms with an unknown status') % status_name
124
            r += htmltext('<ul>')
125
            forms_by_status_name[status_name].sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
126
            for f in forms_by_status_name[status_name]:
127
                if f.formdef.category_id:
128
                    category_url = f.formdef.category.url_name
129
                else:
130
                    category_url = '.'
131
                r += htmltext('<li><a href="%s%s/%s/%s/">%s</a>, %s</li>') % (
132
                        base_url,
133
                        category_url,
134
                        f.formdef.url_name, f.id, f.formdef.name, 
135
                        misc.localstrftime(f.receipt_time))
136
            r += htmltext('</ul>')
137
        return r.getvalue()
138

  
139

  
140
class AnnounceDirectory(Directory):
141
    _q_exports = ['', 'edit', 'delete', 'email']
142

  
143
    def __init__(self, announce):
144
        self.announce = announce
145

  
146
    def _q_index(self):
147
        template.html_top(_('Announces to citizens'))
148
        r = TemplateIO(html=True)
149

  
150
        if self.announce.publication_time:
151
            date_heading = '%s - ' % time.strftime(misc.date_format(), self.announce.publication_time)
152
        else:
153
            date_heading = ''
154

  
155
        r += htmltext('<h3>%s%s</h3>') % (date_heading, self.announce.title)
156

  
157
        r += htmltext('<p>')
158
        r += self.announce.text
159
        r += htmltext('</p>')
160

  
161
        r += htmltext('<p>')
162
        r += htmltext('<a href="../">%s</a>') % _('Back')
163
        r += htmltext('</p>')
164
        return r.getvalue()
165

  
166

  
167
class AnnouncesDirectory(Directory):
168
    _q_exports = ['', 'subscribe', 'email', 'atom', 'sms', 'emailconfirm',
169
            'email_unsubscribe', 'sms_unsubscribe', 'smsconfirm', 'rawlist']
170

  
171
    def _q_traverse(self, path):
172
        get_response().breadcrumb.append(('announces/', _('Announces')))
173
        return Directory._q_traverse(self, path)
174

  
175
    def _q_index(self):
176
        template.html_top(_('Announces to citizens'))
177
        r = TemplateIO(html=True)
178
        r += self.announces_list()
179
        r += htmltext('<ul id="announces-links">')
180
        r += htmltext('<li><a href="subscribe">%s</a></li>') % _('Receiving those Announces')
181
        r += htmltext('</ul>')
182
        return r.getvalue()
183

  
184
    def _get_announce_subscription(self):
185
        """ """
186
        sub = None
187
        if get_request().user:
188
            subs = AnnounceSubscription.select(lambda x: x.user_id == get_request().user.id)
189
            if subs:
190
                sub = subs[0]
191
        return sub
192

  
193
    def rawlist(self):
194
        get_response().filter = None
195
        return self.announces_list()
196

  
197
    def announces_list(self):
198
        announces = Announce.get_published_announces()
199
        if not announces:
200
            raise errors.TraversalError()
201

  
202
        # XXX: will need pagination someday
203
        r = TemplateIO(html=True)
204
        for item in announces:
205
            r += htmltext('<div class="announce-item">\n')
206
            r += htmltext('<h4>')
207
            if item.publication_time:
208
                r += time.strftime(misc.date_format(), item.publication_time)
209
                r += ' - '
210
            r += item.title
211
            r += htmltext('</h4>\n')
212
            r += htmltext('<p>\n')
213
            r += item.text
214
            r += htmltext('\n</p>\n')
215
            r += htmltext('</div>\n')
216
        return r.getvalue()
217

  
218

  
219
    def sms(self):
220
        sms_mode = get_cfg('sms', {}).get('mode', 'none')
221

  
222
        if sms_mode == 'none':
223
            raise errors.TraversalError()
224

  
225
        get_response().breadcrumb.append(('sms', _('SMS')))
226
        template.html_top(_('Receiving announces by SMS'))
227
        r = TemplateIO(html=True)
228

  
229
        if sms_mode == 'demo':
230
            r += TextsDirectory.get_html_text('aq-sms-demo')
231
        else:
232
            announces_cfg = get_cfg('announces',{})
233
            mobile_mask = announces_cfg.get('mobile_mask')
234
            if mobile_mask:
235
                mobile_mask = ' (' + mobile_mask + ')'
236
            else:
237
                mobile_mask = ''
238
            form = Form(enctype='multipart/form-data')
239
            form.add(StringWidget, 'mobile', title = _('Mobile number %s') % mobile_mask, size=12, required=True)
240
            form.add_submit('submit', _('Subscribe'))
241
            form.add_submit('cancel', _('Cancel'))
242

  
243
            if form.get_submit() == 'cancel':
244
                return redirect('subscribe')
245

  
246
            if form.is_submitted() and not form.has_errors():
247
                s = self.sms_submit(form)
248
                if s == False:
249
                    r += form.render()
250
                else:
251
                    return redirect("smsconfirm")
252
            else:
253
                r += form.render()
254
        return r.getvalue()
255

  
256
    def sms_submit(self, form):
257
        mobile = form.get_widget("mobile").parse()
258
        # clean the string, remove any extra character
259
        mobile = re.sub('[^0-9+]','',mobile)
260
        # if a mask was set, validate
261
        announces_cfg = get_cfg('announces',{})
262
        mobile_mask = announces_cfg.get('mobile_mask')
263
        if mobile_mask:
264
            mobile_regexp = re.sub('X','[0-9]', mobile_mask) + '$'
265
            if not re.match(mobile_regexp, mobile):
266
                form.set_error("mobile", _("Phone number invalid ! It must match ") + mobile_mask)
267
                return False
268
        if mobile.startswith('00'):
269
            mobile = '+' + mobile[2:]
270
        else:
271
            # Default to france international prefix
272
            if not mobile.startswith('+'):
273
                mobile = re.sub("^0", "+33", mobile)
274
        sub = self._get_announce_subscription()
275
        if not sub:
276
            sub = AnnounceSubscription()
277
        if get_request().user:
278
            sub.user_id = get_request().user.id
279

  
280
        if mobile:
281
            sub.sms = mobile
282

  
283
        if not get_request().user:
284
            sub.enabled = False
285

  
286
        sub.store()
287

  
288
        # Asking sms confirmation
289
        token = Token(3 * 86400, 4, string.digits)
290
        token.type = 'announces-subscription-confirmation'
291
        token.subscription_id = sub.id
292
        token.store()
293

  
294
        message = _("Confirmation code : %s") % str(token.id)
295
        sms_cfg = get_cfg('sms', {})
296
        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
297
        mode = sms_cfg.get('mode', 'none')
298
        sms = SMS.get_sms_class(mode)
299
        try:
300
            sms.send(sender, [mobile], message)
301
        except errors.SMSError, e:
302
            get_logger().error(e)
303
            form.set_error("mobile", _("Send SMS confirmation failed"))
304
            sub.remove("sms")
305
            return False
306

  
307
    def smsconfirm(self):
308
        template.html_top(_('Receiving announces by SMS confirmation'))
309
        r = TemplateIO(html=True)
310
        r += htmltext("<p>%s</p>") % _("You will receive a confirmation code by SMS.")
311
        form = Form(enctype='multipart/form-data')
312
        form.add(StringWidget, 'code', title = _('Confirmation code (4 characters)'), size=12, required=True)
313
        form.add_submit('submit', _('Subscribe'))
314
        form.add_submit('cancel', _('Cancel'))
315

  
316
        if form.get_submit() == 'cancel':
317
            return redirect('..')
318

  
319
        if form.is_submitted() and not form.has_errors():
320
            token = None
321
            id = form.get_widget("code").parse()
322
            try:
323
                token = Token.get(id)
324
            except KeyError:
325
                form.set_error("code",  _('Invalid confirmation code.'))
326
            else:
327
                if token.type != 'announces-subscription-confirmation':
328
                    form.set_error("code",  _('Invalid confirmation code.'))
329
                else:
330
                    sub = AnnounceSubscription.get(token.subscription_id)
331
                    token.remove_self()
332
                    sub.enabled_sms = True
333
                    sub.store()
334
                    return redirect('.')
335
            r += form.render()
336
        else:
337
            r += form.render()
338

  
339
        return r.getvalue()
340

  
341
    def sms_unsubscribe(self):
342
        sub = self._get_announce_subscription()
343

  
344
        form = Form(enctype='multipart/form-data')
345
        if not sub:
346
            return redirect('..')
347

  
348
        form.add_submit('submit', _('Unsubscribe'))
349
        form.add_submit('cancel', _('Cancel'))
350

  
351
        if form.get_submit() == 'cancel':
352
            return redirect('..')
353

  
354
        get_response().breadcrumb.append(('sms', _('SMS Unsubscription')))
355
        template.html_top()
356
        r = TemplateIO(html=True)
357

  
358
        if form.is_submitted() and not form.has_errors():
359
            if sub:
360
                sub.remove("sms")
361

  
362
            root_url = get_publisher().get_root_url()
363
            r += htmltext('<p>')
364
            r += _('You have been unsubscribed from announces')
365
            r += htmltext('</p>')
366
            if not get_response().iframe_mode:
367
                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
368
        else:
369
            r += htmltext('<p>')
370
            r += _('Do you want to stop receiving announces by sms ?')
371
            r += htmltext('</p>')
372
            r += form.render()
373

  
374
        return r.getvalue()
375

  
376

  
377
    def subscribe(self):
378
        get_response().breadcrumb.append(('subscribe', _('Subscription')))
379
        template.html_top(_('Receiving Announces'))
380
        r = TemplateIO(html=True)
381

  
382
        r += TextsDirectory.get_html_text('aq-announces-subscription')
383

  
384
        sub = self._get_announce_subscription()
385

  
386
        r += htmltext('<ul id="announce-modes">')
387
        if sub and sub.email:
388
            r += htmltext(' <li>')
389
            r += htmltext('<span id="par_mail">%s</span>') % _('Email (currently subscribed)')
390
            r += htmltext(' <a href="email_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
391
        else:
392
            r += htmltext(' <li><a href="email" id="par_mail" rel="popup">%s</a></li>') % _('Email')
393
        if sub and sub.sms:
394
            r += htmltext(' <li>')
395
            if sub.enabled_sms:
396
                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently subscribed)') % sub.sms
397
            else:
398
                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently not confirmed)') % sub.sms
399
                r += htmltext(' <a href="smsconfirm" rel="popup">%s</a> ') % _('Confirmation')
400
            r += htmltext(' <a href="sms_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
401
        elif get_cfg('sms', {}).get('mode', 'none') != 'none':
402
            r += htmltext(' <li><a href="sms" id="par_sms">%s</a></li>') % _('SMS')
403
        r += htmltext(' <li><a class="feed-link" href="atom" id="par_rss">%s</a>') % _('Feed')
404
        r += htmltext('</ul>')
405
        return r.getvalue()
406

  
407
    def email(self):
408
        get_response().breadcrumb.append(('email', _('Email Subscription')))
409
        template.html_top(_('Receiving Announces by email'))
410
        r = TemplateIO(html=True)
411

  
412
        form = Form(enctype='multipart/form-data')
413
        if get_request().user:
414
            if get_request().user.email:
415
                r += htmltext('<p>')
416
                r += _('You are logged in and your email is %s, ok to subscribe ?') % \
417
                        get_request().user.email
418
                r += htmltext('</p>')
419
                form.add_submit('submit', _('Subscribe'))
420
            else:
421
                r += htmltext('<p>')
422
                r += _("You are logged in but there is no email address in your profile.")
423
                r += htmltext('</p>')
424
                form.add(EmailWidget, 'email', title = _('Email'), required = True)
425
                form.add_submit('submit', _('Subscribe'))
426
                form.add_submit('submit-remember', _('Subscribe and add this email to my profile'))
427
        else:
428
            r += htmltext('<p>')
429
            r += _('FIXME will only be used for this purpose etc.')
430
            r += htmltext('</p>')
431
            form.add(EmailWidget, 'email', title = _('Email'), required = True)
432
            form.add_submit('submit', _('Subscribe'))
433

  
434
        form.add_submit('cancel', _('Cancel'))
435

  
436
        if form.get_submit() == 'cancel':
437
            return redirect('subscribe')
438

  
439
        if form.is_submitted() and not form.has_errors():
440
            s = self.email_submit(form)
441
            if s is not False:
442
                return s
443
        else:
444
            r += form.render()
445

  
446
        return r.getvalue()
447

  
448
    def email_submit(self, form):
449
        sub = self._get_announce_subscription()
450
        if not sub:
451
            sub = AnnounceSubscription()
452

  
453
        if get_request().user:
454
            sub.user_id = get_request().user.id
455

  
456
        if form.get_widget('email'):
457
            sub.email = form.get_widget('email').parse()
458
        elif get_request().user.email:
459
            sub.email = get_request().user.email
460

  
461
        if not get_request().user:
462
            sub.enabled = False
463

  
464
        sub.store()
465

  
466
        if get_request().user:
467
            r = TemplateIO(html=True)
468
            root_url = get_publisher().get_root_url()
469
            r += htmltext('<p>')
470
            r += _('You have been subscribed to the announces.')
471
            r += htmltext('</p>')
472
            if not get_response().iframe_mode:
473
                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
474
            return r.getvalue()
475

  
476
        # asking email confirmation before subscribing someone
477
        token = Token(3 * 86400)
478
        token.type = 'announces-subscription-confirmation'
479
        token.subscription_id = sub.id
480
        token.store()
481
        data = {
482
            'confirm_url': get_request().get_url() + 'confirm?t=%s&a=cfm' % token.id,
483
            'cancel_url': get_request().get_url() + 'confirm?t=%s&a=cxl' % token.id,
484
            'time': misc.localstrftime(time.localtime(token.expiration)),
485
        }
486

  
487
        emails.custom_ezt_email('announces-subscription-confirmation',
488
                data, sub.email, exclude_current_user = False)
489

  
490
        r = TemplateIO(html=True)
491
        root_url = get_publisher().get_root_url()
492
        r += htmltext('<p>')
493
        r += _('You have been sent an email for confirmation')
494
        r += htmltext('</p>')
495
        if not get_response().iframe_mode:
496
            r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
497
        return r.getvalue()
498

  
499
    def emailconfirm(self):
500
        tokenv = get_request().form.get('t')
501
        action = get_request().form.get('a')
502

  
503
        root_url = get_publisher().get_root_url()
504

  
505
        try:
506
            token = Token.get(tokenv)
507
        except KeyError:
508
            return template.error_page(
509
                    _('The token you submitted does not exist, has expired, or has been cancelled.'),
510
                    continue_to = (root_url, _('home page')))
511

  
512
        if token.type != 'announces-subscription-confirmation':
513
            return template.error_page(
514
                    _('The token you submitted is not appropriate for the requested task.'),
515
                    continue_to = (root_url, _('home page')))
516

  
517
        sub = AnnounceSubscription.get(token.subscription_id)
518

  
519
        if action == 'cxl':
520
            r = TemplateIO(html=True)
521
            root_url = get_publisher().get_root_url()
522
            template.html_top(_('Email Subscription'))
523
            r += htmltext('<h1>%s</h1>') % _('Request Cancelled')
524
            r += htmltext('<p>%s</p>') % _('The request for subscription has been cancelled.')
525
            r += htmltext('<p>')
526
            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
527
            r += htmltext('</p>')
528
            token.remove_self()
529
            sub.remove_self()
530
            return r.getvalue()
531

  
532
        if action == 'cfm':
533
            token.remove_self()
534
            sub.enabled = True
535
            sub.store()
536
            r = TemplateIO(html=True)
537
            root_url = get_publisher().get_root_url()
538
            template.html_top(_('Email Subscription'))
539
            r += htmltext('<h1>%s</h1>') % _('Subscription Confirmation')
540
            r += htmltext('<p>%s</p>') % _('Your subscription to announces is now effective.')
541
            r += htmltext('<p>')
542
            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
543
            r += htmltext('</p>')
544
            return r.getvalue()
545

  
546
    def atom(self):
547
        response = get_response()
548
        response.set_content_type('application/atom+xml')
549

  
550
        from pyatom import pyatom
551
        xmldoc = pyatom.XMLDoc()
552
        feed = pyatom.Feed()
553
        xmldoc.root_element = feed
554
        feed.title = get_cfg('misc', {}).get('sitename') or 'Publik'
555
        feed.id = get_request().get_url()
556

  
557
        author_email = get_cfg('emails', {}).get('reply_to')
558
        if not author_email:
559
            author_email = get_cfg('emails', {}).get('from')
560
        if author_email:
561
            feed.authors.append(pyatom.Author(author_email))
562

  
563
        announces = Announce.get_published_announces()
564

  
565
        if announces and announces[0].modification_time:
566
            feed.updated = misc.format_time(announces[0].modification_time,
567
                        '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
568
                        gmtime = True)
569
        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
570

  
571
        for item in announces:
572
            entry = item.get_atom_entry()
573
            if entry:
574
                feed.entries.append(entry)
575

  
576
        return str(feed)
577

  
578
    def email_unsubscribe(self):
579
        sub = self._get_announce_subscription()
580

  
581
        form = Form(enctype='multipart/form-data')
582
        if not sub:
583
            form.add(EmailWidget, 'email', title = _('Email'), required = True)
584

  
585
        form.add_submit('submit', _('Unsubscribe'))
586
        form.add_submit('cancel', _('Cancel'))
587

  
588
        if form.get_submit() == 'cancel':
589
            return redirect('..')
590

  
591
        get_response().breadcrumb.append(('email', _('Email Unsubscription')))
592
        template.html_top()
593
        r = TemplateIO(html=True)
594

  
595
        if form.is_submitted() and not form.has_errors():
596
            if sub:
597
                sub.remove("email")
598
            else:
599
                email = form.get_widget('email').parse()
600
                for s in AnnounceSubscription.select():
601
                    if s.email == email:
602
                        s.remove("email")
603

  
604
            root_url = get_publisher().get_root_url()
605
            r += htmltext('<p>')
606
            r += _('You have been unsubscribed from announces')
607
            r += htmltext('</p>')
608
            if not get_response().iframe_mode:
609
                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
610

  
611
        else:
612
            r += htmltext('<p>')
613
            r += _('Do you want to stop receiving announces by email?')
614
            r += htmltext('</p>')
615
            r += form.render()
616

  
617
        return r.getvalue()
618

  
619
    def _q_lookup(self, component):
620
        try:
621
            announce = Announce.get(component)
622
        except KeyError:
623
            raise errors.TraversalError()
624

  
625
        if announce.hidden:
626
            raise errors.TraversalError()
627

  
628
        get_response().breadcrumb.append((str(announce.id), announce.title))
629
        return AnnounceDirectory(announce)
630

  
631
OldRegisterDirectory = wcs.root.RegisterDirectory
632

  
633
class AlternateRegisterDirectory(OldRegisterDirectory):
634
    def _q_traverse(self, path):
635
        get_response().filter['bigdiv'] = 'new_member'
636
        return OldRegisterDirectory._q_traverse(self, path)
637

  
638
    def _q_index(self):
639
        get_logger().info('register')
640
        ident_methods = get_cfg('identification', {}).get('methods', [])
641

  
642
        if len(ident_methods) == 0:
643
            idps = get_cfg('idp', {})
644
            if len(idps) == 0:
645
                return template.error_page(_('Authentication subsystem is not yet configured.'))
646
            ident_methods = ['idp'] # fallback to old behaviour; saml.
647

  
648
        if len(ident_methods) == 1:
649
            method = ident_methods[0]
650
        else:
651
            method = 'password'
652

  
653
        return qommon.ident.register(method)
654

  
655
OldLoginDirectory = wcs.root.LoginDirectory
656

  
657
class AlternateLoginDirectory(OldLoginDirectory):
658
    def _q_traverse(self, path):
659
        get_response().filter['bigdiv'] = 'member'
660
        return OldLoginDirectory._q_traverse(self, path)
661

  
662
    def _q_index(self):
663
        get_logger().info('login')
664
        ident_methods = get_cfg('identification', {}).get('methods', [])
665

  
666
        if get_request().form.get('ReturnUrl'):
667
            get_request().form['next'] = get_request().form.pop('ReturnUrl')
668

  
669
        if 'IsPassive' in get_request().form and 'idp' in ident_methods:
670
            # if isPassive is given in query parameters, we restrict ourselves
671
            # to saml login.
672
            ident_methods = ['idp']
673

  
674
        if len(ident_methods) > 1 and 'idp' in ident_methods:
675
            # if there is more than one identification method, and there is a
676
            # possibility of SSO, if we got there as a consequence of an access
677
            # unauthorized url on admin/ or backoffice/, then idp auth method
678
            # is chosen forcefully.
679
            after_url = get_request().form.get('next')
680
            if after_url:
681
                root_url = get_publisher().get_root_url()
682
                after_path = urlparse.urlparse(after_url)[2]
683
                after_path = after_path[len(root_url):]
684
                if after_path.startswith(str('admin')) or \
685
                        after_path.startswith(str('backoffice')):
686
                    ident_methods = ['idp']
687

  
688
        # don't display authentication system choice
689
        if len(ident_methods) == 1:
690
            method = ident_methods[0]
691
            try:
692
                return qommon.ident.login(method)
693
            except KeyError:
694
                get_logger().error('failed to login with method %s' % method)
695
                return errors.TraversalError()
696

  
697
        if sorted(ident_methods) == ['idp', 'password']:
698
            r = TemplateIO(html=True)
699
            get_response().breadcrumb.append(('login', _('Login')))
700
            identities_cfg = get_cfg('identities', {})
701
            form = Form(enctype = 'multipart/form-data', id = 'login-form', use_tokens = False)
702
            if identities_cfg.get('email-as-username', False):
703
                form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
704
            else:
705
                form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
706
            form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
707
            form.add_submit('submit', _('Connect'))
708
            if form.is_submitted() and not form.has_errors():
709
                tmp = qommon.ident.password.MethodDirectory().login_submit(form)
710
                if not form.has_errors():
711
                    return tmp
712

  
713
            r += htmltext('<div id="login-password">')
714
            r += get_session().display_message()
715
            r += form.render()
716

  
717
            base_url = get_publisher().get_root_url()
718
            r += htmltext('<p><a href="%sident/password/forgotten">%s</a></p>') % (
719
                    base_url, _('Forgotten password ?'))
720

  
721
            r += htmltext('</div>')
722

  
723
            # XXX: this part only supports a single IdP
724
            r += htmltext('<div id="login-sso">')
725
            r += TextsDirectory.get_html_text('aq-sso-text')
726
            form = Form(enctype='multipart/form-data',
727
                    action = '%sident/idp/login' % base_url)
728
            form.add_hidden('method', 'idp')
729
            for kidp, idp in get_cfg('idp', {}).items():
730
                p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
731
                        misc.get_abs_path(idp['metadata']),
732
                        misc.get_abs_path(idp.get('publickey')), None)
733
                form.add_hidden('idp', p.providerId)
734
                break
735
            form.add_submit('submit', _('Connect'))
736

  
737
            r += form.render()
738
            r += htmltext('</div>')
739

  
740
            get_request().environ['REQUEST_METHOD'] = 'GET'
741

  
742
            r += htmltext("""<script type="text/javascript">
743
              document.getElementById('login-form')['username'].focus();
744
            </script>""")
745
            return r.getvalue()
746
        else:
747
            return OldLoginDirectory._q_index(self)
748

  
749

  
750
OldIdentDirectory = wcs.root.IdentDirectory
751
class AlternateIdentDirectory(OldIdentDirectory):
752
    def _q_traverse(self, path):
753
        get_response().filter['bigdiv'] = 'member'
754
        return OldIdentDirectory._q_traverse(self, path)
755

  
756

  
757
class AlternatePreviewDirectory(PreviewDirectory):
758
    def _q_traverse(self, path):
759
        get_response().filter['bigdiv'] = 'rub_service'
760
        return super(AlternatePreviewDirectory, self)._q_traverse(path)
761

  
762

  
763
class AlternateRootDirectory(OldRootDirectory):
764
    _q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout',
765
            'saml', 'register', 'ident', 'afterjobs',
766
            ('informations-editeur', 'informations_editeur'),
767
            ('announces', 'announces_dir'),
768
            'accessibility', 'contact', 'help',
769
            'myspace', 'services', 'agenda', 'categories', 'user',
770
            ('tmp-upload', 'tmp_upload'), 'json', '__version__',
771
            'themes', 'pages', 'payment', 'invoices', 'roles',
772
            'api', 'code', 'fargo', 'tryauth', 'auth', 'preview',
773
            ('reload-top', 'reload_top'), 'static',
774
            ('i18n.js', 'i18n_js')]
775

  
776
    admin = admin.AdminRootDirectory()
777
    announces_dir = AnnouncesDirectory()
778
    register = AlternateRegisterDirectory()
779
    login = AlternateLoginDirectory()
780
    ident = AlternateIdentDirectory()
781
    myspace = MyspaceDirectory()
782
    agenda = AgendaDirectory()
783
    saml = Saml2Directory()
784
    payment = PublicPaymentDirectory()
785
    invoices = InvoicesDirectory()
786
    code = wcs.forms.root.TrackingCodesDirectory()
787
    preview = AlternatePreviewDirectory()
788

  
789
    def get_substitution_variables(self):
790
        d = {}
791
        def print_links(fd):
792
            fd.write(str(self.links()))
793
        d['links'] = print_links
794
        return d
795

  
796
    def _q_traverse(self, path):
797
        self.feed_substitution_parts()
798

  
799
        # set app_label to Publik if none was specified (this is used in
800
        # backoffice header top line)
801
        if not get_publisher().get_site_option('app_label'):
802
            if not get_publisher().site_options.has_section('options'):
803
                get_publisher().site_options.add_section('options')
804
            get_publisher().site_options.set('options', 'app_label', 'Publik')
805

  
806
        response = get_response()
807
        if not hasattr(response, 'filter'):
808
            response.filter = {}
809

  
810
        response.filter['auquotidien'] = True
811
        response.filter['gauche'] = self.box_side(path)
812
        response.filter['keywords'] = template.get_current_theme().get('keywords')
813
        get_publisher().substitutions.feed(self)
814

  
815
        response.breadcrumb = [ ('', _('Home')) ]
816

  
817
        if not self.admin:
818
            self.admin = get_publisher().admin_directory_class()
819

  
820
        if not self.backoffice:
821
            self.backoffice = get_publisher().backoffice_directory_class()
822

  
823
        try:
824
            return Directory._q_traverse(self, path)
825
        except errors.TraversalError, e:
826
            try:
827
                f = FormDef.get_by_urlname(path[0])
828
            except KeyError:
829
                pass
830
            else:
831
                base_url = get_publisher().get_root_url()
832

  
833
                uri_rest = get_request().environ.get('REQUEST_URI')
834
                if not uri_rest:
835
                    # REQUEST_URI doesn't exist when using internal HTTP server
836
                    # (--http)
837
                    uri_rest = get_request().get_path()
838
                    if get_request().get_query():
839
                        uri_rest += '?' + get_request().get_query()
840
                if uri_rest.startswith(base_url):
841
                    uri_rest = uri_rest[len(base_url):]
842
                if f.category:
843
                    return redirect('%s%s/%s' % (base_url, f.category.url_name, uri_rest))
844

  
845
            raise e
846

  
847

  
848
    def _q_lookup(self, component):
849
        # is this a category ?
850
        try:
851
            category = Category.get_by_urlname(component)
852
        except KeyError:
853
            pass
854
        else:
855
            return FormsRootDirectory(category)
856

  
857
        # is this a formdef ?
858
        try:
859
            formdef = FormDef.get_by_urlname(component)
860
        except KeyError:
861
            pass
862
        else:
863
            # if there's no category, or the request is a POST, directly call
864
            # into FormsRootDirectory.
865
            if formdef.category_id is None or get_request().get_method() == 'POST':
866
                get_response().filter['bigdiv'] = 'rub_service'
867
                return FormsRootDirectory()._q_lookup(component)
868

  
869
            # if there is category, let it fall back to raise TraversalError,
870
            # it will get caught in _q_traverse that will redirect it to an
871
            # URL embedding the category
872

  
873
        return None
874

  
875
    def json(self):
876
        return FormsRootDirectory().json()
877

  
878
    def categories(self):
879
        return FormsRootDirectory().categories()
880

  
881
    def _q_index(self):
882
        if get_request().is_json():
883
            return FormsRootDirectory().json()
884

  
885
        root_url = get_publisher().get_root_url()
886
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
887
            return redirect('%smyspace/new' % root_url)
888

  
889
        redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
890
        if redirect_url:
891
            return redirect(misc.get_variadic_url(redirect_url,
892
                get_publisher().substitutions.get_context_variables()))
893

  
894
        if get_response().iframe_mode:
895
            # never display home page in an iframe
896
            return redirect('%sservices' % root_url)
897

  
898
        template.html_top()
899
        r = TemplateIO(html=True)
900
        get_response().filter['is_index'] = True
901

  
902
        if not 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
903
            t = TextsDirectory.get_html_text('aq-home-page')
904
            if not t:
905
                if get_request().user:
906
                    t = TextsDirectory.get_html_text('welcome-logged')
907
                else:
908
                    t = TextsDirectory.get_html_text('welcome-unlogged')
909
            if t:
910
                r += htmltext('<div id="home-page-intro">')
911
                r += t
912
                r += htmltext('</div>')
913

  
914
        r += htmltext('<div id="centre">')
915
        r += self.box_services(position='1st')
916
        r += htmltext('</div>')
917
        r += htmltext('<div id="droite">')
918
        r += self.myspace_snippet()
919
        r += self.box_services(position='2nd')
920
        r += self.consultations()
921
        r += self.announces()
922
        r += htmltext('</div>')
923

  
924
        user = get_request().user
925
        if user and user.can_go_in_backoffice():
926
            get_response().filter['backoffice'] = True
927

  
928
        return r.getvalue()
929

  
930
    def services(self):
931
        template.html_top()
932
        get_response().filter['bigdiv'] = 'rub_service'
933
        return self.box_services(level = 2)
934

  
935
    def box_services(self, level=3, position=None):
936
        ## Services
937
        if get_request().user and get_request().user.roles:
938
            accepted_roles = get_request().user.roles
939
        else:
940
            accepted_roles = []
941

  
942
        cats = Category.select(order_by = 'name')
943
        cats = [x for x in cats if x.url_name != 'consultations']
944
        Category.sort_by_position(cats)
945

  
946
        all_formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
947
                order_by = 'name')
948
        if get_response().page_template_key == 'mobile':
949
            # if we are in 'mobile' mode, and some formdefs have a 'mobile'
950
            # keyword, we limit the display to those
951
            if any((x for x in all_formdefs if x.keywords and 'mobile' in x.keywords)):
952
                all_formdefs = [x for x in all_formdefs if x.keywords and 'mobile' in x.keywords]
953

  
954
        if position:
955
            t = self.display_list_of_formdefs(
956
                            [x for x in cats if x.get_homepage_position() == position],
957
                            all_formdefs, accepted_roles)
958
        else:
959
            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
960

  
961
        if not t:
962
            return
963

  
964
        r = TemplateIO(html=True)
965

  
966
        if position == '2nd':
967
            r += htmltext('<div id="services-2nd">')
968
        else:
969
            r += htmltext('<div id="services">')
970
        if level == 2:
971
            r += htmltext('<h2>%s</h2>') % _('Services')
972
        else:
973
            r += htmltext('<h3>%s</h3>') % _('Services')
974

  
975
        if get_response().iframe_mode:
976
            if get_request().user:
977
                message = TextsDirectory.get_html_text('welcome-logged')
978
            else:
979
                message = TextsDirectory.get_html_text('welcome-unlogged')
980

  
981
            if message:
982
                r += htmltext('<div id="welcome-message">')
983
                r += message
984
                r += htmltext('</div>')
985
        elif 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
986
            homepage_text = TextsDirectory.get_html_text('aq-home-page')
987
            if homepage_text:
988
                r += htmltext('<div id="home-page-intro">')
989
                r += homepage_text
990
                r += htmltext('</div>')
991

  
992
        r += htmltext('<ul>')
993
        r += t
994
        r += htmltext('</ul>')
995

  
996
        r += htmltext('</div>')
997
        return r.getvalue()
998

  
999
    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
1000
        r = TemplateIO(html=True)
1001
        for category in cats:
1002
            if category.url_name == 'consultations':
1003
                self.consultations_category = category
1004
                continue
1005
            formdefs = [x for x in all_formdefs if str(x.category_id) == str(category.id)]
1006
            formdefs_advertise = []
1007

  
1008
            for formdef in formdefs[:]:
1009
                if formdef.is_disabled(): # is a redirection
1010
                    continue
1011
                if not formdef.roles:
1012
                    continue
1013
                if not get_request().user:
1014
                    if formdef.always_advertise:
1015
                        formdefs_advertise.append(formdef)
1016
                    formdefs.remove(formdef)
1017
                    continue
1018
                if logged_users_role().id in formdef.roles:
1019
                    continue
1020
                for q in accepted_roles:
1021
                    if q in formdef.roles:
1022
                        break
1023
                else:
1024
                    if formdef.always_advertise:
1025
                        formdefs_advertise.append(formdef)
1026
                    formdefs.remove(formdef)
1027

  
1028
            if not formdefs and not formdefs_advertise:
1029
                continue
1030

  
1031
            keywords = {}
1032
            for formdef in formdefs:
1033
                for keyword in formdef.keywords_list:
1034
                    keywords[keyword] = True
1035

  
1036
            r += htmltext('<li id="category-%s" data-keywords="%s">') % (
1037
                    category.url_name, ' '.join(keywords))
1038
            r += htmltext('<strong>')
1039
            r += htmltext('<a href="%s/">') % category.url_name
1040
            r += category.name
1041
            r += htmltext('</a></strong>\n')
1042
            r += category.get_description_html_text()
1043
            r += htmltext('<ul>')
1044
            limit = category.get_limit()
1045
            for formdef in formdefs[:limit]:
1046
                r += htmltext('<li data-keywords="%s">') % ' '.join(formdef.keywords_list)
1047
                classes = []
1048
                if formdef.is_disabled() and formdef.disabled_redirection:
1049
                    classes.append('redirection')
1050
                r += htmltext('<a class="%s" href="%s/%s/">%s</a>') % (
1051
                        ' '.join(classes), category.url_name, formdef.url_name, formdef.name)
1052
                r += htmltext('</li>\n')
1053
            if len(formdefs) < limit:
1054
                for formdef in formdefs_advertise[:limit-len(formdefs)]:
1055
                    r += htmltext('<li class="required-authentication">')
1056
                    r += htmltext('<a href="%s/%s/">%s</a>') % (category.url_name, formdef.url_name, formdef.name)
1057
                    r += htmltext('<span> (%s)</span>') % _('authentication required')
1058
                    r += htmltext('</li>\n')
1059
            if (len(formdefs)+len(formdefs_advertise)) > limit:
1060
                r += htmltext('<li class="all-forms"><a href="%s/" title="%s">%s</a></li>') % (category.url_name,
1061
                        _('Access to all forms of the "%s" category') % category.name,
1062
                        _('Access to all forms in this category'))
1063
            r += htmltext('</ul>')
1064
            r += htmltext('</li>\n')
1065

  
1066
        return r.getvalue()
1067

  
1068
    def consultations(self):
1069
        cats = [x for x in Category.select() if x.url_name == 'consultations']
1070
        if not cats:
1071
            return
1072
        consultations_category = cats[0]
1073
        formdefs = FormDef.select(lambda x: (
1074
                    str(x.category_id) == str(consultations_category.id) and
1075
                        (not x.is_disabled() or x.disabled_redirection)),
1076
                    order_by = 'name')
1077
        if not formdefs:
1078
            return
1079
        ## Consultations
1080
        r = TemplateIO(html=True)
1081
        r += htmltext('<div id="consultations">')
1082
        r += htmltext('<h3>%s</h3>') % _('Consultations')
1083
        r += consultations_category.get_description_html_text()
1084
        r += htmltext('<ul>')
1085
        for formdef in formdefs:
1086
            r += htmltext('<li>')
1087
            r += htmltext('<a href="%s/%s/">%s</a>') % (consultations_category.url_name,
1088
                formdef.url_name, formdef.name)
1089
            r += htmltext('</li>')
1090
        r += htmltext('</ul>')
1091
        r += htmltext('</div>')
1092
        return r.getvalue()
1093

  
1094
    def box_side(self, path):
1095
        r = TemplateIO(html=True)
1096
        root_url = get_publisher().get_root_url()
1097

  
1098
        if self.has_anonymous_access_codes() and path == [''] and (
1099
                'include-tracking-code-form' in get_response().filter.get('keywords', [])):
1100
            r += htmltext('<form id="follow-form" action="%scode/load">') % root_url
1101
            r += htmltext('<h3>%s</h3>') % _('Tracking code')
1102
            r += htmltext('<input size="12" name="code" placeholder="%s"/>') % _('ex: RPQDFVCD')
1103
            r += htmltext('<input type="submit" value="%s"/>') % _('Load')
1104
            r += htmltext('</form>')
1105

  
1106
        r += self.links()
1107

  
1108
        cats = Category.select(order_by = 'name')
1109
        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
1110
        Category.sort_by_position(cats)
1111
        if cats:
1112
            r += htmltext('<div id="side-services">')
1113
            r += htmltext('<h3>%s</h3>') % _('Services')
1114
            r += htmltext('<ul>')
1115
            for cat in cats:
1116
                r += htmltext('<li><a href="%s/">%s</a></li>') % (cat.url_name, cat.name)
1117
            r += htmltext('</ul>')
1118
            r += htmltext('</div>')
1119

  
1120
        if Event.keys(): # if there are events, add a link to the agenda
1121
            tags = get_cfg('misc', {}).get('event_tags')
1122
            if not tags:
1123
                tags = get_default_event_tags()
1124
            r += htmltext('<h3 id="agenda-link"><a href="%sagenda/">%s</a></h3>') % (root_url, _('Agenda'))
1125

  
1126
            if path and path[0] == 'agenda':
1127
                r += htmltext('<p class="tags">')
1128
                for tag in tags:
1129
                    r += htmltext('<a href="%sagenda/tag/%s">%s</a> ') % (root_url, tag, tag)
1130
                r += htmltext('</p>')
1131
                r += self.agenda.display_remote_calendars()
1132

  
1133
                r += htmltext('<p>')
1134
                r += htmltext('  <a href="%sagenda/filter">%s</a>') % (root_url, _('Advanced Filter'))
1135
                r += htmltext('</p>')
1136

  
1137
        v = r.getvalue()
1138
        if v:
1139
            r = TemplateIO(html=True)
1140
            r += htmltext('<div id="sidebox">')
1141
            r += v
1142
            r += htmltext('</div>')
1143
            return r.getvalue()
1144

  
1145
        return None
1146

  
1147
    def has_anonymous_access_codes(self):
1148
        return any((x for x in FormDef.select() if x.enable_tracking_codes))
1149

  
1150
    def links(self):
1151
        links = Link.select()
1152
        if not links:
1153
            return ''
1154

  
1155
        Link.sort_by_position(links)
1156

  
1157
        r = TemplateIO(html=True)
1158

  
1159
        r += htmltext('<div id="links">')
1160
        if links[0].url:
1161
            # first link has an URL, so it's not a title, so we display a
1162
            # generic title
1163
            r += htmltext('<h3>%s</h3>') % _('Useful links')
1164
        has_ul = False
1165
        vars = get_publisher().substitutions.get_context_variables()
1166
        for link in links:
1167
            if not link.url:
1168
                # acting title
1169
                if has_ul:
1170
                    r += htmltext('</ul>')
1171
                r += htmltext('<h3>%s</h3>') % link.title
1172
                r += htmltext('<ul>')
1173
                has_ul = True
1174
            else:
1175
                if not has_ul:
1176
                    r += htmltext('<ul>')
1177
                    has_ul = True
1178
                r += htmltext('<li class="link-%s"><a href="%s">%s</a></li>') % (
1179
                        simplify(link.title), get_variadic_url(link.url, vars), link.title)
1180
        if has_ul:
1181
            r += htmltext('</ul>')
1182
        r += htmltext('</div>')
1183
        return r.getvalue()
1184

  
1185
    def announces(self):
1186
        announces = Announce.get_published_announces()
1187
        if not announces:
1188
            return
1189

  
1190
        r = TemplateIO(html=True)
1191
        r += htmltext('<div id="announces">')
1192
        r += htmltext('<h3>%s</h3>') % _('Announces to citizens')
1193
        for item in announces[:3]:
1194
            r += htmltext('<div class="announce-item">')
1195
            r += htmltext('<h4>')
1196
            if item.publication_time:
1197
                r += time.strftime(misc.date_format(), item.publication_time)
1198
                r += ' - '
1199
            r += item.title
1200
            r += htmltext('</h4>')
1201
            r += htmltext('<p>')
1202
            r += item.text
1203
            r += htmltext('</p>')
1204
            r += htmltext('</div>')
1205

  
1206
        r += htmltext('<ul id="announces-links">')
1207
        r += htmltext('<li><a href="announces/subscribe">%s</a></li>') % _('Receiving those Announces')
1208
        r += htmltext('<li><a href="announces/">%s</a></li>') % _('Previous Announces')
1209
        r += htmltext('</ul>')
1210
        r += htmltext('</div>')
1211
        return r.getvalue()
1212

  
1213
    def myspace_snippet(self):
1214
        r = TemplateIO(html=True)
1215
        r += htmltext('<div id="myspace">')
1216
        r += htmltext('<h3>%s</h3>') % _('My Space')
1217
        r += htmltext('<ul>')
1218
        if get_request().user and not get_request().user.anonymous:
1219
            r += htmltext('  <li><a href="myspace/" id="member">%s</a></li>') % _('Access to your personal space')
1220
            r += htmltext('  <li><a href="logout" id="logout">%s</a></li>') % _('Logout')
1221
        else:
1222
            r += htmltext('  <li><a href="register/" id="inscr">%s</a></li>') % _('Registration')
1223
            r += htmltext('  <li><a href="login/" id="login">%s</a></li>') % _('Login')
1224
        r += htmltext('</ul>')
1225
        r += htmltext('</div>')
1226
        return r.getvalue()
1227

  
1228
    def page_view(self, key, title, urlname = None):
1229
        if not urlname:
1230
            urlname = key[3:].replace(str('_'), str('-'))
1231
        get_response().breadcrumb.append((urlname, title))
1232
        template.html_top(title)
1233
        r = TemplateIO(html=True)
1234
        r += htmltext('<div class="article">')
1235
        r += htmltext(TextsDirectory.get_html_text(key))
1236
        r += htmltext('</div>')
1237
        return r.getvalue()
1238

  
1239
    def informations_editeur(self):
1240
        get_response().filter['bigdiv'] = 'info'
1241
        return self.page_view('aq-editor-info', _('Editor Informations'),
1242
                urlname = 'informations_editeur')
1243

  
1244
    def accessibility(self):
1245
        get_response().filter['bigdiv'] = 'accessibility'
1246
        return self.page_view('aq-accessibility', _('Accessibility Statement'))
1247

  
1248
    def contact(self):
1249
        get_response().filter['bigdiv'] = 'contact'
1250
        return self.page_view('aq-contact', _('Contact'))
1251

  
1252
    def help(self):
1253
        get_response().filter['bigdiv'] = 'help'
1254
        return self.page_view('aq-help', _('Help'))
1255

  
1256

  
1257
from qommon.publisher import get_publisher_class
1258
get_publisher_class().root_directory_class = AlternateRootDirectory
1259
get_publisher_class().after_login_url = 'myspace/'
1260
get_publisher_class().use_sms_feature = True
1261

  
1262
# help links
1263
get_publisher_class().backoffice_help_url = {
1264
    'fr': 'https://doc-publik.entrouvert.com/',
1265
}
1266

  
1267

  
1268
EmailsDirectory.register('announces-subscription-confirmation',
1269
        N_('Confirmation of Announces Subscription'),
1270
        N_('Available variables: change_url, cancel_url, time, sitename'),
1271
        default_subject = N_('Announce Subscription Request'),
1272
        default_body = N_("""\
1273
You have (or someone impersonating you has) requested to subscribe to
1274
announces from [sitename].  To confirm this request, visit the
1275
following link:
1276

  
1277
[confirm_url]
1278

  
1279
If you are not the person who made this request, or you wish to cancel
1280
this request, visit the following link:
1281

  
1282
[cancel_url]
1283

  
1284
If you do nothing, the request will lapse after 3 days (precisely on
1285
[time]).
1286
"""))
1287

  
1288

  
1289
TextsDirectory.register('aq-announces-subscription',
1290
        N_('Text on announces subscription page'),
1291
        default = N_('''\
1292
<p>
1293
FIXME
1294
'</p>'''))
1295

  
1296
TextsDirectory.register('aq-sms-demo',
1297
        N_('Text when subscribing to announces SMS and configured as demo'),
1298
        default = N_('''
1299
<p>
1300
Receiving announces by SMS is not possible in this demo
1301
</p>'''))
1302

  
1303
TextsDirectory.register('aq-editor-info', N_('Editor Informations'))
1304
TextsDirectory.register('aq-accessibility', N_('Accessibility Statement'))
1305
TextsDirectory.register('aq-contact', N_('Contact Information'))
1306
TextsDirectory.register('aq-help', N_('Help'))
1307
TextsDirectory.register('aq-sso-text',  N_('Connecting with Identity Provider'),
1308
        default = N_('''<h3>Connecting with Identity Provider</h3>
1309
<p>You can also use your identity provider to connect.
1310
</p>'''))
1311

  
1312
TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg = True)
auquotidien/modules/saml2.py
1
try:
2
    import lasso
3
except ImportError:
4
    pass
5

  
6
from qommon import get_cfg, get_logger
7
import qommon.saml2
8

  
9

  
10
class Saml2Directory(qommon.saml2.Saml2Directory):
11
    def extract_attributes(self, session, login):
12
        '''Separate attributes as two dictionaries: one for last value, one for
13
           the list of values.'''
14
        d = {}
15
        m = {}
16

  
17
        lasso_session = lasso.Session.newFromDump(session.lasso_session_dump)
18
        try:
19
            assertion = lasso_session.getAssertions(None)[0]
20
        except:
21
            get_logger().warn('failed to lookup assertion')
22
            return d, m
23

  
24
        try:
25
            for attribute in assertion.attributeStatement[0].attribute:
26
                try:
27
                    d[attribute.name] = attribute.attributeValue[0].any[0].content
28
                    for attribute_value in attribute.attributeValue:
29
                        l = m.setdefault(attribute.name, [])
30
                        l.append(attribute_value.any[0].content)
31
                except IndexError:
32
                    pass
33
        except IndexError:
34
            pass
35
        return d, m
36

  
37
    def fill_user_attributes(self, session, login, user):
38
        qommon.saml2.Saml2Directory.fill_user_attributes(self, session, login, user)
39

  
40
        idp = qommon.saml2.get_remote_provider_cfg(login)
41
        if not idp.get('attribute-mapping'):
42
            self.legacy_fill_user_attributes(session, login, user)
43

  
44
    def legacy_fill_user_attributes(self, session, login, user):
45
        '''Fill fields using a legacy attribute to field varname mapping'''
46
        d, m = self.extract_attributes(session, login)
47
        users_cfg = get_cfg('users', {}) or {}
48
        get_logger().debug('using legacy attribute filling')
49

  
50
        # standard attributes
51
        user.name = d.get('cn')
52
        user.email = d.get('mail')
53

  
54
        # email field
55
        field_email = users_cfg.get('field_email')
56
        if field_email:
57
            user.form_data[field_email] = d.get('mail') or d.get('email')
58

  
59
        # name field, this only works if there's a single field for the name
60
        field_name_values = users_cfg.get('field_name')
61
        if field_name_values:
62
            if type(field_name_values) is str: # it was a string in previous versions
63
                field_name_values = [field_name_values]
64
            if len(field_name_values) == 1:
65
                user.form_data[field_name_values[0]] = d.get('cn')
66

  
67
        # other fields, matching is done on known LDAP attribute names and
68
        # common variable names
69
        extra_field_mappings = [
70
                ('gn', ('firstname', 'prenom')),
71
                ('givenName', ('firstname', 'prenom')),
72
                ('surname', ('surname', 'name', 'nom',)),
73
                ('sn', ('surname', 'name', 'nom',)),
74
                ('personalTitle', ('personalTitle', 'civilite',)),
75
                ('l', ('location', 'commune', 'ville',)),
76
                ('streetAddress', ('streetAddress', 'address', 'adresse', 'street',)),
77
                ('street', ('streetAddress', 'address', 'adresse', 'street',)),
78
                ('postalCode', ('postalCode', 'codepostal', 'cp',)),
79
                ('telephoneNumber', ('telephoneNumber', 'telephonefixe', 'telephone',)),
80
                ('mobile', ('mobile', 'telephonemobile',)),
81
                ('faxNumber', ('faxNumber', 'fax')),
82
        ]
83

  
84
        for attribute_key, field_varnames in extra_field_mappings:
85
            if not attribute_key in d:
86
                continue
87
            for field in user.get_formdef().fields:
88
                if field.varname in field_varnames:
89
                    user.form_data[field.id] = d.get(attribute_key)
auquotidien/modules/strongbox.py
1
import os
2
import time
3

  
4
from quixote import get_publisher
5
from qommon.storage import StorableObject
6
from qommon import misc
7

  
8
class StrongboxType(StorableObject):
9
    _names = 'strongbox-types'
10

  
11
    id = None
12
    label = None
13
    validation_months = 0
14

  
15

  
16
class StrongboxFile():
17
    id = None
18
    orig_filename = None
19
    base_filename = None
20
    content_type = None
21
    charset = None
22

  
23
    def __init__(self, id, f):
24
        self.id = id
25
        self.orig_filename = f.orig_filename
26
        self.base_filename = f.base_filename
27
        self.content_type = f.content_type
28
        self.charset = f.charset
29
        self.fp = f.fp
30

  
31
    def __getstate__(self):
32
        odict = self.__dict__.copy()
33
        if not odict.has_key('fp'):
34
            return odict
35

  
36
        # XXX: add some locking
37
        del odict['fp']
38
        dirname = os.path.join(get_publisher().app_dir, 'strongbox-files')
39
        if not os.path.exists(dirname):
40
            os.mkdir(dirname)
41
        odict['filename'] = os.path.join(dirname, str(self.id))
42
        self.fp.seek(0)
43
        fd = file(odict['filename'], 'w')
44
        fd.write(self.fp.read())
45
        fd.close()
46
        return odict
47

  
48

  
49
class StrongboxItem(StorableObject):
50
    _names = 'strongbox-items'
51
    _hashed_indexes = ['user_id']
52

  
53
    user_id = None
54
    description = None
55
    file = None
56
    type_id = None
57
    expiration_time = None
58
    proposed_by = None
59
    proposed_time = None
60
    validated_time = None
61

  
62
    def get_display_name(self):
63
        if self.description:
64
            return self.description
65
        return self.file.base_filename
66

  
67
    def set_file(self, file):
68
        self.file = StrongboxFile(self.id, file)
69

  
70
    def remove_file(self):
71
        os.unlink(self.file.filename)
72

  
73
    def set_expiration_time_from_date(self, value):
74
        if not value:
75
            self.expiration_time = None
76
            return
77
        if not StrongboxType.has_key(self.type_id):
78
            return
79

  
80
        if not StrongboxType.get(self.type_id).validation_months:
81
            self.expiration_time = None
82
            return
83

  
84
        date = time.strptime(value, misc.date_format())
85
        year, month, day = date[:3]
86
        month += StrongboxType.get(self.type_id).validation_months
87
        while month > 12:
88
           year += 1
89
           month -= 12
90
        while True:
91
            try:
92
                self.expiration_time = time.strptime(
93
                       '%04d-%02d-%02d' % (year, month, day), '%Y-%m-%d')
94
            except ValueError:
95
                day -= 1
96
                continue
97
            break
98

  
auquotidien/modules/strongbox_ui.py
1
import time
2

  
3
from quixote import get_request, get_response, get_session, redirect
4
from quixote.directory import Directory, AccessControlled
5
from quixote.html import TemplateIO, htmltext
6

  
7
import wcs
8
import wcs.admin.root
9

  
10
from qommon import _
11
from qommon import errors, misc
12
from qommon.form import *
13
from qommon.strftime import strftime
14
from qommon.backoffice.menu import html_top
15
from qommon import get_cfg
16

  
17
from strongbox import StrongboxType, StrongboxItem
18

  
19

  
20

  
21
class StrongboxTypeDirectory(Directory):
22
    _q_exports = ['', 'edit', 'delete']
23

  
24
    def __init__(self, strongboxtype):
25
        self.strongboxtype = strongboxtype
26

  
27
    def _q_index(self):
28
        html_top('strongbox', title = _('Item Type: %s') % self.strongboxtype.label)
29
        r = TemplateIO(html=True)
30
        r += htmltext('<h2>%s</h2>') % _('Item Type: %s') % self.strongboxtype.label
31
        get_response().filter['sidebar'] = self.get_sidebar()
32
        r += get_session().display_message()
33

  
34
        if self.strongboxtype.validation_months:
35
            r += htmltext('<div class="bo-block">')
36
            r += htmltext('<ul>')
37
            r += htmltext('<li>')
38
            r += _('Number of months of validity:')
39
            r += ' '
40
            r += self.strongboxtype.validation_months
41
            r += htmltext('</li>')
42
            r += htmltext('</ul>')
43
            r += htmltext('</div>')
44

  
45
        return r.getvalue()
46

  
47
    def get_sidebar(self):
48
        r = TemplateIO(html=True)
49
        r += htmltext('<ul>')
50
        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
51
        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
52
        r += htmltext('</ul>')
53
        return r.getvalue()
54

  
55
    def edit(self):
56
        form = self.form()
57
        if form.get_submit() == 'cancel':
58
            return redirect('.')
59

  
60
        if form.is_submitted() and not form.has_errors():
61
            self.submit(form)
62
            return redirect('..')
63

  
64
        html_top('strongbox', title = _('Edit Item Type: %s') % self.strongboxtype.label)
65
        r = TemplateIO(html=True)
66
        r += htmltext('<h2>%s</h2>') % _('Edit Item Type: %s') % self.strongboxtype.label
67
        r += form.render()
68
        return r.getvalue()
69

  
70
    def form(self):
71
        form = Form(enctype='multipart/form-data')
72
        form.add(StringWidget, 'label', title = _('Label'), required = True,
73
                value = self.strongboxtype.label)
74
        form.add(IntWidget, 'validation_months', title=_('Number of months of validity'),
75
                value=self.strongboxtype.validation_months,
76
                hint=_('Use 0 if there is no expiration'))
77
        form.add_submit('submit', _('Submit'))
78
        form.add_submit('cancel', _('Cancel'))
79
        return form
80

  
81
    def submit(self, form):
82
        for k in ('label', 'validation_months'):
83
            widget = form.get_widget(k)
84
            if widget:
85
                setattr(self.strongboxtype, k, widget.parse())
86
        self.strongboxtype.store()
87

  
88
    def delete(self):
89
        form = Form(enctype='multipart/form-data')
90
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
91
                        'You are about to irrevocably delete this item type.')))
92
        form.add_submit('submit', _('Submit'))
93
        form.add_submit('cancel', _('Cancel'))
94
        if form.get_submit() == 'cancel':
95
            return redirect('..')
96
        if not form.is_submitted() or form.has_errors():
97
            get_response().breadcrumb.append(('delete', _('Delete')))
98
            html_top('strongbox', title = _('Delete Item Type'))
99
            r = TemplateIO(html=True)
100
            r += htmltext('<h2>%s</h2>') % _('Deleting Item Type: %s') % self.strongboxtype.label
101
            r += form.render()
102
            return r.getvalue()
103
        else:
104
            self.strongboxtype.remove_self()
105
            return redirect('..')
106

  
107

  
108
class StrongboxTypesDirectory(Directory):
109
    _q_exports = ['', 'new']
110

  
111
    def _q_traverse(self, path):
112
        get_response().breadcrumb.append(('types/', _('Item Types')))
113
        return Directory._q_traverse(self, path)
114

  
115
    def _q_index(self):
116
        return redirect('..')
117

  
118
    def new(self):
119
        type_ui = StrongboxTypeDirectory(StrongboxType())
120

  
121
        form = type_ui.form()
122
        if form.get_submit() == 'cancel':
123
            return redirect('.')
124

  
125
        if form.is_submitted() and not form.has_errors():
126
            type_ui.submit(form)
127
            return redirect('%s/' % type_ui.strongboxtype.id)
128

  
129
        get_response().breadcrumb.append(('new', _('New Item Type')))
130
        html_top('strongbox', title = _('New Item Type'))
131
        r = TemplateIO(html=True)
132
        r += htmltext('<h2>%s</h2>') % _('New Item Type')
133
        r += form.render()
134
        return r.getvalue()
135

  
136
    def _q_lookup(self, component):
137
        try:
138
            strongboxtype = StrongboxType.get(component)
139
        except KeyError:
140
            raise errors.TraversalError()
141
        get_response().breadcrumb.append((str(strongboxtype.id), strongboxtype.label))
142
        return StrongboxTypeDirectory(strongboxtype)
143

  
144

  
145
class StrongboxDirectory(AccessControlled, Directory):
146
    _q_exports = ['', 'types', 'add', 'add_to']
147
    label = N_('Strongbox')
148

  
149
    types = StrongboxTypesDirectory()
150

  
151
    def is_accessible(self, user):
152
        from .backoffice import check_visibility
153
        return check_visibility('strongbox', user)
154

  
155
    def _q_access(self):
156
        user = get_request().user
157
        if not user:
158
            raise errors.AccessUnauthorizedError()
159

  
160
        if not self.is_accessible(user):
161
            raise errors.AccessForbiddenError(
162
                    public_msg = _('You are not allowed to access Strongbox Management'),
163
                    location_hint = 'backoffice')
164

  
165
        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
166

  
167

  
168
    def _q_index(self):
169
        html_top('strongbox', _('Strongbox'))
170
        r = TemplateIO(html=True)
171
        get_response().filter['sidebar'] = self.get_sidebar()
172

  
173
        r += get_session().display_message()
174

  
175
        r += htmltext('<div class="splitcontent-left">')
176
        r += htmltext('<div class="bo-block">')
177
        r += htmltext('<h2>%s</h2>') % _('Propose a file to a user')
178
        form = Form(enctype='multipart/form-data')
179
        form.add(StringWidget, 'q', title = _('User'), required=True)
180
        form.add_submit('search', _('Search'))
181
        r += form.render()
182
        if form.is_submitted() and not form.has_errors():
183
            q = form.get_widget('q').parse()
184
            users = self.search_for_users(q)
185
            if users:
186
                if len(users) == 1:
187
                    return redirect('add_to?user_id=%s' % users[0].id)
188
                if len(users) < 50:
189
                    r += _('(first 50 users only)')
190
                r += htmltext('<ul>')
191
                for u in users:
192
                    r += htmltext('<li><a href="add_to?user_id=%s">%s</a></li>') % (u.id, u.display_name)
193
                r += htmltext('</ul>')
194
            else:
195
                r += _('No user found.')
196
        r += htmltext('</div>')
197
        r += htmltext('</div>')
198

  
199
        r += htmltext('<div class="splitcontent-right">')
200
        r += htmltext('<div class="bo-block">')
201
        types = StrongboxType.select()
202
        r += htmltext('<h2>%s</h2>') % _('Item Types')
203
        if not types:
204
            r += htmltext('<p>')
205
            r += _('There is no item types defined at the moment.')
206
            r += htmltext('</p>')
207

  
208
        r += htmltext('<ul class="biglist" id="strongbox-list">')
209
        for l in types:
210
            type_id = l.id
211
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % type_id
212
            r += htmltext('<strong class="label"><a href="types/%s/">%s</a></strong>') % (type_id, l.label)
213
            r += htmltext('</li>')
214
        r += htmltext('</ul>')
215
        r += htmltext('</div>')
216
        r += htmltext('</div>')
217
        return r.getvalue()
218

  
219
    def get_sidebar(self):
220
        r = TemplateIO(html=True)
221
        r += htmltext('<ul id="sidebar-actions">')
222
        r += htmltext('  <li><a class="new-item" href="types/new">%s</a></li>') % _('New Item Type')
223
        r += htmltext('</ul>')
224
        return r.getvalue()
225

  
226
    def search_for_users(self, q):
227
        if hasattr(get_publisher().user_class, 'search'):
228
            return get_publisher().user_class.search(q)
229
        if q:
230
            users = [x for x in get_publisher().user_class.select()
231
                     if q in (x.name or '') or q in (x.email or '')]
232
            return users
233
        else:
234
            return []
235

  
236
    def get_form(self):
237
        types = [(x.id, x.label) for x in StrongboxType.select()]
238
        form = Form(action='add', enctype='multipart/form-data')
239
        form.add(StringWidget, 'description', title=_('Description'), size=60)
240
        form.add(FileWidget, 'file', title=_('File'), required=True)
241
        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
242
                 options = [(None, _('Not specified'))] + types)
243
        form.add(DateWidget, 'date_time', title = _('Document Date'))
244
        form.add_submit('submit', _('Upload'))
245
        return form
246

  
247
    def add(self):
248
        form = self.get_form()
249
        form.add(StringWidget, 'user_id', title=_('User'))
250
        if not form.is_submitted():
251
           return redirect('.')
252

  
253
        sffile = StrongboxItem()
254
        sffile.user_id = form.get_widget('user_id').parse()
255
        sffile.description = form.get_widget('description').parse()
256
        sffile.proposed_time = time.localtime()
257
        sffile.proposed_id = get_request().user.id
258
        sffile.type_id = form.get_widget('type_id').parse()
259
        v = form.get_widget('date_time').parse()
260
        sffile.set_expiration_time_from_date(v)
261
        sffile.store()
262
        sffile.set_file(form.get_widget('file').parse())
263
        sffile.store()
264
        return redirect('.')
265

  
266
    def add_to(self):
267
        form = Form(enctype='multipart/form-data', action='add_to')
268
        form.add(StringWidget, 'user_id', title = _('User'), required=True)
269
        try:
270
            user_id = form.get_widget('user_id').parse()
271
            user = get_publisher().user_class.get(user_id)
272
        except:
273
            return redirect('.')
274
        if not user:
275
            return redirect('.')
276
        get_request().form = {}
277
        get_request().environ['REQUEST_METHOD'] = 'GET'
278

  
279
        html_top('strongbox', _('Strongbox'))
280
        r = TemplateIO(html=True)
281
        r += htmltext('<h2>%s %s</h2>') % (_('Propose a file to:'), user.display_name)
282
        form = self.get_form()
283
        form.add(HiddenWidget, 'user_id', title=_('User'), value=user.id)
284
        r += form.render()
285
        return r.getvalue()
auquotidien/modules/template.py
1
from quixote import get_request, get_publisher, get_response
2
from qommon.publisher import get_publisher_class
3
from quixote.html import htmltext
4

  
5
from qommon import _
6
from qommon import template
7
from qommon.admin.texts import TextsDirectory
8
from wcs.categories import Category
9

  
10
wcs_error_page = template.error_page
11

  
12
def render_response(publisher, body):
13
    response = publisher.get_request().response
14
    body = str(body)
15

  
16
    for key in ('bigdiv', 'gauche'):
17
        if not response.filter.has_key(key):
18
            response.filter[key] = None
19

  
20
    root_url = publisher.get_root_url()
21
    wcs_path = publisher.get_request().get_path()[len(root_url):]
22
    section = wcs_path.split('/')[0]
23

  
24
    if section in ('backoffice', 'admin'):
25
        return template.decorate(body, response)
26

  
27
    section_title = ''
28
    page_title = response.filter.get('title')
29
    if 'error-page' in response.filter:
30
        pass
31
    elif section == 'consultations':
32
        section_title = '<h2 id="consultations">%s</h2>\n' % _('Consultations')
33
        response.filter['bigdiv'] = 'rub_consultation'
34
    elif section == 'announces':
35
        response.filter['bigdiv'] = 'rub_annonce'
36
        section_title = '<h2 id="announces">%s</h2>\n' % _('Announces to citizens')
37
        if page_title == _('Announces to citizens'):
38
            page_title = ''
39
    elif section == 'agenda':
40
        response.filter['bigdiv'] = 'rub_agenda'
41
        section_title = '<h2 id="agenda">%s</h2>\n' % _('Agenda')
42
        if page_title == _('Agenda'):
43
            page_title = ''
44
    elif section and len(section) > 1:
45
        # XXX: this works but is not efficient
46
        if Category.has_urlname(section):
47
            section_title = '<h2 id="services">%s</h2>\n' % _('Services')
48
            response.filter['bigdiv'] = 'rub_service'
49

  
50
    if not 'auquotidien-no-titles-in-section' in response.filter.get('keywords', []):
51
        if page_title:
52
            if section_title:
53
                page_title = '<h3>%s</h3>' % page_title
54
            else:
55
                page_title = '<h2 id="info">%s</h2>' % page_title
56
        else:
57
            page_title = ''
58

  
59
        body = '\n'.join((section_title, page_title, body))
60

  
61
    if hasattr(response, 'breadcrumb') and response.breadcrumb:
62
        if len(response.breadcrumb) == 1:
63
            response.breadcrumb = None
64

  
65
    return template.decorate(body, response)
66

  
67

  
68
def error_page(*args, **kwargs):
69
    get_response().filter = {}
70
    get_response().filter['error-page'] = None
71
    get_response().filter['keywords'] = template.get_current_theme().get('keywords')
72
    get_response().filter['title'] = template.get_current_theme().get('keywords')
73
    get_response().filter['gauche'] = TextsDirectory.get_html_text('aq-error-assistance')
74
    error_page = wcs_error_page(*args, **kwargs)
75
    title = get_response().filter['title']
76
    get_response().filter['title'] = None
77
    return htmltext('<div id="info"><h2>%s</h2>' % title) + error_page + htmltext('</div>')
78

  
79
template.error_page = error_page
80
get_publisher_class().render_response = render_response
81

  
82
TextsDirectory.register('aq-error-assistance', N_('Assistance text next to errors'))
debian/rules
31 31
	dh_installdirs
32 32

  
33 33
	$(PYTHON) setup.py install --prefix=$(DESTDIR)/usr --no-compile
34
	for i in $(CURDIR)/debian/wcs-au-quotidien/usr/lib/python2.*; do \
35
		mv $$i/site-packages/extra \
36
			$$i/site-packages/extra-wcs-au-quotidien; done
37 34
	cd po && make install prefix=$(CURDIR)/debian/wcs-au-quotidien/
38 35
	install -d -m 755 $(DESTDIR)/var/lib/wcs-au-quotidien $(DESTDIR)/etc/wcs
39 36
	install -m 644 wcs-au-quotidien.cfg-sample $(DESTDIR)/etc/wcs/wcs-au-quotidien.cfg
debian/wcs-au-quotidien.init
19 19
SCRIPTNAME=/etc/init.d/$NAME
20 20
PYTHON_VERSION=`/usr/bin/env python -c \
21 21
    "import sys; print '%d.%d' % (sys.version_info[0], sys.version_info[1])"`
22
OPTIONS="--extra /usr/lib/pymodules/python$PYTHON_VERSION/extra-wcs-au-quotidien/"
22
OPTIONS="--extra /usr/lib/pymodules/python$PYTHON_VERSION/auquotidien/"
23 23
WCS_USER=wcs-au-quotidien
24 24
WCS_GROUP=wcs-au-quotidien
25 25
CONFIG_FILE=/etc/wcs/wcs-au-quotidien.cfg
extra/auquotidien.py
1
from quixote import get_publisher
2

  
3
from qommon import _
4
from qommon.publisher import get_publisher_class, get_request
5
from qommon.misc import get_cfg
6

  
7
import modules.admin
8
import modules.backoffice
9
import modules.links_ui
10
import modules.announces_ui
11
import modules.categories_admin
12
import modules.events_ui
13
import modules.payments_ui
14
import modules.strongbox_ui
15
import modules.formpage
16
import modules.template
17
import modules.root
18
import modules.payments
19
import modules.connectors
20
import modules.abelium_domino_ui
21
import modules.abelium_domino_vars
22
import modules.abelium_domino_synchro
23

  
24
get_publisher_class().register_translation_domain('auquotidien')
25
get_publisher_class().default_configuration_path = 'au-quotidien-wcs-settings.xml'
26

  
27
rdb = get_publisher_class().backoffice_directory_class
28

  
29
rdb.items = []
30

  
31
rdb.register_directory('announces', modules.announces_ui.AnnouncesDirectory())
32
rdb.register_menu_item('announces/', _('Announces'))
33

  
34
rdb.register_directory('links', modules.links_ui.LinksDirectory())
35
rdb.register_menu_item('links/', _('Links'))
36

  
37
rdb.register_directory('events', modules.events_ui.EventsDirectory())
38
rdb.register_menu_item('events/', _('Events'))
39

  
40
rdb.register_directory('payments', modules.payments_ui.PaymentsDirectory())
41
rdb.register_menu_item('payments/', _('Payments'))
42

  
43
rdb.register_directory('strongbox', modules.strongbox_ui.StrongboxDirectory())
44
rdb.register_menu_item('strongbox/', _('Strongbox'))
45

  
46
rdb.register_directory('settings', modules.admin.SettingsDirectory())
47
rdb.register_directory('categories', modules.categories_admin.CategoriesDirectory())
48

  
49
import wcs.admin.settings
50
wcs.admin.settings.SettingsDirectory.domino = modules.abelium_domino_ui.AbeliumDominoDirectory()
51
wcs.admin.settings.SettingsDirectory._q_exports.append('domino')
52

  
53
import wcs.categories
54
wcs.categories.Category.XML_NODES = [('name', 'str'), ('url_name', 'str'),
55
        ('description', 'str'), ('position', 'int'),
56
        ('homepage_position', 'str'), ('redirect_url', 'str'), ('limit', 'int')]
extra/modules/abelium_domino_synchro.py
1
import sys
2
from datetime import datetime
3
import collections
4
from decimal import Decimal
5

  
6
from qommon import _
7
from qommon.cron import CronJob
8
from qommon.publisher import get_publisher_class
9
from qommon import get_logger
10

  
11
from wcs.users import User
12

  
13
from abelium_domino_ui import (get_client, is_activated, get_invoice_regie,
14
        abelium_domino_ws)
15
from payments import Invoice, Transaction
16

  
17
DOMINO_ID_PREFIX = 'DOMINO-'
18

  
19
def synchronize_domino(publisher):
20
    regie = get_invoice_regie(publisher)
21
    logger = get_logger()
22
    if not is_activated(publisher) or not regie:
23
        return
24
    client = get_client(publisher)
25
    if client is None:
26
        logger.warning('Unable to create a DominoWS object')
27
        return
28
    client.clear_cache()
29
    users = User.values()
30
    users_by_mail = dict(((user.email, user) for user in users))
31
    users_by_code_interne = {}
32
    for user in users:
33
        if hasattr(user, 'abelium_domino_code_famille'):
34
            users_by_code_interne[user.abelium_domino_code_famille] = user
35
    try:
36
        invoices = client.invoices
37
    except abelium_domino_ws.DominoException, e:
38
        publisher.notify_of_exception(sys.exc_info(), context='[DOMINO]')
39
        logger.error('domino cron: failure to retrieve invoice list from domino '
40
                'for synchronization [error:%s]', e)
41
        return
42
    # import new invoices
43
    logger.info('domino cron: retrieved %i invoices', len(invoices))
44
    for invoice_id, invoice in invoices.iteritems():
45
        user = None
46
        if invoice.family.code_interne in users_by_code_interne:
47
            user = users_by_code_interne[invoice.family.code_interne]
48
        if user is None:
49
            for email in (invoice.family.email_pere, invoice.family.email_mere,
50
                    invoice.family.adresse_internet):
51
                user = users_by_mail.get(email)
52
                if user:
53
                    break
54
            else:
55
                continue
56
        external_id = '%s%s' % (DOMINO_ID_PREFIX, invoice.id)
57
        payment_invoice = Invoice.get_on_index(external_id, 'external_id', ignore_errors=True)
58
        if payment_invoice:
59
            continue
60
        if invoice.reste_du == Decimal(0) or invoice.reste_du < Decimal(0):
61
            continue
62
        payment_invoice = Invoice()
63
        payment_invoice.user_id = user.id
64
        payment_invoice.external_id = external_id
65
        payment_invoice.regie_id = regie.id
66
        payment_invoice.formdef_id = None
67
        payment_invoice.formdata_id = None
68
        payment_invoice.amount = invoice.reste_du
69
        payment_invoice.date = invoice.creation
70
        payment_invoice.domino_synchro_date = datetime.now()
71
        if 'etablissement' in invoice._detail:
72
            etablissement = invoice._detail['etablissement'].encode('utf-8')
73
            payment_invoice.subject = _('%s - Childcare services') % etablissement
74
        else:
75
            payment_invoice.subject = _('Childcare services')
76
        if invoice._detail.get('lignes'):
77
            details = []
78
            details.append('<table class="invoice-details"><thead>')
79
            tpl = '''<tr>
80
    <td>%(designation)s</td>
81
    <td>%(quantite)s</td>
82
    <td>%(prix)s</td>
83
    <td>%(montant)s</td>
84
</tr>'''
85
            captions = {
86
                    'designation': _('Caption'),
87
                    'quantite': _('Quantity'),
88
                    'prix': _('Price'),
89
                    'amount': _('Amount')
90
            }
91
            details.append(tpl % captions)
92
            details.append('</thead>')
93
            details.append('<tbody>')
94
            for ligne in invoice._detail['lignes']:
95
                def encode(x):
96
                    a, b = x
97
                    b = b.encode('utf-8')
98
                    return (a,b)
99
                ligne = map(encode, ligne)
100
                ligne = dict(ligne)
101
                base = collections.defaultdict(lambda:'')
102
                base.update(ligne)
103
                details.append(tpl % base)
104
            details.append('</tbody></table>')
105
            payment_invoice.details = '\n'.join(details)
106
        payment_invoice.store()
107
        logger.info('domino cron: remote invoice %s for family %s added to user %s invoices with id %s',
108
                invoice.id, invoice.family.id, user.id, payment_invoice.id)
109

  
110
    # update invoices
111
    invoices_ids = dict(invoices.iteritems())
112
    for payment_invoice in Invoice.values():
113
        if payment_invoice.external_id is None or not payment_invoice.external_id.startswith(DOMINO_ID_PREFIX):
114
            continue # not a payment related to domino we skip
115
        i = payment_invoice.external_id[len(DOMINO_ID_PREFIX):]
116
        i = int(i)
117
        invoice = invoices_ids.get(i)
118
        if payment_invoice.paid:
119
            if not invoice: 
120
                # invoice has been paid (locally or not) but remote invoice has
121
                # been deleted do, we do nothing.
122
                continue
123
            if getattr(payment_invoice, 'domino_knows_its_paid', None) or getattr(payment_invoice, 'paid_by_domino', None):
124
                # synchronization of payment already done, skip
125
                continue
126
            transactions = Transaction.get_with_indexed_value('invoice_ids', payment_invoice.id)
127
            if not transactions:
128
                logger.warning("domino cron: invoice %s is marked paid but does "
129
                        "not have any linked transaction.", payment_invoice.id)
130
                details = '' # no details about the payment, problem
131
            else:
132
                details = repr(transactions[0].__dict__)
133
            if invoice.montant != payment_invoice.amount:
134
                pass # add warning logs
135
            try:
136
                client.pay_invoice([invoice], invoice.montant, details,
137
                        payment_invoice.paid_date)
138
            except abelium_domino_ws.DominoException, e:
139
                logger.error('domino cron: invoice %s has been paid, but the remote system '
140
                        'is unreachable, notification will be done again '
141
                        'later [error: %s]', invoice.id, e)
142
            else:
143
                # memorize the date of synchronization
144
                payment_invoice.domino_knows_its_paid = datetime.now()
145
                payment_invoice.store()
146
                logger.info('domino cron: domino: invoice %s has been paid; remote system has been '
147
                        'notified', payment_invoice.id)
148
        else: # unpaid
149
            if not invoice:
150
                logger.info('domino cron: remote invoice %s disapearred, so its '
151
                        'still-unpaid local counterpart invoice %s was deleted.',
152
                        i, payment_invoice.id)
153
                payment_invoice.remove_self()
154
            elif invoice.paid():
155
                payment_invoice.paid_by_domino = True
156
                payment_invoice.pay()
157
                logger.info('domino cron: remote invoice %s has beend paid, '
158
                        'local invoice %s of user %s is now marked as paid.',
159
                        invoice.id, payment_invoice.id, payment_invoice.user_id)
160
            else: # not invoice.paid()
161
                pass # still waiting for the payment
162

  
163
get_publisher_class().register_cronjob(CronJob(function=synchronize_domino,
164
    hours=range(0, 24), minutes=range(0, 60, 30)))
extra/modules/abelium_domino_ui.py
1
from quixote import get_publisher, redirect, get_request
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import TemplateIO, htmltext
4

  
5
from qommon import _
6
from qommon import get_cfg, get_logger
7
from qommon.form import Form, StringWidget, CheckboxWidget, SingleSelectWidget
8
from qommon.backoffice.menu import html_top
9
from quixote.html import htmltext
10

  
11
from payments import Regie
12

  
13

  
14
# constants
15
ABELIUM_DOMINO = 'abelium_domino'
16
ACTIVATED = 'activated'
17
WSDL_URL = 'wsdl_url'
18
SERVICE_URL = 'service_url'
19
DOMAIN = 'domain'
20
LOGIN = 'login'
21
PASSWORD = 'password'
22
INVOICE_REGIE = 'invoice_regie'
23

  
24
try:
25
    import abelium_domino_ws
26
except ImportError, e:
27
    abelium_domino_ws = None
28
    import_error = e
29

  
30
def get_abelium_cfg(publisher):
31
    if not publisher:
32
        publisher = get_publisher()
33
    return publisher.cfg.get(ABELIUM_DOMINO, {})
34

  
35
def is_activated(publisher=None):
36
    cfg = get_abelium_cfg(publisher)
37
    return cfg.get(ACTIVATED, False) and abelium_domino_ws is not None
38

  
39
def get_client(publisher=None):
40
    publisher = publisher or get_publisher()
41

  
42
    cfg = get_abelium_cfg(publisher)
43
    try:
44
        publisher._ws_cache = abelium_domino_ws.DominoWs(
45
            url=cfg.get(WSDL_URL, ''),
46
            domain=cfg.get(DOMAIN,''),
47
            login=cfg.get(LOGIN, ''),
48
            password=cfg.get(PASSWORD, ''),
49
            location=cfg.get(SERVICE_URL),
50
            logger=get_logger())
51
    except IOError:
52
        return None
53
    return publisher._ws_cache
54

  
55
def get_family(user, publisher=None):
56
    family = None
57
    if user is None:
58
        return None
59
    client = get_client(publisher)
60
    if not client:
61
        return None
62
    if hasattr(user, 'abelium_domino_code_famille'):
63
        family = client.get_family_by_code_interne(
64
            user.abelium_domino_code_famille)
65
    if family is None and user.email:
66
        family = client.get_family_by_mail(user.email)
67
    return family
68

  
69
def get_invoice_regie(publisher=None):
70
    cfg = get_abelium_cfg(publisher)
71
    regie_id = cfg.get(INVOICE_REGIE)
72
    if not regie_id:
73
        return None
74
    return Regie.get(regie_id, ignore_errors=True)
75

  
76
class AbeliumDominoDirectory(Directory):
77
    _q_exports = [ '' , 'debug' ]
78
    label = N_('Domino')
79

  
80
    def debug(self):
81
        from abelium_domino_vars import SESSION_CACHE
82
        html_top(ABELIUM_DOMINO)
83
        r = TemplateIO(html=True)
84
        r += htmltext('<form method="post"><button>Lancer le cron</button></form>')
85
        if get_request().get_method() == 'POST':
86
            try:
87
                from abelium_domino_synchro import synchronize_domino
88
                synchronize_domino(get_publisher())
89
            except Exception, e:
90
                r += htmltext('<pre>%s</pre>') % repr(e)
91
        r += htmltext('<p>code interne: %s</p>') % getattr(get_request().user, str('abelium_domino_code_famille'), None)
92
        r += htmltext('<dl>')
93
        context = get_publisher().substitutions.get_context_variables()
94
        for var in sorted(context.keys()):
95
            value = context[var]
96
            if value:
97
                r += htmltext('<dt>%s</dt>') % var
98
                r += htmltext('<dd>%s</dt>') % value
99
        r += htmltext('</dl>')
100
        delattr(get_request().session, SESSION_CACHE)
101

  
102
    def _q_index(self):
103
        publisher = get_publisher()
104
        cfg = get_cfg(ABELIUM_DOMINO, {})
105
        form = self.form(cfg)
106

  
107
        title = _('Abelium Domino')
108
        html_top(ABELIUM_DOMINO, title = title)
109
        r = TemplateIO(html=True)
110
        r += htmltext('<h2>%s</h2>') % title
111

  
112
        if form.is_submitted() and not form.has_errors():
113
            if form.get_widget('cancel').parse():
114
                return redirect('..')
115
            if form.get_widget('submit').parse():
116
                for name in [f[0] for f in self.form_desc] + [INVOICE_REGIE]:
117
                    widget = form.get_widget(name)
118
                    if widget:
119
                        cfg[name] = widget.parse()
120
                publisher.cfg[ABELIUM_DOMINO] = cfg
121
                publisher.write_cfg()
122
                return redirect('.')
123

  
124
        if abelium_domino_ws:
125
            r += form.render()
126
        else:
127
            message = _('The Abelium Domino module is not '
128
                    'activated because of this error when '
129
                    'loading it: %r') % import_error
130
            r += htmltext('<p class="errornotice">%s</p>') % message
131
        r += htmltext('<dl style="display: none">')
132
        context = get_publisher().substitutions.get_context_variables()
133
        for var in sorted(context.keys()):
134
            value = context[var]
135
            if value:
136
                r += htmltext('<dt>%s</dt>') % var
137
                r += htmltext('<dd>%s</dt>') % value
138
        r += htmltext('</dl>')
139
        return r.getvalue()
140

  
141
    form_desc = (
142
            # name, required, title, kind
143
            (ACTIVATED, False, _('Activated'), bool),
144
            (WSDL_URL, True, _('WSDL URL'), str),
145
            (SERVICE_URL, False, _('Service URL'), str),
146
            (DOMAIN, True, _('Domain'), str),
147
            (LOGIN, True, _('Login'), str),
148
            (PASSWORD, True, _('Password'), str),
149
    )
150

  
151

  
152
    def form(self, initial_value={}):
153
        form = Form(enctype='multipart/form-data')
154
        kinds = { str: StringWidget, bool: CheckboxWidget }
155
        for name, required, title, kind in self.form_desc:
156
            widget = kinds[kind]
157
            form.add(widget, name, required=required, title=title,
158
                    value=initial_value.get(name, ''))
159
        options = [(regie.id, regie.label) \
160
                        for regie in Regie.values()]
161
        options.insert(0, (None, _('None')))
162
        form.add(SingleSelectWidget, INVOICE_REGIE,
163
                title=_('Regie which will receive payments'),
164
                value=initial_value.get(INVOICE_REGIE),
165
                options=options)
166

  
167
        form.add_submit('submit', _('Submit'))
168
        form.add_submit('cancel', _('Cancel'))
169

  
170
        return form
extra/modules/abelium_domino_vars.py
1
from decimal import Decimal
2
import logging
3

  
4
from quixote.publish import get_publisher
5

  
6
from qommon import _
7
from qommon.substitution import Substitutions
8
from wcs.publisher import WcsPublisher
9

  
10
from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
11

  
12
SESSION_CACHE = 'abelium_domino_variable_cache'
13

  
14
class DominoVariables(object):
15
    VARIABLE_TEMPLATE = 'domino_var_%s'
16
    CHILD_VARIABLE_TEMPLATE = 'domino_var_%s_enfant%s'
17

  
18
    CHILD_COLUMNS = abelium_domino_ws.Child.COLUMNS
19
    FAMILY_COLUMNS = abelium_domino_ws.Family.COLUMNS \
20
        + abelium_domino_ws.Family.MORE_COLUMNS
21
    def __init__(self, publisher=None, request=None):
22
        self.publisher = publisher
23
        self.request = request
24

  
25
    def get_substitution_variables(self):
26
        vars = {}
27
        if not is_activated() or not self.request or not self.request.user \
28
                or not getattr(self.request.user, 'email'):
29
            return vars
30

  
31
        # test cache
32
        cache = getattr(self.request.session, SESSION_CACHE, None)
33
        if cache is not None:
34
            return cache
35
        # call the web service
36
        try:
37
            charset = get_publisher().site_charset
38
            family = get_family(self.request.user)
39
            if family:
40
                family.complete()
41
                for i, child in enumerate(family.children):
42
                    for remote_name, name, converter, desc in self.CHILD_COLUMNS:
43
                        v = getattr(child, name, None)
44
                        if v is None:
45
                            continue
46
                        if hasattr(v, 'encode'):
47
                            v = v.encode(charset)
48
                        vars[self.CHILD_VARIABLE_TEMPLATE % (name, i+1)] = v
49
                vars[self.VARIABLE_TEMPLATE % 'nombre_enfants'] = len(family.children)
50
                for remote_name, name, converted, desc in self.FAMILY_COLUMNS:
51
                    if hasattr(family, name):
52
                        v = getattr(family, name)
53
                        if v is None:
54
                            continue
55
                        if hasattr(v, 'encode'):
56
                            v = v.encode(charset)
57
                        vars[self.VARIABLE_TEMPLATE % name] = v
58
                amount = Decimal(0)
59
                for invoice in family.invoices:
60
                    amount += invoice.reste_du
61
                if amount:
62
                    vars['user_famille_reste_du'] = str(amount)
63
        except abelium_domino_ws.DominoException:
64
            logging.exception('unable to call the domino ws for user %s', self.request.user.id)
65
        setattr(self.request.session, SESSION_CACHE, vars)
66
        self.request.session.store()
67
        return vars
68

  
69
    def get_substitution_variables_list(cls):
70
        if not is_activated():
71
            return ()
72
        vars = []
73
        for remote_name, name, converted, desc in cls.FAMILY_COLUMNS:
74
            vars.append((_('Domino'), cls.VARIABLE_TEMPLATE % name, desc))
75
        for remote_name, name, converted, desc in cls.CHILD_COLUMNS:
76
            vars.append((_('Domino'), cls.CHILD_VARIABLE_TEMPLATE % (name, '{0,1,2,..}'), desc))
77
        return vars
78
    get_substitution_variables_list = classmethod(get_substitution_variables_list)
79

  
80
Substitutions.register_dynamic_source(DominoVariables)
81
WcsPublisher.register_extra_source(DominoVariables)
extra/modules/abelium_domino_workflow.py
1
import re
2
import time
3

  
4
from quixote import get_request, get_publisher, get_session
5
from quixote.directory import Directory
6

  
7
from qommon import _
8
from qommon.substitution import Substitutions
9
from qommon.form import Form, ValidatedStringWidget
10
import qommon.misc
11
from qommon import get_logger
12

  
13
from wcs.workflows import Workflow, WorkflowStatusJumpItem, register_item_class
14
from wcs.forms.common import FormStatusPage
15

  
16
from abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
17
import abelium_domino_ws
18

  
19
class InternalCodeStringWidget(ValidatedStringWidget):
20
    regex = '\d*'
21

  
22
class AbeliumDominoRegisterFamilyWorkflowStatusItem(WorkflowStatusJumpItem):
23
    status = None
24
    description = N_('Abelium Domino: Register a Family')
25
    key = 'abelium-domino-register-family'
26
    category = ('aq-abelium', N_('Abelium'))
27
    label = None
28

  
29
    def render_as_line(self):
30
        return _('Register a Family into Abelium Domino')
31

  
32
    def get_family(self, formdata):
33
        try:
34
            user = formdata.get_user()
35
            if user:
36
                family = get_family(user)
37
                if family:
38
                    family.complete()
39
                return family
40
        except abelium_domino_ws.DominoException:
41
            pass
42
        return None
43

  
44
    def fill_form(self, form, formdata, user):
45
        family = self.get_family(formdata)
46
        if 'family_id' not in form._names:
47
            form.add(InternalCodeStringWidget, 'family_id',
48
                     title=_('Family internal code'),
49
                     value=family and family.code_interne.encode('utf8'),
50
                     hint=_('If a family internal code is present, the '
51
                            'family is updated, if not it is created'))
52
            form.add_submit('create_update_button%s' % self.id,
53
                    _('Create or update the family'))
54

  
55
    def update(self, form, formdata, user, evo):
56
        fid_widget = form.get_widget('family_id')
57
        code_interne = fid_widget.parse()
58
        try:
59
            code_interne = int(code_interne)
60
        except ValueError:
61
            raise ValueError('Le code interne est invalide')
62
        code_interne = '%05d' % code_interne
63
        family = get_client().get_family_by_code_interne(code_interne)
64
        if not family:
65
            raise ValueError('Le code interne est invalide')
66
        family.complete()
67
        self.extract_family(form, formdata, user, evo, family)
68
        family.save()
69
        return family
70

  
71
    def create(self, form, formdata, user, evo):
72
        family = abelium_domino_ws.Family(client=get_client())
73
        self.extract_family(form, formdata, user, evo, family)
74
        return family
75

  
76
    def extract_family(self, form, formdata, user, evo, family):
77
        formdef = formdata.formdef
78
        children = [abelium_domino_ws.Child() for i in range(5)]
79
        max_i = 0
80
        for field in formdef.fields:
81
            value = formdata.data.get(field.id)
82
            if value in (None, ''):
83
                continue
84
            if hasattr(field, 'date_in_the_past'):
85
                value = time.strftime('%Y%m%d', value)
86
            value = unicode(value, 'utf8')
87
            if field.prefill and \
88
               field.prefill.get('type') == 'formula':
89
                v = field.prefill.get('value', '').strip()
90
                i = None
91
                name = None
92
                m = re.search('domino_var_([^ ]*)_enfant([0-9]*)', v)
93
                m2 = re.search('domino_var_([^ ]*)', v)
94
                if m:
95
                    name, i = m.groups()
96
                    try:
97
                        i = int(i)
98
                    except ValueError:
99
                        continue
100
                    max_i = max(i, max_i)
101
                    print 'enfant', name, i-1, value
102
                    setattr(children[i-1], name, value)
103
                elif m2:
104
                    name = m2.group(1)
105
                    print 'family', name, value
106
                    setattr(family, name, value)
107
        for child1, child2 in zip(family.children, children[:max_i]):
108
            child1.__dict__.update(child2.__dict__)
109
        family.save()
110
        if max_i > len(family.children): # add new children
111
            for child in children[len(family.children):max_i]:
112
                family.add_child(child)
113

  
114
    def submit_form(self, form, formdata, user, evo):
115
        logger = get_logger()
116
        if form.get_submit() != 'create_update_button%s' % self.id:
117
            return
118
        try:
119
            if form.get_widget('family_id').parse():
120
                family = self.update(form, formdata, user, evo)
121
                msg = _('Sucessfully updated the family %s')
122
                log_msg = _('Sucessfully updated the family %(code)s of %(user)s')
123
            else:
124
                family = self.create(form, formdata, user, evo)
125
                msg = _('Sucessfully created the family %s')
126
                log_msg = _('Sucessfully created the family %(code)s of %(user)s')
127
            code_interne = family.code_interne.encode('utf8')
128
            msg = msg % code_interne
129
            logger.info(log_msg, {'code': code_interne, 'user': formdata.get_user()})
130
            form_user = formdata.get_user()
131
            form_user.abelium_domino_code_famille = code_interne
132
            form_user.store()
133
        except Exception, e:
134
            if form.get_widget('family_id').parse():
135
                msg = _('Unable to update family: %s') % str(e)
136
            else:
137
                msg = _('Unable to create family: %s') % str(e)
138
            evo.comment = msg
139
            logger.exception(msg  % formdata.get_user())
140
        else:
141
            evo.comment = msg
142
            wf_status = self.get_target_status()
143
            if wf_status:
144
                evo.status = 'wf-%s' % wf_status[0].id
145
        return False
146

  
147
    def is_available(self, workflow=None):
148
        return get_publisher().has_site_option('domino')
149
    is_available = classmethod(is_available)
150

  
151
register_item_class(AbeliumDominoRegisterFamilyWorkflowStatusItem)
extra/modules/abelium_domino_ws.py
1
# -*- coding: utf-8 -*-
2
from decimal import Decimal
3
import time
4
import datetime
5
from xml.etree import ElementTree as etree
6
import logging
7

  
8
try:
9
    from suds.client import Client
10
    from suds.bindings.binding import Binding
11
    # Webdev is bugged and using an HTML generator to produce XML content, 
12
    Binding.replyfilter = lambda self, x: x.replace('&nbsp;', '&#160;')
13
except ImportError:
14
    Client = None
15
    Binding = None
16

  
17
logger = logging.getLogger(__name__)
18

  
19
# cleaning and parsing functions
20

  
21
LINE_SEPARATOR = '\n'
22
COLUMN_SEPARATOR = '\t'
23

  
24
def unicode_and_strip(x):
25
    return unicode(x).strip()
26

  
27
def strip_and_int(x):
28
    try:
29
        return int(x.strip())
30
    except ValueError:
31
        return None
32

  
33
def strip_and_date(x):
34
    try:
35
        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
36
    except ValueError:
37
        return None
38

  
39
def parse_date(date_string):
40
    if date_string:
41
        return datetime.datetime.strptime(date_string, "%Y%m%d")
42
    else:
43
        None
44

  
45
class DominoException(Exception):
46
    pass
47

  
48
def object_cached(function):
49
    '''Decorate an object method so that its results is cached on the object
50
       instance after the first call.
51
    '''
52
    def decorated_function(self, *args, **kwargs):
53
        cache_name = '__%s_cache' % function.__name__
54
        if not hasattr(self, cache_name):
55
            setattr(self, cache_name, (time.time(), {}))
56
        t, d = getattr(self, cache_name)
57
        if time.time() - t > 80:
58
            setattr(self, cache_name, (time.time(), {}))
59
            t, d = getattr(self, cache_name)
60
        k = tuple(*args) + tuple(sorted(kwargs.items()))
61
        if not k in d:
62
            d[k] = function(self, *args, **kwargs)
63
        return d[k]
64
    return decorated_function
65

  
66
# Data model
67
class SimpleObject(object):
68
    '''Base class for object returned by the web service'''
69

  
70
    '''Describe basic columns'''
71
    COLUMNS = ()
72
    '''Describe extended object columns'''
73
    MORE_COLUMNS = ()
74

  
75
    def __init__(self, **kwargs):
76
        self.__dict__.update(kwargs)
77

  
78
    def __repr__(self):
79
        c = {}
80
        for remote_name, name, converter, desc in self.COLUMNS:
81
            if hasattr(self, name):
82
                c[name] = getattr(self, name)
83
        return str(c)
84

  
85
    def serialize(self):
86
        l = []
87
        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
88
            if local_name == 'id':
89
                continue
90
            v = getattr(self, local_name, None)
91
            if v is None:
92
                continue
93
            if isinstance(v, (datetime.date, datetime.datetime)):
94
                v = v.strftime('%Y%m%d')
95
            if remote_name.endswith('_DA') and '-' in v:
96
                v = v.replace('-', '')
97
            l.append(u'{0}: "{1}"'.format(remote_name, v))
98
        return u','.join(l)
99

  
100
    def debug(self):
101
        '''Output a debugging view of this object'''
102
        res = ''
103
        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
104
            if hasattr(self, name):
105
                res += name + ':' + repr(getattr(self, name)) + '\n'
106
        return res
107

  
108
    def __int__(self):
109
        '''Return the object id'''
110
        return self.id
111

  
112
class UrgentContact(SimpleObject):
113
    COLUMNS = (
114
            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
115
            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
116
            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
117
            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
118
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
119
            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
120
            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
121
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
122
            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
123
            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
124
            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
125
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
126
            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
127
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
128
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
129
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
130
    )
131

  
132
class Child(SimpleObject):
133
    COLUMNS = (
134
            ('IDENFANTS', 'id', strip_and_int, 'Identifiant de ENFANTS'),
135
            ('NOM_CH', 'nom', unicode_and_strip, 'Nom'),
136
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'Prénom'),
137
            ('NAISSANCE_DA', 'date_naissance', strip_and_date, 'Date de Naissance'),
138
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'Commentaires / Notes'),
139
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
140
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'Code Postal'),
141
            ('VILLE_CH', 'ville', unicode_and_strip, 'Ville'),
142
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'Code Interne'),
143
            ('LIEUNAISSANCE_CH', 'lieu_naissance', unicode_and_strip, 'Lieu de Naissance'),
144
            ('DEPNAISSANCE_CH', 'departement_naissance', unicode_and_strip, 'Département Naissance'),
145
            ('NUMSECU_CH', 'num_securite_sociale', unicode_and_strip, 'N° de SECU'),
146
            ('NATIONALITE_CH', 'nationalite', unicode_and_strip, 'Nationalité'),
147
            ('PRENOM2_CH', 'prenom2', unicode_and_strip, 'Prénom 2'),
148
            ('SEXE_CH', 'sexe', unicode_and_strip, 'Sexe'),
149
            ('IDTABLELIBRE1', 'IDTABLELIBRE1', unicode_and_strip, 'IDTABLELIBRE1'),
150
            ('IDTABLELIBRE2', 'IDTABLELIBRE2', unicode_and_strip, 'IDTABLELIBRE2'),
151
            ('IDTABLELIBRE3', 'IDTABLELIBRE3', unicode_and_strip, 'IDTABLELIBRE3'),
152
            ('IDTABLELIBRE4', 'IDTABLELIBRE4', unicode_and_strip, 'IDTABLELIBRE4'),
153
            ('CHAMPLIBRE1_CH', 'CHAMPLIBRE1_CH', unicode_and_strip, 'Valeur Champ Libre 1'),
154
            ('CHAMPLIBRE2_CH', 'CHAMPLIBRE2_CH', unicode_and_strip, 'Valeur Champ Libre 2'),
155
            ('CHAMPCALCULE1_CH', 'CHAMPCALCULE1_CH', unicode_and_strip, 'Valeur Champ Calculé 1'),
156
            ('CHAMPCALCULE2_CH', 'CHAMPCALCULE2_CH', unicode_and_strip, 'Valeur Champ Calculé 2'),
157
            ('SOMMEIL_ME', 'sommeil', unicode_and_strip, 'Sommeil'),
158
            ('ACTIVITE_ME', 'activite', unicode_and_strip, 'Activités'),
159
            ('HABITUDE_ME', 'habitude', unicode_and_strip, 'Habitudes'),
160
            ('PHOTO_CH', 'photographie', unicode_and_strip, 'Photographie'),
161
            ('NUMCOMPTE_CH', 'numcompte', unicode_and_strip, 'N° Compte Comptable'),
162
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'Téléphone'),
163
            ('IDFAMILLES2', 'id_famille2', unicode_and_strip, 'Identifiant famille 2'),
164
            ('PERE_CH', 'pere', unicode_and_strip, 'Nom du père'),
165
            ('MERE_CH', 'mere', unicode_and_strip, 'Nom de la mère'),
166
            ('AUTOPARENTALEMERE_IN', 'autorisation_parentale_mere', unicode_and_strip, 'Autorisation Parentale Mère'),
167
            ('AUTOPARENTALEPERE_IN', 'autorisation_parentale_pere', unicode_and_strip, 'Autorisation Parentale de Père'),
168
            ('IDPORTAIL_ENFANTS', 'id_portail_enfants', unicode_and_strip, 'Identifiant de PORTAIL_ENFANTS'),
169
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'Adresse Internet'),
170
    )
171

  
172
    def save(self):
173
        if hasattr(self, 'id'):
174
            self.client.update_child(self)
175
        else:
176
            self.id = self.client.add_child(self)
177
        self.client.clear_cache()
178

  
179
class Family(SimpleObject):
180
    COLUMNS = (
181
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
182
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
183
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
184
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
185
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
186
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
187
    )
188

  
189
    MORE_COLUMNS = (
190
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
191
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
192
            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
193
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
194
            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
195
            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
196
            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
197
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
198
            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
199
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
200
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
201
            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
202
            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
203
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
204
            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
205
            ('REVENUMENSUEL_MO', 'revenu_mensuel', unicode_and_strip, 'revenu mensuel de la famille'),
206
            ('REVENUANNUEL_MO', 'revenu_annuel', unicode_and_strip, 'revenu annuel de la famille'),
207
            ('QUOTIENTFAMILIAL_MO', 'quotient_familial', unicode_and_strip, 'quotient familial'),
208
            ('NBTOTALENFANTS_EN', 'nb_total_enfants', unicode_and_strip, 'nombre total d\'enfants'),
209
            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge', unicode_and_strip, 'nombre d\'enfants à charge'),
210
            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
211
            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
212
            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere', unicode_and_strip, 'autorisation parentale de père'),
213
            ('DATENAISPERE_DA', 'date_naissance_pere', strip_and_date, 'date de naisance du père'),
214
            ('DEPNAISPERE_EN', 'departement_naissance_pere', unicode_and_strip, 'département de naissance du père'),
215
            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
216
            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
217
            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
218
            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
219
            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
220
            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
221
            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
222
            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
223
            ('TELPERE_LR_IN', 'tel_pere_liste_rouge', unicode_and_strip, 'téléphone liste rouge père'),
224
            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge père'),
225
            ('TEL_LR_IN', 'tel_liste_rourge', unicode_and_strip, 'téléphone liste rouge'),
226
            ('TEL2_LR_IN', 'tel2_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge'),
227
            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
228
            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
229
            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere', unicode_and_strip, 'autorisation parentale mère'),
230
            ('DATENAISMERE_DA', 'date_naissance_mere', strip_and_date, 'date de naissance de la mère'),
231
            ('DEPNAISMERE_EN', 'departement_naissance_mere', unicode_and_strip, 'département de naissance de la mère'),
232
            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
233
            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
234
            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere', unicode_and_strip, 'revenu mensuel du père'),
235
            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
236
            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
237
            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
238
            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
239
            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere', unicode_and_strip, 'revenu mensuel mère'),
240
            ('REVANNUELPERE_MO', 'revenu_annuel_pere', unicode_and_strip, 'revenu annuel père'),
241
            ('REVANNUELMERE_MO', 'revenu_annuel_mere', unicode_and_strip, 'revenu annuel mère'),
242
            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
243
            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
244
            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge', unicode_and_strip, 'téléphone liste rouge mère'),
245
            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge mère'),
246
            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
247
            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
248
            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
249
            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
250
            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
251
            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
252
            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
253
            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
254
            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
255
            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
256
            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
257
            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
258
            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
259
            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
260
            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
261
            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
262
            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
263
            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
264
            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
265
            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
266
            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
267
            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
268
            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
269
            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
270
            ('TOTALDU_MO', 'total_du', unicode_and_strip, 'total dû'),
271
            ('TOTALREGLE_MO', 'total_regle', unicode_and_strip, 'total réglé'),
272
            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
273
            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
274
            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
275
            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
276
            ('RIVOLI_EN', 'code_rivoli', unicode_and_strip, 'identifiant code rivoli'),
277
            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
278
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
279
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
280
            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
281
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'commentaires / notes'),
282
            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
283
            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
284
            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
285
            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
286
            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
287
            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
288
            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
289
            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
290
            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
291
            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
292
            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
293
            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
294
            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
295
            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
296
            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
297
            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
298
            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
299
            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
300
            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
301
            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
302
            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
303
            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
304
            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
305
            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
306
            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
307
            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
308
            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
309
#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
310
#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
311
            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
312
            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
313
            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
314
            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
315
            ('ECHEANCEASSURANCE_DA', 'echeance_assurance', unicode_and_strip, 'date echéance assurance'),
316
            ('RM_MIKADO_MO', 'rm_mikado', unicode_and_strip, 'revenus mensuels mikado'),
317
            ('RA_MIKADO_MO', 'ra_mikado', unicode_and_strip, 'revenus annuels mikado'),
318
            ('QF_MIKADO_MO', 'qf_mikado', unicode_and_strip, 'quotient familial mikado'),
319
            ('RM_DIABOLO_MO', 'rm_diabolo', unicode_and_strip, 'revenus mensuels diabolo'),
320
            ('RA_DIABOLO_MO', 'ra_diabolo', unicode_and_strip, 'revenus annuels diabolo'),
321
            ('QF_DIABOLO_MO', 'qf_diabolo', unicode_and_strip, 'quotient familial diabolo'),
322
            ('RM_OLIGO_MO', 'rm_oligo', unicode_and_strip, 'revenus mensuels oligo'),
323
            ('RA_OLIGO_MO', 'ra_oligo', unicode_and_strip, 'revenus annuels oligo'),
324
            ('QF_OLIGO_MO', 'qf_oligo', unicode_and_strip, 'quotient familial oligo'),
325
            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado', unicode_and_strip, 'date d\'application des revenus de mikado'),
326
            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo', unicode_and_strip, 'date d\'application des revenus de diabolo'),
327
            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo', unicode_and_strip, 'date d\'application des revenus de oligo'),
328
    )
329

  
330
    def __init__(self, *args, **kwargs):
331
        self.children = []
332
        super(Family, self).__init__(*args, **kwargs)
333

  
334
    def complete(self):
335
        k = [a for a,b,c,d in self.MORE_COLUMNS]
336
        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
337
                columns=self.MORE_COLUMNS, instances=(self,)))
338
        l = self.client.get_children(self.id).values()
339
        self.children = sorted(l, key=lambda c: c.id)
340
        return self
341

  
342
    @property
343
    def invoices(self):
344
        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
345

  
346
    def add_child(self, child):
347
        if hasattr(self, 'id'):
348
            child.id_famille = self.id
349
            child.client = self.client
350
        self.children.append(child)
351

  
352
    def save(self):
353
        if hasattr(self, 'id'):
354
            self.client.update_family(self)
355
        else:
356
            self.code_interne = self.client.new_code_interne()
357
            self.id = self.client.add_family(self)
358
        for child in self.children:
359
            child.id_famille = self.id
360
            child.save()
361
        self.client.clear_cache()
362

  
363
class Invoice(SimpleObject):
364
    COLUMNS = (
365
        ('', 'id_famille', int, ''),
366
        ('', 'id', int, ''),
367
        ('', 'numero', str, ''),
368
        ('', 'debut_periode', parse_date, ''),
369
        ('', 'fin_periode', parse_date, ''),
370
        ('', 'creation', parse_date, ''),
371
        ('', 'echeance', parse_date, ''),
372
        ('', 'montant', Decimal, ''),
373
        ('', 'reste_du', Decimal, ''),
374
    )
375
    _detail = {}
376

  
377
    def detail(self):
378
        if not self._detail:
379
            self.client.factures_detail([self])
380
        return self._detail
381

  
382
    @property
383
    def family(self):
384
        return self.client.families[self.id_famille]
385

  
386
    def paid(self):
387
        return self.reste_du == Decimal(0)
388

  
389
class DominoWs(object):
390
    '''Interface to the WebService exposed by Abelium Domino.
391

  
392
       It allows to retrieve family and invoices.
393

  
394
       Beware that it does a lot of caching to limit call to the webservice, so
395
       if you need fresh data, call clear_cache()
396

  
397
       All family are in the families dictionnary and all invoices in the
398
       invoices dictionnary.
399
    '''
400

  
401
    def __init__(self, url, domain, login, password, location=None,
402
            logger=logger):
403
        if not Client:
404
            raise ValueError('You need python suds')
405
        self.logger = logger
406
        self.logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)',
407
                url, domain, login, password, location)
408
        self.url = url
409
        self.domain = domain
410
        self.login = login
411
        self.password = password
412
        self.client = Client(url, location=location, timeout=60)
413
        self.client.options.cache.setduration(seconds=60)
414

  
415
    def clear_cache(self):
416
        '''Remove cached attributes from the instance.'''
417

  
418
        for key, value in self.__dict__.items():
419
            if key.startswith('__') and key.endswith('_cache'):
420
                del self.__dict__[key]
421

  
422
    def call(self, function_name, *args):
423
        '''Call SOAP method named function_name passing args list as parameters.
424

  
425
           Any error is converted into the DominoException class.'''
426
        print 'call', function_name, args
427

  
428
        try:
429
            self.logger.debug(('soap call to %s(%s)' % (function_name, args)).encode('utf-8'))
430
            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
431
            self.logger.debug((u'result: %s' % data).encode('utf-8'))
432
            self.data = data
433
        except IOError, e:
434
            raise DominoException('Erreur IO', e)
435
        if data is None:
436
           data = ''
437
        if data.startswith('ERREUR'):
438
            raise DominoException(data[9:].encode('utf8'))
439
        return data
440

  
441
    def parse_tabular_data(self, data):
442
        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
443
           Empty lines (ignoring spaces) are ignored.
444
        '''
445

  
446
        rows = data.split(LINE_SEPARATOR)
447
        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
448
        return rows
449

  
450
    def __call__(self, function_name, cls=None, args=[], instances=None, columns=None):
451
        '''Call SOAP method named function_name, splitlines, map tab separated
452
        values to _map keys in a dictionnary, and use this dictionnary to
453
        initialize an object of class cls.
454

  
455
        - If instances is present, the given instances are updated with the
456
          returned content, in order, row by row.
457
        - If cls is not None and instances is None, a new instance of the class
458
          cls is instancied for every row and initialized with the content of
459
          the row.
460
        - If cls and instances are None, the raw data returned by the SOAP call
461
          is returned.
462
        '''
463

  
464
        data = self.call(function_name, *args)
465
        if cls or instances:
466
            rows = self.parse_tabular_data(data)
467
            kwargs = {}
468
            if instances:
469
                rows = zip(rows, instances)
470
            for row in rows:
471
                if instances:
472
                    row, instance = row
473
                if not row[0]:
474
                    continue
475
                for a, b in zip(columns or cls.COLUMNS, row):
476
                    x, name, converter, desc = a
477
                    kwargs[name] = converter(b.strip())
478
                if instances:
479
                    instance.__dict__.update(kwargs)
480
                    yield instance
481
                else:
482
                    yield cls(client=self, **kwargs)
483
        else:
484
            yield data
485

  
486
    def add_family(self, family):
487
        result = self.call('AJOUTER_FAMILLE', family.serialize())
488
        return int(result.strip())
489

  
490
    def update_family(self, family):
491
        if not hasattr(family, 'id'):
492
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
493
        result = self.call('MODIFIER_FAMILLE', unicode(family.id), family.serialize())
494
        return result.strip() == 'OK'
495

  
496
    def add_child(self, child):
497
        result = self.call('AJOUTER_ENFANT', child.serialize())
498
        return int(result.strip())
499

  
500
    def update_child(self, child):
501
        if not hasattr(child, 'id'):
502
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
503
        result = self.call('MODIFIER_ENFANT', unicode(child.id), child.serialize())
504
        return result.strip() == 'OK'
505

  
506
    @property
507
    @object_cached
508
    def families(self):
509
        '''Dictionary of all families indexed by their id.
510

  
511
           After the first use, the value is cached. Use clear_cache() to reset
512
           it.
513
        '''
514

  
515
        return self.get_families()
516

  
517
    def get_families(self, id_famille=0, full=False):
518
        '''Get families informations.
519
           There is no caching.
520

  
521
           id_famille - if not 0, the family with this id is retrieved. If 0
522
           all families are retrieved. Default to 0.
523
           full - If True return all the columns of the family table. Default
524
           to False.
525
        '''
526
        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
527
        families = self('LISTER_FAMILLES',
528
                Family,
529
                args=(','.join([x[0] for x in columns]), id_famille))
530
        return dict([(int(x), x) for x in families])
531

  
532
    def get_children(self, id_famille=0):
533
        columns = Child.COLUMNS
534
        if id_famille == 0:
535
            children = self('LISTER_ENFANTS',
536
                Child,
537
                args=((','.join([x[0] for x in columns])),))
538
        else:
539
            children = self('LISTER_ENFANTS_FAMILLE',
540
                Child,
541
                args=(id_famille, (','.join([x[0] for x in columns]))))
542
        return dict([(int(x), x) for x in children])
543

  
544
    def get_urgent_contacts(self, id_enfant):
545
        columns = UrgentContact.COLUMNS
546
        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
547
                UrgentContact,
548
                args=((id_enfant, ','.join([x[0] for x in columns]))))
549
        return dict([(int(x), x) for x in urgent_contacts])
550

  
551
    @property
552
    @object_cached
553
    def invoices(self):
554
        '''Dictionnary of all invoices indexed by their id.
555

  
556
           After the first use, the value is cached. Use clear_cache() to reset
557
           it.
558
        '''
559
        invoices = self.get_invoices()
560
        for invoice in invoices.values():
561
            invoice.famille = self.families[invoice.id_famille]
562
        return invoices
563

  
564
    def new_code_interne(self):
565
        max_ci = 0
566
        for family in self.families.values():
567
            try:
568
                max_ci = max(max_ci, int(family.code_interne))
569
            except:
570
                pass
571
        return '%05d' % (max_ci+1)
572

  
573
    def get_invoices(self, id_famille=0, state='TOUTES'):
574
        '''Get invoices informations.
575

  
576
           id_famille - If value is not 0, only invoice for the family with
577
           this id are retrieved. If value is 0, invoices for all families are
578
           retrieved. Default to 0.
579
           etat - state of the invoices to return, possible values are
580
           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
581
        '''
582
        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
583
            args=(id_famille, state))
584
        invoices = list(invoices)
585
        for invoice in invoices:
586
            invoice.famille = self.families[invoice.id_famille]
587
        return dict(((int(x), x) for x in invoices))
588

  
589
    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
590
    def factures_detail(self, invoices):
591
        '''Retrieve details of some invoice'''
592
        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
593
        try:
594
            tree = etree.fromstring(data.encode('utf8'))
595
            for invoice, facture_node in zip(invoices, tree.findall('facture')):
596
                rows = []
597
                for ligne in facture_node.findall('detail_facture/ligne'):
598
                    row = []
599
                    rows.append(row)
600
                    for header in self.FACTURE_DETAIL_HEADERS:
601
                        if header in ligne.attrib:
602
                            row.append((header, ligne.attrib[header]))
603
                etablissement = facture_node.find('detail_etablissements/etablissement')
604
                if etablissement is not None:
605
                    nom = etablissement.get('nom').strip()
606
                else:
607
                    nom = ''
608
                d = { 'etablissement': nom, 'lignes': rows }
609
                invoice._detail = d
610
        except Exception, e:
611
            raise DominoException('Exception when retrieving invoice details', e)
612

  
613
    def get_family_by_mail(self, email):
614
        '''Return the first whose one email attribute matches the given email'''
615
        for famille in self.families.values():
616
            if email in (famille.email_pere, famille.email_mere,
617
                    famille.adresse_internet):
618
                return famille
619
        return None
620

  
621
    def get_family_by_code_interne(self, code_interne):
622
        '''Return the first whose one email attribute matches the given email'''
623
        for famille in self.families.values():
624
            if getattr(famille, 'code_interne', None) == code_interne:
625
                return famille
626
        return None
627

  
628
    def pay_invoice(self, id_invoices, amount, other_information, date=None):
629
        '''Notify Domino of the payment of some invoices.
630

  
631
           id_invoices - integer if of the invoice or Invoice instances
632
           amount - amount as a Decimal object
633
           other_information - free content to attach to the payment, like a
634
           bank transaction number for correlation.
635
           date - date of the payment, must be a datetime object. If None,
636
           now() is used. Default to None.
637
        '''
638

  
639
        if not date:
640
            date = datetime.datetime.now()
641
        due = sum([self.invoices[int(id_invoice)].reste_du
642
                 for id_invoice in id_invoices])
643
        if Decimal(amount) == Decimal(due):
644
            return self('SOLDER_FACTURE', None, args=(str(amount), 
645
                    ''.join([ '%s;' % int(x) for x in id_invoices]),
646
                    date.strftime('%Y-%m-%d'), other_information))
647
        else:
648
            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
extra/modules/admin.py
1
import os
2

  
3
from quixote import get_publisher, redirect
4
from quixote.directory import Directory
5
from quixote.html import htmltext, TemplateIO
6

  
7
import wcs.admin.root
8
import wcs.root
9
from wcs.roles import get_user_roles
10

  
11
from qommon import _
12
from qommon import errors, get_cfg
13
from qommon.form import *
14

  
15
import wcs.admin.settings
16
from wcs.formdef import FormDef
17
from wcs.categories import Category
18
from qommon.backoffice.menu import html_top
19

  
20
from events import get_default_event_tags
21
import re
22
from abelium_domino_ui import AbeliumDominoDirectory
23

  
24
class AdminRootDirectory(wcs.admin.root.RootDirectory):
25
    def __init__(self):
26
        self._q_exports = wcs.admin.root.RootDirectory._q_exports
27
        self.menu_items[-1] = ('/', N_('Publik'))
28

  
29
    def get_intro_text(self):
30
        return _('Welcome on Publik administration interface')
31

  
32

  
33
class PanelDirectory(Directory):
34
    _q_exports = ['', 'update', 'announces', 'permissions', 'event_keywords',
35
            'announce_themes', 'strongbox', 'clicrdv', 'domino']
36
    label = N_('Control Panel')
37

  
38
    domino = AbeliumDominoDirectory()
39

  
40
    def _verify_mask(self, form):
41
        if form.is_submitted():
42
            if not re.match("[0-9Xx]*$", form.get('mobile_mask') or ''):
43
                form.set_error('mobile_mask', _('Invalid value'))
44
            else:
45
                string_value = form.get_widget('mobile_mask').value
46
                if string_value:
47
                    form.get_widget('mobile_mask').value = string_value.upper()
48

  
49
    def announces(self):
50
        announces_cfg = get_cfg('announces', {})
51
        sms_cfg = get_cfg('sms', {})
52
        form = Form(enctype='multipart/form-data')
53
        hint = ""
54
        if sms_cfg.get('mode','') in ("none",""):
55
            hint = htmltext(_('You must also <a href="%s">configure your SMS provider</a>') % "../settings/sms")
56

  
57
        form.add(CheckboxWidget, 'sms_support', title = _('SMS support'),
58
                hint = hint, value = announces_cfg.get('sms_support', 0))
59
        form.add(StringWidget, 'mobile_mask', title = _('Mask for mobile numbers'),
60
                hint = _('example: 06XXXXXXXX'),
61
                value = announces_cfg.get('mobile_mask',''))
62
        form.add_submit('submit', _('Submit'))
63
        form.add_submit('cancel', _('Cancel'))
64

  
65
        self._verify_mask(form)
66

  
67
        if form.get_widget('cancel').parse():
68
            return redirect('..')
69

  
70
        if not form.is_submitted() or form.has_errors():
71
            get_response().breadcrumb.append(('aq/announces', _('Announces Options')))
72
            html_top('settings', _('Announces Options'))
73
            r = TemplateIO(html=True)
74
            r += htmltext('<h2>%s</h2>') % _('Announces Options')
75
            r += form.render()
76
            return r.getvalue()
77
        else:
78
            from wcs.admin.settings import cfg_submit
79
            cfg_submit(form, 'announces', ('sms_support','mobile_mask'))
80
            return redirect('..')
81

  
82
    def permissions(self):
83
        permissions_cfg = get_cfg('aq-permissions', {})
84
        form = Form(enctype='multipart/form-data')
85
        form.add(SingleSelectWidget, 'forms', title = _('Admin role for forms'),
86
                value = permissions_cfg.get('forms', None),
87
                options = [(None, _('Nobody'), None)] + get_user_roles())
88
        form.add(SingleSelectWidget, 'events', title = _('Admin role for events'),
89
                value = permissions_cfg.get('events', None),
90
                options = [(None, _('Nobody'), None)] + get_user_roles())
91
        form.add(SingleSelectWidget, 'links', title = _('Admin role for links'),
92
                value = permissions_cfg.get('links', None),
93
                options = [(None, _('Nobody'), None)] + get_user_roles())
94
        form.add(SingleSelectWidget, 'announces', title = _('Admin role for announces'),
95
                value = permissions_cfg.get('announces', None),
96
                options = [(None, _('Nobody'), None)] + get_user_roles())
97
        form.add(SingleSelectWidget, 'payments', title = _('Admin role for payments'),
98
                value = permissions_cfg.get('payments', None),
99
                options = [(None, _('Nobody'), None)] + get_user_roles())
100
        form.add(SingleSelectWidget, 'strongbox', title = _('Admin role for strongbox'),
101
                value = permissions_cfg.get('strongbox', None),
102
                options = [(None, _('Nobody'), None)] + get_user_roles())
103
        form.add_submit('submit', _('Submit'))
104
        form.add_submit('cancel', _('Cancel'))
105

  
106
        if form.get_widget('cancel').parse():
107
            return redirect('..')
108

  
109
        if not form.is_submitted() or form.has_errors():
110
            get_response().breadcrumb.append(('aq/permissions', _('Permissions')))
111
            html_top('settings', _('Permissions'))
112
            r = TemplateIO(html=True)
113
            r += htmltext('<h2>%s</h2>') % _('Permissions')
114
            r += form.render()
115
            return r.getvalue()
116
        else:
117
            from wcs.admin.settings import cfg_submit
118
            cfg_submit(form, 'aq-permissions',
119
                        ('forms', 'events', 'links', 'announces', 'payments', 'strongbox'))
120
            return redirect('..')
121

  
122
    def event_keywords(self):
123
        misc_cfg = get_cfg('misc', {})
124
        form = Form(enctype='multipart/form-data')
125
        form.add(WidgetList, 'event_tags', title = _('Event Keywords'),
126
                value = misc_cfg.get('event_tags', get_default_event_tags()),
127
                elemnt_type = StringWidget,
128
                add_element_label = _('Add Keyword'),
129
                element_kwargs = {str('render_br'): False, str('size'): 30})
130

  
131
        form.add_submit('submit', _('Submit'))
132
        form.add_submit('cancel', _('Cancel'))
133

  
134
        if form.get_widget('cancel').parse():
135
            return redirect('..')
136

  
137
        if not form.is_submitted() or form.has_errors():
138
            get_response().breadcrumb.append(('aq/event_keywords', _('Event Keywords')))
139
            html_top('settings', _('Event Keywords'))
140
            r = TemplateIO(html=True)
141
            r += htmltext('<h2>%s</h2>') % _('Event Keywords')
142
            r += form.render()
143
            return r.getvalue()
144
        else:
145
            from wcs.admin.settings import cfg_submit
146
            cfg_submit(form, 'misc', ('event_tags',))
147
            return redirect('..')
148

  
149
    def announce_themes(self):
150
        misc_cfg = get_cfg('misc', {})
151
        form = Form(enctype='multipart/form-data')
152
        form.add(WidgetList, 'announce_themes', title = _('Announce Themes'),
153
                value = misc_cfg.get('announce_themes', []),
154
                elemnt_type = StringWidget,
155
                add_element_label = _('Add Theme'),
156
                element_kwargs = {str('render_br'): False, str('size'): 30})
157

  
158
        form.add_submit('submit', _('Submit'))
159
        form.add_submit('cancel', _('Cancel'))
160

  
161
        if form.get_widget('cancel').parse():
162
            return redirect('..')
163

  
164
        if not form.is_submitted() or form.has_errors():
165
            get_response().breadcrumb.append(('aq/announce_themes', _('Announce Themes')))
166
            html_top('settings', _('Announce Themes'))
167
            r = TemplateIO(html=True)
168
            r += htmltext('<h2>%s</h2>') % _('Announce Themes')
169
            r += form.render()
170
            return r.getvalue()
171
        else:
172
            from wcs.admin.settings import cfg_submit
173
            cfg_submit(form, 'misc', ('announce_themes',))
174
            return redirect('..')
175

  
176
    def strongbox(self):
177
        if not get_publisher().has_site_option('strongbox'):
178
            raise errors.TraversalError()
179
        misc_cfg = get_cfg('misc', {})
180
        form = Form(enctype='multipart/form-data')
181
        form.add(CheckboxWidget, 'aq-strongbox', title=_('Strongbox Support'),
182
                value=misc_cfg.get('aq-strongbox'), required=False)
183

  
184
        form.add_submit('submit', _('Submit'))
185
        form.add_submit('cancel', _('Cancel'))
186

  
187
        if form.get_widget('cancel').parse():
188
            return redirect('..')
189

  
190
        if not form.is_submitted() or form.has_errors():
191
            get_response().breadcrumb.append(('aq/strongbox', _('Strongbox Support')))
192
            html_top('settings', _('Strongbox Support'))
193
            r = TemplateIO(html=True)
194
            r += htmltext('<h2>%s</h2>') % _('Strongbox Support')
195
            r += form.render()
196
            return r.getvalue()
197
        else:
198
            from wcs.admin.settings import cfg_submit
199
            cfg_submit(form, 'misc', ('aq-strongbox',))
200
            return redirect('..')
201

  
202
    def clicrdv(self):
203
        if not get_publisher().has_site_option('clicrdv'):
204
            raise errors.TraversalError()
205
        misc_cfg = get_cfg('misc', {})
206
        form = Form(enctype='multipart/form-data')
207
        form.add(SingleSelectWidget, 'aq-clicrdv-server', title=_('ClicRDV Server'),
208
                value=misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), required=True,
209
                options=[(str('www.clicrdv.com'), _('Production Server')),
210
                         (str('sandbox.clicrdv.com'), _('Sandbox Server'))])
211
        form.add(StringWidget, 'aq-clicrdv-api-key', title=_('API Key'),
212
                value=misc_cfg.get('aq-clicrdv-api-key'), required=False,
213
                size=40, hint=_('Empty to disable ClicRDV support'))
214
        form.add(StringWidget, 'aq-clicrdv-api-username', title=_('Username'),
215
                value=misc_cfg.get('aq-clicrdv-api-username'), required=False)
216
        form.add(StringWidget, 'aq-clicrdv-api-password', title=_('Password'),
217
                value=misc_cfg.get('aq-clicrdv-api-password'), required=False)
218

  
219
        form.add_submit('submit', _('Submit'))
220
        form.add_submit('cancel', _('Cancel'))
221

  
222
        if form.get_widget('cancel').parse():
223
            return redirect('..')
224

  
225
        if not form.is_submitted() or form.has_errors():
226
            get_response().breadcrumb.append(('aq/clicrdv', _('ClicRDV Integration')))
227
            html_top('settings', _('ClicRDV Integration'))
228
            r = TemplateIO(html=True)
229
            r += htmltext('<h2>%s</h2>') % _('ClicRDV Integration')
230
            r += form.render()
231
            r += htmltext('<p>%s</p>') % _('Available Interventions: ')
232
            try:
233
                from clicrdv import get_all_intervention_sets
234
                intervention_sets = get_all_intervention_sets()
235
                r += htmltext('<ul>')
236
                for s in intervention_sets:
237
                    r += htmltext('<li><strong>clicrdv_get_interventions_in_set(%s)</strong> - %s') % (
238
                            s['id'], s['name'])
239
                    r += htmltext('<ul>')
240
                    for n, intervention in s['interventions']:
241
                        r += htmltext('<li>%s (id: %s)</li>') % (intervention, n)
242
                    r += htmltext('</ul></li>')
243
                r += htmltext('</ul>')
244
            except Exception, e:
245
                r += htmltext('<p>%s (%s)</p>') % (
246
                        _('Cannot access to ClicRDV service'), str(e))
247
            return r.getvalue()
248
        else:
249
            from wcs.admin.settings import cfg_submit
250
            cfg_submit(form, 'misc', ('aq-clicrdv-server',
251
                                      'aq-clicrdv-api-key',
252
                                      'aq-clicrdv-api-username',
253
                                      'aq-clicrdv-api-password'))
254
            return redirect('..')
255

  
256

  
257
class SettingsDirectory(wcs.admin.settings.SettingsDirectory):
258
    def _q_index(self):
259
        r = TemplateIO(html=True)
260
        r += htmltext(super(SettingsDirectory, self)._q_index())
261
        r += htmltext('<div class="splitcontent-right">')
262
        r += htmltext('<div class="bo-block">')
263
        r += htmltext('<h2>%s</h2>') % _('Extra Options')
264
        r += htmltext('<ul>')
265
        r += htmltext('<li><a href="aq/announces">%s</a></li>') % _('Announces Options')
266
        r += htmltext('<li><a href="aq/permissions">%s</a></li>') % _('Permissions')
267
        r += htmltext('<li><a href="aq/event_keywords">%s</a></li>') % _('Event Keywords')
268
        r += htmltext('<li><a href="aq/announce_themes">%s</a></li>') % _('Announce Themes')
269
        if get_publisher().has_site_option('strongbox'):
270
            r += htmltext('<li><a href="aq/strongbox">%s</a></li>') % _('Strongbox Support')
271
        if get_publisher().has_site_option('clicrdv'):
272
            r += htmltext('<li><a href="aq/clicrdv">%s</a></li>') % _('ClicRDV Integration')
273
        if get_publisher().has_site_option('domino'):
274
            r += htmltext('<li><a href="aq/domino">%s</a></li>') % _('Abelium Domino Integration')
275
        r += htmltext('</ul>')
276
        r += htmltext('</div>')
277
        r += htmltext('</div>')
278
        return r.getvalue()
279

  
280
    def _q_lookup(self, component):
281
        if component == 'aq':
282
            return PanelDirectory()
283
        return super(SettingsDirectory, self)._q_lookup(component)
284

  
285
import categories_admin
extra/modules/agenda.py
1
import time
2
import datetime
3
from sets import Set
4

  
5
from quixote.directory import Directory
6
from quixote import get_publisher, get_request, redirect, get_session, get_response
7
from quixote.html import htmltext, TemplateIO
8

  
9
from qommon import _
10
from qommon import misc, template, errors, get_cfg
11
from qommon.form import *
12

  
13
from events import Event, RemoteCalendar, get_default_event_tags
14

  
15

  
16
class TagDirectory(Directory):
17
    def _q_lookup(self, component):
18
        events = Event.select()
19
        for remote_calendar in RemoteCalendar.select():
20
            if remote_calendar.events:
21
                events.extend(remote_calendar.events)
22
        self.events = [x for x in events if component in (x.keywords or [])]
23
        self.events.sort(lambda x,y: cmp(x.date_start, y.date_start))
24
        self.tag = component
25
        return self.display_events()
26

  
27
    def display_events(self):
28
        template.html_top(_('Agenda'))
29
        r = TemplateIO(html=True)
30
        if len(self.events) > 1:
31
            r += htmltext('<p id="nb-events">')
32
            r += _('%(nb)d events with %(keyword)s keyword') % {
33
                    'nb': len(self.events),
34
                    'keyword': self.tag
35
            }
36
            r += htmltext('</p>')
37

  
38
        if self.events:
39
            r += htmltext('<dl id="events">')
40
            for ev in self.events:
41
                r += htmltext(ev.as_html_dt_dd())
42
            r += htmltext('</dl>')
43
        else:
44
            r += htmltext('<p id="nb-events">')
45
            r += _('No event registered with the %s keyword.') % self.tag
46
            r += htmltext('</p>')
47
        return r.getvalue()
48

  
49

  
50
class AgendaDirectory(Directory):
51
    _q_exports = ['', 'icalendar', 'tag', 'atom', 'filter']
52

  
53
    year = None
54
    month = None
55

  
56
    tag = TagDirectory()
57

  
58
    def _q_traverse(self, path):
59
        get_response().breadcrumb.append(('agenda/', _('Agenda')))
60
        self.year, self.month = time.localtime()[:2]
61
        if len(path) >= 1 and path[0].isdigit():
62
            self.year, self.month = (None, None)
63
            self.year = int(path[0])
64
            get_response().breadcrumb.append(('%s/' % self.year, self.year))
65
            path = path[1:]
66
            if len(path) >= 1 and path[0] in [str(x) for x in range(1, 13)]:
67
                self.month = int(path[0])
68
                get_response().breadcrumb.append(('%s/' % self.month,
69
                            misc.get_month_name(self.month)))
70
                path = path[1:]
71
            if len(path) == 0:
72
                return redirect(get_request().get_path() + '/')
73
        return Directory._q_traverse(self, path)
74

  
75
    def _q_index(self):
76
        if self.month:
77
            r = TemplateIO(html=True)
78
            r += htmltext(self.display_month_links())
79
            r += htmltext(self.display_month())
80
            return r.getvalue()
81
        else:
82
            return redirect('..')
83

  
84
    def display_month(self):
85
        template.html_top(_('Agenda'))
86
        events = Event.select()
87
        remote_cal = get_request().form.get('cal')
88
        if remote_cal != 'local':
89
            if remote_cal:
90
                try:
91
                    events = RemoteCalendar.get(remote_cal).events
92
                except KeyError:
93
                    raise errors.TraversalError()
94
                if not events:
95
                    events = []
96
            else:
97
                for remote_calendar in RemoteCalendar.select():
98
                    if remote_calendar.events:
99
                        events.extend(remote_calendar.events)
100
        events = [x for x in events if x.in_month(self.year, self.month)]
101
        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
102

  
103
        r = TemplateIO(html=True)
104
        if events:
105
            if len(events) > 1:
106
                r += htmltext('<p id="nb-events">')
107
                r += _('%(nb)d events for %(month_name)s %(year)s') % {
108
                        'nb': len(events),
109
                        'month_name': misc.get_month_name(self.month),
110
                        'year': self.year}
111
                r += htmltext('</p>')
112

  
113
            r += htmltext('<dl id="events">')
114
            for ev in events:
115
                r += htmltext(ev.as_html_dt_dd())
116
            r += htmltext('</dl>')
117
        else:
118
            r += htmltext('<p id="nb-events">')
119
            r += _('No event registered for the month of %s.') % '%s %s' % (
120
                    misc.get_month_name(self.month), self.year)
121
            r += htmltext('</p>')
122

  
123
        root_url = get_publisher().get_root_url()
124
        r += htmltext('<div id="agenda-subs">')
125
        r += htmltext('<p>')
126
        r += _('You can subscribe to this calendar:')
127
        r += htmltext('</p>')
128
        r += htmltext('<ul>')
129
        r += htmltext('  <li><a href="%sagenda/icalendar" id="par_ical">%s</a></li>') % (
130
                root_url, _('iCalendar'))
131
        r += htmltext('  <li><a href="%sagenda/atom" id="par_rss">%s</a></li>') % (
132
                root_url, _('Feed'))
133
        r += htmltext('</ul>')
134
        r += htmltext('</div>')
135
        return r.getvalue()
136

  
137
    def display_month_links(self):
138
        today = datetime.date(*(time.localtime()[:2] + (1,)))
139
        r = TemplateIO(html=True)
140
        r += htmltext('<ul id="month-links">')
141
        for i in range(12):
142
            r += htmltext('<li>')
143
            if (today.year, today.month) == (self.year, self.month):
144
                r += htmltext('<strong>')
145
                r += '%s %s' % (misc.get_month_name(today.month), today.year)
146
                r += htmltext('</strong>')
147
            else:
148
                root_url = get_publisher().get_root_url()
149
                r += htmltext('<a href="%sagenda/%s/%s/">') % (root_url, today.year, today.month)
150
                r += '%s %s' % (misc.get_month_name(today.month), today.year)
151
                r += htmltext('</a>')
152
            r += htmltext('</li>')
153
            today += datetime.timedelta(31)
154
        r += htmltext('</ul>')
155
        return r.getvalue()
156

  
157
    def display_remote_calendars(self):
158
        r = TemplateIO(html=True)
159
        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
160
        if not remote_calendars:
161
            return
162
        remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
163
        r += htmltext('<p class="tags">')
164
        remote_cal = get_request().form.get('cal')
165
        agenda_root_url = get_publisher().get_root_url() + 'agenda/'
166
        if remote_cal:
167
            r += htmltext('<a href="%s">%s</a> ') % (agenda_root_url, _('All'))
168
        else:
169
            r += htmltext('<strong><a href="%s">%s</a></strong> ') % (agenda_root_url, _('All'))
170
        if remote_cal != 'local':
171
            r += htmltext('<a href="%s?cal=local">%s</a> ') % (agenda_root_url, _('Local'))
172
        else:
173
            r += htmltext('<strong><a href="%s?cal=local">%s</a></strong> ') % (agenda_root_url, _('Local'))
174
        for cal in remote_calendars:
175
            if remote_cal == str(cal.id):
176
                r += htmltext('<strong><a href="%s?cal=%s">%s</a></strong> ') % (
177
                        agenda_root_url, cal.id, cal.label)
178
            else:
179
                r += htmltext('<a href="%s?cal=%s">%s</a> ') % (agenda_root_url, cal.id, cal.label)
180
        r += htmltext('</p>')
181
        return r.getvalue()
182

  
183
    def icalendar(self):
184
        if not Event.keys():
185
            raise errors.TraversalError()
186
        response = get_response()
187
        response.set_content_type('text/calendar', 'utf-8')
188
        vcal = Event.as_vcalendar()
189
        if type(vcal) is unicode:
190
            return vcal.encode('utf-8')
191
        else:
192
            return vcal
193

  
194
    def atom(self):
195
        response = get_response()
196
        response.set_content_type('application/atom+xml')
197

  
198
        from pyatom import pyatom
199
        xmldoc = pyatom.XMLDoc()
200
        feed = pyatom.Feed()
201
        xmldoc.root_element = feed
202
        feed.title = get_cfg('misc', {}).get('sitename', 'Publik') + ' - ' + _('Agenda')
203
        feed.id = get_request().get_url()
204

  
205
        author_email = get_cfg('emails', {}).get('reply_to')
206
        if not author_email:
207
            author_email = get_cfg('emails', {}).get('from')
208
        if author_email:
209
            feed.authors.append(pyatom.Author(author_email))
210

  
211
        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
212

  
213
        year, month = time.localtime()[:2]
214
        nyear, nmonth = year, month+1
215
        if nmonth > 12:
216
            nyear, nmonth = nyear+1, 1
217

  
218
        events = [x for x in Event.select() if x.in_month(year, month) or x.in_month(nyear, nmonth)]
219
        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
220
        events.reverse()
221

  
222
        for item in events:
223
            entry = item.get_atom_entry()
224
            if entry is not None:
225
                feed.entries.append(entry)
226

  
227
        return str(feed)
228

  
229
    def filter(self, no_event=False):
230
        template.html_top(_('Agenda'))
231
        tags = get_cfg('misc', {}).get('event_tags')
232
        if not tags:
233
            tags = get_default_event_tags()
234
        remote_calendars = [x for x in RemoteCalendar.select() if x.label]
235

  
236
        form = Form(enctype='multipart/form-data')
237
        if tags and remote_calendars:
238
            form.widgets.append(HtmlWidget('<table id="agenda-filter"><tr><td>'))
239
        if tags:
240
            form.add(CheckboxesWidget, 'tags', title=_('Tags'),
241
                    options=[(x,x) for x in tags],
242
                    inline=False)
243
        if tags and remote_calendars:
244
            form.widgets.append(HtmlWidget('</td><td>'))
245
        if remote_calendars:
246
            remote_calendars.sort(lambda x,y: cmp(x.label, y.label))
247
            form.add(CheckboxesWidget, 'calendars', title=_('Calendars'),
248
                    options=[('local', _('Local'))] + [(x.id, x.label) for x in remote_calendars],
249
                    inline=False)
250
        if tags and remote_calendars:
251
            form.widgets.append(HtmlWidget('</td></tr></table>'))
252

  
253
        form.add_submit('submit', _('Submit'))
254
        form.add_submit('cancel', _('Cancel'))
255
        if form.get_widget('cancel').parse():
256
            return redirect('.')
257

  
258
        if no_event or not form.is_submitted():
259
            r = TemplateIO(html=True)
260
            if no_event:
261
                r += htmltext('<p id="nb-events">')
262
                r += _('No events matching the filter.')
263
                r += htmltext('</p>')
264
            r += form.render()
265
            return r.getvalue()
266
        else:
267
            return self.filter_submitted(form, tags, remote_calendars)
268

  
269
    def filter_submitted(self, form, tags, remote_calendars):
270
        if remote_calendars:
271
            selected_remote_calendars = form.get_widget('calendars').parse()
272
            events = []
273
            for remote_calendar in selected_remote_calendars:
274
                if remote_calendar == 'local':
275
                    events.extend(Event.select())
276
                else:
277
                    try:
278
                        events.extend(RemoteCalendar.get(remote_calendar).events)
279
                    except KeyError:
280
                        pass
281
        else:
282
            events = Event.select()
283

  
284
        events = [x for x in events if x.after_today()]
285

  
286
        if tags:
287
            selected_tags = Set(form.get_widget('tags').parse())
288
            if selected_tags and len(selected_tags) != len(tags):
289
                events = [x for x in events if Set(x.keywords).intersection(selected_tags)]
290

  
291
        events.sort(lambda x,y: cmp(x.date_start, y.date_start))
292

  
293
        r = TemplateIO(html=True)
294

  
295
        if len(events) > 1:
296
            r += htmltext('<p id="nb-events">')
297
            r += htmltext(_('%(nb)d events')) % {'nb': len(events)}
298
            r += htmltext('</p>')
299

  
300
        if events:
301
            r += htmltext('<dl id="events">')
302
            for ev in events:
303
                r += htmltext(ev.as_html_dt_dd())
304
            r += htmltext('</dl>')
305
            return r.getvalue()
306
        else:
307
            return self.filter(no_event=True)
extra/modules/announces.py
1
import time
2

  
3
from quixote import get_publisher
4

  
5
from quixote.html import htmlescape
6

  
7
from qommon import _
8
from qommon.storage import StorableObject
9
from qommon import get_cfg, get_logger
10
from qommon import errors
11
from qommon import misc
12

  
13
from qommon import emails
14
from qommon.sms import SMS
15
from qommon.admin.emails import EmailsDirectory
16

  
17
class AnnounceSubscription(StorableObject):
18
    _names = 'announce-subscriptions'
19
    _indexes = ['user_id']
20

  
21
    user_id = None
22
    email = None
23
    sms = None
24
    enabled = True
25
    enabled_sms = False
26
    enabled_themes = None
27

  
28
    def remove(self, type=None):
29
        """ type (string) : email or sms """
30
        if type == "email":
31
            self.email = None
32
        elif type == "sms":
33
            self.sms = None
34
            self.enabled_sms = False
35
        if not type or (not self.sms and not self.email):
36
            self.remove_self()
37
        else:
38
            self.store()
39
        
40
    def get_user(self):
41
        if self.user_id:
42
            try:
43
                return get_publisher().user_class.get(self.user_id)
44
            except KeyError:
45
                return None
46
        return None
47
    user = property(get_user)
48

  
49

  
50
class Announce(StorableObject):
51
    _names = 'announces'
52

  
53
    title = None
54
    text = None
55

  
56
    hidden = False
57

  
58
    publication_time = None
59
    modification_time = None
60
    expiration_time = None
61
    sent_by_email_time = None
62
    sent_by_sms_time = None
63
    theme = None
64

  
65
    position = None
66

  
67
    def sort_by_position(cls, links):
68
        def cmp_position(x, y):
69
            if x.position == y.position:
70
                return 0
71
            if x.position is None:
72
                return 1
73
            if y.position is None:
74
                return -1
75
            return cmp(x.position, y.position)
76
        links.sort(cmp_position)
77
    sort_by_position = classmethod(sort_by_position)
78

  
79
    def get_atom_entry(self):
80
        from pyatom import pyatom
81
        entry = pyatom.Entry()
82
        entry.id = self.get_url()
83
        entry.title = self.title
84

  
85
        entry.content.attrs['type'] = 'html'
86
        entry.content.text = str('<p>' + htmlescape(
87
                    unicode(self.text, get_publisher().site_charset).encode('utf-8')) + '</p>')
88

  
89
        link = pyatom.Link(self.get_url())
90
        entry.links.append(link)
91

  
92
        if self.publication_time:
93
            entry.published = misc.format_time(self.publication_time,
94
                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
95
                    gmtime = True)
96

  
97
        if self.modification_time:
98
            entry.updated = misc.format_time(self.modification_time,
99
                    '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
100
                    gmtime = True)
101

  
102
        return entry
103

  
104
    def get_url(self):
105
        return '%s/announces/%s/' % (get_publisher().get_frontoffice_url(), self.id)
106

  
107
    def store(self):
108
        self.modification_time = time.gmtime()
109
        StorableObject.store(self)
110

  
111
    def email(self, job=None):
112
        self.sent_by_email_time = time.gmtime()
113
        StorableObject.store(self)
114

  
115
        data = {
116
            'title': self.title,
117
            'text': self.text
118
        }
119

  
120
        subscribers = AnnounceSubscription.select(lambda x: x.enabled)
121

  
122
        rcpts = []
123
        for l in subscribers:
124
            if self.theme:
125
                if l.enabled_themes is not None:
126
                    if self.theme not in l.enabled_themes:
127
                        continue
128
            if l.user and l.user.email:
129
                rcpts.append(l.user.email)
130
            elif l.email:
131
                rcpts.append(l.email)
132

  
133
        emails.custom_ezt_email('aq-announce', data, email_rcpt = rcpts, hide_recipients = True)
134

  
135
    def sms(self, job=None):
136
        self.sent_by_sms_time = time.gmtime()
137
        StorableObject.store(self)
138

  
139
        subscribers = AnnounceSubscription.select(lambda x: x.enabled_sms)
140

  
141
        rcpts = []
142
        for sub in subscribers:
143
            if self.theme:
144
                if sub.enabled_themes is not None:
145
                    if self.theme not in sub.enabled_themes:
146
                        continue
147
            if sub.sms:
148
                rcpts.append(sub.sms)
149

  
150
        sms_cfg = get_cfg('sms', {})
151
        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
152
        message = "%s: %s" % (self.title, self.text)
153
        mode = sms_cfg.get('mode', 'none')
154
        sms = SMS.get_sms_class(mode)
155
        try:
156
            sms.send(sender, rcpts, message[:160])
157
        except errors.SMSError, e:
158
            get_logger().error(e)
159

  
160
    def get_published_announces(cls):
161
        announces = cls.select(lambda x: not x.hidden)
162
        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
163
                    y.publication_time or y.modification_time))
164
        announces = [x for x in announces if x.publication_time < time.gmtime()
165
                     and (x.expiration_time is None or x.expiration_time > time.gmtime())]
166
        announces.reverse()
167
        return announces
168
    get_published_announces = classmethod(get_published_announces)
169

  
170

  
171
EmailsDirectory.register('aq-announce',
172
        N_('Publication of announce to subscriber'),
173
        N_('Available variables: title, text'),
174
        default_subject = N_('Announce: [title]'),
175
        default_body = N_("""\
176
[text]
177

  
178
-- 
179
This is an announce sent to you by your city, you can opt to not receive
180
those messages anymore on the city website.
181
"""))
182

  
extra/modules/announces_ui.py
1
from quixote import get_request, get_response, get_session, redirect
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import htmltext, TemplateIO
4

  
5
import wcs
6

  
7
from qommon import _
8
from qommon.backoffice.menu import html_top
9
from qommon.admin.menu import command_icon
10
from qommon import get_cfg
11
from qommon import errors
12
from qommon.form import *
13
from qommon.afterjobs import AfterJob
14

  
15
from announces import Announce, AnnounceSubscription
16

  
17

  
18
class SubscriptionDirectory(Directory):
19
    _q_exports = ['delete_email', "delete_sms"]
20

  
21
    def __init__(self, subscription):
22
        self.subscription = subscription
23

  
24
    def delete_email(self):
25
        form = Form(enctype='multipart/form-data')
26
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
27
                        'You are about to delete this subscription.')))
28
        form.add_submit('submit', _('Submit'))
29
        form.add_submit('cancel', _('Cancel'))
30
        if form.get_submit() == 'cancel':
31
            return redirect('..')
32
        if not form.is_submitted() or form.has_errors():
33
            get_response().breadcrumb.append(('delete', _('Delete')))
34
            html_top('announces', title = _('Delete Subscription'))
35
            r = TemplateIO(html=True)
36
            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
37
            r += form.render()
38
            return r.getvalue()
39
        else:
40
            self.subscription.remove("email")
41
            return redirect('..')
42

  
43
    def delete_sms(self):
44
        form = Form(enctype='multipart/form-data')
45
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
46
                        'You are about to delete this subscription.')))
47
        form.add_submit('submit', _('Submit'))
48
        form.add_submit('cancel', _('Cancel'))
49
        if form.get_submit() == 'cancel':
50
            return redirect('..')
51
        if not form.is_submitted() or form.has_errors():
52
            get_response().breadcrumb.append(('delete', _('Delete')))
53
            html_top('announces', title = _('Delete Subscription'))
54
            r = TemplateIO(html=True)
55
            r += htmltext('<h2>%s</h2>') % _('Deleting Subscription')
56
            r += form.render()
57
            return r.getvalue()
58
        else:
59
            self.subscription.remove("sms")
60
            return redirect('..')
61

  
62

  
63
class SubscriptionsDirectory(Directory):
64
    _q_exports = ['']
65

  
66
    def _q_traverse(self, path):
67
        get_response().breadcrumb.append(('subscriptions', _('Subscriptions')))
68
        return Directory._q_traverse(self, path)
69

  
70
    def _q_index(self):
71
        html_top('announces', _('Announces Subscribers'))
72
        r = TemplateIO(html=True)
73

  
74
        r += htmltext('<h2>%s</h2>') % _('Announces Subscribers')
75

  
76
        subscribers = AnnounceSubscription.select()
77
        r += htmltext('<ul class="biglist" id="subscribers-list">')
78
        for l in subscribers:
79
            if l.email:
80
                if l.enabled is False:
81
                    r += htmltext('<li class="disabled">')
82
                else:
83
                    r += htmltext('<li>')
84
                r += htmltext('<strong class="label">')
85
                if l.user:
86
                    r += l.user.display_name
87
                elif l.email:
88
                    r += l.email
89
                r += htmltext('</strong>')
90
                r += htmltext('<p class="details">')
91
                if l.user:
92
                    r += l.user.email
93
                r += htmltext('</p>')
94
                r += htmltext('<p class="commands">')
95
                r += command_icon('%s/delete_email' % l.id, 'remove', popup = True)
96
                r += htmltext('</p></li>')
97
                r += htmltext('</li>')
98
            if l.sms:
99
                if l.enabled_sms is False:
100
                    r += htmltext('<li class="disabled">')
101
                else:
102
                    r += htmltext('<li>')
103
                r += htmltext('<strong class="label">')
104
                if l.user:
105
                    r += l.user.display_name
106
                elif l.email:
107
                    r += l.email
108
                r += htmltext('</strong>')
109
                r += htmltext('<p class="details">')
110
                r += l.sms
111
                r += htmltext('</p>')
112
                r += htmltext('<p class="commands">')
113
                r += command_icon('%s/delete_sms' % l.id, 'remove', popup = True)
114
                r += htmltext('</p></li>')
115
                r += htmltext('</li>')
116
        r += htmltext('</ul>')
117
        return r.getvalue()
118

  
119
    def _q_lookup(self, component):
120
        try:
121
            sub = AnnounceSubscription.get(component)
122
        except KeyError:
123
            raise errors.TraversalError()
124
        get_response().breadcrumb.append((str(sub.id), str(sub.id)))
125
        return SubscriptionDirectory(sub)
126

  
127
    def listing(self):
128
        return redirect('.')
129

  
130
class AnnounceDirectory(Directory):
131
    _q_exports = ['', 'edit', 'delete', 'email', 'sms']
132

  
133
    def __init__(self, announce):
134
        self.announce = announce
135

  
136
    def _q_index(self):
137
        form = Form(enctype='multipart/form-data')
138
        get_response().filter['sidebar'] = self.get_sidebar()
139

  
140
        if self.announce.sent_by_email_time is None:
141
            form.add_submit('email', _('Send email'))
142

  
143
        announces_cfg = get_cfg('announces', {})
144
        if announces_cfg.get('sms_support', 0) and self.announce.sent_by_sms_time is None:
145
            form.add_submit('sms', _('Send SMS'))
146

  
147
        if form.get_submit() == 'edit':
148
            return redirect('edit')
149
        if form.get_submit() == 'delete':
150
            return redirect('delete')
151
        if form.get_submit() == 'email':
152
            return redirect('email')
153
        if form.get_submit() == 'sms':
154
            return redirect('sms')
155

  
156
        html_top('announces', title = _('Announce: %s') % self.announce.title)
157
        r = TemplateIO(html=True)
158
        r += htmltext('<h2>%s</h2>') % _('Announce: %s') % self.announce.title
159
        r += htmltext('<div class="bo-block">')
160
        r += htmltext('<p>')
161
        r += self.announce.text
162
        r += htmltext('</p>')
163
        r += htmltext('</div>')
164

  
165
        if form.get_submit_widgets():
166
            r += form.render()
167

  
168
        return r.getvalue()
169

  
170
    def get_sidebar(self):
171
        r = TemplateIO(html=True)
172
        r += htmltext('<ul>')
173
        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
174
        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
175
        r += htmltext('</ul>')
176
        return r.getvalue()
177

  
178
    def email(self):
179
        if get_request().form.get('job'):
180
            try:
181
                job = AfterJob.get(get_request().form.get('job'))
182
            except KeyError:
183
                return redirect('..')
184
            html_top('announces', title = _('Announce: %s') % self.announce.title)
185
            r = TemplateIO(html=True)
186
            get_response().add_javascript(['jquery.js', 'afterjob.js'])
187
            r += htmltext('<dl class="job-status">')
188
            r += htmltext('<dt>')
189
            r += _(job.label)
190
            r += htmltext('</dt>')
191
            r += htmltext('<dd>')
192
            r += htmltext('<span class="afterjob" id="%s">') % job.id
193
            r += _(job.status)
194
            r += htmltext('</span>')
195
            r += htmltext('</dd>')
196
            r += htmltext('</dl>')
197

  
198
            r += htmltext('<div class="done">')
199
            r += htmltext('<a href="../">%s</a>') % _('Back')
200
            r += htmltext('</div>')
201

  
202
            return r.getvalue()
203
        else:
204
            job = get_response().add_after_job(
205
                    str(N_('Sending emails for announce')),
206
                    self.announce.email)
207
            return redirect('email?job=%s' % job.id)
208

  
209
    def sms(self):
210
        if get_request().form.get('job'):
211
            try:
212
                job = AfterJob.get(get_request().form.get('job'))
213
            except KeyError:
214
                return redirect('..')
215
            html_top('announces', title = _('Announce: %s') % self.announce.title)
216
            get_response().add_javascript(['jquery.js', 'afterjob.js'])
217
            r = TemplateIO(html=True)
218
            r += htmltext('<dl class="job-status">')
219
            r += htmltext('<dt>')
220
            r += _(job.label)
221
            r += htmltext('</dt>')
222
            r += htmltext('<dd>')
223
            r += htmltext('<span class="afterjob" id="%s">') % job.id
224
            r += _(job.status)
225
            r += htmltext('</span>')
226
            r += htmltext('</dd>')
227
            r += htmltext('</dl>')
228

  
229
            r += htmltext('<div class="done">')
230
            r += htmltext('<a href="../">%s</a>') % _('Back')
231
            r += htmltext('</div>')
232

  
233
            return r.getvalue()
234
        else:
235
            job = get_response().add_after_job(
236
                    str(N_('Sending sms for announce')),
237
                    self.announce.sms)
238
            return redirect('sms?job=%s' % job.id)
239

  
240
    def edit(self):
241
        form = self.form()
242
        if form.get_submit() == 'cancel':
243
            return redirect('.')
244

  
245
        if form.is_submitted() and not form.has_errors():
246
            self.submit(form)
247
            return redirect('..')
248

  
249
        html_top('announces', title = _('Edit Announce: %s') % self.announce.title)
250
        r = TemplateIO(html=True)
251
        r += htmltext('<h2>%s</h2>') % _('Edit Announce: %s') % self.announce.title
252
        r += form.render()
253
        return r.getvalue()
254

  
255
    def form(self):
256
        form = Form(enctype='multipart/form-data')
257
        form.add(StringWidget, 'title', title = _('Title'), required = True,
258
                value = self.announce.title)
259
        if self.announce.publication_time:
260
            pub_time = time.strftime(misc.date_format(), self.announce.publication_time)
261
        else:
262
            pub_time = None
263
        form.add(DateWidget, 'publication_time', title = _('Publication Time'),
264
                value = pub_time)
265
        if self.announce.expiration_time:
266
            exp_time = time.strftime(misc.date_format(), self.announce.expiration_time)
267
        else:
268
            exp_time = None
269
        form.add(DateWidget, 'expiration_time', title = _('Expiration Time'),
270
                value = exp_time)
271
        form.add(TextWidget, 'text', title = _('Text'), required = True,
272
                value = self.announce.text, rows = 10, cols = 70)
273
        if get_cfg('misc', {}).get('announce_themes'):
274
            form.add(SingleSelectWidget, 'theme', title = _('Announce Theme'),
275
                    value = self.announce.theme,
276
                    options = get_cfg('misc', {}).get('announce_themes'))
277
        form.add(CheckboxWidget, 'hidden', title = _('Hidden'),
278
                value = self.announce.hidden)
279
        form.add_submit('submit', _('Submit'))
280
        form.add_submit('cancel', _('Cancel'))
281
        return form
282

  
283
    def submit(self, form):
284
        for k in ('title', 'text', 'hidden', 'theme'):
285
            widget = form.get_widget(k)
286
            if widget:
287
                setattr(self.announce, k, widget.parse())
288
        for k in ('publication_time', 'expiration_time'):
289
            widget = form.get_widget(k)
290
            if widget:
291
                wid_time = widget.parse()
292
                if wid_time:
293
                    setattr(self.announce, k, time.strptime(wid_time, misc.date_format()))
294
                else:
295
                    setattr(self.announce, k, None)
296
        self.announce.store()
297

  
298
    def delete(self):
299
        form = Form(enctype='multipart/form-data')
300
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
301
                        'You are about to irrevocably delete this announce.')))
302
        form.add_submit('submit', _('Submit'))
303
        form.add_submit('cancel', _('Cancel'))
304
        if form.get_submit() == 'cancel':
305
            return redirect('..')
306
        if not form.is_submitted() or form.has_errors():
307
            get_response().breadcrumb.append(('delete', _('Delete')))
308
            html_top('announces', title = _('Delete Announce'))
309
            r = TemplateIO(html=True)
310
            r += htmltext('<h2>%s</h2>') % _('Deleting Announce: %s') % self.announce.title
311
            r += form.render()
312
            return r.getvalue()
313
        else:
314
            self.announce.remove_self()
315
            return redirect('..')
316

  
317

  
318
class AnnouncesDirectory(AccessControlled, Directory):
319
    _q_exports = ['', 'new', 'listing', 'subscriptions', 'update_order', 'log']
320
    label = N_('Announces')
321

  
322
    subscriptions = SubscriptionsDirectory()
323

  
324
    def is_accessible(self, user):
325
        from .backoffice import check_visibility
326
        return check_visibility('announces', user)
327

  
328
    def _q_access(self):
329
        user = get_request().user
330
        if not user:
331
            raise errors.AccessUnauthorizedError()
332

  
333
        if not self.is_accessible(user):
334
            raise errors.AccessForbiddenError(
335
                    public_msg = _('You are not allowed to access Announces Management'),
336
                    location_hint = 'backoffice')
337

  
338
        get_response().breadcrumb.append(('announces/', _('Announces')))
339

  
340
    def _q_index(self):
341
        html_top('announces', _('Announces'))
342
        r = TemplateIO(html=True)
343

  
344
        get_response().filter['sidebar'] = self.get_sidebar()
345

  
346
        announces = Announce.select()
347
        announces.sort(lambda x,y: cmp(x.publication_time or x.modification_time,
348
                    y.publication_time or y.modification_time))
349
        announces.reverse()
350

  
351
        r += htmltext('<ul class="biglist" id="announces-list">')
352
        for l in announces:
353
            announce_id = l.id
354
            if l.hidden:
355
                r += htmltext('<li class="disabled" class="biglistitem" id="itemId_%s">') % announce_id
356
            else:
357
                r += htmltext('<li class="biglistitem" id="itemId_%s">') % announce_id
358
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (l.id, l.title)
359
            if l.publication_time:
360
                r += htmltext('<p class="details">')
361
                r += time.strftime(misc.date_format(), l.publication_time)
362
                r += htmltext('</p>')
363
            r += htmltext('</li>')
364
        r += htmltext('</ul>')
365
        return r.getvalue()
366

  
367
    def get_sidebar(self):
368
        r = TemplateIO(html=True)
369
        r += htmltext('<ul id="sidebar-actions">')
370
        r += htmltext(' <li><a class="new-item" href="new">%s</a></li>') % _('New')
371
        r += htmltext(' <li><a href="subscriptions/">%s</a></li>') % _('Subscriptions')
372
        r += htmltext(' <li><a href="log">%s</a></li>') % _('Log')
373
        r += htmltext('</ul>')
374
        return r.getvalue()
375

  
376
    def log(self):
377
        announces = Announce.select()
378
        log = []
379
        for l in announces:
380
            if l.publication_time:
381
                log.append((l.publication_time, _('Publication'), l))
382
            if l.sent_by_email_time:
383
                log.append((l.sent_by_email_time, _('Email'), l))
384
            if l.sent_by_sms_time:
385
                log.append((l.sent_by_sms_time, _('SMS'), l))
386
        log.sort()
387

  
388
        get_response().breadcrumb.append(('log', _('Log')))
389
        html_top('announces', title = _('Log'))
390
        r = TemplateIO(html=True)
391

  
392
        r += htmltext('<table>')
393
        r += htmltext('<thead>')
394
        r += htmltext('<tr>')
395
        r += htmltext('<th>%s</th>') % _('Time')
396
        r += htmltext('<th>%s</th>') % _('Type')
397
        r += htmltext('<td></td>')
398
        r += htmltext('</tr>')
399
        r += htmltext('</thead>')
400
        r += htmltext('<tbody>')
401
        for log_time, log_type, log_announce in log:
402
            r += htmltext('<tr>')
403
            r += htmltext('<td>')
404
            r += misc.localstrftime(log_time)
405
            r += htmltext('</td>')
406
            r += htmltext('<td>')
407
            r += log_type
408
            r += htmltext('</td>')
409
            r += htmltext('<td>')
410
            r += htmltext('<a href="%s">%s</a>') % (log_announce.id, log_announce.title)
411
            r += htmltext('</td>')
412
            r += htmltext('</tr>')
413
        r += htmltext('</tbody>')
414
        r += htmltext('</table>')
415
        return r.getvalue()
416

  
417
    def update_order(self):
418
        request = get_request()
419
        new_order = request.form['order'].strip(';').split(';')
420
        announces = Announce.select()
421
        dict = {}
422
        for l in announces:
423
            dict[str(l.id)] = l
424
        for i, o in enumerate(new_order):
425
            dict[o].position = i + 1
426
            dict[o].store()
427
        return 'ok'
428

  
429
    def new(self):
430
        announce = Announce()
431
        announce.publication_time = time.gmtime()
432
        announce_ui = AnnounceDirectory(announce)
433

  
434
        form = announce_ui.form()
435
        if form.get_submit() == 'cancel':
436
            return redirect('.')
437

  
438
        if form.is_submitted() and not form.has_errors():
439
            announce_ui.submit(form)
440
            return redirect('%s/' % announce_ui.announce.id)
441

  
442
        get_response().breadcrumb.append(('new', _('New Announce')))
443
        html_top('announces', title = _('New Announce'))
444
        r = TemplateIO(html=True)
445
        r += htmltext('<h2>%s</h2>') % _('New Announce')
446
        r += form.render()
447
        return r.getvalue()
448

  
449
    def _q_lookup(self, component):
450
        try:
451
            announce = Announce.get(component)
452
        except KeyError:
453
            raise errors.TraversalError()
454
        get_response().breadcrumb.append((str(announce.id), announce.title))
455
        return AnnounceDirectory(announce)
456

  
457
    def listing(self):
458
        return redirect('.')
459

  
extra/modules/backoffice.py
1
import os
2

  
3
from quixote import get_publisher, redirect
4
from quixote.directory import Directory
5
from quixote.html import TemplateIO, htmltext
6

  
7
from qommon import _
8
from qommon.publisher import get_publisher_class
9

  
10
import wcs.backoffice.management
11
import wcs.root
12
from wcs.categories import Category
13
from wcs.formdef import FormDef
14

  
15
from qommon import get_cfg, errors
16
from qommon.form import *
17

  
18
CURRENT_USER = object()
19

  
20
def check_visibility(target, user=CURRENT_USER):
21
    if user is CURRENT_USER:
22
        user = get_request().user
23
    if not user:
24
        return False
25
    target = target.strip('/')
26
    if target == 'management':
27
        target = 'forms'
28
    if target == 'strongbox':
29
        if not get_publisher().has_site_option(target):
30
            # strongbox disabled in site-options.cfg
31
            return False
32
        if not get_cfg('misc', {}).get('aq-strongbox'):
33
            # strongbox disabled in settings panel
34
            return False
35
    admin_role = get_cfg('aq-permissions', {}).get(target, None)
36
    if not admin_role:
37
        return False
38
    if not (user.is_admin or admin_role in (user.roles or [])):
39
        return False
40
    return True
41

  
42

  
43
class BackofficeRootDirectory(wcs.backoffice.root.RootDirectory):
44
    def get_intro_text(self):
45
        return _('Welcome on Publik back office interface')
46

  
47
    def _q_index(self):
48
        if len(self.get_menu_items()) == 1:
49
            return redirect(self.get_menu_items()[0]['url'])
50
        return wcs.backoffice.root.RootDirectory._q_index(self)
51

  
52
    def home(self):
53
        return redirect('management/')
54

  
55
get_publisher_class().backoffice_directory_class = BackofficeRootDirectory
extra/modules/categories_admin.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2010  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from quixote import redirect
18
from quixote.directory import Directory
19
from quixote.html import TemplateIO, htmltext
20

  
21
from qommon import _
22
from qommon import misc
23
from wcs.categories import Category
24
from qommon.form import *
25
from qommon.backoffice.menu import html_top
26
from qommon.admin.menu import command_icon, error_page
27
import wcs.admin.categories
28

  
29
class CategoryUI:
30
    def __init__(self, category):
31
        self.category = category
32
        if self.category is None:
33
            self.category = Category()
34

  
35
    def get_form(self):
36
        form = Form(enctype='multipart/form-data')
37
        form.add(StringWidget, 'name', title = _('Category Name'), required = True, size=30,
38
                value = self.category.name)
39
        form.add(TextWidget, 'description', title = _('Description'), cols = 80, rows = 10,
40
                value = self.category.description)
41

  
42
        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
43
        if not homepage_redirect_url:
44
            form.add(SingleSelectWidget, 'homepage_position',
45
                        title=_('Position on homepage'),
46
                        value=self.category.get_homepage_position(),
47
                        options = [('1st', _('First Column')),
48
                                   ('2nd', _('Second Column')),
49
                                   ('side', _('Sidebar')),
50
                                   ('none', _('None'))])
51
            form.add(IntWidget, 'limit',
52
                        title=_('Limit number of items displayed on homepage'),
53
                        value=self.category.get_limit())
54

  
55
        form.add(StringWidget, 'redirect_url', size=32,
56
                title=_('URL Redirection'),
57
                hint=_('If set, redirect the site category page to the given URL.'),
58
                value=self.category.redirect_url)
59

  
60
        form.add_submit('submit', _('Submit'))
61
        form.add_submit('cancel', _('Cancel'))
62
        return form
63

  
64
    def submit_form(self, form):
65
        if self.category.id:
66
            self.category.name = form.get_widget('name').parse()
67
            category = self.category
68
        else:
69
            category = Category(name = form.get_widget('name').parse())
70

  
71
        name = form.get_widget('name').parse()
72
        category_names = [x.name for x in Category.select() if x.id != category.id]
73
        if name in category_names:
74
            form.get_widget('name').set_error(_('This name is already used'))
75
            raise ValueError()
76

  
77
        category.description = form.get_widget('description').parse()
78
        category.redirect_url = form.get_widget('redirect_url').parse()
79

  
80
        homepage_redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
81
        if not homepage_redirect_url:
82
            category.homepage_position = form.get_widget('homepage_position').parse()
83
            category.limit = form.get_widget('limit').parse()
84

  
85
        category.store()
86

  
87

  
88

  
89
class CategoryPage(wcs.admin.categories.CategoryPage):
90
    def __init__(self, component):
91
        self.category = Category.get(component)
92
        self.category_ui = CategoryUI(self.category)
93
        get_response().breadcrumb.append((component + '/', self.category.name))
94

  
95

  
96
class CategoriesDirectory(wcs.admin.categories.CategoriesDirectory):
97
    label = N_('Categories')
98

  
99
    def new(self):
100
        get_response().breadcrumb.append( ('categories/', _('Categories')) )
101
        get_response().breadcrumb.append( ('new', _('New')) )
102
        category_ui = CategoryUI(None)
103
        form = category_ui.get_form()
104
        if form.get_widget('cancel').parse():
105
            return redirect('.')
106

  
107
        if form.is_submitted() and not form.has_errors():
108
            try:
109
                category_ui.submit_form(form)
110
            except ValueError:
111
                pass
112
            else:
113
                return redirect('.')
114

  
115
        html_top('categories', title = _('New Category'))
116
        r = TemplateIO(html=True)
117
        r += htmltext('<h2>%s</h2>') % _('New Category')
118
        r += form.render()
119
        return r.getvalue()
120

  
121
    def _q_lookup(self, component):
122
        get_response().breadcrumb.append( ('categories/', _('Categories')) )
123
        return CategoryPage(component)
extra/modules/clicrdv.py
1
import base64
2
import datetime
3
import urllib2
4

  
5
try:
6
    import json
7
except ImportError:
8
    import simplejson as json
9

  
10
import time
11
import vobject
12

  
13
from qommon import _
14
from qommon import get_cfg
15
from qommon.misc import format_time
16
from qommon.form import *
17

  
18
from wcs.data_sources import register_data_source_function
19
from wcs.formdata import Evolution
20
from wcs.forms.common import FormStatusPage
21
from wcs.workflows import Workflow, WorkflowStatusItem, register_item_class
22

  
23
def get_clicrdv_req(url):
24
    misc_cfg = get_cfg('misc', {})
25

  
26
    url = 'https://%s/api/v1/%s' % (
27
                misc_cfg.get('aq-clicrdv-server', 'sandbox.clicrdv.com'), url)
28
    if '?' in url:
29
       url = url + '&apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
30
    else:
31
       url = url + '?apikey=%s&format=json' % misc_cfg.get('aq-clicrdv-api-key')
32

  
33
    req = urllib2.Request(url)
34
    username = misc_cfg.get('aq-clicrdv-api-username')
35
    password = misc_cfg.get('aq-clicrdv-api-password')
36
    authheader = 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
37
    req.add_header('Authorization', authheader)
38
    return req
39

  
40
def get_json(url):
41
    return json.load(urllib2.urlopen(get_clicrdv_req(url)))
42

  
43
def as_str(s):
44
    if type(s) is unicode:
45
        return s.encode(get_publisher().site_charset)
46
    return s
47

  
48
def get_all_intervention_sets():
49
    interventions_set = []
50
    for interventionset in sorted(get_json('interventionsets').get('records'), 
51
            lambda x,y: cmp(x['sort'],y['sort'])):
52
        interventions = []
53
        for intervention in sorted(get_json('interventionsets/%s/interventions' % interventionset.get('id')).get('records'),
54
                lambda x,y: cmp(x['sort'], y['sort'])):
55
            if intervention.get('deleted') == True:
56
                continue
57
            name = '%s' % as_str(intervention.get('publicname'))
58
            if not name:
59
                name = '%s' % as_str(intervention.get('name'))
60
            interventions.append((intervention.get('id'), as_str(name)))
61
        interventions_set.append({
62
                'id': interventionset.get('id'),
63
                'group_id': interventionset.get('group_id'),
64
                'name': as_str(interventionset.get('name')),
65
                'publicname': as_str(interventionset.get('publicname')) or '',
66
                'description': as_str(interventionset.get('description')) or '',
67
                'interventions': interventions
68
                })
69
    return interventions_set
70

  
71
def get_all_interventions():
72
    interventions = []
73
    for s in get_all_intervention_sets():
74
        for i, publicname in s['interventions']:
75
            intervention_label = '%s - %s' % (s['publicname'], publicname)
76
            interventions.append((i, as_str(intervention_label)))
77
    return interventions
78

  
79
def get_interventions_in_set(interventionset_id):
80
    interventions = []
81
    interventions_json = get_json('interventionsets/%s/interventions' % interventionset_id)
82
    for intervention in interventions_json.get('records'):
83
        if intervention.get('deleted') != True:
84
            name = '%s' % as_str(intervention.get('publicname'))
85
            if not name:
86
                name = '%s' % as_str(intervention.get('name'))
87
            interventions.append((intervention.get('id'), name))
88
    return interventions
89

  
90
def get_available_timeslots(intervention, date_start=None, date_end=None):
91
    timeslots = []
92
    iid = intervention
93
    gid = get_json('interventions/%s' % iid).get('group_id')
94
    request_url = 'availabletimeslots?intervention_ids[]=%s&group_id=%s' % (iid, gid)
95
    if date_start is None:
96
        date_start = datetime.datetime.today().strftime('%Y-%m-%d')
97
    if date_end is None:
98
        date_end = (datetime.datetime.today() + datetime.timedelta(366)).strftime('%Y-%m-%d')
99
    if date_start:
100
        request_url = request_url + '&start=%s' % urllib2.quote(date_start)
101
    if date_end:
102
        request_url = request_url + '&end=%s' % urllib2.quote(date_end)
103
    for timeslot in get_json(request_url).get('availabletimeslots'):
104
        timeslots.append(timeslot.get('start'))
105
    timeslots.sort()
106
    return timeslots
107

  
108
def get_available_dates(intervention):
109
    dates = []
110
    for timeslot in get_available_timeslots(intervention):
111
        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
112
        date_tuple = (time.strftime('%Y-%m-%d', parsed),
113
                      format_time(parsed, '%(weekday_name)s %(day)0.2d/%(month)0.2d/%(year)s'))
114
        if date_tuple in dates:
115
            continue
116
        dates.append(date_tuple)
117
    return dates
118

  
119
def get_available_times(intervention, date):
120
    times = []
121
    timeslots = get_available_timeslots(intervention,
122
                    date_start='%s 00:00:00' % date,
123
                    date_end='%s 23:59:59' % date)
124
    for timeslot in timeslots:
125
        parsed = time.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
126
        time_tuple = (time.strftime('%H:%M:%S', parsed),
127
                      time.strftime('%Hh%M', parsed))
128
        times.append(time_tuple)
129
    times.sort()
130
    return times
131

  
132
register_data_source_function(get_all_interventions, 'clicrdv_get_all_interventions')
133
register_data_source_function(get_interventions_in_set, 'clicrdv_get_interventions_in_set')
134
register_data_source_function(get_available_dates, 'clicrdv_get_available_dates')
135
register_data_source_function(get_available_times, 'clicrdv_get_available_times')
136

  
137
def form_download_event(self):
138
    self.check_receiver()
139

  
140
    found = False
141
    for evo in self.filled.evolution:
142
        if evo.parts:
143
            for p in evo.parts:
144
                if not isinstance(p, AppointmentPart):
145
                    continue
146
                cal = vobject.iCalendar()
147
                cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
148
                vevent = vobject.newFromBehavior('vevent')
149
                vevent.add('uid').value = 'clicrdv-%s' % p.id
150
                vevent.add('summary').value = p.json_dict.get('group_name')
151
                vevent.add('dtstart').value = datetime.datetime.strptime(
152
                                p.json_dict.get('start'), '%Y-%m-%d %H:%M:%S')
153
                vevent.add('dtend').value = datetime.datetime.strptime(
154
                                p.json_dict.get('end'), '%Y-%m-%d %H:%M:%S')
155
                vevent.add('location').value = p.json_dict.get('location')
156
                cal.add(vevent)
157

  
158
                response = get_response()
159
                response.set_content_type('text/calendar')
160
                return cal.serialize()
161

  
162
    raise TraversalError()
163

  
164

  
165
class AppointmentPart(object):
166
    def __init__(self, json_dict):
167
        self.id = json_dict.get('id')
168
        self.json_dict = json_dict
169

  
170
    def view(self):
171
        return htmltext('<p class="appointment"><a href="clicrdvevent">%s</a></p>' % (
172
                                _('Download Appointment')))
173

  
174

  
175
class AppointmentErrorPart(object):
176
    def __init__(self, msg):
177
        self.msg = msg
178

  
179
    def view(self):
180
        return htmltext('<p class="appointment-error">%s</p>' % str(self.msg))
181

  
182

  
183
class ClicRdvCreateAppointment(WorkflowStatusItem):
184
    description = N_('Create a ClicRDV Appointment')
185
    key = 'clicrdv-create'
186
    category = ('aq-clicrdv', N_('ClicRDV'))
187

  
188
    endpoint = False
189

  
190
    var_firstname = None
191
    var_lastname = None
192
    var_email = None
193
    var_firstphone = None
194
    var_secondphone = None
195
    var_datetime = None
196
    var_intervention_id = None
197
    status_on_success = None
198
    status_on_failure = None
199

  
200
    def init(cls):
201
        FormStatusPage._q_extra_exports.append('clicrdvevent')
202
        FormStatusPage.clicrdvevent = form_download_event
203
    init = classmethod(init)
204

  
205
    def is_available(self, workflow=None):
206
        return get_publisher().has_site_option('clicrdv')
207
    is_available = classmethod(is_available)
208

  
209
    def render_as_line(self):
210
        return _('Create an appointment in ClicRDV')
211

  
212
    def get_parameters(self):
213
        return ('var_firstname', 'var_lastname', 'var_email', 'var_firstphone',
214
                'var_secondphone', 'var_datetime', 'var_intervention_id',
215
                'status_on_success', 'status_on_failure')
216

  
217
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
218
        parameter_labels = {
219
            'var_firstname': N_('First Name'),
220
            'var_lastname': N_('Last Name'),
221
            'var_email': N_('Email'),
222
            'var_firstphone': N_('Phone (1st)'),
223
            'var_secondphone':  N_('Phone (2nd)'),
224
            'var_datetime': N_('Date/time'),
225
            'var_intervention_id': N_('Intervention Id'),
226
        }
227
        for parameter in self.get_parameters():
228
            if not parameter in parameter_labels:
229
                continue
230
            if parameter in parameters:
231
                form.add(StringWidget, '%s%s' % (prefix, parameter),
232
                         title=_(parameter_labels.get(parameter)),
233
                         value=getattr(self, parameter),
234
                         required=False)
235
        if 'status_on_success' in parameters:
236
            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
237
                    title=_('Status On Success'), value=self.status_on_success,
238
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
239
        if 'status_on_failure' in parameters:
240
            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
241
                    title=_('Status On Failure'), value=self.status_on_failure,
242
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
243

  
244
    def perform(self, formdata):
245
        args = {}
246
        for parameter in self.get_parameters():
247
            args[parameter] = self.compute(getattr(self, parameter))
248
            if not args.get(parameter):
249
                del args[parameter]
250
        message = {'appointment':
251
                      {'fiche': {'firstname': args.get('var_firstname', '-'),
252
                                 'lastname': args.get('var_lastname', '-'),
253
                                 'email': args.get('var_email'),
254
                                 'firstphone': args.get('var_firstphone'),
255
                                 'secondphone': args.get('var_secondphone'),
256
                                },
257
                       'date': args.get('var_datetime'),
258
                       'intervention_ids': [int(args.get('var_intervention_id'))],
259
                       # 'comments': '-',
260
                       'websource': 'Publik'}
261
                  }
262

  
263
        req = get_clicrdv_req('appointments')
264
        req.add_data(json.dumps(message))
265
        req.add_header('Content-Type', 'application/json')
266

  
267
        try:
268
            fd = urllib2.urlopen(req)
269
        except urllib2.HTTPError, e:
270
            success = False
271
            try:
272
                msg = json.load(e.fp)[0].get('error')
273
            except:
274
                msg = _('unknown error')
275

  
276
            if formdata.evolution:
277
                evo = formdata.evolution[-1]
278
            else:
279
                formdata.evolution = []
280
                evo = Evolution()
281
                evo.time = time.localtime()
282
                evo.status = formdata.status
283
                formdata.evolution.append(evo)
284
            evo.add_part(AppointmentErrorPart(msg))
285
        else:
286
            success = True
287
            response = json.load(fd)
288
            appointment_id = response.get('records')[0].get('id')
289

  
290
            # add a message in formdata.evolution
291
            if formdata.evolution:
292
                evo = formdata.evolution[-1]
293
            else:
294
                formdata.evolution = []
295
                evo = Evolution()
296
                evo.time = time.localtime()
297
                evo.status = formdata.status
298
                formdata.evolution.append(evo)
299
            evo.add_part(AppointmentPart(response.get('records')[0]))
300

  
301
        formdata.store()
302

  
303
        if (success and self.status_on_success) or (success is False and self.status_on_failure):
304
            if success:
305
                formdata.status = 'wf-%s' % self.status_on_success
306
            else:
307
                formdata.status = 'wf-%s' % self.status_on_failure
308

  
309
register_item_class(ClicRdvCreateAppointment)
310

  
311

  
312
class ClicRdvCancelAppointment(WorkflowStatusItem):
313
    description = N_('Cancel a ClicRDV Appointment')
314
    key = 'clicrdv-cancel'
315
    category = ('aq-clicrdv', N_('ClicRDV'))
316

  
317
    endpoint = False
318

  
319
    status_on_success = None
320
    status_on_failure = None
321

  
322
    def get_parameters(self):
323
        return ('status_on_success', 'status_on_failure')
324

  
325
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
326
        if 'status_on_success' in parameters:
327
            form.add(SingleSelectWidget, '%sstatus_on_success' % prefix,
328
                    title=_('Status On Success'), value=self.status_on_success,
329
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
330
        if 'status_on_failure' in parameters:
331
            form.add(SingleSelectWidget, '%sstatus_on_failure' % prefix,
332
                    title=_('Status On Failure'), value=self.status_on_failure,
333
                    options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
334

  
335
    def is_available(self, workflow=None):
336
        return get_publisher().has_site_option('clicrdv')
337
    is_available = classmethod(is_available)
338

  
339
    def render_as_line(self):
340
        return _('Cancel an appointment in ClicRDV')
341

  
342
    def perform(self, formdata):
343
        success = True
344

  
345
        for evo in [evo for evo in formdata.evolution if evo.parts]:
346
            for part in [part for part in evo.parts if isinstance(part, AppointmentPart)]:
347
                appointment_id = part.id
348
                try:
349
                    req = get_clicrdv_req('appointments/%s' % appointment_id)
350
                    req.get_method = (lambda: 'DELETE')
351
                    fd = urllib2.urlopen(req)
352
                    none = fd.read()
353
                except urllib2.URLError:
354
                    # clicrdv will return a "Bad Request" (HTTP 400) response
355
                    # when it's not possible to remove an appointment
356
                    # (for example because it's too late)
357
                    success = False
358

  
359
        if (success and self.status_on_success) or (success is False and self.status_on_failure):
360
            if success:
361
                formdata.status = 'wf-%s' % self.status_on_success
362
            else:
363
                formdata.status = 'wf-%s' % self.status_on_failure
364

  
365
register_item_class(ClicRdvCancelAppointment)
extra/modules/connectors.py
1
import clicrdv
2
import abelium_domino_workflow
extra/modules/events.py
1
import time
2
import datetime
3
import urllib2
4
import vobject
5

  
6
from quixote import get_request, get_publisher, get_response
7
from quixote.html import htmltext, TemplateIO, htmlescape
8

  
9
from qommon import _
10
from qommon.publisher import get_publisher_class
11
from qommon.storage import StorableObject
12
from qommon.cron import CronJob
13
from qommon import misc
14

  
15
class Event(StorableObject):
16
    _names = 'events'
17

  
18
    title = None
19
    description = None
20
    url = None
21
    date_start = None
22
    date_end = None
23
    location = None
24
    organizer = None
25
    more_infos = None
26
    keywords = None
27

  
28
    def in_month(self, year, month):
29
        if not self.date_end: # a single date
30
            return tuple(self.date_start[:2]) == (year, month)
31
        else:
32
            # an interval
33
            if tuple(self.date_start[:2]) > (year, month): # start later
34
                return False
35
            if tuple(self.date_end[:2]) < (year, month): # ended before
36
                return False
37
        return True
38

  
39
    def after_today(self):
40
        today = time.localtime()[:3]
41
        if not self.date_end:
42
            return tuple(self.date_start[:3]) > today
43
        return tuple(self.date_end[:3]) > today
44

  
45
    def format_date(self):
46
        d = {
47
            'year_start': self.date_start[0],
48
            'month_start': misc.get_month_name(self.date_start[1]),
49
            'day_start': self.date_start[2]
50
            }
51
        if self.date_end and self.date_start[:3] != self.date_end[:3]:
52
            d.update({
53
                'year_end': self.date_end[0],
54
                'month_end': misc.get_month_name(self.date_end[1]),
55
                'day_end': self.date_end[2]
56
                })
57
            d2 = datetime.date(*self.date_start[:3]) + datetime.timedelta(days=1)
58
            if tuple(self.date_end[:3]) == (d2.year, d2.month, d2.day):
59
                # two consecutive days
60
                if self.date_start[1] == self.date_end[1]:
61
                    return _('On %(month_start)s %(day_start)s and %(day_end)s') % d
62
                else:
63
                    return _('On %(month_start)s %(day_start)s and %(month_end)s %(day_end)s') % d
64
            else:
65
                if self.date_start[0] == self.date_end[0]: # same year
66
                    if self.date_start[1] == self.date_end[1]: # same month
67
                        return _('From %(month_start)s %(day_start)s to %(day_end)s') % d
68
                    else:
69
                        return _('From %(month_start)s %(day_start)s '
70
                                 'to %(month_end)s %(day_end)s') % d
71
                else:
72
                    return _('From %(month_start)s %(day_start)s %(year_start)s '
73
                             'to %(month_end)s %(day_end)s %(year_end)s') % d
74
        else:
75
            return _('On %(month_start)s %(day_start)s') % d
76

  
77
    def as_vevent(self):
78
        vevent = vobject.newFromBehavior('vevent')
79
        site_charset = get_publisher().site_charset
80
        vevent.add('uid').value = '%04d%02d%02d-%s@%s' % (self.date_start[:3] + (self.id,
81
            get_request().get_server().lower().split(':')[0].rstrip('.')))
82
        vevent.add('summary').value = unicode(self.title, site_charset)
83
        vevent.add('dtstart').value = datetime.date(*self.date_start[:3])
84
        vevent.dtstart.value_param = 'DATE'
85
        if self.date_end:
86
            vevent.add('dtend').value = datetime.date(*self.date_end[:3])
87
            vevent.dtend.value_param = 'DATE'
88
        if self.description:
89
            vevent.add('description').value = unicode(self.description.strip(), site_charset)
90
        if self.url:
91
            vevent.add('url').value = unicode(self.url, site_charset)
92
        if self.location:
93
            vevent.add('location').value = unicode(self.location, site_charset)
94
        if self.organizer:
95
            vevent.add('organizer').value = unicode(self.organizer, site_charset)
96
        if self.keywords:
97
            vevent.add('categories').value = [unicode(x, site_charset) for x in self.keywords]
98
        vevent.add('class').value = 'PUBLIC'
99
        return vevent
100

  
101
    def as_vcalendar(cls):
102
        cal = vobject.iCalendar()
103
        cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
104
        for x in cls.select():
105
            cal.add(x.as_vevent())
106
        return cal.serialize()
107
    as_vcalendar = classmethod(as_vcalendar)
108
    
109
    def as_html_dt_dd(self):
110
        root_url = get_publisher().get_root_url()
111
        r = TemplateIO(html=True)
112
        r += htmltext('<dt>')
113
        r += self.format_date()
114
        r += htmltext('</dt>')
115
        r += htmltext('<p><dd><strong>%s</strong>') % self.title
116
        if self.description:
117
            r += ' - ' + self.description
118
        r += htmltext('</p>')
119
        if (self.location or self.organizer or self.more_infos or self.keywords):
120
            r += htmltext('<ul>')
121
            if self.location:
122
                r += htmltext('<li>%s: %s</li>') % (_('Location'), self.location)
123
            if self.organizer:
124
                r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.organizer)
125
            if self.more_infos:
126
                r += htmltext('<li>%s</li>') % self.more_infos
127
            if self.keywords:
128
                r += htmltext('<li>')
129
                for k in self.keywords:
130
                    r += htmltext('<a class="tag" href="%sagenda/tag/%s">%s</a> ') % (root_url, k, k)
131
                r += htmltext('</li>')
132
            r += htmltext('</ul>')
133

  
134
        if self.url:
135
            if get_response().iframe_mode:
136
                r += htmltext('<a class="external" href="%s" target="_top">%s</a>') % (
137
                        self.url, _('More information'))
138
            else:
139
                r += htmltext('<a class="external" href="%s">%s</a>') % (
140
                        self.url, _('More information'))
141
        r += htmltext('</dd>')
142
        return r.getvalue()
143

  
144
    def get_url(self):
145
        return '%s/agenda/events/%s/' % (get_publisher().get_frontoffice_url(), self.id)
146

  
147
    def get_atom_entry(self):
148
        from pyatom import pyatom
149
        entry = pyatom.Entry()
150
        entry.id = self.get_url()
151
        entry.title = self.title
152

  
153
        entry.content.attrs['type'] = 'html'
154
        entry.content.text = str('<p>' + htmlescape(
155
                    unicode(self.description, get_publisher().site_charset).encode('utf-8')) + '</p>')
156

  
157
        return entry
158

  
159

  
160
class RemoteCalendar(StorableObject):
161
    _names = 'remote_calendars'
162

  
163
    label = None
164
    url = None
165
    content = None
166
    events = None
167
    error = None  # (time, string, params)
168

  
169
    def download_and_parse(self, job=None):
170
        old_content = self.content
171

  
172
        try:
173
            self.content = urllib2.urlopen(self.url).read()
174
        except urllib2.HTTPError, e:
175
            self.error = (time.localtime(), N_('HTTP Error %s on download'), (e.code,))
176
            self.store()
177
            return
178
        except urllib2.URLError, e:
179
            self.error = (time.localtime(), N_('Error on download'), ())
180
            self.store()
181
            return
182

  
183
        if self.error:
184
            self.error = None
185
            self.store()
186

  
187
        if self.content == old_content:
188
            return
189

  
190
        self.events = []
191
        try:
192
            parsed_cal = vobject.readOne(self.content)
193
        except vobject.base.ParseError:
194
            self.error = (time.localtime(), N_('Failed to parse file'), ())
195
            self.store()
196
            return
197

  
198
        site_charset = get_publisher().site_charset
199
        for vevent in parsed_cal.vevent_list:
200
            ev = Event()
201
            ev.title = vevent.summary.value.encode(site_charset, 'replace')
202
            try:
203
                ev.url = vevent.url.value.encode(site_charset, 'replace')
204
            except AttributeError:
205
                pass
206
            ev.date_start = vevent.dtstart.value.timetuple()
207
            try:
208
                ev.date_end = vevent.dtend.value.timetuple()
209
            except AttributeError:
210
                pass
211
            try:
212
                ev.description = vevent.description.value.encode(site_charset, 'replace')
213
            except AttributeError:
214
                pass
215
            try:
216
                ev.keywords = [x.encode(site_charset) for x in vevent.categories.value]
217
            except AttributeError:
218
                pass
219
            self.events.append(ev)
220
        self.store()
221

  
222
        
223
    def get_error_message(self):
224
        if not self.error:
225
            return None
226
        return '(%s) %s' % (misc.localstrftime(self.error[0]),
227
                _(self.error[1]) % self.error[2])
228

  
229

  
230
def update_remote_calendars(publisher):
231
    for source in RemoteCalendar.select():
232
        source.download_and_parse()
233

  
234
def get_default_event_tags():
235
    return [_('All Public'), _('Adults'), _('Children'), _('Free')]
236

  
237
get_publisher_class().register_cronjob(CronJob(update_remote_calendars, minutes = [0]))
238

  
extra/modules/events_ui.py
1
import time
2

  
3
from quixote import get_request, get_response, get_session, redirect
4
from quixote.directory import Directory, AccessControlled
5
from quixote.html import TemplateIO, htmltext
6

  
7
import wcs
8
import wcs.admin.root
9

  
10
from qommon import _
11
from qommon.backoffice.menu import html_top
12
from qommon.admin.menu import command_icon
13
from qommon import get_cfg
14
from qommon import errors, misc
15
from qommon.form import *
16
from qommon.strftime import strftime
17

  
18
from events import Event, RemoteCalendar, get_default_event_tags
19

  
20

  
21

  
22
class RemoteCalendarDirectory(Directory):
23
    _q_exports = ['', 'edit', 'delete', 'update']
24

  
25
    def __init__(self, calendar):
26
        self.calendar = calendar
27

  
28
    def _q_index(self):
29
        form = Form(enctype='multipart/form-data')
30
        form.add_submit('edit', _('Edit'))
31
        form.add_submit('delete', _('Delete'))
32
        form.add_submit('update', _('Update'))
33
        form.add_submit('back', _('Back'))
34

  
35
        if form.get_submit() == 'edit':
36
            return redirect('edit')
37
        if form.get_submit() == 'update':
38
            return redirect('update')
39
        if form.get_submit() == 'delete':
40
            return redirect('delete')
41
        if form.get_submit() == 'back':
42
            return redirect('..')
43

  
44
        html_top('events', title = _('Remote Calendar: %s') % self.calendar.label)
45
        r = TemplateIO(html=True)
46
        r += htmltext('<h2>%s</h2>') % _('Remote Calendar: %s') % self.calendar.label
47

  
48
        r += get_session().display_message()
49

  
50
        r += htmltext('<p>')
51
        self.calendar.url
52
        if self.calendar.error:
53
            r += htmltext(' - <span class="error-message">%s</span>') % self.calendar.get_error_message()
54
        r += htmltext('</p>')
55

  
56
        if not self.calendar.content:
57
            r += htmltext('<p>')
58
            r += _('No content has been retrieved yet.')
59
            r += htmltext('</p>')
60
        else:
61
            r += htmltext('<ul>')
62
            for ev in sorted(self.calendar.events, lambda x,y: cmp(x.date_start, y.date_start)):
63
                r += htmltext('<li>')
64
                if ev.date_start:
65
                    r += strftime(misc.date_format(), ev.date_start)
66
                if ev.date_end and ev.date_start[:3] != ev.date_end[:3]:
67
                    r += ' - '
68
                    r += strftime(misc.date_format(), ev.date_start)
69

  
70
                r += ' : '
71
                if ev.url:
72
                    r += htmltext('<a href="%s">%s</a>') % (ev.url, ev.title)
73
                else:
74
                    r += ev.title
75
                r += htmltext('</li>')
76
            r += htmltext('</ul>')
77

  
78
        r += form.render()
79
        return r.getvalue()
80

  
81
    def edit(self):
82
        form = self.form()
83
        if form.get_submit() == 'cancel':
84
            return redirect('.')
85

  
86
        if form.is_submitted() and not form.has_errors():
87
            self.submit(form)
88
            return redirect('..')
89

  
90
        html_top('events', title = _('Edit Remote Calendar: %s') % self.calendar.label)
91
        r = TemplateIO(html=True)
92
        r += htmltext('<h2>%s</h2>') % _('Edit Remote Calendar: %s') % self.calendar.label
93
        r += form.render()
94
        return r.getvalue()
95

  
96
    def form(self):
97
        form = Form(enctype='multipart/form-data')
98
        form.add(StringWidget, 'label', title = _('Label'), required = True,
99
                value = self.calendar.label)
100
        form.add(StringWidget, 'url', title = _('URL'), required = True,
101
                value = self.calendar.url, size = 40)
102
        form.add_submit('submit', _('Submit'))
103
        form.add_submit('cancel', _('Cancel'))
104
        return form
105

  
106
    def submit(self, form):
107
        for k in ('label', 'url'):
108
            widget = form.get_widget(k)
109
            if widget:
110
                setattr(self.calendar, k, widget.parse())
111
        self.calendar.store()
112

  
113
    def delete(self):
114
        form = Form(enctype='multipart/form-data')
115
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
116
                        'You are about to irrevocably delete this remote calendar.')))
117
        form.add_submit('submit', _('Submit'))
118
        form.add_submit('cancel', _('Cancel'))
119
        if form.get_submit() == 'cancel':
120
            return redirect('..')
121
        if not form.is_submitted() or form.has_errors():
122
            get_response().breadcrumb.append(('delete', _('Delete')))
123
            html_top('events', title = _('Delete Remote Calendar'))
124
            r = TemplateIO(html=True)
125
            r += htmltext('<h2>%s</h2>') % _('Deleting Remote Calendar: %s') % self.calendar.label
126
            r += form.render()
127
            return r.getvalue()
128
        else:
129
            self.calendar.remove_self()
130
            return redirect('..')
131

  
132
    def update(self):
133
        get_session().message = ('info',
134
                _('Calendar update has been requested, reload in a few moments'))
135
        get_response().add_after_job('updating remote calendar',
136
                self.calendar.download_and_parse,
137
                fire_and_forget = True)
138
        return redirect('.')
139

  
140

  
141

  
142
class RemoteCalendarsDirectory(Directory):
143
    _q_exports = ['', 'new']
144

  
145
    def _q_traverse(self, path):
146
        get_response().breadcrumb.append(('remote/', _('Remote Calendars')))
147
        return Directory._q_traverse(self, path)
148

  
149
    def _q_index(self):
150
        return redirect('..')
151

  
152
    def new(self):
153
        calendar_ui = RemoteCalendarDirectory(RemoteCalendar())
154

  
155
        form = calendar_ui.form()
156
        if form.get_submit() == 'cancel':
157
            return redirect('.')
158

  
159
        if form.is_submitted() and not form.has_errors():
160
            calendar_ui.submit(form)
161
            return redirect('%s/' % calendar_ui.calendar.id)
162

  
163
        get_response().breadcrumb.append(('new', _('New Remote Calendar')))
164
        html_top('events', title = _('New Remote Calendar'))
165
        r = TemplateIO(html=True)
166
        r += htmltext('<h2>%s</h2>') % _('New Remote Calendar')
167
        r += form.render()
168
        return r.getvalue()
169

  
170
    def _q_lookup(self, component):
171
        try:
172
            event = RemoteCalendar.get(component)
173
        except KeyError:
174
            raise errors.TraversalError()
175
        get_response().breadcrumb.append((str(event.id), event.label))
176
        return RemoteCalendarDirectory(event)
177

  
178

  
179
class EventDirectory(Directory):
180
    _q_exports = ['', 'edit', 'delete']
181

  
182
    def __init__(self, event):
183
        self.event = event
184

  
185
    def _q_index(self):
186
        form = Form(enctype='multipart/form-data')
187
        form.add_submit('edit', _('Edit'))
188
        form.add_submit('delete', _('Delete'))
189
        form.add_submit('back', _('Back'))
190

  
191
        if form.get_submit() == 'edit':
192
            return redirect('edit')
193
        if form.get_submit() == 'delete':
194
            return redirect('delete')
195
        if form.get_submit() == 'back':
196
            return redirect('..')
197

  
198
        html_top('events', title = _('Event: %s') % self.event.title)
199
        r = TemplateIO(html=True)
200
        r += htmltext('<h2>%s</h2>') % _('Event: %s') % self.event.title
201
        r += htmltext('<p>')
202
        r += self.event.description
203
        r += htmltext('</p>')
204
        r += htmltext('<ul>')
205
        if self.event.location:
206
            r += htmltext('<li>%s: %s</li>') % (_('Location'), self.event.location)
207
        if self.event.organizer:
208
            r += htmltext('<li>%s: %s</li>') % (_('Organizer'), self.event.organizer)
209
        if self.event.url:
210
            r += htmltext('<li>%s: <a href="%s">%s</a></li>') % (_('URL'), self.event.url, self.event.url)
211
        r += htmltext('</ul>')
212

  
213
        if self.event.more_infos:
214
            r += htmltext('<p>')
215
            r += self.event.more_infos
216
            r += htmltext('</p>')
217

  
218
        r += form.render()
219
        return r.getvalue()
220

  
221
    def edit(self):
222
        form = self.form()
223
        if form.get_submit() == 'cancel':
224
            return redirect('.')
225

  
226
        if form.is_submitted() and not form.has_errors():
227
            self.submit(form)
228
            return redirect('..')
229

  
230
        html_top('events', title = _('Edit Event: %s') % self.event.title)
231
        r = TemplateIO(html=True)
232
        r += htmltext('<h2>%s</h2>') % _('Edit Event: %s') % self.event.title
233
        r += form.render()
234
        return r.getvalue()
235

  
236
    def form(self):
237
        form = Form(enctype='multipart/form-data')
238
        form.add(StringWidget, 'title', title = _('Title'), required = True,
239
                value = self.event.title)
240
        form.add(TextWidget, 'description', title = _('Description'),
241
                cols = 70, rows = 10,
242
                required = True, value = self.event.description)
243
        form.add(StringWidget, 'url', title = _('URL'), required = False,
244
                value = self.event.url, size = 40)
245
        form.add(DateWidget, 'date_start', title = _('Start Date'), required = True,
246
                value = strftime(misc.date_format(), self.event.date_start))
247
        form.add(DateWidget, 'date_end', title = _('End Date'), required = False,
248
                value = strftime(misc.date_format(), self.event.date_end))
249
        form.add(TextWidget, 'location', title = _('Location'),
250
                cols = 70, rows = 4,
251
                required = False, value = self.event.location)
252
        form.add(StringWidget, 'organizer', title = _('Organizer'), required = False,
253
                value = self.event.organizer, size = 40)
254
        form.add(TextWidget, 'more_infos', title = _('More informations'),
255
                cols = 70, rows = 10,
256
                required = False, value = self.event.more_infos)
257
        form.add(TagsWidget, 'keywords', title = _('Keywords'),
258
                value = self.event.keywords, size = 50,
259
                known_tags = get_cfg('misc', {}).get('event_tags', get_default_event_tags()))
260
        form.add_submit('submit', _('Submit'))
261
        form.add_submit('cancel', _('Cancel'))
262
        return form
263

  
264
    def submit(self, form):
265
        for k in ('title', 'description', 'url', 'date_start', 'date_end',
266
                    'organizer', 'location', 'more_infos', 'keywords'):
267
            widget = form.get_widget(k)
268
            if widget:
269
                if k in ('date_start', 'date_end'):
270
                    # convert dates to 9-item tuples
271
                    v = widget.parse()
272
                    if v:
273
                        setattr(self.event, k, time.strptime(v, misc.date_format()))
274
                    else:
275
                        setattr(self.event, k, None)
276
                else:
277
                    setattr(self.event, k, widget.parse())
278
        self.event.store()
279

  
280
    def delete(self):
281
        form = Form(enctype='multipart/form-data')
282
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
283
                        'You are about to irrevocably delete this event.')))
284
        form.add_submit('submit', _('Submit'))
285
        form.add_submit('cancel', _('Cancel'))
286
        if form.get_submit() == 'cancel':
287
            return redirect('..')
288
        if not form.is_submitted() or form.has_errors():
289
            get_response().breadcrumb.append(('delete', _('Delete')))
290
            html_top('events', title = _('Delete Event'))
291
            r = TemplateIO(html=True)
292
            r += htmltext('<h2>%s</h2>') % _('Deleting Event: %s') % self.event.title
293
            r += form.render()
294
            return r.getvalue()
295
        else:
296
            self.event.remove_self()
297
            return redirect('..')
298

  
299

  
300

  
301

  
302
class EventsDirectory(AccessControlled, Directory):
303
    _q_exports = ['', 'new', 'listing', 'remote']
304
    label = N_('Events')
305

  
306
    remote = RemoteCalendarsDirectory()
307

  
308
    def is_accessible(self, user):
309
        from .backoffice import check_visibility
310
        return check_visibility('events', user)
311

  
312
    def _q_access(self):
313
        user = get_request().user
314
        if not user:
315
            raise errors.AccessUnauthorizedError()
316

  
317
        if not self.is_accessible(user):
318
            raise errors.AccessForbiddenError(
319
                    public_msg = _('You are not allowed to access Events Management'),
320
                    location_hint = 'backoffice')
321

  
322
        get_response().breadcrumb.append(('events/', _('Events')))
323

  
324

  
325
    def _q_index(self):
326
        html_top('events', _('Events'))
327
        r = TemplateIO(html=True)
328

  
329
        get_response().filter['sidebar'] = self.get_sidebar()
330

  
331
        r += htmltext('<div class="splitcontent-left">')
332

  
333
        r += htmltext('<div class="bo-block">')
334
        events = Event.select()
335
        r += htmltext('<h2>%s</h2>') % _('Events')
336
        if not events:
337
            r += htmltext('<p>')
338
            r += _('There is no event defined at the moment.')
339
            r += htmltext('</p>')
340
        r += htmltext('<ul class="biglist" id="events-list">')
341
        for l in events:
342
            event_id = l.id
343
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % event_id
344
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (event_id, l.title)
345
            r += ' - '
346
            r += l.format_date()
347
            r += htmltext('<p class="commands">')
348
            r += command_icon('%s/edit' % event_id, 'edit')
349
            r += command_icon('%s/delete' % event_id, 'remove')
350
            r += htmltext('</p></li>')
351
        r += htmltext('</ul>')
352
        r += htmltext('</div>')
353
        r += htmltext('</div>')
354

  
355
        r += htmltext('<div class="splitcontent-right">')
356
        r += htmltext('<div class="bo-block">')
357
        rcalendars = RemoteCalendar.select()
358
        r += htmltext('<h2>%s</h2>') % _('Remote Calendars')
359
        if not rcalendars:
360
            r += htmltext('<p>')
361
            r += _('There is no remote calendars defined at the moment.')
362
            r += htmltext('</p>')
363

  
364
        r += htmltext('<ul class="biglist" id="events-list">')
365
        for l in rcalendars:
366
            rcal_id = l.id
367
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % rcal_id
368
            r += htmltext('<strong class="label"><a href="remote/%s/">%s</a></strong>') % (rcal_id, l.label)
369
            r += htmltext('<p class="details">')
370
            r += l.url
371
            if l.error:
372
                r += htmltext('<br /><span class="error-message">%s</span>') % l.get_error_message()
373
            r += htmltext('</p>')
374
            r += htmltext('<p class="commands">')
375
            r += command_icon('remote/%s/edit' % rcal_id, 'edit')
376
            r += command_icon('remote/%s/delete' % rcal_id, 'remove')
377
            r += htmltext('</p></li>')
378
        r += htmltext('</ul>')
379
        r += htmltext('</div>')
380
        r += htmltext('</div>')
381
        return r.getvalue()
382

  
383
    def get_sidebar(self):
384
        r = TemplateIO(html=True)
385
        r += htmltext('<ul id="sidebar-actions">')
386
        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Event')
387
        r += htmltext('  <li><a class="new-item" href="remote/new">%s</a></li>') % _('New Remote Calendar')
388
        r += htmltext('</ul>')
389
        return r.getvalue()
390

  
391
    def new(self):
392
        event_ui = EventDirectory(Event())
393

  
394
        form = event_ui.form()
395
        if form.get_submit() == 'cancel':
396
            return redirect('.')
397

  
398
        if form.is_submitted() and not form.has_errors():
399
            event_ui.submit(form)
400
            return redirect('%s/' % event_ui.event.id)
401

  
402
        get_response().breadcrumb.append(('new', _('New Event')))
403
        html_top('events', title = _('New Event'))
404
        r = TemplateIO(html=True)
405
        r += htmltext('<h2>%s</h2>') % _('New Event')
406
        r += form.render()
407
        return r.getvalue()
408

  
409
    def _q_lookup(self, component):
410
        try:
411
            event = Event.get(component)
412
        except KeyError:
413
            raise errors.TraversalError()
414
        get_response().breadcrumb.append((str(event.id), event.title))
415
        return EventDirectory(event)
416

  
417
    def listing(self):
418
        return redirect('.')
extra/modules/formpage.py
1
from quixote import get_publisher, get_request, redirect
2
from quixote.directory import Directory
3
from quixote.html import htmltext
4

  
5
import os
6

  
7
import wcs
8
import wcs.forms.root
9
import wcs.forms.preview
10
from qommon import _
11
from qommon import template
12
from qommon import errors
13
from qommon.form import *
14
from wcs.roles import logged_users_role
15

  
16
from qommon import emails
17

  
18
OldFormPage = wcs.forms.root.FormPage
19

  
20
class AlternateFormPage(OldFormPage):
21
    def form_side(self, *args, **kwargs):
22
        form_side_html = OldFormPage.form_side(self, *args, **kwargs)
23
        # add a 'Steps' title
24
        form_side_html = str(form_side_html).replace('<ol>', '<h2>%s</h2>\n<ol>' % _('Steps'))
25
        get_response().filter['gauche'] = form_side_html
26
        get_response().filter['steps'] = form_side_html
27
        return
28

  
29
wcs.forms.root.FormPage = AlternateFormPage
30
wcs.forms.root.PublicFormStatusPage.form_page_class = AlternateFormPage
31
wcs.forms.preview.PreviewFormPage.__bases__ = (AlternateFormPage,)
32

  
33

  
34
OldFormsRootDirectory = wcs.forms.root.RootDirectory
35

  
36
class AlternateFormsRootDirectory(OldFormsRootDirectory):
37
    def form_list(self, *args, **kwargs):
38
        form_list = OldFormsRootDirectory.form_list(self, *args, **kwargs)
39
        return htmltext(str(form_list).replace('h2>', 'h3>'))
40

  
41
wcs.forms.root.RootDirectory = AlternateFormsRootDirectory
extra/modules/links.py
1
from qommon.storage import StorableObject
2

  
3
class Link(StorableObject):
4
    _names = 'links'
5

  
6
    title = None
7
    url = None
8
    position = None
9

  
10
    def sort_by_position(cls, links):
11
        def cmp_position(x, y):
12
            if x.position == y.position:
13
                return 0
14
            if x.position is None:
15
                return 1
16
            if y.position is None:
17
                return -1
18
            return cmp(x.position, y.position)
19
        links.sort(cmp_position)
20
    sort_by_position = classmethod(sort_by_position)
21

  
extra/modules/links_ui.py
1
from quixote import get_request, get_response, get_session, redirect
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import TemplateIO, htmltext
4

  
5
import wcs
6
import wcs.admin.root
7

  
8
from qommon import _
9
from qommon import errors
10
from qommon.form import *
11
from qommon.backoffice.menu import html_top
12
from qommon.admin.menu import command_icon
13
from qommon import get_cfg
14

  
15
from links import Link
16

  
17

  
18
class LinkDirectory(Directory):
19
    _q_exports = ['', 'edit', 'delete']
20

  
21
    def __init__(self, link):
22
        self.link = link
23

  
24
    def _q_index(self):
25
        form = Form(enctype='multipart/form-data')
26
        form.add_submit('edit', _('Edit'))
27
        form.add_submit('delete', _('Delete'))
28
        form.add_submit('back', _('Back'))
29

  
30
        if form.get_submit() == 'edit':
31
            return redirect('edit')
32
        if form.get_submit() == 'delete':
33
            return redirect('delete')
34
        if form.get_submit() == 'back':
35
            return redirect('..')
36

  
37
        html_top('links', title = _('Link: %s') % self.link.title)
38
        r = TemplateIO(html=True)
39
        r += htmltext('<h2>%s</h2>') % _('Link: %s') % self.link.title
40
        r += htmltext('<p>')
41
        r += self.link.url
42
        r += htmltext('</p>')
43

  
44
        r += form.render()
45
        return r.getvalue()
46

  
47
    def edit(self):
48
        form = self.form()
49
        if form.get_submit() == 'cancel':
50
            return redirect('.')
51

  
52
        if form.is_submitted() and not form.has_errors():
53
            self.submit(form)
54
            return redirect('..')
55

  
56
        html_top('links', title = _('Edit Link: %s') % self.link.title)
57
        r = TemplateIO(html=True)
58
        r += htmltext('<h2>%s</h2>') % _('Edit Link: %s') % self.link.title
59
        r += form.render()
60
        return r.getvalue()
61

  
62

  
63
    def form(self):
64
        form = Form(enctype='multipart/form-data')
65
        form.add(StringWidget, 'title', title = _('Title'), required = True,
66
                value = self.link.title)
67
        form.add(StringWidget, 'url', title = _('URL'), required=False,
68
                value = self.link.url,
69
                hint=_('Leave empty to create a title'))
70
        form.add_submit('submit', _('Submit'))
71
        form.add_submit('cancel', _('Cancel'))
72
        return form
73

  
74
    def submit(self, form):
75
        for k in ('title', 'url'):
76
            widget = form.get_widget(k)
77
            if widget:
78
                setattr(self.link, k, widget.parse())
79
        self.link.store()
80

  
81
    def delete(self):
82
        form = Form(enctype='multipart/form-data')
83
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
84
                        'You are about to irrevocably delete this link.')))
85
        form.add_submit('submit', _('Submit'))
86
        form.add_submit('cancel', _('Cancel'))
87
        if form.get_submit() == 'cancel':
88
            return redirect('..')
89
        if not form.is_submitted() or form.has_errors():
90
            get_response().breadcrumb.append(('delete', _('Delete')))
91
            html_top('links', title = _('Delete Link'))
92
            r = TemplateIO(html=True)
93
            r += htmltext('<h2>%s</h2>') % _('Deleting Link: %s') % self.link.title
94
            r += form.render()
95
            return r.getvalue()
96
        else:
97
            self.link.remove_self()
98
            return redirect('..')
99

  
100

  
101
class LinksDirectory(AccessControlled, Directory):
102
    _q_exports = ['', 'new', 'listing', 'update_order']
103
    label = N_('Links')
104

  
105
    def is_accessible(self, user):
106
        from .backoffice import check_visibility
107
        return check_visibility('links', user)
108

  
109
    def _q_access(self):
110
        user = get_request().user
111
        if not user:
112
            raise errors.AccessUnauthorizedError()
113

  
114
        if not self.is_accessible(user):
115
            raise errors.AccessForbiddenError(
116
                    public_msg = _('You are not allowed to access Links Management'),
117
                    location_hint = 'backoffice')
118

  
119
        get_response().breadcrumb.append(('links/', _('Links')))
120

  
121

  
122
    def _q_index(self):
123
        html_top('links', _('Links'))
124
        r = TemplateIO(html=True)
125
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'biglist.js'])
126
        get_response().filter['sidebar'] = self.get_sidebar()
127

  
128
        links = Link.select()
129
        Link.sort_by_position(links)
130

  
131
        r += htmltext('<ul class="biglist sortable" id="links-list">')
132
        for l in links:
133
            link_id = l.id
134
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % link_id
135
            r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (link_id, l.title)
136
            r += htmltext('<p class="details">')
137
            r += l.url
138
            r += htmltext('</p>')
139
            r += htmltext('<p class="commands">')
140
            r += command_icon('%s/edit' % link_id, 'edit')
141
            r += command_icon('%s/delete' % link_id, 'remove')
142
            r += htmltext('</p></li>')
143
        r += htmltext('</ul>')
144
        return r.getvalue()
145

  
146
    def get_sidebar(self):
147
        r = TemplateIO(html=True)
148
        r += htmltext('<ul id="sidebar-actions">')
149
        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Link')
150
        r += htmltext('</ul>')
151
        return r.getvalue()
152

  
153
    def update_order(self):
154
        request = get_request()
155
        new_order = request.form['order'].strip(';').split(';')
156
        links = Link.select()
157
        dict = {}
158
        for l in links:
159
            dict[str(l.id)] = l
160
        for i, o in enumerate(new_order):
161
            dict[o].position = i + 1
162
            dict[o].store()
163
        return 'ok'
164

  
165

  
166
    def new(self):
167
        link_ui = LinkDirectory(Link())
168

  
169
        form = link_ui.form()
170
        if form.get_submit() == 'cancel':
171
            return redirect('.')
172

  
173
        if form.is_submitted() and not form.has_errors():
174
            link_ui.submit(form)
175
            return redirect('%s/' % link_ui.link.id)
176

  
177
        get_response().breadcrumb.append(('new', _('New Link')))
178
        html_top('links', title = _('New Link'))
179
        r = TemplateIO(html=True)
180
        r += htmltext('<h2>%s</h2>') % _('New Link')
181
        r += form.render()
182
        return r.getvalue()
183

  
184
    def _q_lookup(self, component):
185
        try:
186
            link = Link.get(component)
187
        except KeyError:
188
            raise errors.TraversalError()
189
        get_response().breadcrumb.append((str(link.id), link.title))
190
        return LinkDirectory(link)
191

  
192
    def listing(self):
193
        return redirect('.')
194

  
extra/modules/myspace.py
1
try:
2
    import lasso
3
except ImportError:
4
    pass
5

  
6
import json
7

  
8
from quixote import get_publisher, get_request, redirect, get_response, get_session_manager, get_session
9
from quixote.directory import AccessControlled, Directory
10
from quixote.html import TemplateIO, htmltext
11
from quixote.util import StaticFile, FileStream
12

  
13
from qommon import _
14
from qommon import template
15
from qommon.form import *
16
from qommon import get_cfg, get_logger
17
from qommon import errors
18
from wcs.api import get_user_from_api_query_string
19

  
20
import qommon.ident.password
21
from qommon.ident.password_accounts import PasswordAccount
22

  
23
from qommon.admin.texts import TextsDirectory
24

  
25
from wcs.formdef import FormDef
26
import wcs.myspace
27
import root
28

  
29
from announces import AnnounceSubscription
30
from strongbox import StrongboxItem, StrongboxType
31
from payments import Invoice, Regie, is_payment_supported
32

  
33
class MyInvoicesDirectory(Directory):
34
    _q_exports = ['']
35

  
36
    def _q_traverse(self, path):
37
        if not is_payment_supported():
38
            raise errors.TraversalError()
39
        get_response().breadcrumb.append(('invoices/', _('Invoices')))
40
        return Directory._q_traverse(self, path)
41

  
42
    def _q_index(self):
43
        user = get_request().user
44
        if not user or user.anonymous:
45
            raise errors.AccessUnauthorizedError()
46

  
47
        template.html_top(_('Invoices'))
48
        r = TemplateIO(html=True)
49
        r += TextsDirectory.get_html_text('aq-myspace-invoice')
50

  
51
        r += get_session().display_message()
52

  
53
        invoices = []
54
        invoices.extend(Invoice.get_with_indexed_value(
55
            str('user_id'), str(user.id)))
56

  
57
        def cmp_invoice(a, b):
58
            t = cmp(a.regie_id, b.regie_id)
59
            if t != 0:
60
                return t
61
            return -cmp(a.date, b.date)
62

  
63
        invoices.sort(cmp_invoice)
64

  
65
        last_regie_id = None
66
        unpaid = False
67
        for invoice in invoices:
68
            if invoice.regie_id != last_regie_id:
69
                if last_regie_id:
70
                    r += htmltext('</ul>')
71
                    if unpaid:
72
                        r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
73
                    r += htmltext('</form>')
74
                last_regie_id = invoice.regie_id
75
                r += htmltext('<h3>%s</h3>') % Regie.get(last_regie_id).label
76
                unpaid = False
77
                r += htmltext('<form action="%s/invoices/multiple">' % get_publisher().get_frontoffice_url())
78
                r += htmltext('<ul>')
79

  
80
            r += htmltext('<li>')
81
            if not (invoice.paid or invoice.canceled):
82
                r += htmltext('<input type="checkbox" name="invoice" value="%s"/>' % invoice.id)
83
                unpaid = True
84
            r += misc.localstrftime(invoice.date)
85
            r += ' - '
86
            r += '%s' % invoice.subject
87
            r += ' - '
88
            r += '%s' % invoice.amount
89
            r += htmltext(' &euro;')
90
            r += ' - '
91
            button = '<span class="paybutton">%s</span>' % _('Pay')
92
            if invoice.canceled:
93
                r += _('canceled on %s') % misc.localstrftime(invoice.canceled_date)
94
                r += ' - '
95
                button = _('Details')
96
            if invoice.paid:
97
                r += _('paid on %s') % misc.localstrftime(invoice.paid_date)
98
                r += ' - '
99
                button = _('Details')
100
            r += htmltext('<a href="%s/invoices/%s">%s</a>' % (get_publisher().get_frontoffice_url(),
101
                    invoice.id, button))
102
            r += htmltext('</li>')
103

  
104
        if last_regie_id:
105
            r += htmltext('</ul>')
106
            if unpaid:
107
                r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
108
            r += htmltext('</form>')
109

  
110
        return r.getvalue()
111

  
112

  
113
class StrongboxDirectory(Directory):
114
    _q_exports = ['', 'add', 'download', 'remove', 'pick', 'validate']
115

  
116
    def _q_traverse(self, path):
117
        if not get_cfg('misc', {}).get('aq-strongbox'):
118
            raise errors.TraversalError()
119
        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
120
        return Directory._q_traverse(self, path)
121

  
122
    def get_form(self):
123
        types = [(x.id, x.label) for x in StrongboxType.select()]
124
        form = Form(action='add', enctype='multipart/form-data')
125
        form.add(StringWidget, 'description', title=_('Description'), size=60)
126
        form.add(FileWidget, 'file', title=_('File'), required=True)
127
        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
128
                 options = [(None, _('Not specified'))] + types)
129
        form.add(DateWidget, 'date_time', title = _('Document Date'))
130
        form.add_submit('submit', _('Upload'))
131
        return form
132

  
133
    def _q_index(self):
134
        template.html_top(_('Strongbox'))
135
        r = TemplateIO(html=True)
136

  
137
        # TODO: a paragraph of explanations here could be useful
138

  
139
        sffiles = StrongboxItem.get_with_indexed_value(
140
                        str('user_id'), str(get_request().user.id))
141
        if sffiles:
142
            r += htmltext('<table id="strongbox-items">')
143
            r += htmltext('<tr><th></th><th>%s</th><th>%s</th><th></th></tr>') % (
144
                _('Type'), _('Expiration'))
145
        else:
146
            r += htmltext('<p>')
147
            r += _('There is currently nothing in your strongbox.')
148
            r += htmltext('</p>')
149
        has_items_to_validate = False
150
        for i, sffile in enumerate(sffiles):
151
            expired = False
152
            if not sffile.validated_time:
153
                has_items_to_validate = True
154
                continue
155
            if sffile.expiration_time and sffile.expiration_time < time.localtime():
156
                expired = True
157
            if i%2:
158
                classnames = ['odd']
159
            else:
160
                classnames = ['even']
161
            if expired:
162
                classnames.append('expired')
163
            r += htmltext('<tr class="%s">') % ' '.join(classnames)
164
            r += htmltext('<td class="label">')
165
            r += sffile.get_display_name()
166
            r += htmltext('</td>')
167
            if sffile.type_id:
168
                r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
169
            else:
170
                r += htmltext('<td class="type">-</td>')
171
            if sffile.expiration_time:
172
                r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
173
                if expired:
174
                    r += ' (%s)' % _('expired')
175
                r += htmltext('</td>')
176
            else:
177
                r += htmltext('<td class="expiration">-</td>')
178
            r += htmltext('<td class="actions">')
179
            r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
180
            r += htmltext('[<a rel="popup" href="remove?id=%s">%s</a>] ') % (sffile.id, _('remove'))
181
            r += htmltext('</td>')
182
            r += htmltext('</tr>')
183

  
184
        if has_items_to_validate:
185
            r += htmltext('<tr><td colspan="4"><h3>%s</h3></td></tr>') % _('Proposed Items')
186
            for sffile in sffiles:
187
                if sffile.validated_time:
188
                    continue
189
                if sffile.expiration_time and sffile.expiration_time < time.localtime():
190
                    expired = True
191
                if i%2:
192
                    classnames = ['odd']
193
                else:
194
                    classnames = ['even']
195
                if expired:
196
                    classnames.append('expired')
197
                r += htmltext('<tr class="%s">') % ' '.join(classnames)
198

  
199
                r += htmltext('<td class="label">')
200
                r += sffile.get_display_name()
201
                r += htmltext('</td>')
202
                if sffile.type_id:
203
                    r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
204
                else:
205
                    r += htmltext('<td class="type">-</td>')
206

  
207
                if sffile.expiration_time:
208
                    r += htmltext('<td class="expiration">%s') % strftime(misc.date_format(), sffile.expiration_time)
209
                    if expired:
210
                        r += ' (%s)' % _('expired')
211
                    r += htmltext('</td>')
212
                else:
213
                    r += htmltext('<td class="expiration">-</td>')
214
                r += htmltext('<td class="actions">')
215
                r += htmltext(' [<a href="download?id=%s">%s</a>] ') % (sffile.id, _('download'))
216
                r += htmltext(' [<a href="validate?id=%s">%s</a>] ') % (sffile.id, _('validate'))
217
                r += htmltext(' [<a href="remove?id=%s">%s</a>] ') % (sffile.id, _('reject'))
218
                r += htmltext('</td>')
219
                r += htmltext('</tr>')
220
        if sffiles:
221
            r += htmltext('</table>')
222

  
223
        r += htmltext('<h3>%s</h3>') % _('Add a file to the strongbox')
224
        form = self.get_form()
225
        r += form.render()
226
        return r.getvalue()
227

  
228
    def add(self):
229
        form = self.get_form()
230
        if not form.is_submitted():
231
            if get_request().form.get('mode') == 'pick':
232
                return redirect('pick')
233
            else:
234
                return redirect('.')
235

  
236
        sffile = StrongboxItem()
237
        sffile.user_id = get_request().user.id
238
        sffile.description = form.get_widget('description').parse()
239
        sffile.validated_time = time.localtime()
240
        sffile.type_id = form.get_widget('type_id').parse()
241
        v = form.get_widget('date_time').parse()
242
        sffile.set_expiration_time_from_date(v)
243
        sffile.store()
244
        sffile.set_file(form.get_widget('file').parse())
245
        sffile.store()
246
        if get_request().form.get('mode') == 'pick':
247
            return redirect('pick')
248
        else:
249
            return redirect('.')
250

  
251
    def download(self):
252
        id = get_request().form.get('id')
253
        if not id:
254
            raise errors.TraversalError()
255
        try:
256
            sffile = StrongboxItem.get(id)
257
        except KeyError:
258
            raise errors.TraversalError()
259
        if str(sffile.user_id) != str(get_request().user.id):
260
            raise errors.TraversalError()
261

  
262
        filename = sffile.file.filename
263
        fd = file(filename)
264
        size = os.path.getsize(filename)
265
        response = get_response()
266
        response.set_content_type('application/octet-stream')
267
        response.set_header('content-disposition', 'attachment; filename="%s"' % sffile.file.base_filename)
268
        return FileStream(fd, size)
269

  
270
    def validate(self):
271
        id = get_request().form.get('id')
272
        if not id:
273
            raise errors.TraversalError()
274
        try:
275
            sffile = StrongboxItem.get(id)
276
        except KeyError:
277
            raise errors.TraversalError()
278
        if str(sffile.user_id) != str(get_request().user.id):
279
            raise errors.TraversalError()
280
        sffile.validated_time = time.time()
281
        sffile.store()
282
        return redirect('.')
283

  
284
    def remove(self):
285
        id = get_request().form.get('id')
286
        if not id:
287
            raise errors.TraversalError()
288
        try:
289
            sffile = StrongboxItem.get(id)
290
        except KeyError:
291
            raise errors.TraversalError()
292
        if str(sffile.user_id) != str(get_request().user.id):
293
            raise errors.TraversalError()
294

  
295
        r = TemplateIO(html=True)
296
        form = Form(enctype='multipart/form-data')
297
        form.add_hidden('id', get_request().form.get('id'))
298
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
299
                        'You are about to irrevocably delete this item from your strongbox.')))
300
        form.add_submit('submit', _('Submit'))
301
        form.add_submit('cancel', _('Cancel'))
302
        if form.get_submit() == 'cancel':
303
            return redirect('.')
304
        if not form.is_submitted() or form.has_errors():
305
            if sffile.type_id:
306
                r += htmltext('<h2>%s</h2>') % _('Deleting %(filetype)s: %(filename)s') % {
307
                        'filetype': StrongboxType.get(sffile.type_id).label,
308
                        'filename': sffile.get_display_name()
309
                    }
310
            else:
311
                r += htmltext('<h2>%s</h2>') % _('Deleting %(filename)s') % {'filename': sffile.get_display_name()}
312
            r += form.render()
313
            return r.getvalue()
314
        else:
315
            sffile.remove_self()
316
            sffile.remove_file()
317
            return redirect('.')
318

  
319
    def picked_file(self):
320
        get_response().set_content_type('application/json')
321
        sffile = StrongboxItem.get(get_request().form.get('val'))
322
        sffile.file.fp = file(sffile.file.filename)
323
        if sffile.user_id != get_request().user.id:
324
            raise errors.TraversalError()
325
        # XXX: this will copy the file, it would be quite nice if it was
326
        # possible to just make it a symlink to the sffile
327
        token = get_session().add_tempfile(sffile.file)
328
        return json.dumps({'token': token, 'filename': sffile.file.base_filename})
329

  
330
    def pick(self):
331
        if get_request().form.get('select') == 'true':
332
            return self.picked_file()
333
        r = TemplateIO(html=True)
334
        root_url = get_publisher().get_root_url()
335
        sffiles = StrongboxItem.get_with_indexed_value(
336
                        str('user_id'), str(get_request().user.id))
337
        r += htmltext('<h2>%s</h2>') % _('Pick a file')
338

  
339
        if not sffiles:
340
            r += htmltext('<p>')
341
            r += _('You do not have any file in your strongbox at the moment.')
342
            r += htmltext('</p>')
343
            r += htmltext('<div class="buttons">')
344
            r += htmltext('<a href="%smyspace/strongbox/" target="_blank">%s</a>') % (root_url,
345
                _('Open Strongbox Management'))
346
            r += htmltext('</div>')
347
        else:
348
            r += htmltext('<form id="strongbox-pick">')
349
            r += htmltext('<ul>')
350
            for sffile in sffiles:
351
                r += htmltext('<li><label><input type="radio" name="file" value="%s"/>%s</label>') % (
352
                                sffile.id, sffile.get_display_name())
353
                r += htmltext(' [<a href="%smyspace/strongbox/download?id=%s">%s</a>] ') % (
354
                                root_url, sffile.id, _('view'))
355
                r += htmltext('</li>')
356
            r += htmltext('</ul>')
357

  
358
            r += htmltext('<div class="buttons">')
359
            r += htmltext('<input name="cancel" type="button" value="%s"/>') % _('Cancel')
360
            r += ' '
361
            r += htmltext('<input name="pick" type="button" value="%s"/>') % _('Pick')
362
            r += htmltext('</div>')
363
            r += htmltext('</form>')
364
        return r.getvalue()
365

  
366

  
367
class JsonDirectory(Directory):
368
    '''Export of several lists in json, related to the current user or the
369
       SAMLv2 NameID we'd get in the URL'''
370

  
371
    _q_exports = ['forms']
372

  
373
    user = None
374

  
375
    def _q_traverse(self, path):
376
        self.user = get_user_from_api_query_string() or get_request().user
377
        if not self.user:
378
            raise errors.AccessUnauthorizedError()
379
        return Directory._q_traverse(self, path)
380

  
381
    def forms(self):
382
        formdefs = FormDef.select(order_by='name', ignore_errors=True)
383
        user_forms = []
384
        for formdef in formdefs:
385
            user_forms.extend(formdef.data_class().get_with_indexed_value(
386
                        'user_id', self.user.id))
387
        user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
388

  
389
        get_response().set_content_type('application/json')
390

  
391

  
392
        forms_output = []
393
        for form in user_forms:
394
            visible_status = form.get_visible_status(user=self.user)
395
            # skip drafts and hidden forms
396
            if not visible_status:
397
                continue
398
            name = form.formdef.name
399
            id = form.get_display_id()
400
            status = visible_status.name
401
            title = _('%(name)s #%(id)s (%(status)s)') % {
402
                    'name': name,
403
                    'id': id,
404
                    'status': status
405
            }
406
            url = form.get_url()
407
            d = { 'title': title, 'url': url }
408
            d.update(form.get_substitution_variables(minimal=True))
409
            forms_output.append(d)
410
        return json.dumps(forms_output, cls=misc.JSONEncoder)
411

  
412

  
413
class MyspaceDirectory(wcs.myspace.MyspaceDirectory):
414
    _q_exports = ['', 'profile', 'new', 'password', 'remove', 'drafts', 'forms',
415
            'announces', 'strongbox', 'invoices', 'json']
416

  
417
    strongbox = StrongboxDirectory()
418
    invoices = MyInvoicesDirectory()
419
    json = JsonDirectory()
420

  
421
    def _q_traverse(self, path):
422
        get_response().filter['bigdiv'] = 'profile'
423
        get_response().breadcrumb.append(('myspace/', _('My Space')))
424

  
425
        # Migrate custom text settings
426
        texts_cfg = get_cfg('texts', {})
427
        if 'text-aq-top-of-profile' in texts_cfg and (
428
                        not 'text-top-of-profile' in texts_cfg):
429
            texts_cfg['text-top-of-profile'] = texts_cfg['text-aq-top-of-profile']
430
            del texts_cfg['text-aq-top-of-profile']
431
            get_publisher().write_cfg()
432

  
433
        return Directory._q_traverse(self, path)
434

  
435

  
436
    def _q_index(self):
437
        user = get_request().user
438
        if not user:
439
            raise errors.AccessUnauthorizedError()
440
        template.html_top(_('My Space'))
441
        r = TemplateIO(html=True)
442
        if user.anonymous:
443
            return redirect('new')
444

  
445
        user_formdef = user.get_formdef()
446

  
447
        user_forms = []
448
        if user:
449
            formdefs = FormDef.select(order_by='name', ignore_errors=True)
450
            user_forms = []
451
            for formdef in formdefs:
452
                user_forms.extend(formdef.data_class().get_with_indexed_value(
453
                            'user_id', user.id))
454
            user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
455

  
456
        profile_links = []
457
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
458
            if user_formdef:
459
                profile_links.append('<a href="#my-profile">%s</a>' % _('My Profile'))
460
        if user_forms:
461
            profile_links.append('<a href="#my-forms">%s</a>' % _('My Forms'))
462
        if get_cfg('misc', {}).get('aq-strongbox'):
463
            profile_links.append('<a href="strongbox/">%s</a>' % _('My Strongbox'))
464
        if is_payment_supported():
465
            profile_links.append('<a href="invoices/">%s</a>' % _('My Invoices'))
466

  
467
        root_url = get_publisher().get_root_url()
468
        if user.can_go_in_backoffice():
469
            profile_links.append('<a href="%sbackoffice/">%s</a>' % (root_url, _('Back office')))
470
        if user.is_admin:
471
            profile_links.append('<a href="%sadmin/">%s</a>' % (root_url, _('Admin')))
472

  
473
        if profile_links:
474
            r += htmltext('<p id="profile-links">')
475
            r += htmltext(' - '.join(profile_links))
476
            r += htmltext('</p>')
477

  
478
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
479
            if user_formdef:
480
                r += self._my_profile(user_formdef, user)
481

  
482
            r += self._index_buttons(user_formdef)
483

  
484
            try:
485
                x = PasswordAccount.get_on_index(get_request().user.id, str('user_id'))
486
            except KeyError:
487
                pass
488
            else:
489
                r += htmltext('<p>')
490
                r += _('You can delete your account freely from the services portal. '
491
                       'This action is irreversible; it will destruct your personal '
492
                       'datas and destruct the access to your request history.')
493
                r += htmltext(' <strong><a href="remove" rel="popup">%s</a></strong>.') % _('Delete My Account')
494
                r += htmltext('</p>')
495

  
496
        options = get_cfg('misc', {}).get('announce_themes')
497
        if options:
498
            try:
499
                subscription = AnnounceSubscription.get_on_index(
500
                        get_request().user.id, str('user_id'))
501
            except KeyError:
502
                pass
503
            else:
504
                r += htmltext('<p class="command"><a href="announces">%s</a></p>') % _(
505
                        'Edit my Subscription to Announces')
506

  
507
        if user_forms:
508
            r += htmltext('<h3 id="my-forms">%s</h3>') % _('My Forms')
509
            r += root.FormsRootDirectory().user_forms(user_forms)
510

  
511
        return r.getvalue()
512

  
513
    def _my_profile(self, user_formdef, user):
514
        r = TemplateIO(html=True)
515
        r += htmltext('<h3 id="my-profile">%s</h3>') % _('My Profile')
516

  
517
        r += TextsDirectory.get_html_text('top-of-profile')
518

  
519
        if user.form_data:
520
            r += htmltext('<ul>')
521
            for field in user_formdef.fields:
522
                if not hasattr(field, str('get_view_value')):
523
                    continue
524
                value = user.form_data.get(field.id)
525
                r += htmltext('<li>')
526
                r += field.label
527
                r += ' : '
528
                if value:
529
                    r += field.get_view_value(value)
530
                r += htmltext('</li>')
531
            r += htmltext('</ul>')
532
        else:
533
            r += htmltext('<p>%s</p>') % _('Empty profile')
534
        return r.getvalue()
535

  
536
    def _index_buttons(self, form_data):
537
        r = TemplateIO(html=True)
538
        passwords_cfg = get_cfg('passwords', {})
539
        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
540
        if get_session().lasso_session_dump:
541
            ident_method = 'idp'
542

  
543
        if form_data and ident_method != 'idp':
544
            r += htmltext('<p class="command"><a href="profile" rel="popup">%s</a></p>') % _('Edit My Profile')
545

  
546
        if ident_method == 'password' and passwords_cfg.get('can_change', False):
547
            r += htmltext('<p class="command"><a href="password" rel="popup">%s</a></p>') % _('Change My Password')
548

  
549
        return r.getvalue()
550

  
551
    def profile(self):
552
        user = get_request().user
553
        if not user or user.anonymous:
554
            raise errors.AccessUnauthorizedError()
555

  
556
        form = Form(enctype = 'multipart/form-data')
557
        formdef = user.get_formdef()
558
        formdef.add_fields_to_form(form, form_data = user.form_data)
559

  
560
        form.add_submit('submit', _('Apply Changes'))
561
        form.add_submit('cancel', _('Cancel'))
562

  
563
        if form.get_submit() == 'cancel':
564
            return redirect('.')
565

  
566
        if form.is_submitted() and not form.has_errors():
567
            self.profile_submit(form, formdef)
568
            return redirect('.')
569

  
570
        template.html_top(_('Edit Profile'))
571
        return form.render()
572

  
573
    def profile_submit(self, form, formdef):
574
        user = get_request().user
575
        data = formdef.get_data(form)
576

  
577
        user.set_attributes_from_formdata(data)
578
        user.form_data = data
579

  
580
        user.store()
581

  
582
    def password(self):
583
        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
584
        if ident_method != 'password':
585
            raise errors.TraversalError()
586

  
587
        user = get_request().user
588
        if not user or user.anonymous:
589
            raise errors.AccessUnauthorizedError()
590

  
591
        form = Form(enctype = 'multipart/form-data')
592
        form.add(PasswordWidget, 'new_password', title = _('New Password'),
593
                required=True)
594
        form.add(PasswordWidget, 'new2_password', title = _('New Password (confirm)'),
595
                required=True) 
596

  
597
        form.add_submit('submit', _('Change Password'))
598
        form.add_submit('cancel', _('Cancel'))
599

  
600
        if form.get_submit() == 'cancel':
601
            return redirect('.')
602

  
603
        if form.is_submitted() and not form.has_errors():
604
            qommon.ident.password.check_password(form, 'new_password')
605
            new_password = form.get_widget('new_password').parse()
606
            new2_password = form.get_widget('new2_password').parse()
607
            if new_password != new2_password:
608
                form.set_error('new2_password', _('Passwords do not match'))
609

  
610
        if form.is_submitted() and not form.has_errors():
611
            self.submit_password(new_password)
612
            return redirect('.')
613

  
614
        template.html_top(_('Change Password'))
615
        return form.render()
616

  
617
    def submit_password(self, new_password):
618
        passwords_cfg = get_cfg('passwords', {})
619
        account = PasswordAccount.get(get_session().username)
620
        account.hashing_algo = passwords_cfg.get('hashing_algo')
621
        account.set_password(new_password)
622
        account.store()
623

  
624
    def new(self):
625
        if not get_request().user or not get_request().user.anonymous:
626
            raise errors.AccessUnauthorizedError()
627

  
628
        form = Form(enctype = 'multipart/form-data')
629
        formdef = get_publisher().user_class.get_formdef()
630
        if formdef:
631
            formdef.add_fields_to_form(form)
632
        else:
633
            get_logger().error('missing user formdef (in myspace/new)')
634

  
635
        form.add_submit('submit', _('Register'))
636

  
637
        if form.is_submitted() and not form.has_errors():
638
            user = get_publisher().user_class()
639
            data = formdef.get_data(form)
640
            user.set_attributes_from_formdata(data)
641
            user.name_identifiers = get_request().user.name_identifiers
642
            user.lasso_dump = get_request().user.lasso_dump
643
            user.set_attributes_from_formdata(data)
644
            user.form_data = data
645
            user.store()
646
            get_session().set_user(user.id)
647
            root_url = get_publisher().get_root_url()
648
            return redirect('%smyspace' % root_url)
649

  
650
        template.html_top(_('Welcome'))
651
        return form.render()
652

  
653

  
654
    def remove(self):
655
        user = get_request().user
656
        if not user or user.anonymous:
657
            raise errors.AccessUnauthorizedError()
658

  
659
        form = Form(enctype = 'multipart/form-data')
660
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
661
                        'Are you really sure you want to remove your account?')))
662
        form.add_submit('submit', _('Remove my account'))
663
        form.add_submit('cancel', _('Cancel'))
664

  
665
        if form.get_submit() == 'cancel':
666
            return redirect('.')
667

  
668
        if form.is_submitted() and not form.has_errors():
669
            user = get_request().user
670
            account = PasswordAccount.get_on_index(user.id, str('user_id'))
671
            get_session_manager().expire_session() 
672
            account.remove_self()
673
            return redirect(get_publisher().get_root_url())
674

  
675
        template.html_top(_('Removing Account'))
676
        return form.render()
677

  
678
    def announces(self):
679
        options = get_cfg('misc', {}).get('announce_themes')
680
        if not options:
681
            raise errors.TraversalError()
682
        user = get_request().user
683
        if not user or user.anonymous:
684
            raise errors.AccessUnauthorizedError()
685
        subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id'))
686
        if not subscription:
687
            raise errors.TraversalError()
688

  
689
        if subscription.enabled_themes is None:
690
            enabled_themes = options
691
        else:
692
            enabled_themes = subscription.enabled_themes
693

  
694
        form = Form(enctype = 'multipart/form-data')
695
        form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'),
696
                value=enabled_themes, options=[(x, x, x) for x in options],
697
                inline=False, required=False)
698

  
699
        form.add_submit('submit', _('Apply Changes'))
700
        form.add_submit('cancel', _('Cancel'))
701

  
702
        if form.get_submit() == 'cancel':
703
            return redirect('.')
704

  
705
        if form.is_submitted() and not form.has_errors():
706
            chosen_themes = form.get_widget('themes').parse()
707
            if chosen_themes == options:
708
                chosen_themes = None
709
            subscription.enabled_themes = chosen_themes
710
            subscription.store()
711
            return redirect('.')
712

  
713
        template.html_top()
714
        get_response().breadcrumb.append(('announces', _('Announce Subscription')))
715
        return form.render()
716

  
717

  
718
TextsDirectory.register('aq-myspace-invoice',
719
        N_('Message on top of invoices page'),
720
        category = N_('Invoices'))
721

  
extra/modules/payments.py
1
import random
2
import string
3
from datetime import datetime as dt
4
import hashlib
5
import time
6
import urllib
7

  
8
from decimal import Decimal
9

  
10
from quixote import (redirect, get_publisher, get_request, get_session,
11
        get_response)
12
from quixote.directory import Directory
13
from quixote.html import TemplateIO, htmltext
14

  
15
if not set:
16
    from sets import Set as set
17

  
18
eopayment = None
19
try:
20
    import eopayment
21
except ImportError:
22
    pass
23

  
24
from qommon import _
25
from qommon import errors, get_logger, get_cfg, emails
26
from qommon.storage import StorableObject
27
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
28
    WidgetDict
29
from qommon.misc import simplify
30

  
31
from wcs.formdef import FormDef
32
from wcs.formdata import Evolution
33
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
34
from wcs.users import User
35

  
36
def is_payment_supported():
37
    if not eopayment:
38
        return False
39
    return get_cfg('aq-permissions', {}).get('payments', None) is not None
40

  
41

  
42
class Regie(StorableObject):
43
    _names = 'regies'
44

  
45
    label = None
46
    description = None
47
    service = None
48
    service_options = None
49

  
50
    def get_payment_object(self):
51
        return eopayment.Payment(kind=self.service,
52
                                 options=self.service_options)
53

  
54

  
55
class Invoice(StorableObject):
56
    _names = 'invoices'
57
    _hashed_indexes = ['user_id', 'regie_id']
58
    _indexes = ['external_id']
59

  
60
    user_id = None
61
    regie_id = None
62
    formdef_id = None
63
    formdata_id = None
64
    subject = None
65
    details = None
66
    amount = None
67
    date = None
68
    paid = False
69
    paid_date = None
70
    canceled = False
71
    canceled_date = None
72
    canceled_reason = None
73
    next_status = None
74
    external_id = None
75
    request_kwargs = {}
76

  
77
    def __init__(self, id=None, regie_id=None, formdef_id=None):
78
        self.id = id
79
        self.regie_id = regie_id
80
        self.formdef_id = formdef_id
81
        if get_publisher() and not self.id:
82
            self.id = self.get_new_id()
83

  
84
    def get_user(self):
85
        if self.user_id:
86
            return User.get(self.user_id, ignore_errors=True)
87
        return None
88

  
89
    @property
90
    def username(self):
91
        user = self.get_user()
92
        return user.name if user else ''
93

  
94
    def get_new_id(self, create=False):
95
        # format : date-regie-formdef-alea-check
96
        r = random.SystemRandom()
97
        self.fresh = True
98
        while True:
99
            id = '-'.join([
100
                dt.now().strftime('%Y%m%d'),
101
                'r%s' % (self.regie_id or 'x'),
102
                'f%s' % (self.formdef_id or 'x'),
103
                ''.join([r.choice(string.digits) for x in range(5)])
104
                ])
105
            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
106
            id = id + '-' + crc
107
            if not self.has_key(id):
108
                return id
109

  
110
    def store(self, *args, **kwargs):
111
        if getattr(self, 'fresh', None) is True:
112
            del self.fresh
113
            notify_new_invoice(self)
114
        return super(Invoice, self).store(*args, **kwargs)
115

  
116
    def check_crc(cls, id):
117
        try:
118
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
119
        except:
120
            return False
121
    check_crc = classmethod(check_crc)
122

  
123
    def pay(self):
124
        self.paid = True
125
        self.paid_date = dt.now()
126
        self.store()
127
        get_logger().info(_('invoice %s paid'), self.id)
128
        notify_paid_invoice(self)
129

  
130
    def unpay(self):
131
        self.paid = False
132
        self.paid_date = None
133
        self.store()
134
        get_logger().info(_('invoice %s unpaid'), self.id)
135

  
136
    def cancel(self, reason=None):
137
        self.canceled = True
138
        self.canceled_date = dt.now()
139
        if reason:
140
            self.canceled_reason = reason
141
        self.store()
142
        notify_canceled_invoice(self)
143
        get_logger().info(_('invoice %s canceled'), self.id)
144

  
145
    def payment_url(self):
146
        base_url = get_publisher().get_frontoffice_url()
147
        return '%s/invoices/%s' % (base_url, self.id)
148

  
149

  
150
INVOICE_EVO_VIEW = {
151
    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
152
    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
153
    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
154
    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
155
}
156

  
157
class InvoiceEvolutionPart:
158
    action = None
159
    id = None
160
    subject = None
161
    amount = None
162
    transaction = None
163

  
164
    def __init__(self, action, invoice, transaction=None):
165
        self.action = action
166
        self.id = invoice.id
167
        self.subject = invoice.subject
168
        self.amount = invoice.amount
169
        self.transaction = transaction
170

  
171
    def view(self):
172
        vars = {
173
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
174
            'id': self.id,
175
            'subject': self.subject,
176
            'amount': self.amount,
177
        }
178
        if self.transaction:
179
            vars['transaction_order_id'] = self.transaction.order_id
180
        return htmltext('<p class="invoice-%s">' % self.action + \
181
                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
182

  
183

  
184
class Transaction(StorableObject):
185
    _names = 'transactions'
186
    _hashed_indexes = ['invoice_ids']
187
    _indexes = ['order_id']
188

  
189
    invoice_ids = None
190

  
191
    order_id = None
192
    start = None
193
    end = None
194
    bank_data = None
195

  
196
    def __init__(self, *args, **kwargs):
197
        self.invoice_ids = list()
198
        StorableObject.__init__(self, *args, **kwargs)
199

  
200
    def get_new_id(cls, create=False):
201
        r = random.SystemRandom()
202
        while True:
203
            id = ''.join([r.choice(string.digits) for x in range(16)])
204
            if not cls.has_key(id):
205
                return id
206
    get_new_id = classmethod(get_new_id)
207

  
208
class PaymentWorkflowStatusItem(WorkflowStatusItem):
209
    description = N_('Payment Creation')
210
    key = 'payment'
211
    endpoint = False
212
    category = ('aq-payment', N_('Payment'))
213
    support_substitution_variables = True
214

  
215
    subject = None
216
    details = None
217
    amount = None
218
    regie_id = None
219
    next_status = None
220
    request_kwargs = {}
221

  
222
    def is_available(self, workflow=None):
223
        return is_payment_supported()
224
    is_available = classmethod(is_available)
225

  
226
    def render_as_line(self):
227
        if self.regie_id:
228
            try:
229
                return _('Payable to %s' % Regie.get(self.regie_id).label)
230
            except KeyError:
231
                return _('Payable (not completed)')
232
        else:
233
            return _('Payable (not completed)')
234

  
235
    def get_parameters(self):
236
        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
237
                'request_kwargs')
238

  
239
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
240
        if 'subject' in parameters:
241
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
242
                     value=self.subject, size=40)
243
        if 'details' in parameters:
244
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
245
                     value=self.details, cols=80, rows=10)
246
        if 'amount' in parameters:
247
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
248
        if 'regie_id' in parameters:
249
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
250
                title=_('Regie'), value=self.regie_id,
251
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
252
        if 'next_status' in parameters:
253
            form.add(SingleSelectWidget, '%snext_status' % prefix,
254
                title=_('Status after validation'), value = self.next_status,
255
                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
256
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
257
        if 'request_kwargs' in parameters:
258
            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
259
            hint = ''
260
            hint +=_('If the value starts by = it will be '
261
                'interpreted as a Python expression.')
262
            hint += ' '
263
            hint += _('Standard keys are: %s.') % (', '.join(keys))
264
            form.add(WidgetDict, 'request_kwargs',
265
                title=_('Parameters for the payment system'),
266
                hint=hint,
267
                value = self.request_kwargs)
268

  
269
    def perform(self, formdata):
270
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
271
        invoice.user_id = formdata.user_id
272
        invoice.formdata_id = formdata.id
273
        invoice.next_status = self.next_status
274
        if self.subject:
275
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
276
        else:
277
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
278
                    'form_name': formdata.formdef.name,
279
                    'formdata_id': formdata.id }
280
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
281
        invoice.amount = Decimal(self.compute(self.amount))
282
        invoice.date = dt.now()
283
        invoice.request_kwargs = {}
284
        if self.request_kwargs:
285
            for key, value in self.request_kwargs.iteritems():
286
                invoice.request_kwargs[key] = self.compute(value)
287
        invoice.store()
288
        # add a message in formdata.evolution
289
        evo = Evolution()
290
        evo.time = time.localtime()
291
        evo.status = formdata.status
292
        evo.add_part(InvoiceEvolutionPart('create', invoice))
293
        if not formdata.evolution:
294
            formdata.evolution = []
295
        formdata.evolution.append(evo)
296
        formdata.store()
297
        # redirect the user to "my invoices"
298
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
299

  
300
register_item_class(PaymentWorkflowStatusItem)
301

  
302
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
303
    description = N_('Payment Cancel')
304
    key = 'payment-cancel'
305
    endpoint = False
306
    category = ('aq-payment', N_('Payment'))
307

  
308
    reason = None
309
    regie_id = None
310

  
311
    def is_available(self, workflow=None):
312
        return is_payment_supported()
313
    is_available = classmethod(is_available)
314

  
315
    def render_as_line(self):
316
        if self.regie_id:
317
            if self.regie_id == '_all':
318
                return _('Cancel all Payments')
319
            else:
320
                try:
321
                    return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
322
                except KeyError:
323
                    return _('Cancel Payments (non completed)')
324
        else:
325
            return _('Cancel Payments (non completed)')
326

  
327
    def get_parameters(self):
328
        return ('reason', 'regie_id')
329

  
330
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
331
        if 'reason' in parameters:
332
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
333
                     value=self.reason, size=40)
334
        if 'regie_id' in parameters:
335
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
336
                title=_('Regie'), value=self.regie_id,
337
                options = [(None, '---'), ('_all', _('All Regies'))] + \
338
                            [(x.id, x.label) for x in Regie.select()])
339

  
340
    def perform(self, formdata):
341
        invoices_id = []
342
        # get all invoices for the formdata and the selected regie
343
        for evo in [evo for evo in formdata.evolution if evo.parts]:
344
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
345
                if part.action == 'create':
346
                    invoices_id.append(part.id)
347
                elif part.id in invoices_id:
348
                    invoices_id.remove(part.id)
349
        invoices = [Invoice.get(id) for id in invoices_id]
350
        # select invoices for the selected regie (if not "all regies")
351
        if self.regie_id != '_all':
352
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
353
        # security filter: check user
354
        invoices = [i for i in invoices if i.user_id == formdata.user_id]
... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.

Formats disponibles : Unified diff