Projet

Général

Profil

0001-convert-remaining-files-from-.ptl-to-.py-3930.patch

Frédéric Péters, 18 avril 2014 16:51

Télécharger (264 ko)

Voir les différences:

Subject: [PATCH] convert remaining files from .ptl to .py (#3930)

 extra/modules/events_ui.ptl    |  390 ------------
 extra/modules/events_ui.py     |  406 +++++++++++++
 extra/modules/formpage.ptl     |   36 --
 extra/modules/formpage.py      |   36 ++
 extra/modules/msp_ui.ptl       |  176 ------
 extra/modules/msp_ui.py        |  183 ++++++
 extra/modules/myspace.ptl      |  715 ----------------------
 extra/modules/myspace.py       |  735 ++++++++++++++++++++++
 extra/modules/payments_ui.ptl  |  571 ------------------
 extra/modules/payments_ui.py   |  595 ++++++++++++++++++
 extra/modules/root.ptl         | 1275 ---------------------------------------
 extra/modules/root.py          | 1308 ++++++++++++++++++++++++++++++++++++++++
 extra/modules/strongbox_ui.ptl |  261 --------
 extra/modules/strongbox_ui.py  |  274 +++++++++
 14 files changed, 3537 insertions(+), 3424 deletions(-)
 delete mode 100644 extra/modules/events_ui.ptl
 create mode 100644 extra/modules/events_ui.py
 delete mode 100644 extra/modules/formpage.ptl
 create mode 100644 extra/modules/formpage.py
 delete mode 100644 extra/modules/msp_ui.ptl
 create mode 100644 extra/modules/msp_ui.py
 delete mode 100644 extra/modules/myspace.ptl
 create mode 100644 extra/modules/myspace.py
 delete mode 100644 extra/modules/payments_ui.ptl
 create mode 100644 extra/modules/payments_ui.py
 delete mode 100644 extra/modules/root.ptl
 create mode 100644 extra/modules/root.py
 delete mode 100644 extra/modules/strongbox_ui.ptl
 create mode 100644 extra/modules/strongbox_ui.py
extra/modules/events_ui.ptl
1
import time
2

  
3
from quixote import get_request, get_response, get_session, redirect
4
from quixote.directory import Directory, AccessControlled
5

  
6
import wcs
7
import wcs.admin.root
8
from wcs.backoffice.menu import *
9

  
10
from qommon import errors, misc
11
from qommon.form import *
12
from qommon.strftime import strftime
13

  
14
from events import Event, RemoteCalendar, get_default_event_tags
15

  
16

  
17

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

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

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

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

  
40
        html_top('events', title = _('Remote Calendar: %s') % self.calendar.label)
41
        '<h2>%s</h2>' % _('Remote Calendar: %s') % self.calendar.label
42

  
43
        get_session().display_message()
44

  
45
        '<p>'
46
        self.calendar.url
47
        if self.calendar.error:
48
            ' - <span class="error-message">%s</span>' % self.calendar.get_error_message()
49
        '</p>'
50

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

  
65
                ' : '
66
                if ev.url:
67
                    '<a href="%s">%s</a>' % (ev.url, ev.title)
68
                else:
69
                    ev.title
70
                '</li>'
71
            '</ul>'
72

  
73
        form.render()
74

  
75
    def edit [html] (self):
76
        form = self.form()
77
        if form.get_submit() == 'cancel':
78
            return redirect('.')
79

  
80
        if form.is_submitted() and not form.has_errors():
81
            self.submit(form)
82
            return redirect('..')
83

  
84
        html_top('events', title = _('Edit Remote Calendar: %s') % self.calendar.label)
85
        '<h2>%s</h2>' % _('Edit Remote Calendar: %s') % self.calendar.label
86
        form.render()
87

  
88

  
89
    def form(self):
90
        form = Form(enctype='multipart/form-data')
91
        form.add(StringWidget, 'label', title = _('Label'), required = True,
92
                value = self.calendar.label)
93
        form.add(StringWidget, 'url', title = _('URL'), required = True,
94
                value = self.calendar.url, size = 40)
95
        form.add_submit('submit', _('Submit'))
96
        form.add_submit('cancel', _('Cancel'))
97
        return form
98

  
99
    def submit(self, form):
100
        for k in ('label', 'url'):
101
            widget = form.get_widget(k)
102
            if widget:
103
                setattr(self.calendar, k, widget.parse())
104
        self.calendar.store()
105

  
106
    def delete [html] (self):
107
        form = Form(enctype='multipart/form-data')
108
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
109
                        'You are about to irrevocably delete this remote calendar.')))
110
        form.add_submit('submit', _('Submit'))
111
        form.add_submit('cancel', _('Cancel'))
112
        if form.get_submit() == 'cancel':
113
            return redirect('..')
114
        if not form.is_submitted() or form.has_errors():
115
            get_response().breadcrumb.append(('delete', _('Delete')))
116
            html_top('events', title = _('Delete Remote Calendar'))
117
            '<h2>%s</h2>' % _('Deleting Remote Calendar: %s') % self.calendar.label
118
            form.render()
119
        else:
120
            self.calendar.remove_self()
121
            return redirect('..')
122

  
123
    def update(self):
124
        get_session().message = ('info',
125
                _('Calendar update has been requested, reload in a few moments'))
126
        get_response().add_after_job('updating remote calendar',
127
                self.calendar.download_and_parse,
128
                fire_and_forget = True)
129
        return redirect('.')
130

  
131

  
132

  
133
class RemoteCalendarsDirectory(Directory):
134
    _q_exports = ['', 'new']
135

  
136
    def _q_traverse(self, path):
137
        get_response().breadcrumb.append(('remote/', _('Remote Calendars')))
138
        return Directory._q_traverse(self, path)
139

  
140
    def _q_index [html] (self):
141
        return redirect('..')
142

  
143
    def new [html] (self):
144
        calendar_ui = RemoteCalendarDirectory(RemoteCalendar())
145

  
146
        form = calendar_ui.form()
147
        if form.get_submit() == 'cancel':
148
            return redirect('.')
149

  
150
        if form.is_submitted() and not form.has_errors():
151
            calendar_ui.submit(form)
152
            return redirect('%s/' % calendar_ui.calendar.id)
153

  
154
        get_response().breadcrumb.append(('new', _('New Remote Calendar')))
155
        html_top('events', title = _('New Remote Calendar'))
156
        '<h2>%s</h2>' % _('New Remote Calendar')
157
        form.render()
158

  
159
    def _q_lookup(self, component):
160
        try:
161
            event = RemoteCalendar.get(component)
162
        except KeyError:
163
            raise errors.TraversalError()
164
        get_response().breadcrumb.append((str(event.id), event.label))
165
        return RemoteCalendarDirectory(event)
166

  
167

  
168
class EventDirectory(Directory):
169
    _q_exports = ['', 'edit', 'delete']
170

  
171
    def __init__(self, event):
172
        self.event = event
173

  
174
    def _q_index [html] (self):
175
        form = Form(enctype='multipart/form-data')
176
        form.add_submit('edit', _('Edit'))
177
        form.add_submit('delete', _('Delete'))
178
        form.add_submit('back', _('Back'))
179

  
180
        if form.get_submit() == 'edit':
181
            return redirect('edit')
182
        if form.get_submit() == 'delete':
183
            return redirect('delete')
184
        if form.get_submit() == 'back':
185
            return redirect('..')
186

  
187
        html_top('events', title = _('Event: %s') % self.event.title)
188
        '<h2>%s</h2>' % _('Event: %s') % self.event.title
189
        '<p>'
190
        self.event.description
191
        '</p>'
192
        '<ul>'
193
        if self.event.location:
194
            '<li>%s: %s</li>' % (_('Location'), self.event.location)
195
        if self.event.organizer:
196
            '<li>%s: %s</li>' % (_('Organizer'), self.event.organizer)
197
        if self.event.url:
198
            '<li>%s: <a href="%s">%s</a></li>' % (_('URL'), self.event.url, self.event.url)
199
        '</ul>'
200

  
201
        if self.event.more_infos:
202
            '<p>'
203
            self.event.more_infos
204
            '</p>'
205

  
206
        form.render()
207

  
208
    def edit [html] (self):
209
        form = self.form()
210
        if form.get_submit() == 'cancel':
211
            return redirect('.')
212

  
213
        if form.is_submitted() and not form.has_errors():
214
            self.submit(form)
215
            return redirect('..')
216

  
217
        html_top('events', title = _('Edit Event: %s') % self.event.title)
218
        '<h2>%s</h2>' % _('Edit Event: %s') % self.event.title
219
        form.render()
220

  
221

  
222
    def form(self):
223
        form = Form(enctype='multipart/form-data')
224
        form.add(StringWidget, 'title', title = _('Title'), required = True,
225
                value = self.event.title)
226
        form.add(TextWidget, 'description', title = _('Description'),
227
                cols = 70, rows = 10,
228
                required = True, value = self.event.description)
229
        form.add(StringWidget, 'url', title = _('URL'), required = False,
230
                value = self.event.url, size = 40)
231
        form.add(DateWidget, 'date_start', title = _('Start Date'), required = True,
232
                value = strftime(misc.date_format(), self.event.date_start))
233
        form.add(DateWidget, 'date_end', title = _('End Date'), required = False,
234
                value = strftime(misc.date_format(), self.event.date_end))
235
        form.add(TextWidget, 'location', title = _('Location'),
236
                cols = 70, rows = 4,
237
                required = False, value = self.event.location)
238
        form.add(StringWidget, 'organizer', title = _('Organizer'), required = False,
239
                value = self.event.organizer, size = 40)
240
        form.add(TextWidget, 'more_infos', title = _('More informations'),
241
                cols = 70, rows = 10,
242
                required = False, value = self.event.more_infos)
243
        form.add(TagsWidget, 'keywords', title = _('Keywords'),
244
                value = self.event.keywords, size = 50,
245
                known_tags = get_cfg('misc', {}).get('event_tags', get_default_event_tags()))
246
        form.add_submit('submit', _('Submit'))
247
        form.add_submit('cancel', _('Cancel'))
248
        return form
249

  
250
    def submit(self, form):
251
        for k in ('title', 'description', 'url', 'date_start', 'date_end',
252
                    'organizer', 'location', 'more_infos', 'keywords'):
253
            widget = form.get_widget(k)
254
            if widget:
255
                if k in ('date_start', 'date_end'):
256
                    # convert dates to 9-item tuples
257
                    v = widget.parse()
258
                    if v:
259
                        setattr(self.event, k, time.strptime(v, misc.date_format()))
260
                    else:
261
                        setattr(self.event, k, None)
262
                else:
263
                    setattr(self.event, k, widget.parse())
264
        self.event.store()
265

  
266
    def delete [html] (self):
267
        form = Form(enctype='multipart/form-data')
268
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
269
                        'You are about to irrevocably delete this event.')))
270
        form.add_submit('submit', _('Submit'))
271
        form.add_submit('cancel', _('Cancel'))
272
        if form.get_submit() == 'cancel':
273
            return redirect('..')
274
        if not form.is_submitted() or form.has_errors():
275
            get_response().breadcrumb.append(('delete', _('Delete')))
276
            html_top('events', title = _('Delete Event'))
277
            '<h2>%s</h2>' % _('Deleting Event: %s') % self.event.title
278
            form.render()
279
        else:
280
            self.event.remove_self()
281
            return redirect('..')
282

  
283

  
284

  
285

  
286
class EventsDirectory(AccessControlled, Directory):
287
    _q_exports = ['', 'new', 'listing', 'remote']
288
    label = N_('Events')
289

  
290
    remote = RemoteCalendarsDirectory()
291

  
292
    def _q_access(self):
293
        user = get_request().user
294
        if not user:
295
            raise errors.AccessUnauthorizedError()
296
        admin_role = get_cfg('aq-permissions', {}).get('events', None)
297
        if not (user.is_admin or admin_role in (user.roles or [])):
298
            raise errors.AccessForbiddenError(
299
                    public_msg = _('You are not allowed to access Events Management'),
300
                    location_hint = 'backoffice')
301

  
302
        get_response().breadcrumb.append(('events/', _('Events')))
303

  
304

  
305
    def _q_index [html] (self):
306
        html_top('events', _('Events'))
307

  
308
        '<ul id="main-actions">'
309
        '  <li><a class="new-item" href="new">%s</a></li>' % _('New Event')
310
        '  <li><a class="new-item" href="remote/new">%s</a></li>' % _('New Remote Calendar')
311
        '</ul>'
312

  
313
        '<div class="splitcontent-left">'
314

  
315
        '<div class="bo-block">'
316
        events = Event.select()
317
        '<h2>%s</h2>' % _('Events')
318
        if not events:
319
            '<p>'
320
            _('There is no event defined at the moment.')
321
            '</p>'
322
        '<ul class="biglist" id="events-list">'
323
        for l in events:
324
            event_id = l.id
325
            '<li class="biglistitem" id="itemId_%s">' % event_id
326
            '<strong class="label"><a href="%s/">%s</a></strong>' % (event_id, l.title)
327
            ' - '
328
            l.format_date()
329
            '<p class="commands">'
330
            command_icon('%s/edit' % event_id, 'edit')
331
            command_icon('%s/delete' % event_id, 'remove')
332
            '</p></li>'
333
        '</ul>'
334
        '</div>'
335
        '</div>'
336

  
337
        '<div class="splitcontent-right">'
338
        '<div class="bo-block">'
339
        rcalendars = RemoteCalendar.select()
340
        '<h2>%s</h2>' % _('Remote Calendars')
341
        if not rcalendars:
342
            '<p>'
343
            _('There is no remote calendars defined at the moment.')
344
            '</p>'
345

  
346
        '<ul class="biglist" id="events-list">'
347
        for l in rcalendars:
348
            rcal_id = l.id
349
            '<li class="biglistitem" id="itemId_%s">' % rcal_id
350
            '<strong class="label"><a href="remote/%s/">%s</a></strong>' % (rcal_id, l.label)
351
            '<p class="details">'
352
            l.url
353
            if l.error:
354
                '<br /><span class="error-message">%s</span>' % l.get_error_message()
355
            '</p>'
356
            '<p class="commands">'
357
            command_icon('remote/%s/edit' % rcal_id, 'edit')
358
            command_icon('remote/%s/delete' % rcal_id, 'remove')
359
            '</p></li>'
360
        '</ul>'
361
        '</div>'
362
        '</div>'
363

  
364
    def new [html] (self):
365
        event_ui = EventDirectory(Event())
366

  
367
        form = event_ui.form()
368
        if form.get_submit() == 'cancel':
369
            return redirect('.')
370

  
371
        if form.is_submitted() and not form.has_errors():
372
            event_ui.submit(form)
373
            return redirect('%s/' % event_ui.event.id)
374

  
375
        get_response().breadcrumb.append(('new', _('New Event')))
376
        html_top('events', title = _('New Event'))
377
        '<h2>%s</h2>' % _('New Event')
378
        form.render()
379

  
380
    def _q_lookup(self, component):
381
        try:
382
            event = Event.get(component)
383
        except KeyError:
384
            raise errors.TraversalError()
385
        get_response().breadcrumb.append((str(event.id), event.title))
386
        return EventDirectory(event)
387

  
388
    def listing(self):
389
        return redirect('.')
390

  
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
from wcs.backoffice.menu import *
10

  
11
from qommon import errors, misc
12
from qommon.form import *
13
from qommon.strftime import strftime
14

  
15
from events import Event, RemoteCalendar, get_default_event_tags
16

  
17

  
18

  
19
class RemoteCalendarDirectory(Directory):
20
    _q_exports = ['', 'edit', 'delete', 'update']
21

  
22
    def __init__(self, calendar):
23
        self.calendar = calendar
24

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

  
32
        if form.get_submit() == 'edit':
33
            return redirect('edit')
34
        if form.get_submit() == 'update':
35
            return redirect('update')
36
        if form.get_submit() == 'delete':
37
            return redirect('delete')
38
        if form.get_submit() == 'back':
39
            return redirect('..')
40

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

  
45
        r += get_session().display_message()
46

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

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

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

  
75
        r += form.render()
76
        return r.getvalue()
77

  
78
    def edit(self):
79
        form = self.form()
80
        if form.get_submit() == 'cancel':
81
            return redirect('.')
82

  
83
        if form.is_submitted() and not form.has_errors():
84
            self.submit(form)
85
            return redirect('..')
86

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

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

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

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

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

  
137

  
138

  
139
class RemoteCalendarsDirectory(Directory):
140
    _q_exports = ['', 'new']
141

  
142
    def _q_traverse(self, path):
143
        get_response().breadcrumb.append(('remote/', _('Remote Calendars')))
144
        return Directory._q_traverse(self, path)
145

  
146
    def _q_index(self):
147
        return redirect('..')
148

  
149
    def new(self):
150
        calendar_ui = RemoteCalendarDirectory(RemoteCalendar())
151

  
152
        form = calendar_ui.form()
153
        if form.get_submit() == 'cancel':
154
            return redirect('.')
155

  
156
        if form.is_submitted() and not form.has_errors():
157
            calendar_ui.submit(form)
158
            return redirect('%s/' % calendar_ui.calendar.id)
159

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

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

  
175

  
176
class EventDirectory(Directory):
177
    _q_exports = ['', 'edit', 'delete']
178

  
179
    def __init__(self, event):
180
        self.event = event
181

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

  
188
        if form.get_submit() == 'edit':
189
            return redirect('edit')
190
        if form.get_submit() == 'delete':
191
            return redirect('delete')
192
        if form.get_submit() == 'back':
193
            return redirect('..')
194

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

  
210
        if self.event.more_infos:
211
            r += htmltext('<p>')
212
            r += self.event.more_infos
213
            r += htmltext('</p>')
214

  
215
        r += form.render()
216
        return r.getvalue()
217

  
218
    def edit(self):
219
        form = self.form()
220
        if form.get_submit() == 'cancel':
221
            return redirect('.')
222

  
223
        if form.is_submitted() and not form.has_errors():
224
            self.submit(form)
225
            return redirect('..')
226

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

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

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

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

  
296

  
297

  
298

  
299
class EventsDirectory(AccessControlled, Directory):
300
    _q_exports = ['', 'new', 'listing', 'remote']
301
    label = N_('Events')
302

  
303
    remote = RemoteCalendarsDirectory()
304

  
305
    def _q_access(self):
306
        user = get_request().user
307
        if not user:
308
            raise errors.AccessUnauthorizedError()
309
        admin_role = get_cfg('aq-permissions', {}).get('events', None)
310
        if not (user.is_admin or admin_role in (user.roles or [])):
311
            raise errors.AccessForbiddenError(
312
                    public_msg = _('You are not allowed to access Events Management'),
313
                    location_hint = 'backoffice')
314

  
315
        get_response().breadcrumb.append(('events/', _('Events')))
316

  
317

  
318
    def _q_index(self):
319
        html_top('events', _('Events'))
320
        r = TemplateIO(html=True)
321

  
322
        r += htmltext('<ul id="main-actions">')
323
        r += htmltext('  <li><a class="new-item" href="new">%s</a></li>') % _('New Event')
324
        r += htmltext('  <li><a class="new-item" href="remote/new">%s</a></li>') % _('New Remote Calendar')
325
        r += htmltext('</ul>')
326

  
327
        r += htmltext('<div class="splitcontent-left">')
328

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

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

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

  
379
    def new(self):
380
        event_ui = EventDirectory(Event())
381

  
382
        form = event_ui.form()
383
        if form.get_submit() == 'cancel':
384
            return redirect('.')
385

  
386
        if form.is_submitted() and not form.has_errors():
387
            event_ui.submit(form)
388
            return redirect('%s/' % event_ui.event.id)
389

  
390
        get_response().breadcrumb.append(('new', _('New Event')))
391
        html_top('events', title = _('New Event'))
392
        r = TemplateIO(html=True)
393
        r += htmltext('<h2>%s</h2>') % _('New Event')
394
        r += form.render()
395
        return r.getvalue()
396

  
397
    def _q_lookup(self, component):
398
        try:
399
            event = Event.get(component)
400
        except KeyError:
401
            raise errors.TraversalError()
402
        get_response().breadcrumb.append((str(event.id), event.title))
403
        return EventDirectory(event)
404

  
405
    def listing(self):
406
        return redirect('.')
extra/modules/formpage.ptl
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
from qommon import template
10
from qommon import errors
11
from qommon.form import *
12
from wcs.roles import logged_users_role
13

  
14
from qommon import emails
15

  
16
OldFormPage = wcs.forms.root.FormPage
17

  
18
class AlternateFormPage(OldFormPage):
19
    def step(self, *args, **kwargs):
20
        steps_html = OldFormPage.step(self, *args, **kwargs)
21
        steps_html = str(steps_html).replace('<ol>', '<h2>%s</h2>\n<ol>' % _('Steps'))
22
        get_response().filter['gauche'] = steps_html
23
        get_response().filter['steps'] = steps_html
24
        return
25

  
26
wcs.forms.root.FormPage = AlternateFormPage
27

  
28

  
29
OldFormsRootDirectory = wcs.forms.root.RootDirectory
30

  
31
class AlternateFormsRootDirectory(OldFormsRootDirectory):
32
    def form_list(self, *args, **kwargs):
33
        form_list = OldFormsRootDirectory.form_list(self, *args, **kwargs)
34
        return htmltext(str(form_list).replace('h2>', 'h3>'))
35

  
36
wcs.forms.root.RootDirectory = AlternateFormsRootDirectory
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
from qommon import template
10
from qommon import errors
11
from qommon.form import *
12
from wcs.roles import logged_users_role
13

  
14
from qommon import emails
15

  
16
OldFormPage = wcs.forms.root.FormPage
17

  
18
class AlternateFormPage(OldFormPage):
19
    def step(self, *args, **kwargs):
20
        steps_html = OldFormPage.step(self, *args, **kwargs)
21
        steps_html = str(steps_html).replace('<ol>', '<h2>%s</h2>\n<ol>' % _('Steps'))
22
        get_response().filter['gauche'] = steps_html
23
        get_response().filter['steps'] = steps_html
24
        return
25

  
26
wcs.forms.root.FormPage = AlternateFormPage
27

  
28

  
29
OldFormsRootDirectory = wcs.forms.root.RootDirectory
30

  
31
class AlternateFormsRootDirectory(OldFormsRootDirectory):
32
    def form_list(self, *args, **kwargs):
33
        form_list = OldFormsRootDirectory.form_list(self, *args, **kwargs)
34
        return htmltext(str(form_list).replace('h2>', 'h3>'))
35

  
36
wcs.forms.root.RootDirectory = AlternateFormsRootDirectory
extra/modules/msp_ui.ptl
1
import urllib
2
import urllib2
3
import urlparse
4
import json
5
import base64
6
import datetime
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

  
11
import qommon.form
12
from qommon.misc import http_post_request, http_get_page
13

  
14

  
15
class MSPDirectory(Directory):
16
    _q_exports = ['', 'pick', 'download']
17

  
18
    @property
19
    def msp_gateway_base_url(self):
20
        return get_publisher().get_site_option('msp')
21

  
22
    @property
23
    def authorize_url(self):
24
        return urlparse.urljoin(self.msp_gateway_base_url, 'authorize/')
25

  
26
    @property
27
    def access_token_url(self):
28
        return urlparse.urljoin(self.msp_gateway_base_url, 'access_token/')
29

  
30
    @property
31
    def documents_url(self):
32
        return urlparse.urljoin(self.msp_gateway_base_url, 'documents/')
33

  
34
    @property
35
    def document_url(self):
36
        return urlparse.urljoin(self.msp_gateway_base_url, 'documents/%s/')
37

  
38
    @property
39
    def client_id(self):
40
        return get_publisher().get_site_option('msp_client_id')
41

  
42
    @property
43
    def client_secret(self):
44
        return get_publisher().get_site_option('msp_client_secret')
45

  
46
    def fix_document(self, document):
47
        site_charset = get_publisher().site_charset
48
        for key, value in document.iteritems():
49
            # date are returned as millisecond POSIX timestamp,
50
            # it should be ISO-8601 instead
51
            if key.endswith('Date') and value is not None:
