Projet

Général

Profil

Télécharger (23,6 ko) Statistiques
| Branche: | Tag: | Révision:

root / auquotidien / modules / root.py @ a9ee9454

1
from quixote import get_publisher, get_response, get_request, redirect, get_session
2
from quixote.directory import Directory
3
from quixote.html import TemplateIO, htmltext
4

    
5
from wcs.qommon import _
6
from wcs.qommon.misc import get_variadic_url, simplify
7

    
8
import os
9
import re
10
import string
11

    
12
from django.utils.six.moves.urllib import parse as urlparse
13

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

    
19
import wcs
20
import wcs.root
21
from wcs import qommon
22
from wcs.forms.root import RootDirectory as FormsRootDirectory
23
from wcs.qommon import get_cfg, get_logger
24
from wcs.qommon import template
25
from wcs.qommon import errors
26
from wcs.qommon.form import *
27
from wcs.qommon import logger
28
from wcs.roles import logged_users_role
29

    
30
from wcs.qommon import emails
31
from wcs.qommon.sms import SMS
32
from wcs.categories import Category
33
from wcs.formdef import FormDef
34
from wcs.data_sources import NamedDataSource
35
from wcs.qommon.tokens import Token
36
from wcs.qommon.admin.emails import EmailsDirectory
37
from wcs.qommon.admin.texts import TextsDirectory
38

    
39
from .myspace import MyspaceDirectory
40
from .payments import PublicPaymentDirectory
41
from .payments_ui import InvoicesDirectory
42

    
43
from . import admin
44

    
45
import wcs.forms.root
46
from wcs.workflows import Workflow
47
from wcs.forms.preview import PreviewDirectory
48

    
49
from .saml2 import Saml2Directory
50

    
51
OldRootDirectory = wcs.root.RootDirectory
52

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

    
56

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

    
65
def category_get_limit(self):
66
    if hasattr(self, 'limit') and self.limit is not None:
67
        return self.limit
68
    return 7
69
Category.get_limit = category_get_limit
70

    
71
Category.TEXT_ATTRIBUTES = ['name', 'url_name', 'description', 'homepage_position']
72
Category.INT_ATTRIBUTES = ['position', 'limit']
73

    
74
OldRegisterDirectory = wcs.root.RegisterDirectory
75

    
76
class AlternateRegisterDirectory(OldRegisterDirectory):
77
    def _q_traverse(self, path):
78
        get_response().filter['bigdiv'] = 'new_member'
79
        return OldRegisterDirectory._q_traverse(self, path)
80

    
81
    def _q_index(self):
82
        get_logger().info('register')
83
        ident_methods = get_cfg('identification', {}).get('methods', [])
84

    
85
        if len(ident_methods) == 0:
86
            idps = get_cfg('idp', {})
87
            if len(idps) == 0:
88
                return template.error_page(_('Authentication subsystem is not yet configured.'))
89
            ident_methods = ['idp'] # fallback to old behaviour; saml.
90

    
91
        if len(ident_methods) == 1:
92
            method = ident_methods[0]
93
        else:
94
            method = 'password'
95

    
96
        return wcs.qommon.ident.register(method)
97

    
98
OldLoginDirectory = wcs.root.LoginDirectory
99

    
100
class AlternateLoginDirectory(OldLoginDirectory):
101
    def _q_traverse(self, path):
102
        get_response().filter['bigdiv'] = 'member'
103
        return OldLoginDirectory._q_traverse(self, path)
104

    
105
    def _q_index(self):
106
        get_logger().info('login')
107
        ident_methods = get_cfg('identification', {}).get('methods', [])
108

    
109
        if get_request().form.get('ReturnUrl'):
110
            get_request().form['next'] = get_request().form.pop('ReturnUrl')
111

    
112
        if 'IsPassive' in get_request().form and 'idp' in ident_methods:
113
            # if isPassive is given in query parameters, we restrict ourselves
114
            # to saml login.
115
            ident_methods = ['idp']
116

    
117
        if len(ident_methods) > 1 and 'idp' in ident_methods:
118
            # if there is more than one identification method, and there is a
119
            # possibility of SSO, if we got there as a consequence of an access