52
                document[key] = unicode(datetime.date.fromtimestamp(value // 1000))
53
                value = document[key] = document[key].replace(u'-', u'\u2011')
54
            if isinstance(value, unicode) and key != 'content':
55
                document[key] = value.encode(site_charset)
56

  
57
    def get_documents(self, access_token):
58
        response, status, content, auth_header = http_get_page(self.documents_url, {
59
            'Authorization': 'Bearer %s' % access_token,
60
        });
61
        documents = json.loads(content)
62
        for document in documents:
63
            self.fix_document(document)
64
        return documents
65

  
66
    def get_document(self, doc_id, access_token):
67
        response, status, content, auth_header = http_get_page(
68
                self.document_url % doc_id, 
69
                { 'Authorization': 'Bearer %s' % access_token, }
70
        );
71
        document = json.loads(content)
72
        self.fix_document(document)
73
        return document
74

  
75
    def authorize(self, self_url, scope):
76
        params = {
77
                'redirect_uri': self_url,
78
                'response_type': 'code',
79
                'scope': scope,
80
        }
81
        return redirect('%s?%s' % (
82
            self.authorize_url, urllib.urlencode(params)))
83

  
84
    def access_token(self, self_url, code):
85
        params = {
86
                'code': code,
87
                'client_id': self.client_id,
88
                'client_secret': self.client_secret,
89
                'grant_type': 'authorization_code',
90
                'redirect_uri': self_url,
91
        }
92
        response, status, data, auth_header = http_post_request(self.access_token_url,
93
                urllib.urlencode(params), { 'Content-Type': 'application/x-www-form-urlencoded' })
94
        return json.loads(data)['access_token']
95

  
96
    def pick (self):
97
        request = get_request()
98
        frontoffice_url = get_publisher().get_frontoffice_url()
99
        self_url = frontoffice_url
100
        self_url += '/msp/pick'
101
        self_url = self_url.replace('://', '://iframe-')
102

  
103
        if 'code' not in request.form and 'error' not in request.form:
104
            return self.authorize(self_url, 'LIST_DOCS')
105
        access_token = self.access_token(self_url, request.form['code'])
106
        return self.pick_display(access_token)
107

  
108
    def pick_display [html] (self, access_token):
109
        request = get_request()
110

  
111
        get_response().add_javascript(['jquery.js',
112
            'tablesorter/jquery.tablesorter.min.js'])
113
        get_response().add_javascript_code(
114
                 str('''$(function() { $("table.sortable").tablesorter(); });'''))
115
        get_response().add_css_include('../js/tablesorter/themes/blue/style.css')
116

  
117
        frontoffice_url = get_publisher().get_frontoffice_url()
118

  
119
        '<h2>%s</h2>' % _('Pick a file')
120

  
121
        if 'error' not in request.form:
122
            documents = self.get_documents(access_token)
123
            '<table id="msp-pick-file-table" class="sortable tablesorter">'
124
            '<thead>'
125
            '<tr>'
126
            '<th>%s</th>' % _('Filename')
127
            '<th>%s</th>' % _('Expiration date')
128
            '</tr>'
129
            '</thead>'
130
            '<tbody>'
131
            for document in documents:
132
                '<tr>'
133
                for key in ('name', 'expirationDate'):
134
                    '<td class="msp-pick-file-table-%s">' % key
135
                    if key == 'name':
136
                        '<a href="%s/msp/download?doc_id=%s">' % \
137
                            (frontoffice_url, document['id'])
138
                    '%s' % (document[key] or '')
139
                    if key == 'name':
140
                        '</a>'
141
                    '</td>'
142
                '</tr>'
143
            '</tbody>'
144
            '</table>'
145
        else:
146
            '<p>%s</p>' % _('Unable to access your mon.Service-Public.fr documents')
147

  
148
    def set_token [html] (self, token, document):
149
        get_response().add_javascript(['jquery.js'])
150
        get_response().page_template_key = 'iframe'
151
        '<html><body>'
152
        '<pre>Token: %s</pre>' % token
153
        '<script>window.top.document.set_token("%s", "%s");</script>' % (token, document['name'])
154
        '</body></html>'
155

  
156
    def download(self):
157
        request = get_request()
158
        assert 'doc_id' in request.form
159
        doc_id = request.form['doc_id']
160
        frontoffice_url = get_publisher().get_frontoffice_url()
161
        self_url = frontoffice_url
162
        self_url += '/msp/download?%s' % urllib.urlencode({'doc_id': doc_id})
163
        if 'code' not in request.form and 'error' not in request.form:
164
            return self.authorize(self_url, 'GET_DOC')
165
        if 'error' in request.form:
166
            return self.download_error()
167
        else:
168
            access_token = self.access_token(self_url, request.form['code'])
169
            document = self.get_document(doc_id, access_token)
170
            download = qommon.form.PicklableUpload(document['name'],
171
                    content_type='application/pdf')
172
            download.__setstate__({
173
                'data': base64.b64decode(document['content']),
174
            })
175
            token = get_session().add_tempfile(download)
176
            return self.set_token(token, document)
extra/modules/msp_ui.py
1
import urllib
2
import urllib2
3
import urlparse
4
import json
5
import base64
6
import datetime
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

  
12
import qommon.form
13
from qommon.misc import http_post_request, http_get_page
14

  
15

  
16
class MSPDirectory(Directory):
17
    _q_exports = ['', 'pick', 'download']
18

  
19
    @property
20
    def msp_gateway_base_url(self):
21
        return get_publisher().get_site_option('msp')
22

  
23
    @property
24
    def authorize_url(self):
25
        return urlparse.urljoin(self.msp_gateway_base_url, 'authorize/')
26

  
27
    @property
28
    def access_token_url(self):
29
        return urlparse.urljoin(self.msp_gateway_base_url, 'access_token/')
30

  
31
    @property
32
    def documents_url(self):
33
        return urlparse.urljoin(self.msp_gateway_base_url, 'documents/')
34

  
35
    @property
36
    def document_url(self):
37
        return urlparse.urljoin(self.msp_gateway_base_url, 'documents/%s/')
38

  
39
    @property
40
    def client_id(self):
41
        return get_publisher().get_site_option('msp_client_id')
42

  
43
    @property
44
    def client_secret(self):
45
        return get_publisher().get_site_option('msp_client_secret')
46

  
47
    def fix_document(self, document):
48
        site_charset = get_publisher().site_charset
49
        for key, value in document.iteritems():
50
            # date are returned as millisecond POSIX timestamp,
51
            # it should be ISO-8601 instead
52
            if key.endswith('Date') and value is not None:
53
                document[key] = unicode(datetime.date.fromtimestamp(value // 1000))
54
                value = document[key] = document[key].replace(u'-', u'\u2011')
55
            if isinstance(value, unicode) and key != 'content':
56
                document[key] = value.encode(site_charset)
57

  
58
    def get_documents(self, access_token):
59
        response, status, content, auth_header = http_get_page(self.documents_url, {
60
            'Authorization': 'Bearer %s' % access_token,
61
        });
62
        documents = json.loads(content)
63
        for document in documents:
64
            self.fix_document(document)
65
        return documents
66

  
67
    def get_document(self, doc_id, access_token):
68
        response, status, content, auth_header = http_get_page(
69
                self.document_url % doc_id, 
70
                { 'Authorization': 'Bearer %s' % access_token, }
71
        );
72
        document = json.loads(content)
73
        self.fix_document(document)
74
        return document
75

  
76
    def authorize(self, self_url, scope):
77
        params = {
78
                'redirect_uri': self_url,
79
                'response_type': 'code',
80
                'scope': scope,
81
        }
82
        return redirect('%s?%s' % (
83
            self.authorize_url, urllib.urlencode(params)))
84

  
85
    def access_token(self, self_url, code):
86
        params = {
87
                'code': code,
88
                'client_id': self.client_id,
89
                'client_secret': self.client_secret,
90
                'grant_type': 'authorization_code',
91
                'redirect_uri': self_url,
92
        }
93
        response, status, data, auth_header = http_post_request(self.access_token_url,
94
                urllib.urlencode(params), { 'Content-Type': 'application/x-www-form-urlencoded' })
95
        return json.loads(data)['access_token']
96

  
97
    def pick (self):
98
        request = get_request()
99
        frontoffice_url = get_publisher().get_frontoffice_url()
100
        self_url = frontoffice_url
101
        self_url += '/msp/pick'
102
        self_url = self_url.replace('://', '://iframe-')
103

  
104
        if 'code' not in request.form and 'error' not in request.form:
105
            return self.authorize(self_url, 'LIST_DOCS')
106
        access_token = self.access_token(self_url, request.form['code'])
107
        return self.pick_display(access_token)
108

  
109
    def pick_display(self, access_token):
110
        r = TemplateIO(html=True)
111

  
112
        request = get_request()
113

  
114
        get_response().add_javascript(['jquery.js',
115
            'tablesorter/jquery.tablesorter.min.js'])
116
        get_response().add_javascript_code(
117
                 str('''$(function() { $("table.sortable").tablesorter(); });'''))
118
        get_response().add_css_include('../js/tablesorter/themes/blue/style.css')
119

  
120
        frontoffice_url = get_publisher().get_frontoffice_url()
121

  
122
        r += htmltext('<h2>%s</h2>') % _('Pick a file')
123

  
124
        if 'error' not in request.form:
125
            documents = self.get_documents(access_token)
126
            r += htmltext('<table id="msp-pick-file-table" class="sortable tablesorter">')
127
            r += htmltext('<thead>')
128
            r += htmltext('<tr>')
129
            r += htmltext('<th>%s</th>') % _('Filename')
130
            r += htmltext('<th>%s</th>') % _('Expiration date')
131
            r += htmltext('</tr>')
132
            r += htmltext('</thead>')
133
            r += htmltext('<tbody>')
134
            for document in documents:
135
                r += htmltext('<tr>')
136
                for key in ('name', 'expirationDate'):
137
                    r += htmltext('<td class="msp-pick-file-table-%s">' % key)
138
                    if key == 'name':
139
                        r += htmltext('<a href="%s/msp/download?doc_id=%s">' % \
140
                            (frontoffice_url, document['id']))
141
                    r += '%s' % (document[key] or '')
142
                    if key == 'name':
143
                        r += htmltext('</a>')
144
                    r += htmltext('</td>')
145
                r += htmltext('</tr>')
146
            r += htmltext('</tbody>')
147
            r += htmltext('</table>')
148
        else:
149
            r += htmltext('<p>%s</p>') % _('Unable to access your mon.Service-Public.fr documents')
150
        return r.getvalue()
151

  
152
    def set_token(self, token, document):
153
        get_response().add_javascript(['jquery.js'])
154
        get_response().page_template_key = 'iframe'
155
        r = TemplateIO(html=True)
156
        r += htmltext('<html><body>')
157
        r += htmltext('<pre>Token: %s</pre>') % token
158
        r += htmltext('<script>window.top.document.set_token("%s", "%s");</script>' % (
159
            token, document['name']))
160
        r += htmltext('</body></html>')
161
        return r.getvalue()
162

  
163
    def download(self):
164
        request = get_request()
165
        assert 'doc_id' in request.form
166
        doc_id = request.form['doc_id']
167
        frontoffice_url = get_publisher().get_frontoffice_url()
168
        self_url = frontoffice_url
169
        self_url += '/msp/download?%s' % urllib.urlencode({'doc_id': doc_id})
170
        if 'code' not in request.form and 'error' not in request.form:
171
            return self.authorize(self_url, 'GET_DOC')
172
        if 'error' in request.form:
173
            return self.download_error()
174
        else:
175
            access_token = self.access_token(self_url, request.form['code'])
176
            document = self.get_document(doc_id, access_token)
177
            download = qommon.form.PicklableUpload(document['name'],
178
                    content_type='application/pdf')
179
            download.__setstate__({
180
                'data': base64.b64decode(document['content']),
181
            })
182
            token = get_session().add_tempfile(download)
183
            return self.set_token(token, document)
extra/modules/myspace.ptl
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.util import StaticFile, FileStream
11

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

  
18
import qommon.ident.password
19
from qommon.ident.password_accounts import PasswordAccount
20

  
21
from qommon.admin.texts import TextsDirectory
22

  
23
from wcs.formdef import FormDef
24
import root
25

  
26
from announces import AnnounceSubscription
27
from strongbox import StrongboxItem, StrongboxType
28
from payments import Invoice, Regie, is_payment_supported
29
import msp_ui
30

  
31
class MyInvoicesDirectory(Directory):
32
    _q_exports = ['']
33

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

  
40
    def _q_index [html] (self):
41
        user = get_request().user
42
        if not user or user.anonymous:
43
            raise errors.AccessUnauthorizedError()
44

  
45
        template.html_top(_('Invoices'))
46
        TextsDirectory.get_html_text('aq-myspace-invoice')
47

  
48
        get_session().display_message()
49

  
50
        invoices = []
51
        invoices.extend(Invoice.get_with_indexed_value(
52
            str('user_id'), str(user.id)))
53
        try:
54
            invoices.extend(Invoice.get_with_indexed_value(
55
                str('user_hash'), str(user.hash)))
56
        except AttributeError:
57
            pass
58

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

  
65
        invoices.sort(cmp_invoice)
66

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

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

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

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

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

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

  
132
    def _q_index [html] (self):
133
        template.html_top(_('Strongbox'))
134

  
135
        # TODO: a paragraph of explanations here could be useful
136

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

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

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

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

  
221
        '<h3>%s</h3>' % _('Add a file to the strongbox')
222
        form = self.get_form()
223
        form.render()
224

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

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

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

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

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

  
281
    def remove [html] (self):
282
        id = get_request().form.get('id')
283
        if not id:
284
            raise errors.TraversalError()
285
        try:
286
            sffile = StrongboxItem.get(id)
287
        except KeyError:
288
            raise errors.TraversalError()
289
        if str(sffile.user_id) != str(get_request().user.id):
290
            raise errors.TraversalError()
291

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

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

  
325
    def pick [html] (self):
326
        if get_request().form.get('select') == 'true':
327
            return self.picked_file()
328
        root_url = get_publisher().get_root_url()
329
        sffiles = StrongboxItem.get_with_indexed_value(
330
                        str('user_id'), str(get_request().user.id))
331
        '<h2>%s</h2>' % _('Pick a file')
332

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

  
352
            '<div class="buttons">'
353
            '<input name="cancel" type="button" value="%s"/>' % _('Cancel')
354
            ' '
355
            '<input name="pick" type="button" value="%s"/>' % _('Pick')
356
            '</div>'
357
            '</form>'
358

  
359
class JsonDirectory(Directory):
360
    '''Export of several lists in json, related to the current user or the
361
       SAMLv2 NameID we'd get in the URL'''
362

  
363
    _q_exports = ['forms']
364

  
365
    user = None
366

  
367
    def _q_traverse(self, path):
368
        self.user = get_user_from_api_query_string() or get_request().user
369
        if not self.user:
370
            raise errors.AccessUnauthorizedError()
371
        return Directory._q_traverse(self, path)
372

  
373
    def forms(self):
374
        formdefs = FormDef.select(lambda x: not x.is_disabled(), order_by = 'name')
375
        user_forms = []
376
        for formdef in formdefs:
377
            user_forms.extend(formdef.data_class().get_with_indexed_value(
378
                        'user_id', self.user.id))
379
            try:
380
                user_forms.extend(formdef.data_class().get_with_indexed_value(
381
                        'user_hash', self.user.hash))
382
            except AttributeError:
383
                pass
384
        user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
385

  
386
        get_response().set_content_type('application/json')
387

  
388

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

  
409

  
410
class MyspaceDirectory(Directory):
411
    _q_exports = ['', 'profile', 'new', 'password', 'remove', 'announces',
412
                  'strongbox', 'invoices', 'json', 'msp']
413

  
414
    msp = msp_ui.MSPDirectory()
415
    strongbox = StrongboxDirectory()
416
    invoices = MyInvoicesDirectory()
417
    json = JsonDirectory()
418

  
419
    def _q_traverse(self, path):
420
        if (path[0] not in ('new', 'json')) and (not get_request().user or get_request().user.anonymous):
421
            raise errors.AccessUnauthorizedError()
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 [html] (self):
437
        user = get_request().user
438
        if not user:
439
            raise errors.AccessUnauthorizedError()
440
        template.html_top(_('My Space'))
441
        if user.anonymous:
442
            return redirect('new')
443

  
444
        user_formdef = user.get_formdef()
445

  
446
        user_forms = []
447
        if user:
448
            formdefs = FormDef.select(lambda x: not x.is_disabled(), order_by = 'name')
449
            user_forms = []
450
            for formdef in formdefs:
451
                user_forms.extend(formdef.data_class().get_with_indexed_value(
452
                            'user_id', user.id))
453
                try:
454
                    user_forms.extend(formdef.data_class().get_with_indexed_value(
455
                            'user_hash', user.hash))
456
                except AttributeError:
457
                    pass
458
            user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
459

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

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

  
477
        if profile_links:
478
            '<p id="profile-links">'
479
            ' - '.join(profile_links)
480
            '</p>'
481

  
482
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
483
            if user_formdef:
484
                self._my_profile(user_formdef, user)
485

  
486
            self._index_buttons(user_formdef)
487

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

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

  
511
        if user_forms:
512
            '<h3 id="my-forms">%s</h3>' % _('My Forms')
513
            root.FormsRootDirectory().user_forms(user_forms)
514

  
515
    def _my_profile [html] (self, user_formdef, user):
516
        '<h3 id="my-profile">%s</h3>' % _('My Profile')
517

  
518
        TextsDirectory.get_html_text('top-of-profile')
519

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

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

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

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

  
548
    def profile [html] (self):
549
        user = get_request().user
550
        if not user or user.anonymous:
551
            raise errors.AccessUnauthorizedError()
552

  
553
        form = Form(enctype = 'multipart/form-data')
554
        formdef = user.get_formdef()
555
        formdef.add_fields_to_form(form, form_data = user.form_data)
556

  
557
        form.add_submit('submit', _('Apply Changes'))
558
        form.add_submit('cancel', _('Cancel'))
559

  
560
        if form.get_submit() == 'cancel':
561
            return redirect('.')
562

  
563
        if form.is_submitted() and not form.has_errors():
564
            self.profile_submit(form, formdef)
565
            return redirect('.')
566

  
567
        template.html_top(_('Edit Profile'))
568
        form.render()
569

  
570
    def profile_submit(self, form, formdef):
571
        user = get_request().user
572
        data = formdef.get_data(form)
573

  
574
        user.set_attributes_from_formdata(data)
575
        user.form_data = data
576

  
577
        user.store()
578

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

  
584
        user = get_request().user
585
        if not user or user.anonymous:
586
            raise errors.AccessUnauthorizedError()
587

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

  
594
        form.add_submit('submit', _('Change Password'))
595
        form.add_submit('cancel', _('Cancel'))
596

  
597
        if form.get_submit() == 'cancel':
598
            return redirect('.')
599

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

  
607
        if form.is_submitted() and not form.has_errors():
608
            self.submit_password(new_password)
609
            return redirect('.')
610

  
611
        template.html_top(_('Change Password'))
612
        form.render()
613

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

  
621
    def new [html] (self):
622
        if not get_request().user or not get_request().user.anonymous:
623
            raise errors.AccessUnauthorizedError()
624

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

  
632
        form.add_submit('submit', _('Register'))
633

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

  
647
        template.html_top(_('Welcome'))
648
        form.render()
649

  
650

  
651
    def remove [html] (self):
652
        user = get_request().user
653
        if not user or user.anonymous:
654
            raise errors.AccessUnauthorizedError()
655

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

  
662
        if form.get_submit() == 'cancel':
663
            return redirect('.')
664

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

  
672
        template.html_top(_('Removing Account'))
673
        form.render()
674

  
675
    def announces [html] (self):
676
        options = get_cfg('misc', {}).get('announce_themes')
677
        if not options:
678
            raise errors.TraversalError()
679
        subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id'))
680
        if not subscription:
681
            raise errors.TraversalError()
682

  
683
        if subscription.enabled_themes is None:
684
            enabled_themes = options
685
        else:
686
            enabled_themes = subscription.enabled_themes
687

  
688
        form = Form(enctype = 'multipart/form-data')
689
        form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'),
690
                value=enabled_themes, elements=options,
691
                inline=False, required=False)
692

  
693
        form.add_submit('submit', _('Apply Changes'))
694
        form.add_submit('cancel', _('Cancel'))
695

  
696
        if form.get_submit() == 'cancel':
697
            return redirect('.')
698

  
699
        if form.is_submitted() and not form.has_errors():
700
            chosen_themes = form.get_widget('themes').parse()
701
            if chosen_themes == options:
702
                chosen_themes = None
703
            subscription.enabled_themes = chosen_themes
704
            subscription.store()
705
            return redirect('.')
706

  
707
        template.html_top()
708
        get_response().breadcrumb.append(('announces', _('Announce Subscription')))
709
        form.render()
710

  
711

  
712
TextsDirectory.register('aq-myspace-invoice',
713
        N_('Message on top of invoices page'),
714
        category = N_('Invoices'))
715

  
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 template
14
from qommon.form import *
15
from qommon import get_cfg, get_logger
16
from qommon import errors
17
from wcs.api import get_user_from_api_query_string
18

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

  
22
from qommon.admin.texts import TextsDirectory
23

  
24
from wcs.formdef import FormDef
25
import root
26

  
27
from announces import AnnounceSubscription
28
from strongbox import StrongboxItem, StrongboxType
29
from payments import Invoice, Regie, is_payment_supported
30
import msp_ui
31

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

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

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

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

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

  
52
        invoices = []
53
        invoices.extend(Invoice.get_with_indexed_value(
54
            str('user_id'), str(user.id)))
55
        try:
56
            invoices.extend(Invoice.get_with_indexed_value(
57
                str('user_hash'), str(user.hash)))
58
        except AttributeError:
59
            pass
60

  
61
        def cmp_invoice(a, b):
62
            t = cmp(a.regie_id, b.regie_id)
63
            if t != 0:
64
                return t
65
            return -cmp(a.date, b.date)
66

  
67
        invoices.sort(cmp_invoice)
68

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

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

  
108
        if last_regie_id:
109
            r += htmltext('</ul>')
110
            if unpaid:
111
                r += htmltext('<input type="submit" value="%s"/>') % _('Pay Selected Invoices')
112
            r += htmltext('</form>')
113

  
114
        return r.getvalue()
115

  
116

  
117
class StrongboxDirectory(Directory):
118
    _q_exports = ['', 'add', 'download', 'remove', 'pick', 'validate']
119

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

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

  
137
    def _q_index(self):
138
        template.html_top(_('Strongbox'))
139
        r = TemplateIO(html=True)
140

  
141
        # TODO: a paragraph of explanations here could be useful
142

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

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

  
203
                r += htmltext('<td class="label">')
204
                r += sffile.get_display_name()
205
                r += htmltext('</td>')
206
                if sffile.type_id:
207
                    r += htmltext('<td class="type">%s</td>') % StrongboxType.get(sffile.type_id).label
208
                else:
209
                    r += htmltext('<td class="type">-</td>')
210

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

  
227
        r += htmltext('<h3>%s</h3>') % _('Add a file to the strongbox')
228
        form = self.get_form()
229
        r += form.render()
230
        return r.getvalue()
231

  
232
    def add(self):
233
        form = self.get_form()
234
        if not form.is_submitted():
235
            if get_request().form.get('mode') == 'pick':
236
                return redirect('pick')
237
            else:
238
                return redirect('.')
239

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

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

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

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

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

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

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

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

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

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

  
370

  
371
class JsonDirectory(Directory):
372
    '''Export of several lists in json, related to the current user or the
373
       SAMLv2 NameID we'd get in the URL'''
374

  
375
    _q_exports = ['forms']
376

  
377
    user = None
378

  
379
    def _q_traverse(self, path):
380
        self.user = get_user_from_api_query_string() or get_request().user
381
        if not self.user:
382
            raise errors.AccessUnauthorizedError()
383
        return Directory._q_traverse(self, path)
384

  
385
    def forms(self):
386
        formdefs = FormDef.select(lambda x: not x.is_disabled(), order_by = 'name')
387
        user_forms = []
388
        for formdef in formdefs:
389
            user_forms.extend(formdef.data_class().get_with_indexed_value(
390
                        'user_id', self.user.id))
391
            try:
392
                user_forms.extend(formdef.data_class().get_with_indexed_value(
393
                        'user_hash', self.user.hash))
394
            except AttributeError:
395
                pass
396
        user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
397

  
398
        get_response().set_content_type('application/json')
399

  
400

  
401
        forms_output = []
402
        for form in user_forms:
403
            visible_status = form.get_visible_status(user=self.user)
404
            # skip hidden forms
405
            if not visible_status:
406
                continue
407
            name = form.formdef.name
408
            id = form.get_display_id()
409
            status = visible_status.name
410
            title = _('%(name)s #%(id)s (%(status)s)') % {
411
                    'name': name,
412
                    'id': id,
413
                    'status': status
414
            }
415
            url = form.get_url()
416
            d = { 'title': title, 'url': url }
417
            d.update(form.get_substitution_variables(minimal=True))
418
            forms_output.append(d)
419
        return json.dumps(forms_output)
420

  
421

  
422
class MyspaceDirectory(Directory):
423
    _q_exports = ['', 'profile', 'new', 'password', 'remove', 'announces',
424
                  'strongbox', 'invoices', 'json', 'msp']
425

  
426
    msp = msp_ui.MSPDirectory()
427
    strongbox = StrongboxDirectory()
428
    invoices = MyInvoicesDirectory()
429
    json = JsonDirectory()
430

  
431
    def _q_traverse(self, path):
432
        if (path[0] not in ('new', 'json')) and (not get_request().user or get_request().user.anonymous):
433
            raise errors.AccessUnauthorizedError()
434
        get_response().filter['bigdiv'] = 'profile'
435
        get_response().breadcrumb.append(('myspace/', _('My Space')))
436

  
437
        # Migrate custom text settings
438
        texts_cfg = get_cfg('texts', {})
439
        if 'text-aq-top-of-profile' in texts_cfg and (
440
                        not 'text-top-of-profile' in texts_cfg):
441
            texts_cfg['text-top-of-profile'] = texts_cfg['text-aq-top-of-profile']
442
            del texts_cfg['text-aq-top-of-profile']
443
            get_publisher().write_cfg()
444

  
445
        return Directory._q_traverse(self, path)
446

  
447

  
448
    def _q_index(self):
449
        user = get_request().user
450
        if not user:
451
            raise errors.AccessUnauthorizedError()
452
        template.html_top(_('My Space'))
453
        r = TemplateIO(html=True)
454
        if user.anonymous:
455
            return redirect('new')
456

  
457
        user_formdef = user.get_formdef()
458

  
459
        user_forms = []
460
        if user:
461
            formdefs = FormDef.select(lambda x: not x.is_disabled(), order_by = 'name')
462
            user_forms = []
463
            for formdef in formdefs:
464
                user_forms.extend(formdef.data_class().get_with_indexed_value(
465
                            'user_id', user.id))
466
                try:
467
                    user_forms.extend(formdef.data_class().get_with_indexed_value(
468
                            'user_hash', user.hash))
469
                except AttributeError:
470
                    pass
471
            user_forms.sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
472

  
473
        profile_links = []
474
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
475
            if user_formdef:
476
                profile_links.append('<a href="#my-profile">%s</a>' % _('My Profile'))
477
        if user_forms:
478
            profile_links.append('<a href="#my-forms">%s</a>' % _('My Forms'))
479
        if get_cfg('misc', {}).get('aq-strongbox'):
480
            profile_links.append('<a href="strongbox/">%s</a>' % _('My Strongbox'))
481
        if is_payment_supported():
482
            profile_links.append('<a href="invoices/">%s</a>' % _('My Invoices'))
483

  
484
        root_url = get_publisher().get_root_url()
485
        if user.can_go_in_backoffice():
486
            profile_links.append('<a href="%sbackoffice/">%s</a>' % (root_url, _('Back office')))
487
        if user.is_admin:
488
            profile_links.append('<a href="%sadmin/">%s</a>' % (root_url, _('Admin')))
489

  
490
        if profile_links:
491
            r += htmltext('<p id="profile-links">')
492
            r += ' - '.join(profile_links)
493
            r += htmltext('</p>')
494

  
495
        if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
496
            if user_formdef:
497
                r += self._my_profile(user_formdef, user)
498

  
499
            r += self._index_buttons(user_formdef)
500

  
501
            try:
502
                x = PasswordAccount.get_on_index(get_request().user.id, str('user_id'))
503
            except KeyError:
504
                pass
505
            else:
506
                r += htmltext('<p>')
507
                r += _('You can delete your account freely from the services portal. '
508
                       'This action is irreversible; it will destruct your personal '
509
                       'datas and destruct the access to your request history.')
510
                r += htmltext(' <strong><a href="remove" rel="popup">%s</a></strong>.') % _('Delete My Account')
511
                r += htmltext('</p>')
512

  
513
        options = get_cfg('misc', {}).get('announce_themes')
514
        if options:
515
            try:
516
                subscription = AnnounceSubscription.get_on_index(
517
                        get_request().user.id, str('user_id'))
518
            except KeyError:
519
                pass
520
            else:
521
                r += htmltext('<p class="command"><a href="announces">%s</a></p>') % _(
522
                        'Edit my Subscription to Announces')
523

  
524
        if user_forms:
525
            r += htmltext('<h3 id="my-forms">%s</h3>') % _('My Forms')
526
            r += root.FormsRootDirectory().user_forms(user_forms)
527

  
528
        return r.getvalue()
529

  
530
    def _my_profile(self, user_formdef, user):
531
        r = TemplateIO(html=True)
532
        r += htmltext('<h3 id="my-profile">%s</h3>') % _('My Profile')
533

  
534
        r += TextsDirectory.get_html_text('top-of-profile')
535

  
536
        if user.form_data:
537
            r += htmltext('<ul>')
538
            for field in user_formdef.fields:
539
                if not hasattr(field, str('get_view_value')):
540
                    continue
541
                value = user.form_data.get(field.id)
542
                r += htmltext('<li>')
543
                r += field.label
544
                r += ' : '
545
                if value:
546
                    r += field.get_view_value(value)
547
                r += htmltext('</li>')
548
            r += htmltext('</ul>')
549
        else:
550
            r += htmltext('<p>%s</p>') % _('Empty profile')
551
        return r.getvalue()
552

  
553
    def _index_buttons(self, form_data):
554
        r = TemplateIO(html=True)
555
        passwords_cfg = get_cfg('passwords', {})
556
        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
557
        if get_session().lasso_session_dump:
558
            ident_method = 'idp'
559

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

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

  
566
        return r.getvalue()
567

  
568
    def profile(self):
569
        user = get_request().user
570
        if not user or user.anonymous:
571
            raise errors.AccessUnauthorizedError()
572

  
573
        form = Form(enctype = 'multipart/form-data')
574
        formdef = user.get_formdef()
575
        formdef.add_fields_to_form(form, form_data = user.form_data)
576

  
577
        form.add_submit('submit', _('Apply Changes'))
578
        form.add_submit('cancel', _('Cancel'))
579

  
580
        if form.get_submit() == 'cancel':
581
            return redirect('.')
582

  
583
        if form.is_submitted() and not form.has_errors():
584
            self.profile_submit(form, formdef)
585
            return redirect('.')
586

  
587
        template.html_top(_('Edit Profile'))
588
        return form.render()
589

  
590
    def profile_submit(self, form, formdef):
591
        user = get_request().user
592
        data = formdef.get_data(form)
593

  
594
        user.set_attributes_from_formdata(data)
595
        user.form_data = data
596

  
597
        user.store()
598

  
599
    def password(self):
600
        ident_method = get_cfg('identification', {}).get('methods', ['idp'])[0]
601
        if ident_method != 'password':
602
            raise errors.TraversalError()
603

  
604
        user = get_request().user
605
        if not user or user.anonymous:
606
            raise errors.AccessUnauthorizedError()
607

  
608
        form = Form(enctype = 'multipart/form-data')
609
        form.add(PasswordWidget, 'new_password', title = _('New Password'),
610
                required=True)
611
        form.add(PasswordWidget, 'new2_password', title = _('New Password (confirm)'),
612
                required=True) 
613

  
614
        form.add_submit('submit', _('Change Password'))
615
        form.add_submit('cancel', _('Cancel'))
616

  
617
        if form.get_submit() == 'cancel':
618
            return redirect('.')
619

  
620
        if form.is_submitted() and not form.has_errors():
621
            qommon.ident.password.check_password(form, 'new_password')
622
            new_password = form.get_widget('new_password').parse()
623
            new2_password = form.get_widget('new2_password').parse()
624
            if new_password != new2_password:
625
                form.set_error('new2_password', _('Passwords do not match'))
626

  
627
        if form.is_submitted() and not form.has_errors():
628
            self.submit_password(new_password)
629
            return redirect('.')
630

  
631
        template.html_top(_('Change Password'))
632
        return form.render()
633

  
634
    def submit_password(self, new_password):
635
        passwords_cfg = get_cfg('passwords', {})
636
        account = PasswordAccount.get(get_session().username)
637
        account.hashing_algo = passwords_cfg.get('hashing_algo')
638
        account.set_password(new_password)
639
        account.store()
640

  
641
    def new(self):
642
        if not get_request().user or not get_request().user.anonymous:
643
            raise errors.AccessUnauthorizedError()
644

  
645
        form = Form(enctype = 'multipart/form-data')
646
        formdef = get_publisher().user_class.get_formdef()
647
        if formdef:
648
            formdef.add_fields_to_form(form)
649
        else:
650
            get_logger().error('missing user formdef (in myspace/new)')
651

  
652
        form.add_submit('submit', _('Register'))
653

  
654
        if form.is_submitted() and not form.has_errors():
655
            user = get_publisher().user_class()
656
            data = formdef.get_data(form)
657
            user.set_attributes_from_formdata(data)
658
            user.name_identifiers = get_request().user.name_identifiers
659
            user.lasso_dump = get_request().user.lasso_dump
660
            user.set_attributes_from_formdata(data)
661
            user.form_data = data
662
            user.store()
663
            get_session().set_user(user.id)
664
            root_url = get_publisher().get_root_url()
665
            return redirect('%smyspace' % root_url)
666

  
667
        template.html_top(_('Welcome'))
668
        return form.render()
669

  
670

  
671
    def remove(self):
672
        user = get_request().user
673
        if not user or user.anonymous:
674
            raise errors.AccessUnauthorizedError()
675

  
676
        form = Form(enctype = 'multipart/form-data')
677
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
678
                        'Are you really sure you want to remove your account?')))
679
        form.add_submit('submit', _('Remove my account'))
680
        form.add_submit('cancel', _('Cancel'))
681

  
682
        if form.get_submit() == 'cancel':
683
            return redirect('.')
684

  
685
        if form.is_submitted() and not form.has_errors():
686
            user = get_request().user
687
            account = PasswordAccount.get_on_index(user.id, str('user_id'))
688
            get_session_manager().expire_session() 
689
            account.remove_self()
690
            return redirect(get_publisher().get_root_url())
691

  
692
        template.html_top(_('Removing Account'))
693
        return form.render()
694

  
695
    def announces(self):
696
        options = get_cfg('misc', {}).get('announce_themes')
697
        if not options:
698
            raise errors.TraversalError()
699
        subscription = AnnounceSubscription.get_on_index(get_request().user.id, str('user_id'))
700
        if not subscription:
701
            raise errors.TraversalError()
702

  
703
        if subscription.enabled_themes is None:
704
            enabled_themes = options
705
        else:
706
            enabled_themes = subscription.enabled_themes
707

  
708
        form = Form(enctype = 'multipart/form-data')
709
        form.add(CheckboxesWidget, 'themes', title=_('Announce Themes'),
710
                value=enabled_themes, elements=options,
711
                inline=False, required=False)
712

  
713
        form.add_submit('submit', _('Apply Changes'))
714
        form.add_submit('cancel', _('Cancel'))
715

  
716
        if form.get_submit() == 'cancel':
717
            return redirect('.')
718

  
719
        if form.is_submitted() and not form.has_errors():
720
            chosen_themes = form.get_widget('themes').parse()
721
            if chosen_themes == options:
722
                chosen_themes = None
723
            subscription.enabled_themes = chosen_themes
724
            subscription.store()
725
            return redirect('.')
726

  
727
        template.html_top()
728
        get_response().breadcrumb.append(('announces', _('Announce Subscription')))
729
        return form.render()
730

  
731

  
732
TextsDirectory.register('aq-myspace-invoice',
733
        N_('Message on top of invoices page'),
734
        category = N_('Invoices'))
735

  
extra/modules/payments_ui.ptl
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

  
10
import wcs
11
import wcs.admin.root
12
from wcs.backoffice.menu import *
13
from wcs.formdef import FormDef
14

  
15
from qommon import errors, misc, template, get_logger
16
from qommon.form import *
17
from qommon.strftime import strftime
18
from qommon.admin.emails import EmailsDirectory
19

  
20
from payments import (eopayment, Regie, is_payment_supported, Invoice,
21
        Transaction, notify_paid_invoice)
22

  
23
from qommon.admin.texts import TextsDirectory
24

  
25
if not set:
26
    from sets import Set as set
27

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

  
67
class InvoicesDirectory(Directory):
68
    _q_exports = ['', 'multiple']
69

  
70
    def _q_traverse(self, path):
71
        if not is_payment_supported():
72
            raise errors.TraversalError()
73
        get_response().filter['bigdiv'] = 'profile'
74
        if get_session().user:
75
            # fake breadcrumb
76
            get_response().breadcrumb.append(('myspace/', _('My Space')))
77
            get_response().breadcrumb.append(('invoices/', _('Invoices')))
78
        return Directory._q_traverse(self, path)
79

  
80
    def multiple [html] (self):
81
        invoice_ids = get_request().form.get('invoice')
82
        if type(invoice_ids) is not list:
83
            return redirect('%s' % invoice_ids)
84
        return redirect('+'.join(invoice_ids))
85

  
86
    def _q_lookup [html] (self, component):
87
        if str('+') in component:
88
            invoice_ids = component.split(str('+'))
89
        else:
90
            invoice_ids = [component]
91
        for invoice_id in invoice_ids:
92
            if not Invoice.check_crc(invoice_id):
93
                raise errors.TraversalError()
94

  
95
        template.html_top(_('Invoices'))
96
        TextsDirectory.get_html_text('aq-invoice')
97

  
98
        regies_id = set()
99
        for invoice_id in invoice_ids:
100
            try:
101
                invoice = Invoice.get(invoice_id)
102
            except KeyError:
103
                raise errors.TraversalError()
104
            invoice_as_html(invoice)
105
            if not (invoice.paid or invoice.canceled):
106
                regies_id.add(invoice.regie_id)
107

  
108
        if len(regies_id) == 1:
109
            '<p class="command">'
110
            '<a href="%s/payment/init?invoice_ids=%s">' % (get_publisher().get_frontoffice_url(), component)
111
            if len(invoice_ids) > 1:
112
                _('Pay Selected Invoices')
113
            else:
114
                _('Pay')
115
            '</a></p>'
116
        if len(regies_id) > 1:
117
            _('You can not pay to different regies.')
118

  
119
    def _q_index(self):
120
        return redirect('..')
121

  
122

  
123
class RegieDirectory(Directory):
124
    _q_exports = ['', 'edit', 'delete', 'options']
125

  
126
    def __init__(self, regie):
127
        self.regie = regie
128

  
129
    def _q_index [html] (self):
130
        html_top('payments', title = _('Regie: %s') % self.regie.label)
131
        get_response().filter['sidebar'] = self.get_sidebar()
132
        '<h2>%s</h2>' % _('Regie: %s') % self.regie.label
133

  
134
        get_session().display_message()
135

  
136
        if self.regie.description:
137
            '<div class="bo-block">'
138
            '<p>'
139
            self.regie.description
140
            '</p>'
141
            '</div>'
142

  
143
        if self.regie.service:
144
            '<div class="bo-block">'
145
            url = get_publisher().get_frontoffice_url() + '/payment/back_asynchronous/'
146
            url += str(self.regie.id)
147
            '<p>'
148
            '%s %s' % (_('Banking Service:'), self.regie.service)
149
            ' (<a href="options">%s</a>)' % _('options')
150
            '</p>'
151
            '<p>'
152
            '%s %s' % (_('Payment notification URL:'), url)
153
            '</div>'
154

  
155
        self.invoice_listing()
156

  
157
    def get_sidebar [html] (self):
158
        '<ul>'
159
        '<li><a href="edit">%s</a></li>' % _('Edit')
160
        '<li><a href="delete">%s</a></li>' % _('Delete')
161
        '</ul>'
162

  
163
    def edit [html] (self):
164
        form = self.form()
165
        if form.get_submit() == 'cancel':
166
            return redirect('.')
167

  
168
        if form.is_submitted() and not form.has_errors():
169
            self.submit(form)
170
            return redirect('..')
171

  
172
        html_top('payments', title = _('Edit Regie: %s') % self.regie.label)
173
        '<h2>%s</h2>' % _('Edit Regie: %s') % self.regie.label
174
        form.render()
175

  
176

  
177
    def form(self):
178
        form = Form(enctype='multipart/form-data')
179
        form.add(StringWidget, 'label', title=_('Label'), required=True,
180
                value=self.regie.label)
181
        form.add(TextWidget, 'description', title=_('Description'),
182
                value=self.regie.description, rows=5, cols=60)
183
        form.add(SingleSelectWidget, 'service', title=_('Banking Service'),
184
                value=self.regie.service, required=True,
185
                options = [
186
                        ('dummy', _('Dummy (for tests)')),
187
                        ('sips', 'SIPS'),
188
                        ('systempayv2', 'systempay (Banque Populaire)'),
189
                        ('spplus', _('SP+ (Caisse d\'epargne)'))])
190
        form.add_submit('submit', _('Submit'))
191
        form.add_submit('cancel', _('Cancel'))
192
        return form
193

  
194
    def submit(self, form):
195
        for k in ('label', 'description', 'service'):
196
            widget = form.get_widget(k)
197
            if widget:
198
                setattr(self.regie, k, widget.parse())
199
        self.regie.store()
200

  
201
    def delete [html] (self):
202
        form = Form(enctype='multipart/form-data')
203
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
204
                        'You are about to irrevocably delete this regie.')))
205
        form.add_submit('submit', _('Submit'))
206
        form.add_submit('cancel', _('Cancel'))
207
        if form.get_submit() == 'cancel':
208
            return redirect('..')
209
        if not form.is_submitted() or form.has_errors():
210
            get_response().breadcrumb.append(('delete', _('Delete')))
211
            html_top('payments', title = _('Delete Regie'))
212
            '<h2>%s</h2>' % _('Deleting Regie: %s') % self.regie.label
213
            form.render()
214
        else:
215
            self.regie.remove_self()
216
            return redirect('..')
217

  
218
    def option_form(self):
219
        form = Form(enctype='multipart/form-data')
220
        module = eopayment.get_backend(self.regie.service)
221
        service_options = {}
222
        for infos in module.description['parameters']:
223
            if 'default' in infos:
224
                service_options[infos['name']] = infos['default']
225
        service_options.update(self.regie.service_options or {})
226

  
227
        banking_titles = {
228
            ('dummy', 'direct_notification_url'): N_('Direct Notification URL'),
229
            ('dummy', 'siret'): N_('Dummy SIRET'),
230
        }
231

  
232
        for infos in module.description['parameters']:
233
            name = infos['name']
234
            caption = infos.get('caption', name).encode(get_publisher().site_charset)
235
            title = banking_titles.get((self.regie.service, name), caption)
236
            kwargs = {}
237
            widget = StringWidget
238
            if infos.get('help_text') is not None:
239
                kwargs['hint'] = _(infos['help_text'].encode(get_publisher().site_charset))
240
            if infos.get('required', False):
241
                kwargs['required'] = True
242
            if infos.get('max_length') is not None:
243
                kwargs['size'] = infos['max_length']
244
            elif infos.get('length') is not None:
245
                kwargs['size'] = infos['length']
246
            else:
247
                kwargs['size'] = 80
248
            if kwargs['size'] > 100:
249
                widget = TextWidget
250
                kwargs['cols'] = 80
251
                kwargs['rows'] = 5
252
            if 'type' not in infos or infos['type'] is str:
253
                form.add(widget, name, title=_(title),
254
                         value=service_options.get(name), **kwargs)
255
            elif infos['type'] is bool:
256
                form.add(CheckboxWidget, name, title=title,
257
                         value=service_options.get(name), **kwargs)
258
        form.add_submit('submit', _('Submit'))
259
        form.add_submit('cancel', _('Cancel'))
260
        return form
261

  
262
    def options [html] (self):
263
        form = self.option_form()
264

  
265
        module = eopayment.get_backend(self.regie.service)
266
        try:
267
            '<!-- Payment backend description: \n'
268
            pprint.pformat(module.description)
269
            '-->'
270
        except:
271
            return template.error_page(_('Payment backend do not list its options'))
272
            raise errors.TraversalError()
273
        '<!-- \n'
274
        'Service options\n'
275
        pprint.pformat(self.regie.service_options)
276
        '-->'
277

  
278
        if form.get_submit() == 'cancel':
279
            return redirect('.')
280

  
281
        if form.is_submitted() and not form.has_errors():
282
             if self.submit_options(form, module):
283
                return redirect('..')
284

  
285
        html_top('payments', title=_('Edit Service Options'))
286
        '<h2>%s</h2>' % _('Edit Service Options')
287
        form.render()
288

  
289
    def submit_options(self, form, module):
290
        # extra validation
291
        error = False
292
        for infos in module.description['parameters']:
293
            widget = form.get_widget(infos['name'])
294
            value = widget.parse()
295
            if value and 'validation' in infos:
296
                try:
297
                    if not infos['validation'](value):
298
                        widget.set_error(_('Valeur invalide'))
299
                        error = True
300
                except ValueError, e:
301
                    widget.set_error(_(e.message))
302
                    error = True
303
        if error:
304
            return False
305
        if not self.regie.service_options:
306
            self.regie.service_options = {}
307
        for infos in module.description['parameters']:
308
            name = infos['name']
309
            value = form.get_widget(name).parse()
310
            if value is None:
311
                value = ''
312
            if hasattr(value, 'strip'):
313
                value = value.strip()
314
            if infos.get('default') is not None:
315
                if value == infos['default']:
316
                    self.regie.service_options.pop(name, None)
317
                else:
318
                    self.regie.service_options[name] = form.get_widget(name).parse()
319
            elif not value:
320
                self.regie.service_options.pop(name, None)
321
            else:
322
                self.regie.service_options[name] = form.get_widget(name).parse()
323
        self.regie.store()
324
        return True
325

  
326
    PAGINATION = 50
327

  
328
    def monetary_amount(self, val):
329
        if isinstance(val, basestring):
330
            val = val.replace(',', '.')
331
        return '%.2f' % decimal.Decimal(val)
332

  
333
    def get_sort_by(self):
334
        request = get_request()
335
        sort_by = request.form.get('sort_by')
336
        if sort_by not in ('date', 'paid_date', 'username'):
337
            sort_by = 'date'
338
        return sort_by
339

  
340
    def get_invoices(self):
341
        sort_by = self.get_sort_by()
342
        invoices = Invoice.get_with_indexed_value('regie_id', self.regie.id,
343
                ignore_errors=True)
344
        if 'date' in sort_by:
345
            reverse = True
346
            key = lambda i: getattr(i, sort_by) or datetime.datetime.now()
347
        else:
348
            reverse = False
349
            key = lambda i: getattr(i, sort_by) or ''
350
        invoices.sort(reverse=reverse, key=key)
351
        return invoices
352

  
353
    def unpay(self, request, invoice):
354
        get_logger().info(_('manually set unpaid invoice %(invoice_id)s in regie %(regie)s')
355
            % dict(invoice_id=invoice.id, regie=self.regie.id))
356
        transaction = Transaction()
357
        transaction.invoice_ids = [ invoice.id ]
358
        transaction.order_id = 'Manual action'
359
        transaction.start = datetime.datetime.now()
360
        transaction.end = transaction.start
361
        transaction.bank_data = { 
362
            'action': 'Set unpaid', 
363
            'by': request.user.get_display_name() + ' (%s)' % request.user.id
364
        }
365
        transaction.store()
366
        invoice.unpay()
367

  
368
    def pay(self, request, invoice):
369
        get_logger().info(_('manually set paid invoice %(invoice_id)s in regie %(regie)s')
370
            % dict(invoice_id=invoice.id, regie=self.regie.id))
371
        transaction = Transaction()
372
        transaction.invoice_ids = [ invoice.id ]
373
        transaction.order_id = 'Manual action'
374
        transaction.start = datetime.datetime.now()
375
        transaction.end = transaction.start
376
        transaction.bank_data = { 
377
            'action': 'Set paid', 
378
            'by': request.user.get_display_name() + ' (%s)' % request.user.id
379
        }
380
        transaction.store()
381
        invoice.pay()
382

  
383
    def invoice_listing [html] (self):
384
        request = get_request()
385
        get_response().add_css_include('../../themes/auquotidien/admin.css')
386
        if request.get_method() == 'POST':
387
            invoice_id = request.form.get('id')
388
            invoice = Invoice.get(invoice_id, ignore_errors=True)
389
            if invoice:
390
                if 'unpay' in request.form:
391
                    self.unpay(request, invoice)
392
                elif 'pay' in request.form:
393
                    self.pay(request, invoice)
394
                return redirect('')
395
        try:
396
            offset = int(request.form.get('offset', 0))
397
        except ValueError:
398
            offset = 0
399
        '<table id="invoice-listing" borderspacing="0">'
400
        '<thead>'
401
        '<tr>'
402
        '<td><a href="?sort_by=date&offset=%d">Creation</a></td>' % offset
403
        '<td>Amount</td>'
404
        '<td><a href="?sort_by=paid_date&offset=%d">Paid</a></td>' % offset
405
        '<td><a href="?sort_by=username&offset=%d">User</a></td>' % offset
406
        '<td>Titre</td>'
407
        '<td></td>'
408
        '</tr>'
409
        '</thead>'
410
        invoices = self.get_invoices()
411
        for invoice in invoices[offset:offset+self.PAGINATION]:
412
            '<tbody class="invoice-rows">'
413
            '<tr class="invoice-row"><td>'
414
            misc.localstrftime(invoice.date)