120
            # unauthorized url on admin/ or backoffice/, then idp auth method
121
            # is chosen forcefully.
122
            after_url = get_request().form.get('next')
123
            if after_url:
124
                root_url = get_publisher().get_root_url()
125
                after_path = urlparse.urlparse(after_url)[2]
126
                after_path = after_path[len(root_url):]
127
                if after_path.startswith(str('admin')) or \
128
                        after_path.startswith(str('backoffice')):
129
                    ident_methods = ['idp']
130

    
131
        # don't display authentication system choice
132
        if len(ident_methods) == 1:
133
            method = ident_methods[0]
134
            try:
135
                return wcs.qommon.ident.login(method)
136
            except KeyError:
137
                get_logger().error('failed to login with method %s' % method)
138
                return errors.TraversalError()
139

    
140
        if sorted(ident_methods) == ['idp', 'password']:
141
            r = TemplateIO(html=True)
142
            get_response().breadcrumb.append(('login', _('Login')))
143
            identities_cfg = get_cfg('identities', {})
144
            form = Form(enctype = 'multipart/form-data', id = 'login-form', use_tokens = False)
145
            if identities_cfg.get('email-as-username', False):
146
                form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
147
            else:
148
                form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
149
            form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
150
            form.add_submit('submit', _('Connect'))
151
            if form.is_submitted() and not form.has_errors():
152
                tmp = wcs.qommon.ident.password.MethodDirectory().login_submit(form)
153
                if not form.has_errors():
154
                    return tmp
155

    
156
            r += htmltext('<div id="login-password">')
157
            r += get_session().display_message()
158
            r += form.render()
159

    
160
            base_url = get_publisher().get_root_url()
161
            r += htmltext('<p><a href="%sident/password/forgotten">%s</a></p>') % (
162
                    base_url, _('Forgotten password ?'))
163

    
164
            r += htmltext('</div>')
165

    
166
            # XXX: this part only supports a single IdP
167
            r += htmltext('<div id="login-sso">')
168
            r += TextsDirectory.get_html_text('aq-sso-text')
169
            form = Form(enctype='multipart/form-data',
170
                    action = '%sident/idp/login' % base_url)
171
            form.add_hidden('method', 'idp')
172
            for kidp, idp in get_cfg('idp', {}).items():
173
                p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
174
                        misc.get_abs_path(idp['metadata']),
175
                        misc.get_abs_path(idp.get('publickey')), None)
176
                form.add_hidden('idp', p.providerId)
177
                break
178
            form.add_submit('submit', _('Connect'))
179

    
180
            r += form.render()
181
            r += htmltext('</div>')
182

    
183
            get_request().environ['REQUEST_METHOD'] = 'GET'
184

    
185
            r += htmltext("""<script type="text/javascript">
186
              document.getElementById('login-form')['username'].focus();
187
            </script>""")
188
            return r.getvalue()
189
        else:
190
            return OldLoginDirectory._q_index(self)
191

    
192

    
193
OldIdentDirectory = wcs.root.IdentDirectory
194
class AlternateIdentDirectory(OldIdentDirectory):
195
    def _q_traverse(self, path):
196
        get_response().filter['bigdiv'] = 'member'
197
        return OldIdentDirectory._q_traverse(self, path)
198

    
199

    
200
class AlternatePreviewDirectory(PreviewDirectory):
201
    def _q_traverse(self, path):
202
        get_response().filter['bigdiv'] = 'rub_service'
203
        return super(AlternatePreviewDirectory, self)._q_traverse(path)
204

    
205

    
206
class AlternateRootDirectory(OldRootDirectory):
207
    _q_exports = ['', 'admin', 'backoffice', 'forms', 'login', 'logout',
208
            'saml', 'register', 'ident', 'afterjobs',
209
            ('informations-editeur', 'informations_editeur'),
210
            'myspace', 'services', 'categories', 'user',
211
            ('tmp-upload', 'tmp_upload'), 'json', '__version__',
212
            'themes', 'pages', 'payment', 'invoices', 'roles',
213
            'api', 'code', 'fargo', 'tryauth', 'auth', 'preview',
214
            ('reload-top', 'reload_top'), 'static',
215
            ('i18n.js', 'i18n_js'), 'actions',]