415
            '</td><td class="amount">'
416
            self.monetary_amount(invoice.amount)
417
            '</td><td>'
418
            if invoice.paid:
419
                misc.localstrftime(invoice.paid_date)
420
            else:
421
                ''
422
            '</td><td>'
423
            user = invoice.get_user()
424
            if user:
425
                user.name
426
            '</td><td class="subject">%s</td>' % (invoice.subject or '')
427
            '<td>'
428
            '<form method="post">'
429
            '<input type="hidden" name="id" value="%s"/> ' % invoice.id
430
            if invoice.paid:
431
                '<input type="submit" name="unpay" value="%s"/>' % _('Set unpaid')
432
            else:
433
                '<input type="submit" name="pay" value="%s"/>' % _('Set paid')
434
            '</form>'
435

  
436
            '</td></tr>'
437
            transactions = Transaction.get_with_indexed_value('invoice_ids',
438
                    invoice.id)
439
            for transaction in sorted(transactions, key=lambda x: x.start):
440
                '<tr>'
441
                '<td></td>'
442
                '<td colspan="5">'
443
                'OrderID: %s' % transaction.order_id
444
                ' Start: %s' % transaction.start
445
                if transaction.end:
446
                    ' End: %s' % transaction.end
447
                if transaction.bank_data:
448
                    ' Bank data: %r' % transaction.bank_data
449
                '</td>'
450
                '</tr>'
451
            '</tbody>'
452
        '</tbody></table>'
453
        if offset != 0:
454
            '<a href="?offset=%d>%s</a> ' % (
455
                 max(0, offset-self.PAGINATION), _('Previous'))
456
        if offset + self.PAGINATION < len(invoices):
457
            '<a href="?offset=%d>%s</a> ' % (
458
                 max(0, offset-self.PAGINATION), _('Previous'))
459

  
460

  
461
class RegiesDirectory(Directory):
462
    _q_exports = ['', 'new']
463

  
464
    def _q_traverse(self, path):
465
        get_response().breadcrumb.append(('regie/', _('Regies')))
466
        return Directory._q_traverse(self, path)
467

  
468
    def _q_index [html] (self):
469
        return redirect('..')
470

  
471
    def new [html] (self):
472
        regie_ui = RegieDirectory(Regie())
473

  
474
        form = regie_ui.form()
475
        if form.get_submit() == 'cancel':
476
            return redirect('.')
477

  
478
        if form.is_submitted() and not form.has_errors():
479
            regie_ui.submit(form)
480
            return redirect('%s/' % regie_ui.regie.id)
481

  
482
        get_response().breadcrumb.append(('new', _('New Regie')))
483
        html_top('payments', title = _('New Regie'))
484
        '<h2>%s</h2>' % _('New Regie')
485
        form.render()
486

  
487
    def _q_lookup(self, component):
488
        try:
489
            regie = Regie.get(component)
490
        except KeyError:
491
            raise errors.TraversalError()
492
        get_response().breadcrumb.append((str(regie.id), regie.label))
493
        return RegieDirectory(regie)
494

  
495

  
496
class PaymentsDirectory(AccessControlled, Directory):
497
    _q_exports = ['', 'regie']
498
    label = N_('Payments')
499

  
500
    regie = RegiesDirectory()
501

  
502
    def _q_access(self):
503
        user = get_request().user
504
        if not user:
505
            raise errors.AccessUnauthorizedError()
506
        admin_role = get_cfg('aq-permissions', {}).get('payments', None)
507
        if not (user.is_admin or admin_role in (user.roles or [])):
508
            raise errors.AccessForbiddenError(
509
                    public_msg = _('You are not allowed to access Payments Management'),
510
                    location_hint = 'backoffice')
511

  
512
        get_response().breadcrumb.append(('payments/', _('Payments')))
513

  
514

  
515
    def _q_index [html] (self):
516
        html_top('payments', _('Payments'))
517

  
518
        '<ul id="main-actions">'
519
        '  <li><a class="new-item" href="regie/new">%s</a></li>' % _('New Regie')
520
        '</ul>'
521

  
522
        if not is_payment_supported:
523
            '<p class="infonotice">'
524
            _('Payment is not supported.')
525
            '</p>'
526

  
527
        regies = Regie.select()
528
        '<h2>%s</h2>' % _('Regies')
529
        if not regies:
530
            '<p>'
531
            _('There are no regies defined at the moment.')
532
            '</p>'
533
        '<ul class="biglist" id="regies-list">'
534
        for l in regies:
535
            regie_id = l.id
536
            '<li class="biglistitem" id="itemId_%s">' % regie_id
537
            '<strong class="label"><a href="regie/%s/">%s</a></strong>' % (regie_id, l.label)
538
            '</li>'
539
        '</ul>'
540

  
541

  
542
TextsDirectory.register('aq-invoice',
543
        N_('Message on top of an invoice'),
544
        category = N_('Invoices'))
545

  
546
EmailsDirectory.register('payment-new-invoice-email',
547
        N_('New invoice'),
548
        N_('Available variables: user, regie, invoice, invoice_url'),
549
        category = N_('Invoices'),
550
        default_subject = N_('New invoice'),
551
        default_body = N_('''
552
A new invoice is available at [invoice_url].
553
'''))
554

  
555
EmailsDirectory.register('payment-invoice-paid-email',
556
        N_('Paid invoice'),
557
        N_('Available variables: user, regie, invoice, invoice_url'),
558
        category = N_('Invoices'),
559
        default_subject = N_('Paid invoice'),
560
        default_body = N_('''
561
The invoice [invoice_url] has been paid.
562
'''))
563

  
564
EmailsDirectory.register('payment-invoice-canceled-email',
565
        N_('Canceled invoice'),
566
        N_('Available variables: user, regie, invoice, invoice_url'),
567
        category = N_('Invoices'),
568
        default_subject = N_('Canceled invoice'),
569
        default_body = N_('''
570
The invoice [invoice.id] has been canceled.
571
'''))
extra/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.backoffice.menu import *
14
from wcs.formdef import FormDef
15

  
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

  
21
from payments import (eopayment, Regie, is_payment_supported, Invoice,
22
        Transaction, notify_paid_invoice)
23

  
24
from qommon.admin.texts import TextsDirectory
25

  
26
if not set:
27
    from sets import Set as set
28

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

  
70

  
71
class InvoicesDirectory(Directory):
72
    _q_exports = ['', 'multiple']
73

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

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

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

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

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

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

  
125
        return r.getvalue()
126

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

  
130

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

  
134
    def __init__(self, regie):
135
        self.regie = regie
136

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

  
143
        r += get_session().display_message()
144

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

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

  
164
        r += self.invoice_listing()
165
        return r.getvalue()
166

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

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

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

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

  
190

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

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

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

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

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

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

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

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

  
295
        if form.get_submit() == 'cancel':
296
            return redirect('.')
297

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

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

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

  
344
    PAGINATION = 50
345

  
346
    def monetary_amount(self, val):
347
        if isinstance(val, basestring):
348
            val = val.replace(',', '.')
349
        return '%.2f' % decimal.Decimal(val)
350

  
351
    def get_sort_by(self):
352
        request = get_request()
353
        sort_by = request.form.get('sort_by')
354
        if sort_by not in ('date', 'paid_date', 'username'):
355
            sort_by = 'date'
356
        return sort_by
357

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

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

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

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

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

  
480

  
481
class RegiesDirectory(Directory):
482
    _q_exports = ['', 'new']
483

  
484
    def _q_traverse(self, path):
485
        get_response().breadcrumb.append(('regie/', _('Regies')))
486
        return Directory._q_traverse(self, path)
487

  
488
    def _q_index(self):
489
        return redirect('..')
490

  
491
    def new(self):
492
        regie_ui = RegieDirectory(Regie())
493

  
494
        form = regie_ui.form()
495
        if form.get_submit() == 'cancel':
496
            return redirect('.')
497

  
498
        if form.is_submitted() and not form.has_errors():
499
            regie_ui.submit(form)
500
            return redirect('%s/' % regie_ui.regie.id)
501

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

  
509
    def _q_lookup(self, component):
510
        try:
511
            regie = Regie.get(component)
512
        except KeyError:
513
            raise errors.TraversalError()
514
        get_response().breadcrumb.append((str(regie.id), regie.label))
515
        return RegieDirectory(regie)
516

  
517

  
518
class PaymentsDirectory(AccessControlled, Directory):
519
    _q_exports = ['', 'regie']
520
    label = N_('Payments')
521

  
522
    regie = RegiesDirectory()
523

  
524
    def _q_access(self):
525
        user = get_request().user
526
        if not user:
527
            raise errors.AccessUnauthorizedError()
528
        admin_role = get_cfg('aq-permissions', {}).get('payments', None)
529
        if not (user.is_admin or admin_role in (user.roles or [])):
530
            raise errors.AccessForbiddenError(
531
                    public_msg = _('You are not allowed to access Payments Management'),
532
                    location_hint = 'backoffice')
533

  
534
        get_response().breadcrumb.append(('payments/', _('Payments')))
535

  
536

  
537
    def _q_index(self):
538
        html_top('payments', _('Payments'))
539
        r = TemplateIO(html=True)
540

  
541
        r += htmltext('<ul id="main-actions">')
542
        r += htmltext('  <li><a class="new-item" href="regie/new">%s</a></li>') % _('New Regie')
543
        r += htmltext('</ul>')
544

  
545
        if not is_payment_supported:
546
            r += htmltext('<p class="infonotice">')
547
            r += _('Payment is not supported.')
548
            r += htmltext('</p>')
549

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

  
565

  
566
TextsDirectory.register('aq-invoice',
567
        N_('Message on top of an invoice'),
568
        category = N_('Invoices'))
569

  
570
EmailsDirectory.register('payment-new-invoice-email',
571
        N_('New invoice'),
572
        N_('Available variables: user, regie, invoice, invoice_url'),
573
        category = N_('Invoices'),
574
        default_subject = N_('New invoice'),
575
        default_body = N_('''
576
A new invoice is available at [invoice_url].
577
'''))
578

  
579
EmailsDirectory.register('payment-invoice-paid-email',
580
        N_('Paid invoice'),
581
        N_('Available variables: user, regie, invoice, invoice_url'),
582
        category = N_('Invoices'),
583
        default_subject = N_('Paid invoice'),
584
        default_body = N_('''
585
The invoice [invoice_url] has been paid.
586
'''))
587

  
588
EmailsDirectory.register('payment-invoice-canceled-email',
589
        N_('Canceled invoice'),
590
        N_('Available variables: user, regie, invoice, invoice_url'),
591
        category = N_('Invoices'),
592
        default_subject = N_('Canceled invoice'),
593
        default_body = N_('''
594
The invoice [invoice.id] has been canceled.
595
'''))
extra/modules/root.ptl
1
from quixote import get_publisher, get_response, get_request, redirect, get_session
2
from quixote.directory import Directory
3
from quixote.html import htmltext
4
from quixote.util import StaticDirectory
5

  
6
from wcs.qommon.misc import get_variadic_url
7

  
8
import os
9
import re
10
import string
11
import urlparse
12

  
13
try:
14
    import lasso
15
except ImportError:
16
    pass
17

  
18
import wcs
19
import wcs.root
20
import qommon
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 qommon.tokens import Token
33
from qommon.admin.emails import EmailsDirectory
34
from qommon.admin.texts import TextsDirectory
35

  
36
from links import Link
37
from announces import Announce, AnnounceSubscription
38
from myspace import MyspaceDirectory
39
from agenda import AgendaDirectory
40
from events import Event, get_default_event_tags
41
from payments import PublicPaymentDirectory
42
from payments_ui import InvoicesDirectory
43

  
44
import admin
45

  
46
import wcs.forms.root
47
from wcs.workflows import Workflow
48

  
49
from saml2 import Saml2Directory
50

  
51
OldRootDirectory = wcs.root.RootDirectory
52

  
53
import qommon.ident.password
54
import qommon.ident.idp
55

  
56
import drupal
57
import ezldap_ui
58
import msp_ui
59

  
60
def category_get_homepage_position(self):
61
    if hasattr(self, 'homepage_position') and self.homepage_position:
62
        return self.homepage_position
63
    if self.url_name == 'consultations':
64
        return '2nd'
65
    return '1st'
66
Category.get_homepage_position = category_get_homepage_position
67

  
68
def category_get_limit(self):
69
    if hasattr(self, 'limit') and self.limit is not None:
70
        return self.limit
71
    return 3
72
Category.get_limit = category_get_limit
73

  
74

  
75
class FormsRootDirectory(wcs.forms.root.RootDirectory):
76

  
77
    def _q_index(self, *args):
78
        get_response().filter['is_index'] = True
79
        return wcs.forms.root.RootDirectory._q_index(self, *args)
80

  
81
    def user_forms [html] (self, user_forms):
82
        base_url = get_publisher().get_root_url()
83

  
84
        draft = [x for x in user_forms if x.is_draft()]
85
        if draft:
86
            '<h4 id="drafts">%s</h4>' % _('My Current Drafts')
87
            '<ul>'
88
            for f in draft:
89
                '<li><a href="%s%s/%s/%s">%s</a>, %s</li>' % (base_url,
90
                    f.formdef.category.url_name,
91
                    f.formdef.url_name, f.id, f.formdef.name,
92
                    misc.localstrftime(f.receipt_time))
93
            '</ul>'
94

  
95
        forms_by_status_name = {}
96
        for f in user_forms:
97
            if f.is_draft():
98
                continue
99
            status = f.get_visible_status()
100
            if status:
101
                status_name = status.name
102
            else:
103
                status_name = None
104
            if status_name in forms_by_status_name:
105
                forms_by_status_name[status_name].append(f)
106
            else:
107
                forms_by_status_name[status_name] = [f]
108
        for status_name in forms_by_status_name:
109
            if status_name:
110
                '<h4>%s</h4>' % _('My forms with status "%s"') % status_name
111
            else:
112
                '<h4>%s</h4>' % _('My forms with an unknown status') % status_name
113
            '<ul>'
114
            forms_by_status_name[status_name].sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
115
            for f in forms_by_status_name[status_name]:
116
                if f.formdef.category_id:
117
                    category_url = f.formdef.category.url_name
118
                else:
119
                    category_url = '.'
120
                '<li><a href="%s%s/%s/%s/">%s</a>, %s</li>' % (
121
                        base_url,
122
                        category_url,
123
                        f.formdef.url_name, f.id, f.formdef.name, 
124
                        misc.localstrftime(f.receipt_time))
125
            '</ul>'
126

  
127

  
128
class AnnounceDirectory(Directory):
129
    _q_exports = ['', 'edit', 'delete', 'email']
130

  
131
    def __init__(self, announce):
132
        self.announce = announce
133

  
134
    def _q_index [html] (self):
135
        template.html_top(_('Announces to citizens'))
136

  
137
        if self.announce.publication_time:
138
            date_heading = '%s - ' % time.strftime(misc.date_format(), self.announce.publication_time)
139
        else:
140
            date_heading = ''
141

  
142
        '<h3>%s%s</h3>' % (date_heading, self.announce.title)
143

  
144
        '<p>'
145
        self.announce.text
146
        '</p>'
147

  
148
        '<p>'
149
        '<a href="../">%s</a>' % _('Back')
150
        '</p>'
151

  
152

  
153
class AnnouncesDirectory(Directory):
154
    _q_exports = ['', 'subscribe', 'email', 'atom', 'sms', 'emailconfirm',
155
            'email_unsubscribe', 'sms_unsubscribe', 'smsconfirm', 'rawlist']
156

  
157
    
158
    def _q_traverse(self, path):
159
        get_response().breadcrumb.append(('announces/', _('Announces')))
160
        return Directory._q_traverse(self, path)
161

  
162
    def _q_index [html] (self):
163
        template.html_top(_('Announces to citizens'))
164
        self.announces_list()
165
        '<ul id="announces-links">'
166
        '<li><a href="subscribe">%s</a></li>' % _('Receiving those Announces')
167
        '</ul>'
168

  
169
    def _get_announce_subscription(self):
170
        """ """
171
        sub = None
172
        if get_request().user:
173
            subs = AnnounceSubscription.select(lambda x: x.user_id == get_request().user.id)
174
            if subs:
175
                sub = subs[0]
176
        return sub
177

  
178
    def rawlist [html] (self):
179
        self.announces_list()
180
        get_response().filter = None
181

  
182
    def announces_list [html] (self):
183
        announces = Announce.get_published_announces()
184
        if not announces:
185
            raise errors.TraversalError()
186

  
187
        # XXX: will need pagination someday
188
        for item in announces:
189
            '<div class="announce-item">\n'
190
            '<h4>'
191
            if item.publication_time:
192
                time.strftime(misc.date_format(), item.publication_time)
193
                ' - '
194
            item.title
195
            '</h4>\n'
196
            '<p>\n'
197
            item.text
198
            '\n</p>\n'
199
            '</div>\n'
200

  
201

  
202
    def sms [html] (self):
203
        sms_mode = get_cfg('sms', {}).get('mode', 'none')
204

  
205
        if sms_mode == 'none':
206
            raise errors.TraversalError()
207

  
208
        get_response().breadcrumb.append(('sms', _('SMS')))
209
        template.html_top(_('Receiving announces by SMS'))
210

  
211
        if sms_mode == 'demo':
212
            TextsDirectory.get_html_text('aq-sms-demo')
213
        else:
214
            announces_cfg = get_cfg('announces',{})
215
            mobile_mask = announces_cfg.get('mobile_mask')
216
            if mobile_mask:
217
                mobile_mask = ' (' + mobile_mask + ')'
218
            else:
219
                mobile_mask = ''
220
            form = Form(enctype='multipart/form-data')
221
            form.add(StringWidget, 'mobile', title = _('Mobile number %s') % mobile_mask, size=12, required=True)
222
            form.add_submit('submit', _('Subscribe'))
223
            form.add_submit('cancel', _('Cancel'))
224

  
225
            if form.get_submit() == 'cancel':
226
                return redirect('subscribe')
227

  
228
            if form.is_submitted() and not form.has_errors():
229
                s = self.sms_submit(form)
230
                if s == False:
231
                    form.render()
232
                else:
233
                    return redirect("smsconfirm")
234
            else:
235
                form.render()
236

  
237
    def sms_submit(self, form):
238
        mobile = form.get_widget("mobile").parse()
239
        # clean the string, remove any extra character
240
        mobile = re.sub('[^0-9+]','',mobile)
241
        # if a mask was set, validate
242
        announces_cfg = get_cfg('announces',{})
243
        mobile_mask = announces_cfg.get('mobile_mask')
244
        if mobile_mask:
245
            mobile_regexp = re.sub('X','[0-9]', mobile_mask) + '$'
246
            if not re.match(mobile_regexp, mobile):
247
                form.set_error("mobile", _("Phone number invalid ! It must match ") + mobile_mask)
248
                return False
249
        if mobile.startswith('00'):
250
            mobile = '+' + mobile[2:]
251
        else:
252
            # Default to france international prefix
253
            if not mobile.startswith('+'):
254
                mobile = re.sub("^0", "+33", mobile)
255
        sub = self._get_announce_subscription()
256
        if not sub:
257
            sub = AnnounceSubscription()
258
        if get_request().user:
259
            sub.user_id = get_request().user.id
260

  
261
        if mobile:
262
            sub.sms = mobile
263

  
264
        if not get_request().user:
265
            sub.enabled = False
266

  
267
        sub.store()
268

  
269
        # Asking sms confirmation
270
        token = Token(3 * 86400, 4, string.digits)
271
        token.type = 'announces-subscription-confirmation'
272
        token.subscription_id = sub.id
273
        token.store()
274

  
275
        message = _("Confirmation code : %s") % str(token.id)
276
        sms_cfg = get_cfg('sms', {})
277
        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
278
        mode = sms_cfg.get('mode', 'none')
279
        sms = SMS.get_sms_class(mode)
280
        try:
281
            sms.send(sender, [mobile], message)
282
        except errors.SMSError, e:
283
            get_logger().error(e)
284
            form.set_error("mobile", _("Send SMS confirmation failed"))
285
            sub.remove("sms")
286
            return False
287

  
288
    def smsconfirm [html] (self):
289
        template.html_top(_('Receiving announces by SMS confirmation'))
290
        "<p>%s</p>" % _("You will receive a confirmation code by SMS.")
291
        form = Form(enctype='multipart/form-data')
292
        form.add(StringWidget, 'code', title = _('Confirmation code (4 characters)'), size=12, required=True)
293
        form.add_submit('submit', _('Subscribe'))
294
        form.add_submit('cancel', _('Cancel'))
295

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

  
299
        if form.is_submitted() and not form.has_errors():
300
            token = None
301
            id = form.get_widget("code").parse()
302
            try:
303
                token = Token.get(id)
304
            except KeyError:
305
                form.set_error("code",  _('Invalid confirmation code.'))
306
            else:
307
                if token.type != 'announces-subscription-confirmation':
308
                    form.set_error("code",  _('Invalid confirmation code.'))
309
                else:
310
                    sub = AnnounceSubscription.get(token.subscription_id)
311
                    token.remove_self()
312
                    sub.enabled_sms = True
313
                    sub.store()
314
                    return redirect('.')
315
            form.render()
316
        else:
317
            form.render()
318

  
319
    def sms_unsubscribe [html] (self):
320
        sub = self._get_announce_subscription()
321

  
322
        form = Form(enctype='multipart/form-data')
323
        if not sub:
324
            return redirect('..')
325

  
326
        form.add_submit('submit', _('Unsubscribe'))
327
        form.add_submit('cancel', _('Cancel'))
328

  
329
        if form.get_submit() == 'cancel':
330
            return redirect('..')
331

  
332
        get_response().breadcrumb.append(('sms', _('SMS Unsubscription')))
333
        template.html_top()
334

  
335
        if form.is_submitted() and not form.has_errors():
336
            if sub:
337
                sub.remove("sms")
338

  
339
            def sms_unsub_ok [html] ():
340
                root_url = get_publisher().get_root_url()
341
                '<p>'
342
                _('You have been unsubscribed from announces')
343
                '</p>'
344
                if not get_response().iframe_mode:
345
                    '<a href="%s">%s</a>' % (root_url, _('Back Home'))
346

  
347
            return sms_unsub_ok()
348

  
349
        else:
350
            '<p>'
351
            _('Do you want to stop receiving announces by sms ?')
352
            '</p>'
353
            form.render()
354

  
355

  
356
    def subscribe [html] (self):
357
        get_response().breadcrumb.append(('subscribe', _('Subscription')))
358
        template.html_top(_('Receiving Announces'))
359

  
360
        TextsDirectory.get_html_text('aq-announces-subscription')
361

  
362
        sub = self._get_announce_subscription()
363

  
364
        '<ul id="announce-modes">'
365
        if sub and sub.email:
366
            ' <li>'
367
            '<span id="par_mail">%s</span>' % _('Email (currently subscribed)')
368
            ' <a href="email_unsubscribe" rel="popup">%s</a></li>' % _('Unsubscribe')
369
        else:
370
            ' <li><a href="email" id="par_mail" rel="popup">%s</a></li>' % _('Email')
371
        if sub and sub.sms:
372
            ' <li>'
373
            if sub.enabled_sms:
374
                '<span id="par_sms">%s</span>' % _('SMS %s (currently subscribed)') % sub.sms
375
            else:
376
                '<span id="par_sms">%s</span>' % _('SMS %s (currently not confirmed)') % sub.sms
377
                ' <a href="smsconfirm" rel="popup">%s</a> ' % _('Confirmation')
378
            ' <a href="sms_unsubscribe" rel="popup">%s</a></li>' % _('Unsubscribe')
379
        elif get_cfg('sms', {}).get('mode', 'none') != 'none':
380
            ' <li><a href="sms" id="par_sms">%s</a></li>' % _('SMS')
381
        ' <li><a class="feed-link" href="atom" id="par_rss">%s</a>' % _('Feed')
382
        '</ul>'
383

  
384

  
385
    def email [html] (self):
386
        get_response().breadcrumb.append(('email', _('Email Subscription')))
387
        template.html_top(_('Receiving Announces by email'))
388

  
389
        form = Form(enctype='multipart/form-data')
390
        if get_request().user:
391
            if get_request().user.email:
392
                '<p>'
393
                _('You are logged in and your email is %s, ok to subscribe ?') % \
394
                    get_request().user.email
395
                '</p>'
396
                form.add_submit('submit', _('Subscribe'))
397
            else:
398
                '<p>'
399
                _("You are logged in but there is no email address in your profile.")
400
                '</p>'
401
                form.add(EmailWidget, 'email', title = _('Email'), required = True)
402
                form.add_submit('submit', _('Subscribe'))
403
                form.add_submit('submit-remember', _('Subscribe and add this email to my profile'))
404
        else:
405
            '<p>'
406
            _('FIXME will only be used for this purpose etc.')
407
            '</p>'
408
            form.add(EmailWidget, 'email', title = _('Email'), required = True)
409
            form.add_submit('submit', _('Subscribe'))
410
        
411
        form.add_submit('cancel', _('Cancel'))
412

  
413
        if form.get_submit() == 'cancel':
414
            return redirect('subscribe')
415

  
416
        if form.is_submitted() and not form.has_errors():
417
            s = self.email_submit(form)
418
            if s is not False:
419
                return s
420
        else:
421
            form.render()
422

  
423
    def email_submit(self, form):
424
        sub = self._get_announce_subscription()
425
        if not sub:
426
            sub = AnnounceSubscription()
427

  
428
        if get_request().user:
429
            sub.user_id = get_request().user.id
430

  
431
        if form.get_widget('email'):
432
            sub.email = form.get_widget('email').parse()
433
        elif get_request().user.email:
434
            sub.email = get_request().user.email
435

  
436
        if not get_request().user:
437
            sub.enabled = False
438

  
439
        sub.store()
440

  
441
        if get_request().user:
442
            def email_submit_ok [html] ():
443
                root_url = get_publisher().get_root_url()
444
                '<p>'
445
                _('You have been subscribed to the announces.')
446
                '</p>'
447
                if not get_response().iframe_mode:
448
                    '<a href="%s">%s</a>' % (root_url, _('Back Home'))
449

  
450
            return email_submit_ok()
451

  
452
        # asking email confirmation before subscribing someone
453
        token = Token(3 * 86400)
454
        token.type = 'announces-subscription-confirmation'
455
        token.subscription_id = sub.id
456
        token.store()
457
        data = {
458
            'confirm_url': get_request().get_url() + 'confirm?t=%s&a=cfm' % token.id,
459
            'cancel_url': get_request().get_url() + 'confirm?t=%s&a=cxl' % token.id,
460
            'time': misc.localstrftime(time.localtime(token.expiration)),
461
        }
462

  
463
        emails.custom_ezt_email('announces-subscription-confirmation',
464
                data, sub.email, exclude_current_user = False)
465

  
466
        def email_submit_ok [html] ():
467
            root_url = get_publisher().get_root_url()
468
            '<p>'
469
            _('You have been sent an email for confirmation')
470
            '</p>'
471
            if not get_response().iframe_mode:
472
                '<a href="%s">%s</a>' % (root_url, _('Back Home'))
473

  
474
        return email_submit_ok()
475

  
476
    def emailconfirm(self):
477
        tokenv = get_request().form.get('t')
478
        action = get_request().form.get('a')
479

  
480
        root_url = get_publisher().get_root_url()
481

  
482
        try:
483
            token = Token.get(tokenv)
484
        except KeyError:
485
            return template.error_page(
486
                    _('The token you submitted does not exist, has expired, or has been cancelled.'),
487
                    continue_to = (root_url, _('home page')))
488

  
489
        if token.type != 'announces-subscription-confirmation':
490
            return template.error_page(
491
                    _('The token you submitted is not appropriate for the requested task.'),
492
                    continue_to = (root_url, _('home page')))
493

  
494
        sub = AnnounceSubscription.get(token.subscription_id)
495

  
496
        if action == 'cxl':
497
            def cancel [html]():
498
                root_url = get_publisher().get_root_url()
499
                template.html_top(_('Email Subscription'))
500
                '<h1>%s</h1>' % _('Request Cancelled')
501
                '<p>%s</p>' % _('The request for subscription has been cancelled.')
502
                '<p>'
503
                htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
504
                '</p>'
505
            token.remove_self()
506
            sub.remove_self()
507
            return cancel()
508

  
509
        if action == 'cfm':
510
            token.remove_self()
511
            sub.enabled = True
512
            sub.store()
513
            def sub [html] ():
514
                root_url = get_publisher().get_root_url()
515
                template.html_top(_('Email Subscription'))
516
                '<h1>%s</h1>' % _('Subscription Confirmation')
517
                '<p>%s</p>' % _('Your subscription to announces is now effective.')
518
                '<p>'
519
                htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
520
                '</p>'
521
            return sub()
522

  
523

  
524
    def atom [plain] (self):
525
        response = get_response()
526
        response.set_content_type('application/atom+xml')
527

  
528
        from pyatom import pyatom
529
        xmldoc = pyatom.XMLDoc()
530
        feed = pyatom.Feed()
531
        xmldoc.root_element = feed
532
        feed.title = get_cfg('misc', {}).get('sitename') or 'Au Quotidien'
533
        feed.id = get_request().get_url()
534

  
535
        author_email = get_cfg('emails', {}).get('reply_to')
536
        if not author_email:
537
            author_email = get_cfg('emails', {}).get('from')
538
        if author_email:
539
            feed.authors.append(pyatom.Author(author_email))
540

  
541
        announces = Announce.get_published_announces()
542

  
543
        if announces and announces[0].modification_time:
544
            feed.updated = misc.format_time(announces[0].modification_time,
545
                        '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
546
                        gmtime = True)
547
        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
548

  
549
        for item in announces:
550
            entry = item.get_atom_entry()
551
            if entry:
552
                feed.entries.append(entry)
553

  
554
        str(feed)
555

  
556
    def email_unsubscribe [html] (self):
557
        sub = self._get_announce_subscription()
558

  
559
        form = Form(enctype='multipart/form-data')
560
        if not sub:
561
            form.add(EmailWidget, 'email', title = _('Email'), required = True)
562

  
563
        form.add_submit('submit', _('Unsubscribe'))
564
        form.add_submit('cancel', _('Cancel'))
565

  
566
        if form.get_submit() == 'cancel':
567
            return redirect('..')
568

  
569
        get_response().breadcrumb.append(('email', _('Email Unsubscription')))
570
        template.html_top()
571

  
572
        if form.is_submitted() and not form.has_errors():
573
            if sub:
574
                sub.remove("email")
575
            else:
576
                email = form.get_widget('email').parse()
577
                for s in AnnounceSubscription.select():
578
                    if s.email == email:
579
                        s.remove("email")
580

  
581
            def email_unsub_ok [html] ():
582
                root_url = get_publisher().get_root_url()
583
                '<p>'
584
                _('You have been unsubscribed from announces')
585
                '</p>'
586
                if not get_response().iframe_mode:
587
                    '<a href="%s">%s</a>' % (root_url, _('Back Home'))
588

  
589
            return email_unsub_ok()
590

  
591
        else:
592
            '<p>'
593
            _('Do you want to stop receiving announces by email?')
594
            '</p>'
595
            form.render()
596

  
597
    def _q_lookup(self, component):
598
        try:
599
            announce = Announce.get(component)
600
        except KeyError:
601
            raise errors.TraversalError()
602

  
603
        if announce.hidden:
604
            raise errors.TraversalError()
605

  
606
        get_response().breadcrumb.append((str(announce.id), announce.title))
607
        return AnnounceDirectory(announce)
608

  
609
OldRegisterDirectory = wcs.root.RegisterDirectory
610

  
611
class AlternateRegisterDirectory(OldRegisterDirectory):
612
    def _q_traverse(self, path):
613
        get_response().filter['bigdiv'] = 'new_member'
614
        return OldRegisterDirectory._q_traverse(self, path)
615

  
616
    def _q_index [html] (self):
617
        get_logger().info('register')
618
        ident_methods = get_cfg('identification', {}).get('methods', [])
619

  
620
        if len(ident_methods) == 0:
621
            idps = get_cfg('idp', {})
622
            if len(idps) == 0:
623
                return template.error_page(_('Authentication subsystem is not yet configured.'))
624
            ident_methods = ['idp'] # fallback to old behaviour; liberty.
625

  
626
        if len(ident_methods) == 1:
627
            method = ident_methods[0]
628
        else:
629
            method = 'password'
630

  
631
        return qommon.ident.register(method)
632

  
633
OldLoginDirectory = wcs.root.LoginDirectory
634

  
635
class AlternateLoginDirectory(OldLoginDirectory):
636
    def _q_traverse(self, path):
637
        get_response().filter['bigdiv'] = 'member'
638
        return OldLoginDirectory._q_traverse(self, path)
639

  
640
    def _q_index [html] (self):
641
        get_logger().info('login')
642
        ident_methods = get_cfg('identification', {}).get('methods', [])
643

  
644
        if len(ident_methods) > 1 and 'idp' in ident_methods:
645
            # if there is more than one identification method, and there is a
646
            # possibility of SSO, if we got there as a consequence of an access
647
            # unauthorized url on admin/ or backoffice/, then idp auth method
648
            # is chosen forcefully.
649
            after_url = get_session().after_url
650
            if after_url:
651
                root_url = get_publisher().get_root_url()
652
                after_path = urlparse.urlparse(after_url)[2]
653
                after_path = after_path[len(root_url):]
654
                if after_path.startswith(str('admin')) or \
655
                        after_path.startswith(str('backoffice')):
656
                    ident_methods = ['idp']
657

  
658
        # don't display authentication system choice
659
        if len(ident_methods) == 1:
660
            method = ident_methods[0]
661
            try:
662
                return qommon.ident.login(method)
663
            except KeyError:
664
                get_logger().error('failed to login with method %s' % method)
665
                return errors.TraversalError()
666

  
667
        if sorted(ident_methods) == ['idp', 'password']:
668
            get_response().breadcrumb.append(('login', _('Login')))
669
            identities_cfg = get_cfg('identities', {})
670
            form = Form(enctype = 'multipart/form-data', id = 'login-form', use_tokens = False)
671
            if identities_cfg.get('email-as-username', False):
672
                form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
673
            else:
674
                form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
675
            form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
676
            form.add_submit('submit', _('Connect'))
677
            if form.is_submitted() and not form.has_errors():
678
                tmp = qommon.ident.password.MethodDirectory().login_submit(form)
679
                if not form.has_errors():
680
                    return tmp
681

  
682
            '<div id="login-password">'
683
            get_session().display_message()
684
            form.render()
685

  
686
            base_url = get_publisher().get_root_url()
687
            '<p><a href="%sident/password/forgotten">%s</a></p>' % (
688
                    base_url, _('Forgotten password ?'))
689

  
690
            '</div>'
691

  
692
            # XXX: this part only supports a single IdP
693
            '<div id="login-sso">'
694
            TextsDirectory.get_html_text('aq-sso-text')
695
            form = Form(enctype='multipart/form-data',
696
                    action = '%sident/idp/login' % base_url)
697
            form.add_hidden('method', 'idp')
698
            for kidp, idp in get_cfg('idp', {}).items():
699
                p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
700
                        misc.get_abs_path(idp['metadata']),
701
                        misc.get_abs_path(idp.get('publickey')), None)
702
                form.add_hidden('idp', p.providerId)
703
                break
704
            form.add_submit('submit', _('Connect'))
705
            
706
            form.render()
707
            '</div>'
708

  
709
            get_request().environ['REQUEST_METHOD'] = 'GET'
710

  
711
            """<script type="text/javascript">
712
              document.getElementById('login-form')['username'].focus();
713
            </script>"""
714

  
715
        else:
716
            return OldLoginDirectory._q_index(self)
717

  
718

  
719
OldIdentDirectory = wcs.root.IdentDirectory
720
class AlternateIdentDirectory(OldIdentDirectory):
721
    def _q_traverse(self, path):
722
        get_response().filter['bigdiv'] = 'member'
723
        return OldIdentDirectory._q_traverse(self, path)
724

  
725

  
726
class AlternateRootDirectory(OldRootDirectory):
727
    _q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout',
728
            'liberty', 'token', 'saml', 'register', 'ident', 'afterjobs',
729
            ('informations-editeur', 'informations_editeur'), 'index2',
730
            ('announces', 'announces_dir'),
731
            'accessibility', 'contact', 'help',
732
            'myspace', 'services', 'agenda', 'categories', 'user',
733
            ('tmp-upload', 'tmp_upload'), 'json', '__version__',
734
            'themes', 'pages', 'payment', 'invoices', 'accesscode', 'roles',
735
            'msp']
736

  
737
    admin = admin.AdminRootDirectory()
738
    announces_dir = AnnouncesDirectory()
739
    register = AlternateRegisterDirectory()
740
    login = AlternateLoginDirectory()
741
    ident = AlternateIdentDirectory()
742
    myspace = MyspaceDirectory()
743
    agenda = AgendaDirectory()
744
    saml = Saml2Directory()
745
    payment = PublicPaymentDirectory()
746
    invoices = InvoicesDirectory()
747
    msp = msp_ui.MSPDirectory()
748

  
749
    def get_substitution_variables(self):
750
        d = {}
751
        def print_links(fd):
752
            fd.write(str(self.links()))
753
        d['links'] = print_links
754
        return d
755

  
756
    def _q_traverse(self, path):
757
        if get_publisher().has_site_option('drupal'):
758
            drupal.try_auth()
759
        if get_publisher().has_site_option('ezldap'):
760
            ezldap_ui.try_auth(self)
761

  
762
        session = get_session()
763
        if session:
764
            get_request().user = session.get_user()
765
        else:
766
            get_request().user = None
767

  
768
        get_publisher().substitutions.feed(get_request().user)
769

  
770
        response = get_response()
771
        if not hasattr(response, 'filter'):
772
            response.filter = {}
773

  
774
        response.filter['gauche'] = self.box_side(path)
775
        response.filter['keywords'] = template.get_current_theme().get('keywords')
776
        get_publisher().substitutions.feed(self)
777

  
778
        response.breadcrumb = [ ('', _('Home')) ]
779

  
780
        if not self.admin:
781
            self.admin = get_publisher().admin_directory_class()
782

  
783
        if not self.backoffice:
784
            self.backoffice = get_publisher().backoffice_directory_class()
785

  
786
        try:
787
            return Directory._q_traverse(self, path)
788
        except errors.TraversalError, e:
789
            try:
790
                f = FormDef.get_by_urlname(path[0])
791
            except KeyError:
792
                pass
793
            else:
794
                base_url = get_publisher().get_root_url()
795

  
796
                uri_rest = get_request().environ.get('REQUEST_URI')
797
                if not uri_rest:
798
                    uri_rest = get_request().get_path()
799
                if uri_rest.startswith(base_url):
800
                    uri_rest = uri_rest[len(base_url):]
801
                elif uri_rest.startswith('/'):
802
                    # dirty hack, ezldap reverseproxy uses a fake base_url
803
                    uri_rest = uri_rest[1:]
804
                if f.category_id:
805
                    return redirect('%s%s/%s' % (base_url, f.category.url_name, uri_rest))
806

  
807
            raise e
808

  
809

  
810
    def _q_lookup(self, component):
811
        if component == 'qo':
812
            dirname = os.path.join(get_publisher().data_dir, 'qommon')
813
            return StaticDirectory(dirname, follow_symlinks = True)
814

  
815
        if component == 'aq':
816
            dirname = os.path.join(get_publisher().data_dir, 'qommon', 'auquotidien')
817
            return StaticDirectory(dirname, follow_symlinks = True)
818

  
819
        if component in ('css','images'):
820
            return OldRootDirectory._q_lookup(self, component)
821

  
822
        # is this a category ?
823
        try:
824
            category = Category.get_by_urlname(component)
825
        except KeyError:
826
            pass
827
        else:
828
            return FormsRootDirectory(category)
829

  
830
        # is this a formdef ?
831
        try:
832
            formdef = FormDef.get_by_urlname(component)
833
        except KeyError:
834
            pass
835
        else:
836
            if formdef.category_id is None:
837
                get_response().filter['bigdiv'] = 'rub_service'
838
                return FormsRootDirectory()._q_lookup(component)
839
            # if there is category, let it fall back to raise TraversalError,
840
            # it will get caught in _q_traverse that will redirect it to an
841
            # URL embedding the category
842

  
843
        return None
844

  
845
    def json(self):
846
        return FormsRootDirectory().json()
847

  
848
    def categories(self):
849
        return FormsRootDirectory().categories()
850

  
851
    def _q_index [html] (self):
852
        if get_request().is_json():
853
            return FormsRootDirectory().json()
854

  
855
        root_url = get_publisher().get_root_url()
856
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
857
            return redirect('%smyspace/new' % root_url)
858

  
859
        if get_response().iframe_mode:
860
            # never display home page in an iframe
861
            return redirect('%sservices' % root_url)
862

  
863
        template.html_top()
864
        get_response().filter['is_index'] = True
865

  
866
        if not 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
867
            t = TextsDirectory.get_html_text('aq-home-page')
868
            if not t:
869
                if get_request().user:
870
                    t = TextsDirectory.get_html_text('welcome-logged')
871
                else:
872
                    t = TextsDirectory.get_html_text('welcome-unlogged')
873
            if t:
874
                '<div id="home-page-intro">'
875
                t
876
                '</div>'
877

  
878
        '<div id="centre">'
879
        self.box_services(position='1st')
880
        '</div>'
881
        '<div id="droite">'
882
        self.myspace_snippet()
883
        self.box_services(position='2nd')
884
        self.consultations()
885
        self.announces()
886
        '</div>'
887

  
888
        user = get_request().user
889
        if user and user.can_go_in_backoffice():
890
            get_response().filter['backoffice'] = True
891

  
892
    def services [html] (self):
893
        template.html_top()
894
        get_response().filter['bigdiv'] = 'rub_service'
895
        self.box_services(level = 2)
896

  
897
    def box_services [html] (self, level=3, position=None):
898
        ## Services
899
        if get_request().user and get_request().user.roles:
900
            accepted_roles = get_request().user.roles
901
        else:
902
            accepted_roles = []
903

  
904
        cats = Category.select(order_by = 'name')
905
        cats = [x for x in cats if x.url_name != 'consultations']
906
        Category.sort_by_position(cats)
907

  
908
        all_formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
909
                order_by = 'name')
910

  
911
        if position:
912
            t = self.display_list_of_formdefs(
913
                            [x for x in cats if x.get_homepage_position() == position],
914
                            all_formdefs, accepted_roles)
915
        else:
916
            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
917

  
918
        if not t:
919
            return
920

  
921
        if position == '2nd':
922
            '<div id="services-2nd">'
923
        else:
924
            '<div id="services">'
925
        if level == 2:
926
            '<h2>%s</h2>' % _('Services')
927
        else:
928
            '<h3>%s</h3>' % _('Services')
929

  
930
        if get_response().iframe_mode:
931
            if get_request().user:
932
                message = TextsDirectory.get_html_text('welcome-logged')
933
            else:
934
                message = TextsDirectory.get_html_text('welcome-unlogged')
935

  
936
            if message:
937
                '<div id="welcome-message">'
938
                message
939
                '</div>'
940
        elif 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
941
            homepage_text = TextsDirectory.get_html_text('aq-home-page')
942
            if homepage_text:
943
                '<div id="home-page-intro">'
944
                homepage_text
945
                '</div>'
946

  
947
        '<ul>'
948
        t
949
        '</ul>'
950

  
951
        '</div>'
952

  
953
    def display_list_of_formdefs [html] (self, cats, all_formdefs, accepted_roles):
954
        for category in cats:
955
            if category.url_name == 'consultations':
956
                self.consultations_category = category
957
                continue
958
            formdefs = [x for x in all_formdefs if x.category_id == category.id]
959
            formdefs_advertise = []
960

  
961
            for formdef in formdefs[:]:
962
                if formdef.is_disabled(): # is a redirection
963
                    continue
964
                if not formdef.roles:
965
                    continue
966
                if not get_request().user:
967
                    if formdef.always_advertise:
968
                        formdefs_advertise.append(formdef)
969
                    formdefs.remove(formdef)
970
                    continue
971
                if logged_users_role().id in formdef.roles:
972
                    continue
973
                for q in accepted_roles:
974
                    if q in formdef.roles:
975
                        break
976
                else:
977
                    if formdef.always_advertise:
978
                        formdefs_advertise.append(formdef)
979
                    formdefs.remove(formdef)
980

  
981
            if not formdefs and not formdefs_advertise:
982
                continue
983

  
984
            '<li>'
985
            '<strong>'
986
            '<a href="%s/">' % category.url_name
987
            category.name
988
            '</a></strong>\n'
989
            if category.description:
990
                if category.description[0] == '<':
991
                    htmltext(category.description)
992
                else:
993
                    '<p>'
994
                    category.description
995
                    '</p>'
996
            '<ul>'
997
            limit = category.get_limit()
998
            for formdef in formdefs[:limit]:
999
                '<li>'
1000
                '<a href="%s/%s/">%s</a>' % (category.url_name, formdef.url_name, formdef.name)
1001
                '</li>\n'
1002
            if len(formdefs) < limit:
1003
                for formdef in formdefs_advertise[:limit-len(formdefs)]:
1004
                    '<li>'
1005
                    '<a href="%s/%s/">%s</a>' % (category.url_name, formdef.url_name, formdef.name)
1006
                    ' (%s)' % _('authentication required')
1007
                    '</li>\n'
1008
            if (len(formdefs)+len(formdefs_advertise)) > limit:
1009
                '<li class="all-forms"><a href="%s/" title="%s">%s</a></li>' % (category.url_name,
1010
                        _('Access to all forms of the "%s" category') % category.name,
1011
                        _('Access to all forms in this category'))
1012
            '</ul>'
1013
            '</li>\n'
1014

  
1015
    def consultations [html] (self):
1016
        cats = [x for x in Category.select() if x.url_name == 'consultations']
1017
        if not cats:
1018
            return
1019
        consultations_category = cats[0]
1020
        formdefs = FormDef.select(lambda x: (
1021
                    x.category_id == consultations_category.id and
1022
                        (not x.is_disabled() or x.disabled_redirection)),
1023
                    order_by = 'name')
1024
        if not formdefs:
1025
            return
1026
        ## Consultations
1027
        '<div id="consultations">'
1028
        '<h3>%s</h3>' % _('Consultations')
1029
        if consultations_category.description:
1030
            if consultations_category.description[0] == '<':
1031
                htmltext(consultations_category.description)
1032
            else:
1033
                '<p>'
1034
                consultations_category.description
1035
                '</p>'
1036
        '<ul>'
1037
        for formdef in formdefs:
1038
            '<li>'
1039
            '<a href="%s/%s/">%s</a>' % (consultations_category.url_name,
1040
                formdef.url_name, formdef.name)
1041
            '</li>'
1042
        '</ul>'
1043
        '</div>'
1044

  
1045
    def box_side [html] (self, path):
1046
        '<div id="sidebox">'
1047
        root_url = get_publisher().get_root_url()
1048

  
1049
        if self.has_anonymous_access_codes():
1050
            '<form id="follow-form" action="%saccesscode">' % root_url
1051
            '<h3>%s</h3>' % _('Tracking')
1052
            '<label>%s</label> ' % _('Code:')
1053
            '<input name="code" size="10"/>'
1054
            '</form>'
1055

  
1056
        self.links()
1057

  
1058
        cats = Category.select(order_by = 'name')
1059
        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
1060
        Category.sort_by_position(cats)
1061
        if cats:
1062
            '<div id="side-services">'
1063
            '<h3>%s</h3>' % _('Services')
1064
            '<ul>'
1065
            for cat in cats:
1066
                '<li><a href="%s/">%s</a></li>' % (cat.url_name, cat.name)
1067
            '</ul>'
1068
            '</div>'
1069

  
1070
        if Event.keys(): # if there are events, add a link to the agenda
1071
            tags = get_cfg('misc', {}).get('event_tags')
1072
            if not tags:
1073
                tags = get_default_event_tags()
1074
            '<h3 id="agenda-link"><a href="%sagenda/">%s</a></h3>' % (root_url, _('Agenda'))
1075

  
1076
            if path and path[0] == 'agenda':
1077
                '<p class="tags">'
1078
                for tag in tags:
1079
                    '<a href="%sagenda/tag/%s">%s</a> ' % (root_url, tag, tag)
1080
                '</p>'
1081
                self.agenda.display_remote_calendars()
1082

  
1083
                '<p>'
1084
                '  <a href="%sagenda/filter">%s</a>' % (root_url, _('Advanced Filter'))
1085
                '</p>'
1086

  
1087
        '</div>'
1088

  
1089
    def has_anonymous_access_codes(self):
1090
        for workflow in Workflow.select():
1091
            for wfstatus in workflow.possible_status:
1092
                for wfitem in wfstatus.items:
1093
                    if wfitem.key == 'create-anonymous-access-code':
1094
                        return True
1095
        return False
1096

  
1097
    def accesscode(self):
1098
        code = get_request().form.get('code')
1099
        if not code:
1100
            return redirect(get_publisher().get_root_url())
1101
        try:
1102
            token = Token.get(code)
1103
        except KeyError:
1104
            return redirect(get_publisher().get_root_url())
1105
        if token.type != 'anonymous-access-code':
1106
            return redirect(get_publisher().get_root_url())
1107
        formdef_urlname, formdata_id = token.formdata_reference
1108
        try:
1109
            formdata = FormDef.get_by_urlname(formdef_urlname).data_class().get(formdata_id)
1110
        except KeyError:
1111
            return redirect(get_publisher().get_root_url())
1112
        session = get_session()