216

    
217
    register = AlternateRegisterDirectory()
218
    login = AlternateLoginDirectory()
219
    ident = AlternateIdentDirectory()
220
    myspace = MyspaceDirectory()
221
    saml = Saml2Directory()
222
    payment = PublicPaymentDirectory()
223
    invoices = InvoicesDirectory()
224
    code = wcs.forms.root.TrackingCodesDirectory()
225
    preview = AlternatePreviewDirectory()
226

    
227
    def get_substitution_variables(self):
228
        return {'links': ''}
229

    
230
    def _q_traverse(self, path):
231
        self.feed_substitution_parts()
232

    
233
        # set app_label to Publik if none was specified (this is used in
234
        # backoffice header top line)
235
        if not get_publisher().get_site_option('app_label'):
236
            if not get_publisher().site_options.has_section('options'):
237
                get_publisher().site_options.add_section('options')
238
            get_publisher().site_options.set('options', 'app_label', 'Publik')
239

    
240
        response = get_response()
241
        if not hasattr(response, 'filter'):
242
            response.filter = {}
243

    
244
        response.filter['auquotidien'] = True
245
        if not path or (path[0] not in ('api', 'backoffice') and not get_request().is_json()):
246
            # api & backoffice have no use for a side box
247
            response.filter['gauche'] = self.box_side(path)
248
            response.filter['keywords'] = template.get_current_theme().get('keywords')
249
        get_publisher().substitutions.feed(self)
250

    
251
        response.breadcrumb = [ ('', _('Home')) ]
252

    
253
        if not self.admin:
254
            self.admin = get_publisher().admin_directory_class()
255

    
256
        if not self.backoffice:
257
            self.backoffice = get_publisher().backoffice_directory_class()
258

    
259
        try:
260
            return Directory._q_traverse(self, path)
261
        except errors.TraversalError as e:
262
            try:
263
                f = FormDef.get_by_urlname(path[0])
264
            except KeyError:
265
                pass
266
            else:
267
                base_url = get_publisher().get_root_url()
268

    
269
                uri_rest = get_request().environ.get('REQUEST_URI')
270
                if not uri_rest:
271
                    # REQUEST_URI doesn't exist when using internal HTTP server
272
                    # (--http)
273
                    uri_rest = get_request().get_path()
274
                    if get_request().get_query():
275
                        uri_rest += '?' + get_request().get_query()
276
                if uri_rest.startswith(base_url):
277
                    uri_rest = uri_rest[len(base_url):]
278
                if f.category:
279
                    if f.category.url_name == f.url_name:
280
                        return FormsRootDirectory(f.category)._q_traverse(path[1:])
281
                    return redirect('%s%s/%s' % (base_url, f.category.url_name, uri_rest))
282

    
283
            raise e
284

    
285

    
286
    def _q_lookup(self, component):
287
        # is this a category ?
288
        try:
289
            category = Category.get_by_urlname(component)
290
        except KeyError:
291
            category = None
292

    
293
        # is this a formdef ?
294
        try:
295
            formdef = FormDef.get_by_urlname(component)
296
        except KeyError:
297
            if category:
298
                return FormsRootDirectory(category)
299
        else:
300
            # if the form has no category, or the request is a POST, or the
301
            # slug matches both a category and a formdef, directly call
302
            # into FormsRootDirectory.
303
            if formdef.category_id is None or get_request().get_method() == 'POST' or (
304
                    formdef and category):
305
                get_response().filter['bigdiv'] = 'rub_service'
306
                return FormsRootDirectory()._q_lookup(component)
307

    
308
            # if there is category, let it fall back to raise TraversalError,
309
            # it will get caught in _q_traverse that will redirect it to an
310
            # URL embedding the category
311

    
312
        if component in ('accessibility', 'contact', 'help'):
313
            if TextsDirectory.get_html_text('aq-' + component):
314
                return getattr(self, component)()
315

    
316
        return None
317

    
318
    def json(self):
319
        return FormsRootDirectory().json()
320

    
321
    def categories(self):