1113
        if not hasattr(session, '_wf_anonymous_access_authorized'):
1114
            session._wf_anonymous_access_authorized = []
1115
        session._wf_anonymous_access_authorized.append(formdata.get_url())
1116
        return redirect(formdata.get_url() + 'access/')
1117

  
1118
    def links [html] (self):
1119
        links = Link.select()
1120
        if not links:
1121
            return
1122

  
1123
        Link.sort_by_position(links)
1124

  
1125
        '<div id="links">'
1126
        if links[0].url:
1127
            # first link has an URL, so it's not a title, so we display a
1128
            # generic title
1129
            '<h3>%s</h3>' % _('Useful links')
1130
        has_ul = False
1131
        vars = get_publisher().substitutions.get_context_variables()
1132
        for link in links:
1133
            if not link.url:
1134
                # acting title
1135
                if has_ul:
1136
                    '</ul>'
1137
                '<h3>%s</h3>' % link.title
1138
                '<ul>'
1139
                has_ul = True
1140
            else:
1141
                if not has_ul:
1142
                    '<ul>'
1143
                    has_ul = True
1144
                '<li><a href="%s">%s</a></li>' % (get_variadic_url(link.url, vars), link.title)
1145
        if has_ul:
1146
            '</ul>'
1147
        '</div>'
1148

  
1149

  
1150
    def announces [html] (self):
1151
        announces = Announce.get_published_announces()
1152
        if not announces:
1153
            return
1154

  
1155
        '<div id="announces">'
1156
        '<h3>%s</h3>' % _('Announces to citizens')
1157
        for item in announces[:3]:
1158
            '<div class="announce-item">'
1159
            '<h4>'
1160
            if item.publication_time:
1161
                time.strftime(misc.date_format(), item.publication_time)
1162
                ' - '
1163
            item.title
1164
            '</h4>'
1165
            '<p>'
1166
            item.text
1167
            '</p>'
1168
            '</div>'
1169

  
1170
        '<ul id="announces-links">'
1171
        '<li><a href="announces/subscribe">%s</a></li>' % _('Receiving those Announces')
1172
        '<li><a href="announces/">%s</a></li>' % _('Previous Announces')
1173
        '</ul>'
1174
        '</div>'
1175

  
1176
    def myspace_snippet [html] (self):
1177
        '<div id="myspace">'
1178
        '<h3>%s</h3>' % _('My Space')
1179
        '<ul>'
1180
        if get_request().user and not get_request().user.anonymous:
1181
            '  <li><a href="myspace/" id="member">%s</a></li>' % _('Access to your personal space')
1182
            '  <li><a href="logout" id="logout">%s</a></li>' % _('Logout')
1183
        else:
1184
            '  <li><a href="register/" id="inscr">%s</a></li>' % _('Registration')
1185
            '  <li><a href="login/" id="login">%s</a></li>' % _('Login')
1186
        '</ul>'
1187
        '</div>'
1188

  
1189

  
1190
    def page_view [html] (self, key, title, urlname = None):
1191
        if not urlname:
1192
            urlname = key[3:].replace(str('_'), str('-'))
1193
        get_response().breadcrumb.append((urlname, title))
1194
        template.html_top(title)
1195
        '<div class="article">'
1196
        htmltext(TextsDirectory.get_html_text(key))
1197
        '</div>'
1198

  
1199
    def informations_editeur [html] (self):
1200
        get_response().filter['bigdiv'] = 'info'
1201
        return self.page_view('aq-editor-info', _('Editor Informations'),
1202
                urlname = 'informations_editeur')
1203

  
1204
    def accessibility(self):
1205
        get_response().filter['bigdiv'] = 'accessibility'
1206
        return self.page_view('aq-accessibility', _('Accessibility Statement'))
1207

  
1208
    def contact(self):
1209
        get_response().filter['bigdiv'] = 'contact'
1210
        return self.page_view('aq-contact', _('Contact'))
1211

  
1212
    def help(self):
1213
        get_response().filter['bigdiv'] = 'help'
1214
        return self.page_view('aq-help', _('Help'))
1215

  
1216

  
1217
from qommon.publisher import get_publisher_class
1218
get_publisher_class().root_directory_class = AlternateRootDirectory
1219
get_publisher_class().after_login_url = 'myspace/'
1220
get_publisher_class().use_sms_feature = True
1221

  
1222
# help links
1223
get_publisher_class().backoffice_help_url = {
1224
    'fr': 'https://doc.entrouvert.org/au-quotidien/stable/guide-gestionnaire.html',
1225
}
1226
get_publisher_class().admin_help_url = {
1227
    'fr': 'https://doc.entrouvert.org/auquotidien/dev/',
1228
}
1229

  
1230

  
1231
EmailsDirectory.register('announces-subscription-confirmation',
1232
        N_('Confirmation of Announces Subscription'),
1233
        N_('Available variables: change_url, cancel_url, time, sitename'),
1234
        default_subject = N_('Announce Subscription Request'),
1235
        default_body = N_("""\
1236
You have (or someone impersonating you has) requested to subscribe to
1237
announces from [sitename].  To confirm this request, visit the
1238
following link:
1239

  
1240
[confirm_url]
1241

  
1242
If you are not the person who made this request, or you wish to cancel
1243
this request, visit the following link:
1244

  
1245
[cancel_url]
1246

  
1247
If you do nothing, the request will lapse after 3 days (precisely on
1248
[time]).
1249
"""))
1250

  
1251

  
1252
TextsDirectory.register('aq-announces-subscription',
1253
        N_('Text on announces subscription page'),
1254
        default = N_('''\
1255
<p>
1256
FIXME
1257
'</p>'''))
1258

  
1259
TextsDirectory.register('aq-sms-demo',
1260
        N_('Text when subscribing to announces SMS and configured as demo'),
1261
        default = N_('''
1262
<p>
1263
Receiving announces by SMS is not possible in this demo
1264
</p>'''))
1265

  
1266
TextsDirectory.register('aq-editor-info', N_('Editor Informations'))
1267
TextsDirectory.register('aq-accessibility', N_('Accessibility Statement'))
1268
TextsDirectory.register('aq-contact', N_('Contact Information'))
1269
TextsDirectory.register('aq-help', N_('Help'))
1270
TextsDirectory.register('aq-sso-text',  N_('Connecting with Identity Provider'),
1271
        default = N_('''<h3>Connecting with Identity Provider</h3>
1272
<p>You can also use your identity provider to connect.
1273
</p>'''))
1274

  
1275
TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg = True)
extra/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
from quixote.util import StaticDirectory
5

  
6
from wcs.qommon.misc import get_variadic_url
7

  
8
import os
9
import re
10
import string
11
import urlparse
12

  
13
try:
14
    import lasso
15
except ImportError:
16
    pass
17

  
18
import wcs
19
import wcs.root
20
import qommon
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 qommon.tokens import Token
33
from qommon.admin.emails import EmailsDirectory
34
from qommon.admin.texts import TextsDirectory
35

  
36
from links import Link
37
from announces import Announce, AnnounceSubscription
38
from myspace import MyspaceDirectory
39
from agenda import AgendaDirectory
40
from events import Event, get_default_event_tags
41
from payments import PublicPaymentDirectory
42
from payments_ui import InvoicesDirectory
43

  
44
import admin
45

  
46
import wcs.forms.root
47
from wcs.workflows import Workflow
48

  
49
from saml2 import Saml2Directory
50

  
51
OldRootDirectory = wcs.root.RootDirectory
52

  
53
import qommon.ident.password
54
import qommon.ident.idp
55

  
56
import drupal
57
import ezldap_ui
58
import msp_ui
59

  
60
def category_get_homepage_position(self):
61
    if hasattr(self, 'homepage_position') and self.homepage_position:
62
        return self.homepage_position
63
    if self.url_name == 'consultations':
64
        return '2nd'
65
    return '1st'
66
Category.get_homepage_position = category_get_homepage_position
67

  
68
def category_get_limit(self):
69
    if hasattr(self, 'limit') and self.limit is not None:
70
        return self.limit
71
    return 3
72
Category.get_limit = category_get_limit
73

  
74

  
75
class FormsRootDirectory(wcs.forms.root.RootDirectory):
76

  
77
    def _q_index(self, *args):
78
        get_response().filter['is_index'] = True
79
        return wcs.forms.root.RootDirectory._q_index(self, *args)
80

  
81
    def user_forms(self, user_forms):
82
        r = TemplateIO(html=True)
83
        base_url = get_publisher().get_root_url()
84

  
85
        draft = [x for x in user_forms if x.is_draft()]
86
        if draft:
87
            r += htmltext('<h4 id="drafts">%s</h4>') % _('My Current Drafts')
88
            r += htmltext('<ul>')
89
            for f in draft:
90
                r += htmltext('<li><a href="%s%s/%s/%s">%s</a>, %s</li>') % (base_url,
91
                    f.formdef.category.url_name,
92
                    f.formdef.url_name, f.id, f.formdef.name,
93
                    misc.localstrftime(f.receipt_time))
94
            r += htmltext('</ul>')
95

  
96
        forms_by_status_name = {}
97
        for f in user_forms:
98
            if f.is_draft():
99
                continue
100
            status = f.get_visible_status()
101
            if status:
102
                status_name = status.name
103
            else:
104
                status_name = None
105
            if status_name in forms_by_status_name:
106
                forms_by_status_name[status_name].append(f)
107
            else:
108
                forms_by_status_name[status_name] = [f]
109
        for status_name in forms_by_status_name:
110
            if status_name:
111
                r += htmltext('<h4>%s</h4>') % _('My forms with status "%s"') % status_name
112
            else:
113
                r += htmltext('<h4>%s</h4>') % _('My forms with an unknown status') % status_name
114
            r += htmltext('<ul>')
115
            forms_by_status_name[status_name].sort(lambda x,y: cmp(x.receipt_time, y.receipt_time))
116
            for f in forms_by_status_name[status_name]:
117
                if f.formdef.category_id:
118
                    category_url = f.formdef.category.url_name
119
                else:
120
                    category_url = '.'
121
                r += htmltext('<li><a href="%s%s/%s/%s/">%s</a>, %s</li>') % (
122
                        base_url,
123
                        category_url,
124
                        f.formdef.url_name, f.id, f.formdef.name, 
125
                        misc.localstrftime(f.receipt_time))
126
            r += htmltext('</ul>')
127
        return r.getvalue()
128

  
129

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

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

  
136
    def _q_index(self):
137
        template.html_top(_('Announces to citizens'))
138
        r = TemplateIO(html=True)
139

  
140
        if self.announce.publication_time:
141
            date_heading = '%s - ' % time.strftime(misc.date_format(), self.announce.publication_time)
142
        else:
143
            date_heading = ''
144

  
145
        r += htmltext('<h3>%s%s</h3>') % (date_heading, self.announce.title)
146

  
147
        r += htmltext('<p>')
148
        r += self.announce.text
149
        r += htmltext('</p>')
150

  
151
        r += htmltext('<p>')
152
        r += htmltext('<a href="../">%s</a>') % _('Back')
153
        r += htmltext('</p>')
154
        return r.getvalue()
155

  
156

  
157
class AnnouncesDirectory(Directory):
158
    _q_exports = ['', 'subscribe', 'email', 'atom', 'sms', 'emailconfirm',
159
            'email_unsubscribe', 'sms_unsubscribe', 'smsconfirm', 'rawlist']
160

  
161
    def _q_traverse(self, path):
162
        get_response().breadcrumb.append(('announces/', _('Announces')))
163
        return Directory._q_traverse(self, path)
164

  
165
    def _q_index(self):
166
        template.html_top(_('Announces to citizens'))
167
        r = TemplateIO(html=True)
168
        r += self.announces_list()
169
        r += htmltext('<ul id="announces-links">')
170
        r += htmltext('<li><a href="subscribe">%s</a></li>') % _('Receiving those Announces')
171
        r += htmltext('</ul>')
172
        return r.getvalue()
173

  
174
    def _get_announce_subscription(self):
175
        """ """
176
        sub = None
177
        if get_request().user:
178
            subs = AnnounceSubscription.select(lambda x: x.user_id == get_request().user.id)
179
            if subs:
180
                sub = subs[0]
181
        return sub
182

  
183
    def rawlist(self):
184
        get_response().filter = None
185
        return self.announces_list()
186

  
187
    def announces_list(self):
188
        announces = Announce.get_published_announces()
189
        if not announces:
190
            raise errors.TraversalError()
191

  
192
        # XXX: will need pagination someday
193
        r = TemplateIO(html=True)
194
        for item in announces:
195
            r += htmltext('<div class="announce-item">\n')
196
            r += htmltext('<h4>')
197
            if item.publication_time:
198
                r += time.strftime(misc.date_format(), item.publication_time)
199
                r += ' - '
200
            r += item.title
201
            r += htmltext('</h4>\n')
202
            r += htmltext('<p>\n')
203
            r += item.text
204
            r += htmltext('\n</p>\n')
205
            r += htmltext('</div>\n')
206
        return r.getvalue()
207

  
208

  
209
    def sms(self):
210
        sms_mode = get_cfg('sms', {}).get('mode', 'none')
211

  
212
        if sms_mode == 'none':
213
            raise errors.TraversalError()
214

  
215
        get_response().breadcrumb.append(('sms', _('SMS')))
216
        template.html_top(_('Receiving announces by SMS'))
217
        r = TemplateIO(html=True)
218

  
219
        if sms_mode == 'demo':
220
            r += TextsDirectory.get_html_text('aq-sms-demo')
221
        else:
222
            announces_cfg = get_cfg('announces',{})
223
            mobile_mask = announces_cfg.get('mobile_mask')
224
            if mobile_mask:
225
                mobile_mask = ' (' + mobile_mask + ')'
226
            else:
227
                mobile_mask = ''
228
            form = Form(enctype='multipart/form-data')
229
            form.add(StringWidget, 'mobile', title = _('Mobile number %s') % mobile_mask, size=12, required=True)
230
            form.add_submit('submit', _('Subscribe'))
231
            form.add_submit('cancel', _('Cancel'))
232

  
233
            if form.get_submit() == 'cancel':
234
                return redirect('subscribe')
235

  
236
            if form.is_submitted() and not form.has_errors():
237
                s = self.sms_submit(form)
238
                if s == False:
239
                    r += form.render()
240
                else:
241
                    return redirect("smsconfirm")
242
            else:
243
                r += form.render()
244
        return r.getvalue()
245

  
246
    def sms_submit(self, form):
247
        mobile = form.get_widget("mobile").parse()
248
        # clean the string, remove any extra character
249
        mobile = re.sub('[^0-9+]','',mobile)
250
        # if a mask was set, validate
251
        announces_cfg = get_cfg('announces',{})
252
        mobile_mask = announces_cfg.get('mobile_mask')
253
        if mobile_mask:
254
            mobile_regexp = re.sub('X','[0-9]', mobile_mask) + '$'
255
            if not re.match(mobile_regexp, mobile):
256
                form.set_error("mobile", _("Phone number invalid ! It must match ") + mobile_mask)
257
                return False
258
        if mobile.startswith('00'):
259
            mobile = '+' + mobile[2:]
260
        else:
261
            # Default to france international prefix
262
            if not mobile.startswith('+'):
263
                mobile = re.sub("^0", "+33", mobile)
264
        sub = self._get_announce_subscription()
265
        if not sub:
266
            sub = AnnounceSubscription()
267
        if get_request().user:
268
            sub.user_id = get_request().user.id
269

  
270
        if mobile:
271
            sub.sms = mobile
272

  
273
        if not get_request().user:
274
            sub.enabled = False
275

  
276
        sub.store()
277

  
278
        # Asking sms confirmation
279
        token = Token(3 * 86400, 4, string.digits)
280
        token.type = 'announces-subscription-confirmation'
281
        token.subscription_id = sub.id
282
        token.store()
283

  
284
        message = _("Confirmation code : %s") % str(token.id)
285
        sms_cfg = get_cfg('sms', {})
286
        sender = sms_cfg.get('sender', 'AuQuotidien')[:11]
287
        mode = sms_cfg.get('mode', 'none')
288
        sms = SMS.get_sms_class(mode)
289
        try:
290
            sms.send(sender, [mobile], message)
291
        except errors.SMSError, e:
292
            get_logger().error(e)
293
            form.set_error("mobile", _("Send SMS confirmation failed"))
294
            sub.remove("sms")
295
            return False
296

  
297
    def smsconfirm(self):
298
        template.html_top(_('Receiving announces by SMS confirmation'))
299
        r = TemplateIO(html=True)
300
        r += htmltext("<p>%s</p>") % _("You will receive a confirmation code by SMS.")
301
        form = Form(enctype='multipart/form-data')
302
        form.add(StringWidget, 'code', title = _('Confirmation code (4 characters)'), size=12, required=True)
303
        form.add_submit('submit', _('Subscribe'))
304
        form.add_submit('cancel', _('Cancel'))
305

  
306
        if form.get_submit() == 'cancel':
307
            return redirect('..')
308

  
309
        if form.is_submitted() and not form.has_errors():
310
            token = None
311
            id = form.get_widget("code").parse()
312
            try:
313
                token = Token.get(id)
314
            except KeyError:
315
                form.set_error("code",  _('Invalid confirmation code.'))
316
            else:
317
                if token.type != 'announces-subscription-confirmation':
318
                    form.set_error("code",  _('Invalid confirmation code.'))
319
                else:
320
                    sub = AnnounceSubscription.get(token.subscription_id)
321
                    token.remove_self()
322
                    sub.enabled_sms = True
323
                    sub.store()
324
                    return redirect('.')
325
            r += form.render()
326
        else:
327
            r += form.render()
328

  
329
        return r.getvalue()
330

  
331
    def sms_unsubscribe(self):
332
        sub = self._get_announce_subscription()
333

  
334
        form = Form(enctype='multipart/form-data')
335
        if not sub:
336
            return redirect('..')
337

  
338
        form.add_submit('submit', _('Unsubscribe'))
339
        form.add_submit('cancel', _('Cancel'))
340

  
341
        if form.get_submit() == 'cancel':
342
            return redirect('..')
343

  
344
        get_response().breadcrumb.append(('sms', _('SMS Unsubscription')))
345
        template.html_top()
346
        r = TemplateIO(html=True)
347

  
348
        if form.is_submitted() and not form.has_errors():
349
            if sub:
350
                sub.remove("sms")
351

  
352
            root_url = get_publisher().get_root_url()
353
            r += htmltext('<p>')
354
            r += _('You have been unsubscribed from announces')
355
            r += htmltext('</p>')
356
            if not get_response().iframe_mode:
357
                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
358
        else:
359
            r += htmltext('<p>')
360
            r += _('Do you want to stop receiving announces by sms ?')
361
            r += htmltext('</p>')
362
            r += form.render()
363

  
364
        return r.getvalue()
365

  
366

  
367
    def subscribe(self):
368
        get_response().breadcrumb.append(('subscribe', _('Subscription')))
369
        template.html_top(_('Receiving Announces'))
370
        r = TemplateIO(html=True)
371

  
372
        r += TextsDirectory.get_html_text('aq-announces-subscription')
373

  
374
        sub = self._get_announce_subscription()
375

  
376
        r += htmltext('<ul id="announce-modes">')
377
        if sub and sub.email:
378
            r += htmltext(' <li>')
379
            r += htmltext('<span id="par_mail">%s</span>') % _('Email (currently subscribed)')
380
            r += htmltext(' <a href="email_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
381
        else:
382
            r += htmltext(' <li><a href="email" id="par_mail" rel="popup">%s</a></li>') % _('Email')
383
        if sub and sub.sms:
384
            r += htmltext(' <li>')
385
            if sub.enabled_sms:
386
                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently subscribed)') % sub.sms
387
            else:
388
                r += htmltext('<span id="par_sms">%s</span>') % _('SMS %s (currently not confirmed)') % sub.sms
389
                r += htmltext(' <a href="smsconfirm" rel="popup">%s</a> ') % _('Confirmation')
390
            r += htmltext(' <a href="sms_unsubscribe" rel="popup">%s</a></li>') % _('Unsubscribe')
391
        elif get_cfg('sms', {}).get('mode', 'none') != 'none':
392
            r += htmltext(' <li><a href="sms" id="par_sms">%s</a></li>') % _('SMS')
393
        r += htmltext(' <li><a class="feed-link" href="atom" id="par_rss">%s</a>') % _('Feed')
394
        r += htmltext('</ul>')
395
        return r.getvalue()
396

  
397
    def email(self):
398
        get_response().breadcrumb.append(('email', _('Email Subscription')))
399
        template.html_top(_('Receiving Announces by email'))
400
        r = TemplateIO(html=True)
401

  
402
        form = Form(enctype='multipart/form-data')
403
        if get_request().user:
404
            if get_request().user.email:
405
                r += htmltext('<p>')
406
                r += _('You are logged in and your email is %s, ok to subscribe ?') % \
407
                        get_request().user.email
408
                r += htmltext('</p>')
409
                form.add_submit('submit', _('Subscribe'))
410
            else:
411
                r += htmltext('<p>')
412
                r += _("You are logged in but there is no email address in your profile.")
413
                r += htmltext('</p>')
414
                form.add(EmailWidget, 'email', title = _('Email'), required = True)
415
                form.add_submit('submit', _('Subscribe'))
416
                form.add_submit('submit-remember', _('Subscribe and add this email to my profile'))
417
        else:
418
            r += htmltext('<p>')
419
            r += _('FIXME will only be used for this purpose etc.')
420
            r += htmltext('</p>')
421
            form.add(EmailWidget, 'email', title = _('Email'), required = True)
422
            form.add_submit('submit', _('Subscribe'))
423

  
424
        form.add_submit('cancel', _('Cancel'))
425

  
426
        if form.get_submit() == 'cancel':
427
            return redirect('subscribe')
428

  
429
        if form.is_submitted() and not form.has_errors():
430
            s = self.email_submit(form)
431
            if s is not False:
432
                return s
433
        else:
434
            r += form.render()
435

  
436
        return r.getvalue()
437

  
438
    def email_submit(self, form):
439
        sub = self._get_announce_subscription()
440
        if not sub:
441
            sub = AnnounceSubscription()
442

  
443
        if get_request().user:
444
            sub.user_id = get_request().user.id
445

  
446
        if form.get_widget('email'):
447
            sub.email = form.get_widget('email').parse()
448
        elif get_request().user.email:
449
            sub.email = get_request().user.email
450

  
451
        if not get_request().user:
452
            sub.enabled = False
453

  
454
        sub.store()
455

  
456
        if get_request().user:
457
            r = TemplateIO(html=True)
458
            root_url = get_publisher().get_root_url()
459
            r += htmltext('<p>')
460
            r += _('You have been subscribed to the announces.')
461
            r += htmltext('</p>')
462
            if not get_response().iframe_mode:
463
                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
464
            return r.getvalue()
465

  
466
        # asking email confirmation before subscribing someone
467
        token = Token(3 * 86400)
468
        token.type = 'announces-subscription-confirmation'
469
        token.subscription_id = sub.id
470
        token.store()
471
        data = {
472
            'confirm_url': get_request().get_url() + 'confirm?t=%s&a=cfm' % token.id,
473
            'cancel_url': get_request().get_url() + 'confirm?t=%s&a=cxl' % token.id,
474
            'time': misc.localstrftime(time.localtime(token.expiration)),
475
        }
476

  
477
        emails.custom_ezt_email('announces-subscription-confirmation',
478
                data, sub.email, exclude_current_user = False)
479

  
480
        r = TemplateIO(html=True)
481
        root_url = get_publisher().get_root_url()
482
        r += htmltext('<p>')
483
        r += _('You have been sent an email for confirmation')
484
        r += htmltext('</p>')
485
        if not get_response().iframe_mode:
486
            r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
487
        return r.getvalue()
488

  
489
    def emailconfirm(self):
490
        tokenv = get_request().form.get('t')
491
        action = get_request().form.get('a')
492

  
493
        root_url = get_publisher().get_root_url()
494

  
495
        try:
496
            token = Token.get(tokenv)
497
        except KeyError:
498
            return template.error_page(
499
                    _('The token you submitted does not exist, has expired, or has been cancelled.'),
500
                    continue_to = (root_url, _('home page')))
501

  
502
        if token.type != 'announces-subscription-confirmation':
503
            return template.error_page(
504
                    _('The token you submitted is not appropriate for the requested task.'),
505
                    continue_to = (root_url, _('home page')))
506

  
507
        sub = AnnounceSubscription.get(token.subscription_id)
508

  
509
        if action == 'cxl':
510
            r = TemplateIO(html=True)
511
            root_url = get_publisher().get_root_url()
512
            template.html_top(_('Email Subscription'))
513
            r += htmltext('<h1>%s</h1>') % _('Request Cancelled')
514
            r += htmltext('<p>%s</p>') % _('The request for subscription has been cancelled.')
515
            r += htmltext('<p>')
516
            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
517
            r += htmltext('</p>')
518
            token.remove_self()
519
            sub.remove_self()
520
            return r.getvalue()
521

  
522
        if action == 'cfm':
523
            token.remove_self()
524
            sub.enabled = True
525
            sub.store()
526
            r = TemplateIO(html=True)
527
            root_url = get_publisher().get_root_url()
528
            template.html_top(_('Email Subscription'))
529
            r += htmltext('<h1>%s</h1>') % _('Subscription Confirmation')
530
            r += htmltext('<p>%s</p>') % _('Your subscription to announces is now effective.')
531
            r += htmltext('<p>')
532
            r += htmltext(_('Continue to <a href="%s">home page</a>') % root_url)
533
            r += htmltext('</p>')
534
            return r.getvalue()
535

  
536
    def atom(self):
537
        response = get_response()
538
        response.set_content_type('application/atom+xml')
539

  
540
        from pyatom import pyatom
541
        xmldoc = pyatom.XMLDoc()
542
        feed = pyatom.Feed()
543
        xmldoc.root_element = feed
544
        feed.title = get_cfg('misc', {}).get('sitename') or 'Au Quotidien'
545
        feed.id = get_request().get_url()
546

  
547
        author_email = get_cfg('emails', {}).get('reply_to')
548
        if not author_email:
549
            author_email = get_cfg('emails', {}).get('from')
550
        if author_email:
551
            feed.authors.append(pyatom.Author(author_email))
552

  
553
        announces = Announce.get_published_announces()
554

  
555
        if announces and announces[0].modification_time:
556
            feed.updated = misc.format_time(announces[0].modification_time,
557
                        '%(year)s-%(month)02d-%(day)02dT%(hour)02d:%(minute)02d:%(second)02dZ',
558
                        gmtime = True)
559
        feed.links.append(pyatom.Link(get_request().get_url(1) + '/'))
560

  
561
        for item in announces:
562
            entry = item.get_atom_entry()
563
            if entry:
564
                feed.entries.append(entry)
565

  
566
        return str(feed)
567

  
568
    def email_unsubscribe(self):
569
        sub = self._get_announce_subscription()
570

  
571
        form = Form(enctype='multipart/form-data')
572
        if not sub:
573
            form.add(EmailWidget, 'email', title = _('Email'), required = True)
574

  
575
        form.add_submit('submit', _('Unsubscribe'))
576
        form.add_submit('cancel', _('Cancel'))
577

  
578
        if form.get_submit() == 'cancel':
579
            return redirect('..')
580

  
581
        get_response().breadcrumb.append(('email', _('Email Unsubscription')))
582
        template.html_top()
583
        r = TemplateIO(html=True)
584

  
585
        if form.is_submitted() and not form.has_errors():
586
            if sub:
587
                sub.remove("email")
588
            else:
589
                email = form.get_widget('email').parse()
590
                for s in AnnounceSubscription.select():
591
                    if s.email == email:
592
                        s.remove("email")
593

  
594
            root_url = get_publisher().get_root_url()
595
            r += htmltext('<p>')
596
            r += _('You have been unsubscribed from announces')
597
            r += htmltext('</p>')
598
            if not get_response().iframe_mode:
599
                r += htmltext('<a href="%s">%s</a>') % (root_url, _('Back Home'))
600

  
601
        else:
602
            r += htmltext('<p>')
603
            r += _('Do you want to stop receiving announces by email?')
604
            r += htmltext('</p>')
605
            r += form.render()
606

  
607
        return r.getvalue()
608

  
609
    def _q_lookup(self, component):
610
        try:
611
            announce = Announce.get(component)
612
        except KeyError:
613
            raise errors.TraversalError()
614

  
615
        if announce.hidden:
616
            raise errors.TraversalError()
617

  
618
        get_response().breadcrumb.append((str(announce.id), announce.title))
619
        return AnnounceDirectory(announce)
620

  
621
OldRegisterDirectory = wcs.root.RegisterDirectory
622

  
623
class AlternateRegisterDirectory(OldRegisterDirectory):
624
    def _q_traverse(self, path):
625
        get_response().filter['bigdiv'] = 'new_member'
626
        return OldRegisterDirectory._q_traverse(self, path)
627

  
628
    def _q_index(self):
629
        get_logger().info('register')
630
        ident_methods = get_cfg('identification', {}).get('methods', [])
631

  
632
        if len(ident_methods) == 0:
633
            idps = get_cfg('idp', {})
634
            if len(idps) == 0:
635
                return template.error_page(_('Authentication subsystem is not yet configured.'))
636
            ident_methods = ['idp'] # fallback to old behaviour; liberty.
637

  
638
        if len(ident_methods) == 1:
639
            method = ident_methods[0]
640
        else:
641
            method = 'password'
642

  
643
        return qommon.ident.register(method)
644

  
645
OldLoginDirectory = wcs.root.LoginDirectory
646

  
647
class AlternateLoginDirectory(OldLoginDirectory):
648
    def _q_traverse(self, path):
649
        get_response().filter['bigdiv'] = 'member'
650
        return OldLoginDirectory._q_traverse(self, path)
651

  
652
    def _q_index(self):
653
        get_logger().info('login')
654
        ident_methods = get_cfg('identification', {}).get('methods', [])
655

  
656
        if len(ident_methods) > 1 and 'idp' in ident_methods:
657
            # if there is more than one identification method, and there is a
658
            # possibility of SSO, if we got there as a consequence of an access
659
            # unauthorized url on admin/ or backoffice/, then idp auth method
660
            # is chosen forcefully.
661
            after_url = get_session().after_url
662
            if after_url:
663
                root_url = get_publisher().get_root_url()
664
                after_path = urlparse.urlparse(after_url)[2]
665
                after_path = after_path[len(root_url):]
666
                if after_path.startswith(str('admin')) or \
667
                        after_path.startswith(str('backoffice')):
668
                    ident_methods = ['idp']
669

  
670
        # don't display authentication system choice
671
        if len(ident_methods) == 1:
672
            method = ident_methods[0]
673
            try:
674
                return qommon.ident.login(method)
675
            except KeyError:
676
                get_logger().error('failed to login with method %s' % method)
677
                return errors.TraversalError()
678

  
679
        if sorted(ident_methods) == ['idp', 'password']:
680
            r = TemplateIO(html=True)
681
            get_response().breadcrumb.append(('login', _('Login')))
682
            identities_cfg = get_cfg('identities', {})
683
            form = Form(enctype = 'multipart/form-data', id = 'login-form', use_tokens = False)
684
            if identities_cfg.get('email-as-username', False):
685
                form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
686
            else:
687
                form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
688
            form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
689
            form.add_submit('submit', _('Connect'))
690
            if form.is_submitted() and not form.has_errors():
691
                tmp = qommon.ident.password.MethodDirectory().login_submit(form)
692
                if not form.has_errors():
693
                    return tmp
694

  
695
            r += htmltext('<div id="login-password">')
696
            r += get_session().display_message()
697
            r += form.render()
698

  
699
            base_url = get_publisher().get_root_url()
700
            r += htmltext('<p><a href="%sident/password/forgotten">%s</a></p>') % (
701
                    base_url, _('Forgotten password ?'))
702

  
703
            r += htmltext('</div>')
704

  
705
            # XXX: this part only supports a single IdP
706
            r += htmltext('<div id="login-sso">')
707
            r += TextsDirectory.get_html_text('aq-sso-text')
708
            form = Form(enctype='multipart/form-data',
709
                    action = '%sident/idp/login' % base_url)
710
            form.add_hidden('method', 'idp')
711
            for kidp, idp in get_cfg('idp', {}).items():
712
                p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
713
                        misc.get_abs_path(idp['metadata']),
714
                        misc.get_abs_path(idp.get('publickey')), None)
715
                form.add_hidden('idp', p.providerId)
716
                break
717
            form.add_submit('submit', _('Connect'))
718

  
719
            r += form.render()
720
            r += htmltext('</div>')
721

  
722
            get_request().environ['REQUEST_METHOD'] = 'GET'
723

  
724
            r += htmltext("""<script type="text/javascript">
725
              document.getElementById('login-form')['username'].focus();
726
            </script>""")
727
            return r.getvalue()
728
        else:
729
            return OldLoginDirectory._q_index(self)
730

  
731

  
732
OldIdentDirectory = wcs.root.IdentDirectory
733
class AlternateIdentDirectory(OldIdentDirectory):
734
    def _q_traverse(self, path):
735
        get_response().filter['bigdiv'] = 'member'
736
        return OldIdentDirectory._q_traverse(self, path)
737

  
738

  
739
class AlternateRootDirectory(OldRootDirectory):
740
    _q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout',
741
            'liberty', 'token', 'saml', 'register', 'ident', 'afterjobs',
742
            ('informations-editeur', 'informations_editeur'), 'index2',
743
            ('announces', 'announces_dir'),
744
            'accessibility', 'contact', 'help',
745
            'myspace', 'services', 'agenda', 'categories', 'user',
746
            ('tmp-upload', 'tmp_upload'), 'json', '__version__',
747
            'themes', 'pages', 'payment', 'invoices', 'accesscode', 'roles',
748
            'msp']
749

  
750
    admin = admin.AdminRootDirectory()
751
    announces_dir = AnnouncesDirectory()
752
    register = AlternateRegisterDirectory()
753
    login = AlternateLoginDirectory()
754
    ident = AlternateIdentDirectory()
755
    myspace = MyspaceDirectory()
756
    agenda = AgendaDirectory()
757
    saml = Saml2Directory()
758
    payment = PublicPaymentDirectory()
759
    invoices = InvoicesDirectory()
760
    msp = msp_ui.MSPDirectory()
761

  
762
    def get_substitution_variables(self):
763
        d = {}
764
        def print_links(fd):
765
            fd.write(str(self.links()))
766
        d['links'] = print_links
767
        return d
768

  
769
    def _q_traverse(self, path):
770
        if get_publisher().has_site_option('drupal'):
771
            drupal.try_auth()
772
        if get_publisher().has_site_option('ezldap'):
773
            ezldap_ui.try_auth(self)
774

  
775
        session = get_session()
776
        if session:
777
            get_request().user = session.get_user()
778
        else:
779
            get_request().user = None
780

  
781
        get_publisher().substitutions.feed(get_request().user)
782

  
783
        response = get_response()
784
        if not hasattr(response, 'filter'):
785
            response.filter = {}
786

  
787
        response.filter['gauche'] = self.box_side(path)
788
        response.filter['keywords'] = template.get_current_theme().get('keywords')
789
        get_publisher().substitutions.feed(self)
790

  
791
        response.breadcrumb = [ ('', _('Home')) ]
792

  
793
        if not self.admin:
794
            self.admin = get_publisher().admin_directory_class()
795

  
796
        if not self.backoffice:
797
            self.backoffice = get_publisher().backoffice_directory_class()
798

  
799
        try:
800
            return Directory._q_traverse(self, path)
801
        except errors.TraversalError, e:
802
            try:
803
                f = FormDef.get_by_urlname(path[0])
804
            except KeyError:
805
                pass
806
            else:
807
                base_url = get_publisher().get_root_url()
808

  
809
                uri_rest = get_request().environ.get('REQUEST_URI')
810
                if not uri_rest:
811
                    uri_rest = get_request().get_path()
812
                if uri_rest.startswith(base_url):
813
                    uri_rest = uri_rest[len(base_url):]
814
                elif uri_rest.startswith('/'):
815
                    # dirty hack, ezldap reverseproxy uses a fake base_url
816
                    uri_rest = uri_rest[1:]
817
                if f.category_id:
818
                    return redirect('%s%s/%s' % (base_url, f.category.url_name, uri_rest))
819

  
820
            raise e
821

  
822

  
823
    def _q_lookup(self, component):
824
        if component == 'qo':
825
            dirname = os.path.join(get_publisher().data_dir, 'qommon')
826
            return StaticDirectory(dirname, follow_symlinks = True)
827

  
828
        if component == 'aq':
829
            dirname = os.path.join(get_publisher().data_dir, 'qommon', 'auquotidien')
830
            return StaticDirectory(dirname, follow_symlinks = True)
831

  
832
        if component in ('css','images'):
833
            return OldRootDirectory._q_lookup(self, component)
834

  
835
        # is this a category ?
836
        try:
837
            category = Category.get_by_urlname(component)
838
        except KeyError:
839
            pass
840
        else:
841
            return FormsRootDirectory(category)
842

  
843
        # is this a formdef ?
844
        try:
845
            formdef = FormDef.get_by_urlname(component)
846
        except KeyError:
847
            pass
848
        else:
849
            if formdef.category_id is None:
850
                get_response().filter['bigdiv'] = 'rub_service'
851
                return FormsRootDirectory()._q_lookup(component)
852
            # if there is category, let it fall back to raise TraversalError,
853
            # it will get caught in _q_traverse that will redirect it to an
854
            # URL embedding the category
855

  
856
        return None
857

  
858
    def json(self):
859
        return FormsRootDirectory().json()
860

  
861
    def categories(self):
862
        return FormsRootDirectory().categories()
863

  
864
    def _q_index(self):
865
        if get_request().is_json():
866
            return FormsRootDirectory().json()
867

  
868
        root_url = get_publisher().get_root_url()
869
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
870
            return redirect('%smyspace/new' % root_url)
871

  
872
        if get_response().iframe_mode:
873
            # never display home page in an iframe
874
            return redirect('%sservices' % root_url)
875

  
876
        template.html_top()
877
        r = TemplateIO(html=True)
878
        get_response().filter['is_index'] = True
879

  
880
        if not 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
881
            t = TextsDirectory.get_html_text('aq-home-page')
882
            if not t:
883
                if get_request().user:
884
                    t = TextsDirectory.get_html_text('welcome-logged')
885
                else:
886
                    t = TextsDirectory.get_html_text('welcome-unlogged')
887
            if t:
888
                r += htmltext('<div id="home-page-intro">')
889
                r += t
890
                r += htmltext('</div>')
891

  
892
        r += htmltext('<div id="centre">')
893
        r += self.box_services(position='1st')
894
        r += htmltext('</div>')
895
        r += htmltext('<div id="droite">')
896
        r += self.myspace_snippet()
897
        r += self.box_services(position='2nd')
898
        r += self.consultations()
899
        r += self.announces()
900
        r += htmltext('</div>')
901

  
902
        user = get_request().user
903
        if user and user.can_go_in_backoffice():
904
            get_response().filter['backoffice'] = True
905

  
906
        return r.getvalue()
907

  
908
    def services(self):
909
        template.html_top()
910
        get_response().filter['bigdiv'] = 'rub_service'
911
        return self.box_services(level = 2)
912

  
913
    def box_services(self, level=3, position=None):
914
        ## Services
915
        if get_request().user and get_request().user.roles:
916
            accepted_roles = get_request().user.roles
917
        else:
918
            accepted_roles = []
919

  
920
        cats = Category.select(order_by = 'name')
921
        cats = [x for x in cats if x.url_name != 'consultations']
922
        Category.sort_by_position(cats)
923

  
924
        all_formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
925
                order_by = 'name')
926

  
927
        if position:
928
            t = self.display_list_of_formdefs(
929
                            [x for x in cats if x.get_homepage_position() == position],
930
                            all_formdefs, accepted_roles)
931
        else:
932
            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
933

  
934
        if not t:
935
            return
936

  
937
        r = TemplateIO(html=True)
938

  
939
        if position == '2nd':
940
            r += htmltext('<div id="services-2nd">')
941
        else:
942
            r += htmltext('<div id="services">')
943
        if level == 2:
944
            r += htmltext('<h2>%s</h2>') % _('Services')
945
        else:
946
            r += htmltext('<h3>%s</h3>') % _('Services')
947

  
948
        if get_response().iframe_mode:
949
            if get_request().user:
950
                message = TextsDirectory.get_html_text('welcome-logged')
951
            else:
952
                message = TextsDirectory.get_html_text('welcome-unlogged')
953

  
954
            if message:
955
                r += htmltext('<div id="welcome-message">')
956
                r += message
957
                r += htmltext('</div>')
958
        elif 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
959
            homepage_text = TextsDirectory.get_html_text('aq-home-page')
960
            if homepage_text:
961
                r += htmltext('<div id="home-page-intro">')
962
                r += homepage_text
963
                r += htmltext('</div>')
964

  
965
        r += htmltext('<ul>')
966
        r += t
967
        r += htmltext('</ul>')
968

  
969
        r += htmltext('</div>')
970
        return r.getvalue()
971

  
972
    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
973
        r = TemplateIO(html=True)
974
        for category in cats:
975
            if category.url_name == 'consultations':
976
                self.consultations_category = category
977
                continue
978
            formdefs = [x for x in all_formdefs if x.category_id == category.id]
979
            formdefs_advertise = []
980

  
981
            for formdef in formdefs[:]:
982
                if formdef.is_disabled(): # is a redirection
983
                    continue
984
                if not formdef.roles:
985
                    continue
986
                if not get_request().user:
987
                    if formdef.always_advertise:
988
                        formdefs_advertise.append(formdef)
989
                    formdefs.remove(formdef)
990
                    continue
991
                if logged_users_role().id in formdef.roles:
992
                    continue
993
                for q in accepted_roles:
994
                    if q in formdef.roles:
995
                        break
996
                else:
997
                    if formdef.always_advertise:
998
                        formdefs_advertise.append(formdef)
999
                    formdefs.remove(formdef)
1000

  
1001
            if not formdefs and not formdefs_advertise:
1002
                continue
1003

  
1004
            r += htmltext('<li>')
1005
            r += htmltext('<strong>')
1006
            r += htmltext('<a href="%s/">') % category.url_name
1007
            r += category.name
1008
            r += htmltext('</a></strong>\n')
1009
            if category.description:
1010
                if category.description[0] == '<':
1011
                    r += htmltext(category.description)
1012
                else:
1013
                    r += htmltext('<p>')
1014
                    r += category.description
1015
                    r += htmltext('</p>')
1016
            r += htmltext('<ul>')
1017
            limit = category.get_limit()
1018
            for formdef in formdefs[:limit]:
1019
                r += htmltext('<li>')
1020
                r += htmltext('<a href="%s/%s/">%s</a>') % (category.url_name, formdef.url_name, formdef.name)
1021
                r += htmltext('</li>\n')
1022
            if len(formdefs) < limit:
1023
                for formdef in formdefs_advertise[:limit-len(formdefs)]:
1024
                    r += htmltext('<li>')
1025
                    r += htmltext('<a href="%s/%s/">%s</a>') % (category.url_name, formdef.url_name, formdef.name)
1026
                    r += ' (%s)' % _('authentication required')
1027
                    r += htmltext('</li>\n')
1028
            if (len(formdefs)+len(formdefs_advertise)) > limit:
1029
                r += htmltext('<li class="all-forms"><a href="%s/" title="%s">%s</a></li>') % (category.url_name,
1030
                        _('Access to all forms of the "%s" category') % category.name,
1031
                        _('Access to all forms in this category'))
1032
            r += htmltext('</ul>')
1033
            r += htmltext('</li>\n')
1034

  
1035
        return r.getvalue()
1036

  
1037
    def consultations(self):
1038
        cats = [x for x in Category.select() if x.url_name == 'consultations']
1039
        if not cats:
1040
            return
1041
        consultations_category = cats[0]
1042
        formdefs = FormDef.select(lambda x: (
1043
                    x.category_id == consultations_category.id and
1044
                        (not x.is_disabled() or x.disabled_redirection)),
1045
                    order_by = 'name')
1046
        if not formdefs:
1047
            return
1048
        ## Consultations
1049
        r = TemplateIO(html=True)
1050
        r += htmltext('<div id="consultations">')
1051
        r += htmltext('<h3>%s</h3>') % _('Consultations')
1052
        if consultations_category.description:
1053
            if consultations_category.description[0] == '<':
1054
                r += htmltext(consultations_category.description)
1055
            else:
1056
                r += htmltext('<p>')
1057
                r += consultations_category.description
1058
                r += htmltext('</p>')
1059
        r += htmltext('<ul>')
1060
        for formdef in formdefs:
1061
            r += htmltext('<li>')
1062
            r += htmltext('<a href="%s/%s/">%s</a>') % (consultations_category.url_name,
1063
                formdef.url_name, formdef.name)
1064
            r += htmltext('</li>')
1065
        r += htmltext('</ul>')
1066
        r += htmltext('</div>')
1067
        return r.getvalue()
1068

  
1069
    def box_side(self, path):
1070
        r = TemplateIO(html=True)
1071
        r += htmltext('<div id="sidebox">')
1072
        root_url = get_publisher().get_root_url()
1073

  
1074
        if self.has_anonymous_access_codes():
1075
            r += htmltext('<form id="follow-form" action="%saccesscode">') % root_url
1076
            r += htmltext('<h3>%s</h3>') % _('Tracking')
1077
            r += htmltext('<label>%s</label> ') % _('Code:')
1078
            r += htmltext('<input name="code" size="10"/>')
1079
            r += htmltext('</form>')
1080

  
1081
        r += self.links()
1082

  
1083
        cats = Category.select(order_by = 'name')
1084
        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
1085
        Category.sort_by_position(cats)
1086
        if cats:
1087
            r += htmltext('<div id="side-services">')
1088
            r += htmltext('<h3>%s</h3>') % _('Services')
1089
            r += htmltext('<ul>')
1090
            for cat in cats:
1091
                r += htmltext('<li><a href="%s/">%s</a></li>') % (cat.url_name, cat.name)
1092
            r += htmltext('</ul>')
1093
            r += htmltext('</div>')
1094

  
1095
        if Event.keys(): # if there are events, add a link to the agenda
1096
            tags = get_cfg('misc', {}).get('event_tags')
1097
            if not tags:
1098
                tags = get_default_event_tags()
1099
            r += htmltext('<h3 id="agenda-link"><a href="%sagenda/">%s</a></h3>') % (root_url, _('Agenda'))
1100

  
1101
            if path and path[0] == 'agenda':
1102
                r += htmltext('<p class="tags">')
1103
                for tag in tags:
1104
                    r += htmltext('<a href="%sagenda/tag/%s">%s</a> ') % (root_url, tag, tag)
1105
                r += htmltext('</p>')
1106
                r += self.agenda.display_remote_calendars()
1107

  
1108
                r += htmltext('<p>')
1109
                r += htmltext('  <a href="%sagenda/filter">%s</a>') % (root_url, _('Advanced Filter'))
1110
                r += htmltext('</p>')
1111

  
1112
        r += htmltext('</div>')
1113
        return r.getvalue()
1114

  
1115
    def has_anonymous_access_codes(self):
1116
        for workflow in Workflow.select():
1117
            for wfstatus in workflow.possible_status:
1118
                for wfitem in wfstatus.items:
1119
                    if wfitem.key == 'create-anonymous-access-code':
1120
                        return True
1121
        return False
1122

  
1123
    def accesscode(self):
1124
        code = get_request().form.get('code')
1125
        if not code:
1126
            return redirect(get_publisher().get_root_url())
1127
        try:
1128
            token = Token.get(code)
1129
        except KeyError:
1130
            return redirect(get_publisher().get_root_url())
1131
        if token.type != 'anonymous-access-code':
1132
            return redirect(get_publisher().get_root_url())
1133
        formdef_urlname, formdata_id = token.formdata_reference
1134
        try:
1135
            formdata = FormDef.get_by_urlname(formdef_urlname).data_class().get(formdata_id)
1136
        except KeyError:
1137
            return redirect(get_publisher().get_root_url())
1138
        session = get_session()
1139
        if not hasattr(session, '_wf_anonymous_access_authorized'):
1140
            session._wf_anonymous_access_authorized = []
1141
        session._wf_anonymous_access_authorized.append(formdata.get_url())
1142
        return redirect(formdata.get_url() + 'access/')
1143

  
1144
    def links(self):
1145
        links = Link.select()
1146
        if not links:
1147
            return
1148

  
1149
        Link.sort_by_position(links)
1150

  
1151
        r = TemplateIO(html=True)
1152

  
1153
        r += htmltext('<div id="links">')
1154
        if links[0].url:
1155
            # first link has an URL, so it's not a title, so we display a
1156
            # generic title
1157
            r += htmltext('<h3>%s</h3>') % _('Useful links')
1158
        has_ul = False
1159
        vars = get_publisher().substitutions.get_context_variables()
1160
        for link in links:
1161
            if not link.url:
1162
                # acting title
1163
                if has_ul:
1164
                    r += htmltext('</ul>')
1165
                r += htmltext('<h3>%s</h3>') % link.title
1166
                r += htmltext('<ul>')
1167
                has_ul = True
1168
            else:
1169
                if not has_ul:
1170
                    r += htmltext('<ul>')
1171
                    has_ul = True
1172
                r += htmltext('<li><a href="%s">%s</a></li>') % (get_variadic_url(link.url, vars), link.title)
1173
        if has_ul:
1174
            r += htmltext('</ul>')
1175
        r += htmltext('</div>')
1176
        return r.getvalue()