322
        return FormsRootDirectory().categories()
323

    
324
    def _q_index(self):
325
        if get_request().is_json():
326
            return FormsRootDirectory().json()
327

    
328
        root_url = get_publisher().get_root_url()
329
        if get_request().user and get_request().user.anonymous and get_request().user.lasso_dump:
330
            return redirect('%smyspace/new' % root_url)
331

    
332
        redirect_url = get_cfg('misc', {}).get('homepage-redirect-url')
333
        if redirect_url:
334
            return redirect(misc.get_variadic_url(redirect_url,
335
                get_publisher().substitutions.get_context_variables()))
336

    
337
        template.html_top()
338
        r = TemplateIO(html=True)
339
        get_response().filter['is_index'] = True
340

    
341
        if not 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
342
            t = TextsDirectory.get_html_text('aq-home-page')
343
            if not t:
344
                if get_request().user:
345
                    t = TextsDirectory.get_html_text('welcome-logged')
346
                else:
347
                    t = TextsDirectory.get_html_text('welcome-unlogged')
348
            if t:
349
                r += htmltext('<div id="home-page-intro">')
350
                r += t
351
                r += htmltext('</div>')
352

    
353
        r += htmltext('<div id="centre">')
354
        r += self.box_services(position='1st')
355
        r += htmltext('</div>')
356
        r += htmltext('<div id="droite">')
357
        r += self.myspace_snippet()
358
        r += self.box_services(position='2nd')
359
        r += self.consultations()
360
        r += htmltext('</div>')
361

    
362
        user = get_request().user
363
        if user and user.can_go_in_backoffice():
364
            get_response().filter['backoffice'] = True
365

    
366
        return r.getvalue()
367

    
368
    def services(self):
369
        template.html_top()
370
        get_response().filter['bigdiv'] = 'rub_service'
371
        return self.box_services(level = 2)
372

    
373
    def box_services(self, level=3, position=None):
374
        ## Services
375
        if get_request().user and get_request().user.roles:
376
            accepted_roles = get_request().user.roles
377
        else:
378
            accepted_roles = []
379

    
380
        cats = Category.select(order_by = 'name')
381
        cats = [x for x in cats if x.url_name != 'consultations']
382
        Category.sort_by_position(cats)
383

    
384
        all_formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
385
                order_by = 'name')
386

    
387
        if position:
388
            t = self.display_list_of_formdefs(
389
                            [x for x in cats if x.get_homepage_position() == position],
390
                            all_formdefs, accepted_roles)
391
        else:
392
            t = self.display_list_of_formdefs(cats, all_formdefs, accepted_roles)
393

    
394
        if not t:
395
            return
396

    
397
        r = TemplateIO(html=True)
398

    
399
        if position == '2nd':
400
            r += htmltext('<div id="services-2nd">')
401
        else:
402
            r += htmltext('<div id="services">')
403
        if level == 2:
404
            r += htmltext('<h2>%s</h2>') % _('Services')
405
        else:
406
            r += htmltext('<h3>%s</h3>') % _('Services')
407

    
408
        if 'auquotidien-welcome-in-services' in get_response().filter.get('keywords', []):
409
            homepage_text = TextsDirectory.get_html_text('aq-home-page')
410
            if homepage_text:
411
                r += htmltext('<div id="home-page-intro">')
412
                r += homepage_text
413
                r += htmltext('</div>')
414

    
415
        r += htmltext('<ul>')
416
        r += t
417
        r += htmltext('</ul>')
418

    
419
        r += htmltext('</div>')
420
        return r.getvalue()
421

    
422
    def display_list_of_formdefs(self, cats, all_formdefs, accepted_roles):
423
        r = TemplateIO(html=True)
424
        for category in cats:
425
            if category.url_name == 'consultations':
426
                self.consultations_category = category
427
                continue
428
            formdefs = [x for x in all_formdefs if str(x.category_id) == str(category.id)]
429
            formdefs_advertise = []
430

    
431
            for formdef in formdefs[:]:
432
                if formdef.is_disabled(): # is a redirection
433
                    continue
434
                if not formdef.roles:
435
                    continue