1177

  
1178
    def announces(self):
1179
        announces = Announce.get_published_announces()
1180
        if not announces:
1181
            return
1182

  
1183
        r = TemplateIO(html=True)
1184
        r += htmltext('<div id="announces">')
1185
        r += htmltext('<h3>%s</h3>') % _('Announces to citizens')
1186
        for item in announces[:3]:
1187
            r += htmltext('<div class="announce-item">')
1188
            r += htmltext('<h4>')
1189
            if item.publication_time:
1190
                r += time.strftime(misc.date_format(), item.publication_time)
1191
                r += ' - '
1192
            r += item.title
1193
            r += htmltext('</h4>')
1194
            r += htmltext('<p>')
1195
            r += item.text
1196
            r += htmltext('</p>')
1197
            r += htmltext('</div>')
1198

  
1199
        r += htmltext('<ul id="announces-links">')
1200
        r += htmltext('<li><a href="announces/subscribe">%s</a></li>') % _('Receiving those Announces')
1201
        r += htmltext('<li><a href="announces/">%s</a></li>') % _('Previous Announces')
1202
        r += htmltext('</ul>')
1203
        r += htmltext('</div>')
1204
        return r.getvalue()
1205

  
1206
    def myspace_snippet(self):
1207
        r = TemplateIO(html=True)
1208
        r += htmltext('<div id="myspace">')
1209
        r += htmltext('<h3>%s</h3>') % _('My Space')
1210
        r += htmltext('<ul>')
1211
        if get_request().user and not get_request().user.anonymous:
1212
            r += htmltext('  <li><a href="myspace/" id="member">%s</a></li>') % _('Access to your personal space')
1213
            r += htmltext('  <li><a href="logout" id="logout">%s</a></li>') % _('Logout')
1214
        else:
1215
            r += htmltext('  <li><a href="register/" id="inscr">%s</a></li>') % _('Registration')
1216
            r += htmltext('  <li><a href="login/" id="login">%s</a></li>') % _('Login')
1217
        r += htmltext('</ul>')
1218
        r += htmltext('</div>')
1219
        return r.getvalue()
1220

  
1221
    def page_view(self, key, title, urlname = None):
1222
        if not urlname:
1223
            urlname = key[3:].replace(str('_'), str('-'))
1224
        get_response().breadcrumb.append((urlname, title))
1225
        template.html_top(title)
1226
        r = TemplateIO(html=True)
1227
        r += htmltext('<div class="article">')
1228
        r += htmltext(TextsDirectory.get_html_text(key))
1229
        r += htmltext('</div>')
1230
        return r.getvalue()
1231

  
1232
    def informations_editeur(self):
1233
        get_response().filter['bigdiv'] = 'info'
1234
        return self.page_view('aq-editor-info', _('Editor Informations'),
1235
                urlname = 'informations_editeur')
1236

  
1237
    def accessibility(self):
1238
        get_response().filter['bigdiv'] = 'accessibility'
1239
        return self.page_view('aq-accessibility', _('Accessibility Statement'))
1240

  
1241
    def contact(self):
1242
        get_response().filter['bigdiv'] = 'contact'
1243
        return self.page_view('aq-contact', _('Contact'))
1244

  
1245
    def help(self):
1246
        get_response().filter['bigdiv'] = 'help'
1247
        return self.page_view('aq-help', _('Help'))
1248

  
1249

  
1250
from qommon.publisher import get_publisher_class
1251
get_publisher_class().root_directory_class = AlternateRootDirectory
1252
get_publisher_class().after_login_url = 'myspace/'
1253
get_publisher_class().use_sms_feature = True
1254

  
1255
# help links
1256
get_publisher_class().backoffice_help_url = {
1257
    'fr': 'https://doc.entrouvert.org/au-quotidien/stable/guide-gestionnaire.html',
1258
}
1259
get_publisher_class().admin_help_url = {
1260
    'fr': 'https://doc.entrouvert.org/auquotidien/dev/',
1261
}
1262

  
1263

  
1264
EmailsDirectory.register('announces-subscription-confirmation',
1265
        N_('Confirmation of Announces Subscription'),
1266
        N_('Available variables: change_url, cancel_url, time, sitename'),
1267
        default_subject = N_('Announce Subscription Request'),
1268
        default_body = N_("""\
1269
You have (or someone impersonating you has) requested to subscribe to
1270
announces from [sitename].  To confirm this request, visit the
1271
following link:
1272

  
1273
[confirm_url]
1274

  
1275
If you are not the person who made this request, or you wish to cancel
1276
this request, visit the following link:
1277

  
1278
[cancel_url]
1279

  
1280
If you do nothing, the request will lapse after 3 days (precisely on
1281
[time]).
1282
"""))
1283

  
1284

  
1285
TextsDirectory.register('aq-announces-subscription',
1286
        N_('Text on announces subscription page'),
1287
        default = N_('''\
1288
<p>
1289
FIXME
1290
'</p>'''))
1291

  
1292
TextsDirectory.register('aq-sms-demo',
1293
        N_('Text when subscribing to announces SMS and configured as demo'),
1294
        default = N_('''
1295
<p>
1296
Receiving announces by SMS is not possible in this demo
1297
</p>'''))
1298

  
1299
TextsDirectory.register('aq-editor-info', N_('Editor Informations'))
1300
TextsDirectory.register('aq-accessibility', N_('Accessibility Statement'))
1301
TextsDirectory.register('aq-contact', N_('Contact Information'))
1302
TextsDirectory.register('aq-help', N_('Help'))
1303
TextsDirectory.register('aq-sso-text',  N_('Connecting with Identity Provider'),
1304
        default = N_('''<h3>Connecting with Identity Provider</h3>
1305
<p>You can also use your identity provider to connect.
1306
</p>'''))
1307

  
1308
TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg = True)
extra/modules/strongbox_ui.ptl
1
import time
2

  
3
from quixote import get_request, get_response, get_session, redirect
4
from quixote.directory import Directory, AccessControlled
5

  
6
import wcs
7
import wcs.admin.root
8
from wcs.backoffice.menu import *
9

  
10
from qommon import errors, misc
11
from qommon.form import *
12
from qommon.strftime import strftime
13

  
14
from strongbox import StrongboxType, StrongboxItem
15

  
16

  
17

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

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

  
24
    def _q_index [html] (self):
25
        html_top('strongbox', title = _('Item Type: %s') % self.strongboxtype.label)
26
        '<h2>%s</h2>' % _('Item Type: %s') % self.strongboxtype.label
27
        get_response().filter['sidebar'] = self.get_sidebar()
28
        get_session().display_message()
29

  
30
        if self.strongboxtype.validation_months:
31
            '<div class="bo-block">'
32
            '<ul>'
33
            '<li>'
34
            _('Number of months of validity:')
35
            ' '
36
            self.strongboxtype.validation_months
37
            '</li>'
38
            '</ul>'
39
            '</div>'
40

  
41
    def get_sidebar [html] (self):
42
        '<ul>'
43
        '<li><a href="edit">%s</a></li>' % _('Edit')
44
        '<li><a href="delete">%s</a></li>' % _('Delete')
45
        '</ul>'
46

  
47
    def edit [html] (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('strongbox', title = _('Edit Item Type: %s') % self.strongboxtype.label)
57
        '<h2>%s</h2>' % _('Edit Item Type: %s') % self.strongboxtype.label
58
        form.render()
59

  
60

  
61
    def form(self):
62
        form = Form(enctype='multipart/form-data')
63
        form.add(StringWidget, 'label', title = _('Label'), required = True,
64
                value = self.strongboxtype.label)
65
        form.add(IntWidget, 'validation_months', title=_('Number of months of validity'),
66
                value=self.strongboxtype.validation_months,
67
                hint=_('Use 0 if there is no expiration'))
68
        form.add_submit('submit', _('Submit'))
69
        form.add_submit('cancel', _('Cancel'))
70
        return form
71

  
72
    def submit(self, form):
73
        for k in ('label', 'validation_months'):
74
            widget = form.get_widget(k)
75
            if widget:
76
                setattr(self.strongboxtype, k, widget.parse())
77
        self.strongboxtype.store()
78

  
79
    def delete [html] (self):
80
        form = Form(enctype='multipart/form-data')
81
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
82
                        'You are about to irrevocably delete this item type.')))
83
        form.add_submit('submit', _('Submit'))
84
        form.add_submit('cancel', _('Cancel'))
85
        if form.get_submit() == 'cancel':
86
            return redirect('..')
87
        if not form.is_submitted() or form.has_errors():
88
            get_response().breadcrumb.append(('delete', _('Delete')))
89
            html_top('strongbox', title = _('Delete Item Type'))
90
            '<h2>%s</h2>' % _('Deleting Item Type: %s') % self.strongboxtype.label
91
            form.render()
92
        else:
93
            self.strongboxtype.remove_self()
94
            return redirect('..')
95

  
96

  
97
class StrongboxTypesDirectory(Directory):
98
    _q_exports = ['', 'new']
99

  
100
    def _q_traverse(self, path):
101
        get_response().breadcrumb.append(('types/', _('Item Types')))
102
        return Directory._q_traverse(self, path)
103

  
104
    def _q_index [html] (self):
105
        return redirect('..')
106

  
107
    def new [html] (self):
108
        type_ui = StrongboxTypeDirectory(StrongboxType())
109

  
110
        form = type_ui.form()
111
        if form.get_submit() == 'cancel':
112
            return redirect('.')
113

  
114
        if form.is_submitted() and not form.has_errors():
115
            type_ui.submit(form)
116
            return redirect('%s/' % type_ui.strongboxtype.id)
117

  
118
        get_response().breadcrumb.append(('new', _('New Item Type')))
119
        html_top('strongbox', title = _('New Item Type'))
120
        '<h2>%s</h2>' % _('New Item Type')
121
        form.render()
122

  
123
    def _q_lookup(self, component):
124
        try:
125
            strongboxtype = StrongboxType.get(component)
126
        except KeyError:
127
            raise errors.TraversalError()
128
        get_response().breadcrumb.append((str(strongboxtype.id), strongboxtype.label))
129
        return StrongboxTypeDirectory(strongboxtype)
130

  
131

  
132
class StrongboxDirectory(AccessControlled, Directory):
133
    _q_exports = ['', 'types', 'add', 'add_to']
134
    label = N_('Strongbox')
135

  
136
    types = StrongboxTypesDirectory()
137

  
138
    def _q_access(self):
139
        user = get_request().user
140
        if not user:
141
            raise errors.AccessUnauthorizedError()
142
        admin_role = get_cfg('aq-permissions', {}).get('strongbox', None)
143
        if not (user.is_admin or admin_role in (user.roles or [])):
144
            raise errors.AccessForbiddenError(
145
                    public_msg = _('You are not allowed to access Strongbox Management'),
146
                    location_hint = 'backoffice')
147

  
148
        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
149

  
150

  
151
    def _q_index [html] (self):
152
        html_top('strongbox', _('Strongbox'))
153

  
154
        '<ul id="main-actions">'
155
        '  <li><a class="new-item" href="types/new">%s</a></li>' % _('New Item Type')
156
        '</ul>'
157

  
158
        get_session().display_message()
159

  
160
        '<div class="splitcontent-left">'
161
        '<div class="bo-block">'
162
        '<h2>%s</h2>' % _('Propose a file to a user')
163
        form = Form(enctype='multipart/form-data')
164
        form.add(StringWidget, 'q', title = _('User'), required=True)
165
        form.add_submit('search', _('Search'))
166
        form.render()
167
        if form.is_submitted() and not form.has_errors():
168
            q = form.get_widget('q').parse()
169
            users = self.search_for_users(q)
170
            if users:
171
                if len(users) == 1:
172
                    return redirect('add_to?user_id=%s' % users[0].id)
173
                if len(users) < 50:
174
                    _('(first 50 users only)')
175
                '<ul>'
176
                for u in users:
177
                    '<li><a href="add_to?user_id=%s">%s</a></li>' % (u.id, u.display_name)
178
                '</ul>'
179
            else:
180
                _('No user found.')
181
        '</div>'
182
        '</div>'
183

  
184
        '<div class="splitcontent-right">'
185
        '<div class="bo-block">'
186
        types = StrongboxType.select()
187
        '<h2>%s</h2>' % _('Item Types')
188
        if not types:
189
            '<p>'
190
            _('There is no item types defined at the moment.')
191
            '</p>'
192

  
193
        '<ul class="biglist" id="strongbox-list">'
194
        for l in types:
195
            type_id = l.id
196
            '<li class="biglistitem" id="itemId_%s">' % type_id
197
            '<strong class="label"><a href="types/%s/">%s</a></strong>' % (type_id, l.label)
198
            '</li>'
199
        '</ul>'
200
        '</div>'
201
        '</div>'
202

  
203
    def search_for_users(self, q):
204
        if hasattr(get_publisher().user_class, 'search'):
205
            return get_publisher().user_class.search(q)
206
        if q:
207
            users = [x for x in get_publisher().user_class.select()
208
                     if q in (x.name or '') or q in (x.email or '')]
209
            return users
210
        else:
211
            return []
212

  
213
    def get_form(self):
214
        types = [(x.id, x.label) for x in StrongboxType.select()]
215
        form = Form(action='add', enctype='multipart/form-data')
216
        form.add(StringWidget, 'description', title=_('Description'), size=60)
217
        form.add(FileWidget, 'file', title=_('File'), required=True)
218
        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
219
                 options = [(None, _('Not specified'))] + types)
220
        form.add(DateWidget, 'date_time', title = _('Document Date'))
221
        form.add_submit('submit', _('Upload'))
222
        return form
223

  
224
    def add(self):
225
        form = self.get_form()
226
        form.add(StringWidget, 'user_id', title=_('User'))
227
        if not form.is_submitted():
228
           return redirect('.')
229

  
230
        sffile = StrongboxItem()
231
        sffile.user_id = form.get_widget('user_id').parse()
232
        sffile.description = form.get_widget('description').parse()
233
        sffile.proposed_time = time.localtime()
234
        sffile.proposed_id = get_request().user.id
235
        sffile.type_id = form.get_widget('type_id').parse()
236
        v = form.get_widget('date_time').parse()
237
        sffile.set_expiration_time_from_date(v)
238
        sffile.store()
239
        sffile.set_file(form.get_widget('file').parse())
240
        sffile.store()
241
        return redirect('.')
242

  
243
    def add_to [html] (self):
244
        form = Form(enctype='multipart/form-data', action='add_to')
245
        form.add(StringWidget, 'user_id', title = _('User'), required=True)
246
        try:
247
            user_id = form.get_widget('user_id').parse()
248
            user = get_publisher().user_class.get(user_id)
249
        except:
250
            return redirect('.')
251
        if not user:
252
            return redirect('.')
253
        get_request().form = {}
254
        get_request().environ['REQUEST_METHOD'] = 'GET'
255

  
256
        html_top('strongbox', _('Strongbox'))
257
        '<h2>%s %s</h2>' % (_('Propose a file to:'), user.display_name)
258
        form = self.get_form()
259
        form.add(HiddenWidget, 'user_id', title=_('User'), value=user.id)
260
        form.render()
261

  
extra/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
from wcs.backoffice.menu import *
10

  
11
from qommon import errors, misc
12
from qommon.form import *
13
from qommon.strftime import strftime
14

  
15
from strongbox import StrongboxType, StrongboxItem
16

  
17

  
18

  
19
class StrongboxTypeDirectory(Directory):
20
    _q_exports = ['', 'edit', 'delete']
21

  
22
    def __init__(self, strongboxtype):
23
        self.strongboxtype = strongboxtype
24

  
25
    def _q_index(self):
26
        html_top('strongbox', title = _('Item Type: %s') % self.strongboxtype.label)
27
        r += htmltext('<h2>%s</h2>') % _('Item Type: %s') % self.strongboxtype.label
28
        get_response().filter['sidebar'] = self.get_sidebar()
29
        r += get_session().display_message()
30

  
31
        if self.strongboxtype.validation_months:
32
            r += htmltext('<div class="bo-block">')
33
            r += htmltext('<ul>')
34
            r += htmltext('<li>')
35
            r += _('Number of months of validity:')
36
            r += ' '
37
            r += self.strongboxtype.validation_months
38
            r += htmltext('</li>')
39
            r += htmltext('</ul>')
40
            r += htmltext('</div>')
41

  
42
        return r.getvalue()
43

  
44
    def get_sidebar(self):
45
        r = TemplateIO(html=True)
46
        r += htmltext('<ul>')
47
        r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
48
        r += htmltext('<li><a href="delete">%s</a></li>') % _('Delete')
49
        r += htmltext('</ul>')
50
        return r.getvalue()
51

  
52
    def edit(self):
53
        form = self.form()
54
        if form.get_submit() == 'cancel':
55
            return redirect('.')
56

  
57
        if form.is_submitted() and not form.has_errors():
58
            self.submit(form)
59
            return redirect('..')
60

  
61
        html_top('strongbox', title = _('Edit Item Type: %s') % self.strongboxtype.label)
62
        r = TemplateIO(html=True)
63
        r += htmltext('<h2>%s</h2>') % _('Edit Item Type: %s') % self.strongboxtype.label
64
        r += form.render()
65
        return r.getvalue()
66

  
67
    def form(self):
68
        form = Form(enctype='multipart/form-data')
69
        form.add(StringWidget, 'label', title = _('Label'), required = True,
70
                value = self.strongboxtype.label)
71
        form.add(IntWidget, 'validation_months', title=_('Number of months of validity'),
72
                value=self.strongboxtype.validation_months,
73
                hint=_('Use 0 if there is no expiration'))
74
        form.add_submit('submit', _('Submit'))
75
        form.add_submit('cancel', _('Cancel'))
76
        return form
77

  
78
    def submit(self, form):
79
        for k in ('label', 'validation_months'):
80
            widget = form.get_widget(k)
81
            if widget:
82
                setattr(self.strongboxtype, k, widget.parse())
83
        self.strongboxtype.store()
84

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

  
104

  
105
class StrongboxTypesDirectory(Directory):
106
    _q_exports = ['', 'new']
107

  
108
    def _q_traverse(self, path):
109
        get_response().breadcrumb.append(('types/', _('Item Types')))
110
        return Directory._q_traverse(self, path)
111

  
112
    def _q_index(self):
113
        return redirect('..')
114

  
115
    def new(self):
116
        type_ui = StrongboxTypeDirectory(StrongboxType())
117

  
118
        form = type_ui.form()
119
        if form.get_submit() == 'cancel':
120
            return redirect('.')
121

  
122
        if form.is_submitted() and not form.has_errors():
123
            type_ui.submit(form)
124
            return redirect('%s/' % type_ui.strongboxtype.id)
125

  
126
        get_response().breadcrumb.append(('new', _('New Item Type')))
127
        html_top('strongbox', title = _('New Item Type'))
128
        r = TemplateIO(html=True)
129
        r += htmltext('<h2>%s</h2>') % _('New Item Type')
130
        r += form.render()
131
        return r.getvalue()
132

  
133
    def _q_lookup(self, component):
134
        try:
135
            strongboxtype = StrongboxType.get(component)
136
        except KeyError:
137
            raise errors.TraversalError()
138
        get_response().breadcrumb.append((str(strongboxtype.id), strongboxtype.label))
139
        return StrongboxTypeDirectory(strongboxtype)
140

  
141

  
142
class StrongboxDirectory(AccessControlled, Directory):
143
    _q_exports = ['', 'types', 'add', 'add_to']
144
    label = N_('Strongbox')
145

  
146
    types = StrongboxTypesDirectory()
147

  
148
    def _q_access(self):
149
        user = get_request().user
150
        if not user:
151
            raise errors.AccessUnauthorizedError()
152
        admin_role = get_cfg('aq-permissions', {}).get('strongbox', None)
153
        if not (user.is_admin or admin_role in (user.roles or [])):
154
            raise errors.AccessForbiddenError(
155
                    public_msg = _('You are not allowed to access Strongbox Management'),
156
                    location_hint = 'backoffice')
157

  
158
        get_response().breadcrumb.append(('strongbox/', _('Strongbox')))
159

  
160

  
161
    def _q_index(self):
162
        html_top('strongbox', _('Strongbox'))
163
        r = TemplateIO(html=True)
164

  
165
        r += htmltext('<ul id="main-actions">')
166
        r += htmltext('  <li><a class="new-item" href="types/new">%s</a></li>') % _('New Item Type')
167
        r += htmltext('</ul>')
168

  
169
        r += get_session().display_message()
170

  
171
        r += htmltext('<div class="splitcontent-left">')
172
        r += htmltext('<div class="bo-block">')
173
        r += htmltext('<h2>%s</h2>') % _('Propose a file to a user')
174
        form = Form(enctype='multipart/form-data')
175
        form.add(StringWidget, 'q', title = _('User'), required=True)
176
        form.add_submit('search', _('Search'))
177
        r += form.render()
178
        if form.is_submitted() and not form.has_errors():
179
            q = form.get_widget('q').parse()
180
            users = self.search_for_users(q)
181
            if users:
182
                if len(users) == 1:
183
                    return redirect('add_to?user_id=%s' % users[0].id)
184
                if len(users) < 50:
185
                    r += _('(first 50 users only)')
186
                r += htmltext('<ul>')
187
                for u in users:
188
                    r += htmltext('<li><a href="add_to?user_id=%s">%s</a></li>') % (u.id, u.display_name)
189
                r += htmltext('</ul>')
190
            else:
191
                r += _('No user found.')
192
        r += htmltext('</div>')
193
        r += htmltext('</div>')
194

  
195
        r += htmltext('<div class="splitcontent-right">')
196
        r += htmltext('<div class="bo-block">')
197
        types = StrongboxType.select()
198
        r += htmltext('<h2>%s</h2>') % _('Item Types')
199
        if not types:
200
            r += htmltext('<p>')
201
            r += _('There is no item types defined at the moment.')
202
            r += htmltext('</p>')
203

  
204
        r += htmltext('<ul class="biglist" id="strongbox-list">')
205
        for l in types:
206
            type_id = l.id
207
            r += htmltext('<li class="biglistitem" id="itemId_%s">') % type_id
208
            r += htmltext('<strong class="label"><a href="types/%s/">%s</a></strong>') % (type_id, l.label)
209
            r += htmltext('</li>')
210
        r += htmltext('</ul>')
211
        r += htmltext('</div>')
212
        r += htmltext('</div>')
213
        return r.getvalue()
214

  
215
    def search_for_users(self, q):
216
        if hasattr(get_publisher().user_class, 'search'):
217
            return get_publisher().user_class.search(q)
218
        if q:
219
            users = [x for x in get_publisher().user_class.select()
220
                     if q in (x.name or '') or q in (x.email or '')]
221
            return users
222
        else:
223
            return []
224

  
225
    def get_form(self):
226
        types = [(x.id, x.label) for x in StrongboxType.select()]
227
        form = Form(action='add', enctype='multipart/form-data')
228
        form.add(StringWidget, 'description', title=_('Description'), size=60)
229
        form.add(FileWidget, 'file', title=_('File'), required=True)
230
        form.add(SingleSelectWidget, 'type_id', title=_('Document Type'),
231
                 options = [(None, _('Not specified'))] + types)
232
        form.add(DateWidget, 'date_time', title = _('Document Date'))
233
        form.add_submit('submit', _('Upload'))
234
        return form
235

  
236
    def add(self):
237
        form = self.get_form()
238
        form.add(StringWidget, 'user_id', title=_('User'))
239
        if not form.is_submitted():
240
           return redirect('.')
241

  
242
        sffile = StrongboxItem()
243
        sffile.user_id = form.get_widget('user_id').parse()
244
        sffile.description = form.get_widget('description').parse()
245
        sffile.proposed_time = time.localtime()
246
        sffile.proposed_id = get_request().user.id
247
        sffile.type_id = form.get_widget('type_id').parse()
248
        v = form.get_widget('date_time').parse()
249
        sffile.set_expiration_time_from_date(v)
250
        sffile.store()
251
        sffile.set_file(form.get_widget('file').parse())
252
        sffile.store()
253
        return redirect('.')
254

  
255
    def add_to(self):
256
        form = Form(enctype='multipart/form-data', action='add_to')
257
        form.add(StringWidget, 'user_id', title = _('User'), required=True)
258
        try:
259
            user_id = form.get_widget('user_id').parse()
260
            user = get_publisher().user_class.get(user_id)
261
        except:
262
            return redirect('.')
263
        if not user:
264
            return redirect('.')
265
        get_request().form = {}
266
        get_request().environ['REQUEST_METHOD'] = 'GET'
267

  
268
        html_top('strongbox', _('Strongbox'))
269
        r = TemplateIO(html=True)
270
        r += htmltext('<h2>%s %s</h2>') % (_('Propose a file to:'), user.display_name)
271
        form = self.get_form()
272
        form.add(HiddenWidget, 'user_id', title=_('User'), value=user.id)
273
        r += form.render()
274
        return r.getvalue()
0
-