436
                if not get_request().user:
437
                    if formdef.always_advertise:
438
                        formdefs_advertise.append(formdef)
439
                    formdefs.remove(formdef)
440
                    continue
441
                if logged_users_role().id in formdef.roles:
442
                    continue
443
                for q in accepted_roles:
444
                    if q in formdef.roles:
445
                        break
446
                else:
447
                    if formdef.always_advertise:
448
                        formdefs_advertise.append(formdef)
449
                    formdefs.remove(formdef)
450

    
451
            if not formdefs and not formdefs_advertise:
452
                continue
453

    
454
            keywords = {}
455
            for formdef in formdefs:
456
                for keyword in formdef.keywords_list:
457
                    keywords[keyword] = True
458

    
459
            r += htmltext('<li id="category-%s" data-keywords="%s">') % (
460
                    category.url_name, ' '.join(keywords))
461
            r += htmltext('<strong>')
462
            r += htmltext('<a href="%s/">') % category.url_name
463
            r += category.name
464
            r += htmltext('</a></strong>\n')
465
            r += category.get_description_html_text()
466
            r += htmltext('<ul>')
467
            limit = category.get_limit()
468
            for formdef in formdefs[:limit]:
469
                r += htmltext('<li data-keywords="%s">') % ' '.join(formdef.keywords_list)
470
                classes = []
471
                if formdef.is_disabled() and formdef.disabled_redirection:
472
                    classes.append('redirection')
473
                r += htmltext('<a class="%s" href="%s/%s/">%s</a>') % (
474
                        ' '.join(classes), category.url_name, formdef.url_name, formdef.name)
475
                r += htmltext('</li>\n')
476
            if len(formdefs) < limit:
477
                for formdef in formdefs_advertise[:limit-len(formdefs)]:
478
                    r += htmltext('<li class="required-authentication">')
479
                    r += htmltext('<a href="%s/%s/">%s</a>') % (category.url_name, formdef.url_name, formdef.name)
480
                    r += htmltext('<span> (%s)</span>') % _('authentication required')
481
                    r += htmltext('</li>\n')
482
            if (len(formdefs)+len(formdefs_advertise)) > limit:
483
                r += htmltext('<li class="all-forms"><a href="%s/" title="%s">%s</a></li>') % (category.url_name,
484
                        _('Access to all forms of the "%s" category') % category.name,
485
                        _('Access to all forms in this category'))
486
            r += htmltext('</ul>')
487
            r += htmltext('</li>\n')
488

    
489
        return r.getvalue()
490

    
491
    def consultations(self):
492
        cats = [x for x in Category.select() if x.url_name == 'consultations']
493
        if not cats:
494
            return
495
        consultations_category = cats[0]
496
        formdefs = FormDef.select(lambda x: (
497
                    str(x.category_id) == str(consultations_category.id) and
498
                        (not x.is_disabled() or x.disabled_redirection)),
499
                    order_by = 'name')
500
        if not formdefs:
501
            return
502
        ## Consultations
503
        r = TemplateIO(html=True)
504
        r += htmltext('<div id="consultations">')
505
        r += htmltext('<h3>%s</h3>') % _('Consultations')
506
        r += consultations_category.get_description_html_text()
507
        r += htmltext('<ul>')
508
        for formdef in formdefs:
509
            r += htmltext('<li>')
510
            r += htmltext('<a href="%s/%s/">%s</a>') % (consultations_category.url_name,
511
                formdef.url_name, formdef.name)
512
            r += htmltext('</li>')
513
        r += htmltext('</ul>')
514
        r += htmltext('</div>')
515
        return r.getvalue()
516

    
517
    def box_side(self, path):
518
        r = TemplateIO(html=True)
519
        root_url = get_publisher().get_root_url()
520

    
521
        if (path == [''] and
522
                'include-tracking-code-form' in get_response().filter.get('keywords', []) and
523
                self.has_anonymous_access_codes()):
524
            r += htmltext('<form id="follow-form" action="%scode/load">') % root_url
525
            r += htmltext('<h3>%s</h3>') % _('Tracking code')
526
            r += htmltext('<input size="12" name="code" placeholder="%s"/>') % _('ex: RPQDFVCD')
527
            r += htmltext('<input type="submit" value="%s"/>') % _('Load')
528
            r += htmltext('</form>')
529

    
530
        cats = Category.select(order_by = 'name')
531
        cats = [x for x in cats if x.url_name != 'consultations' and x.get_homepage_position() == 'side']
532
        Category.sort_by_position(cats)
533
        if cats:
534
            r += htmltext('<div id="side-services">')
535
            r += htmltext('<h3>%s</h3>') % _('Services')
536
            r += htmltext('<ul>')
537
            for cat in cats:
538
                r += htmltext('<li><a href="%s/">%s</a></li>') % (cat.url_name, cat.name)
539
            r += htmltext('</ul>')
540
            r += htmltext('</div>')
541

    
542
        v = r.getvalue()
543
        if v:
544
            r = TemplateIO(html=True)
545
            r += htmltext('<div id="sidebox">')
546
            r += v
547
            r += htmltext('</div>')
548
            return r.getvalue()
549

    
550
        return None
551

    
552
    def has_anonymous_access_codes(self):
553
        return any((x for x in FormDef.select() if x.enable_tracking_codes))
554

    
555
    def myspace_snippet(self):
556
        r = TemplateIO(html=True)
557
        r += htmltext('<div id="myspace">')
558
        r += htmltext('<h3>%s</h3>') % _('My Space')
559
        r += htmltext('<ul>')
560
        if get_request().user and not get_request().user.anonymous:
561
            r += htmltext('  <li><a href="myspace/" id="member">%s</a></li>') % _('Access to your personal space')
562
            r += htmltext('  <li><a href="logout" id="logout">%s</a></li>') % _('Logout')
563
        else:
564
            r += htmltext('  <li><a href="register/" id="inscr">%s</a></li>') % _('Registration')
565
            r += htmltext('  <li><a href="login/" id="login">%s</a></li>') % _('Login')
566
        r += htmltext('</ul>')
567
        r += htmltext('</div>')
568
        return r.getvalue()
569

    
570
    def page_view(self, key, title, urlname = None):
571
        if not urlname:
572
            urlname = key[3:].replace(str('_'), str('-'))
573
        get_response().breadcrumb.append((urlname, title))
574
        template.html_top(title)
575
        r = TemplateIO(html=True)
576
        r += htmltext('<div class="article">')
577
        r += htmltext(TextsDirectory.get_html_text(key))
578
        r += htmltext('</div>')
579
        return r.getvalue()
580

    
581
    def informations_editeur(self):
582
        get_response().filter['bigdiv'] = 'info'
583
        return self.page_view('aq-editor-info', _('Editor Informations'),
584
                urlname = 'informations_editeur')
585

    
586
    def accessibility(self):
587
        get_response().filter['bigdiv'] = 'accessibility'
588
        return self.page_view('aq-accessibility', _('Accessibility Statement'))
589

    
590
    def contact(self):
591
        get_response().filter['bigdiv'] = 'contact'
592
        return self.page_view('aq-contact', _('Contact'))
593

    
594
    def help(self):
595
        get_response().filter['bigdiv'] = 'help'
596
        return self.page_view('aq-help', _('Help'))
597

    
598

    
599
from qommon.publisher import get_publisher_class
600
get_publisher_class().root_directory_class = AlternateRootDirectory
601
get_publisher_class().after_login_url = 'myspace/'
602
get_publisher_class().use_sms_feature = True
603

    
604

    
605
TextsDirectory.register('aq-editor-info', N_('Editor Informations'))
606
TextsDirectory.register('aq-accessibility', N_('Accessibility Statement'))
607
TextsDirectory.register('aq-contact', N_('Contact Information'))
608
TextsDirectory.register('aq-help', N_('Help'))
609
TextsDirectory.register('aq-sso-text',  N_('Connecting with Identity Provider'),
610
        default = N_('''<h3>Connecting with Identity Provider</h3>
611
<p>You can also use your identity provider to connect.
612
</p>'''))
613

    
614
TextsDirectory.register('aq-home-page', N_('Home Page'), wysiwyg = True)
(16-16/18)