Always use territories related to base_territory as competent territories.
[infos-pratiques:etalage.git] / etalage / controllers.py
1 # -*- coding: utf-8 -*-
2
3
4 # Etalage -- Open Data POIs portal
5 # By: Emmanuel Raviart <eraviart@easter-eggs.com>
6 #
7 # Copyright (C) 2011, 2012 Easter-eggs
8 # http://gitorious.org/infos-pratiques/etalage
9 #
10 # This file is part of Etalage.
11 #
12 # Etalage is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU Affero General Public License as
14 # published by the Free Software Foundation, either version 3 of the
15 # License, or (at your option) any later version.
16 #
17 # Etalage is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU Affero General Public License for more details.
21 #
22 # You should have received a copy of the GNU Affero General Public License
23 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
25
26 """Controllers for territories"""
27
28
29 from cStringIO import StringIO
30 import datetime
31 import json
32 import logging
33 import math
34 import urllib2
35 import zipfile
36
37 from biryani import strings
38 import markupsafe
39
40 from . import conf, contexts, conv, model, pagers, ramdb, templates, urls, wsgihelpers
41
42
43 log = logging.getLogger(__name__)
44 N_ = lambda message: message
45
46
47 @wsgihelpers.wsgify
48 def about(req):
49     ctx = contexts.Ctx(req)
50
51     params = req.GET
52     init_base(ctx, params)
53     return templates.render(ctx, '/about.mako')
54
55
56 @wsgihelpers.wsgify
57 @ramdb.ramdb_based
58 def autocomplete_category(req):
59     ctx = contexts.Ctx(req)
60     ctx.controller_name = 'autocomplete_category'
61
62     headers = []
63     params = req.GET
64     inputs = dict(
65         context = params.get('context'),
66         jsonp = params.get('jsonp'),
67         page = params.get('page'),
68         tag = params.getall('tag'),
69         term = params.get('term'),
70         )
71     data, errors = conv.pipe(
72         conv.struct(
73             dict(
74                 page = conv.pipe(
75                     conv.input_to_int,
76                     conv.test_greater_or_equal(1),
77                     conv.default(1),
78                     ),
79                 tag = conv.uniform_sequence(conv.input_to_tag_slug),
80                 term = conv.make_input_to_slug(separator = u' ', transform = strings.upper),
81                 ),
82             default = 'drop',
83             keep_none_values = True,
84             ),
85         conv.rename_item('page', 'page_number'),
86         conv.rename_item('tag', 'tags_slug'),
87         )(inputs, state = ctx)
88     if errors is not None:
89         return wsgihelpers.respond_json(ctx,
90             dict(
91                 apiVersion = '1.0',
92                 context = inputs['context'],
93                 error = dict(
94                     code = 400,  # Bad Request
95                     errors = [
96                         dict(
97                             location = key,
98                             message = error,
99                             )
100                         for key, error in sorted(errors.iteritems())
101                         ],
102                     # message will be automatically defined.
103                     ),
104                 method = ctx.controller_name,
105                 params = inputs,
106                 ),
107             headers = headers,
108             jsonp = inputs['jsonp'],
109             )
110
111     possible_pois_id = ramdb.intersection_set(
112         model.Poi.ids_by_category_slug[category_slug]
113         for category_slug in (data['tags_slug'] or [])
114         )
115     if possible_pois_id is None:
116         categories_infos = sorted(
117             (-len(model.Poi.ids_by_category_slug.get(category_slug, [])), category_slug)
118             for category_slug in ramdb.iter_categories_slug(tags_slug = data['tags_slug'], term = data['term'])
119             if category_slug not in (data['tags_slug'] or [])
120             )
121     else:
122         categories_infos = sorted(
123             (-count, category_slug)
124             for count, category_slug in (
125                 (
126                     len(set(model.Poi.ids_by_category_slug.get(category_slug, [])).intersection(possible_pois_id)),
127                     category_slug,
128                     )
129                 for category_slug in ramdb.iter_categories_slug(tags_slug = data['tags_slug'], term = data['term'])
130                 if category_slug not in (data['tags_slug'] or [])
131                 )
132             if count > 0 and count != len(possible_pois_id)
133             )
134     pager = pagers.Pager(item_count = len(categories_infos), page_number = data['page_number'])
135     pager.items = [
136         dict(
137             count = -category_infos[0],
138             tag = ramdb.category_by_slug[category_infos[1]].name,
139             )
140         for category_infos in categories_infos[pager.first_item_index:pager.last_item_number]
141         ]
142     return wsgihelpers.respond_json(ctx,
143         dict(
144             apiVersion = '1.0',
145             context = inputs['context'],
146             data = dict(
147                 currentItemCount = len(pager.items),
148                 items = pager.items,
149                 itemsPerPage = pager.page_size,
150                 pageIndex = pager.page_number,
151                 startIndex = pager.first_item_index,
152                 totalItems = pager.item_count,
153                 totalPages = pager.page_count,
154                 ),
155             method = ctx.controller_name,
156             params = inputs,
157             ),
158         headers = headers,
159         jsonp = inputs['jsonp'],
160         )
161
162
163 @wsgihelpers.wsgify
164 @ramdb.ramdb_based
165 def csv(req):
166     ctx = contexts.Ctx(req)
167
168     if conf['hide_export']:
169         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
170
171     params = req.GET
172     inputs = init_base(ctx, params)
173     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
174
175     csv_bytes_by_name, errors = conv.pipe(
176         conv.inputs_to_pois_csv_infos,
177         conv.csv_infos_to_csv_bytes,
178         )(inputs, state = ctx)
179     if errors is not None:
180         return wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
181     if not csv_bytes_by_name:
182         return wsgihelpers.no_content(ctx)
183     if len(csv_bytes_by_name) == 1:
184         csv_filename, csv_bytes = csv_bytes_by_name.items()[0]
185         req.response.content_type = 'text/csv; charset=utf-8'
186         req.response.content_disposition = 'attachment;filename={0}'.format(csv_filename)
187         return csv_bytes
188     zip_file = StringIO()
189     with zipfile.ZipFile(zip_file, 'w') as zip_archive:
190         for csv_filename, csv_bytes in csv_bytes_by_name.iteritems():
191             zip_archive.writestr(csv_filename, csv_bytes)
192     req.response.content_type = 'application/zip'
193     req.response.content_disposition = 'attachment;filename=export.zip'
194     return zip_file.getvalue()
195
196
197 @wsgihelpers.wsgify
198 @ramdb.ramdb_based
199 def excel(req):
200     ctx = contexts.Ctx(req)
201
202     if conf['hide_export']:
203         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
204
205     params = req.GET
206     inputs = init_base(ctx, params)
207     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
208
209     excel_bytes, errors = conv.pipe(
210         conv.inputs_to_pois_csv_infos,
211         conv.csv_infos_to_excel_bytes,
212         )(inputs, state = ctx)
213     if errors is not None:
214         return wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
215     if not excel_bytes:
216         return wsgihelpers.no_content(ctx)
217     req.response.content_type = 'application/vnd.ms-excel'
218     req.response.content_disposition = 'attachment;filename=export.xls'
219     return excel_bytes
220
221
222 @wsgihelpers.wsgify
223 @ramdb.ramdb_based
224 def export_directory_csv(req):
225     ctx = contexts.Ctx(req)
226
227     if conf['hide_export']:
228         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
229
230     params = req.GET
231     inputs = init_base(ctx, params)
232     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
233     inputs.update(dict(
234         accept = params.get('accept'),
235         submit = params.get('submit'),
236         ))
237
238     format = u'csv'
239     mode = u'export'
240     type = u'annuaire'
241
242     accept, error = conv.pipe(conv.guess_bool, conv.default(False), conv.test_is(True))(inputs['accept'], state = ctx)
243     if error is None:
244         url_params = dict(
245             (model.Poi.rename_input_to_param(input_name), value)
246             for input_name, value in inputs.iteritems()
247             )
248         del url_params['accept']
249         del url_params['submit']
250         return wsgihelpers.redirect(ctx, location = urls.get_url(ctx, u'api/v1/{0}/{1}'.format(type, format),
251             **url_params))
252
253     data, errors = conv.merge(
254         model.Poi.make_inputs_to_search_data(),
255         conv.struct(
256             dict(
257                 accept = conv.test(lambda value: not inputs['submit'],
258                     error = N_(u"You must accept license to be allowed to download data."),
259                     handle_none_value = True,
260                     ),
261                 ),
262             default = 'drop',
263             keep_none_values = True,
264             ),
265         )(inputs, state = ctx)
266     return templates.render(ctx, '/export-accept-license.mako',
267         export_title = ctx._(u"Directory Export in CSV Format"),
268         errors = errors,
269         format = format,
270         inputs = inputs,
271         mode = mode,
272         type = type,
273         **model.Poi.extract_non_territorial_search_data(ctx, data))
274
275
276 @wsgihelpers.wsgify
277 @ramdb.ramdb_based
278 def export_directory_excel(req):
279     ctx = contexts.Ctx(req)
280
281     if conf['hide_export']:
282         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
283
284     params = req.GET
285     inputs = init_base(ctx, params)
286     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
287     inputs.update(dict(
288         accept = params.get('accept'),
289         submit = params.get('submit'),
290         ))
291
292     format = u'excel'
293     mode = u'export'
294     type = u'annuaire'
295
296     accept, error = conv.pipe(conv.guess_bool, conv.default(False), conv.test_is(True))(inputs['accept'], state = ctx)
297     if error is None:
298         url_params = dict(
299             (model.Poi.rename_input_to_param(input_name), value)
300             for input_name, value in inputs.iteritems()
301             )
302         del url_params['accept']
303         del url_params['submit']
304         return wsgihelpers.redirect(ctx, location = urls.get_url(ctx, u'api/v1/{0}/{1}'.format(type, format),
305             **url_params))
306
307     data, errors = conv.merge(
308         model.Poi.make_inputs_to_search_data(),
309         conv.struct(
310             dict(
311                 accept = conv.test(lambda value: not inputs['submit'],
312                     error = N_(u"You must accept license to be allowed to download data."),
313                     handle_none_value = True,
314                     ),
315                 ),
316             default = 'drop',
317             keep_none_values = True,
318             ),
319         )(inputs, state = ctx)
320     return templates.render(ctx, '/export-accept-license.mako',
321         export_title = ctx._(u"Directory Export in Excel Format"),
322         errors = errors,
323         format = format,
324         inputs = inputs,
325         mode = mode,
326         type = type,
327         **model.Poi.extract_non_territorial_search_data(ctx, data))
328
329
330 @wsgihelpers.wsgify
331 @ramdb.ramdb_based
332 def export_directory_geojson(req):
333     ctx = contexts.Ctx(req)
334
335     if conf['hide_export']:
336         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
337
338     params = req.GET
339     inputs = init_base(ctx, params)
340     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
341     inputs.update(dict(
342         accept = params.get('accept'),
343         submit = params.get('submit'),
344         ))
345
346     format = u'geojson'
347     mode = u'export'
348     type = u'annuaire'
349
350     accept, error = conv.pipe(conv.guess_bool, conv.default(False), conv.test_is(True))(inputs['accept'], state = ctx)
351     if error is None:
352         url_params = dict(
353             (model.Poi.rename_input_to_param(input_name), value)
354             for input_name, value in inputs.iteritems()
355             )
356         del url_params['accept']
357         del url_params['submit']
358         return wsgihelpers.redirect(ctx, location = urls.get_url(ctx, u'api/v1/{0}/{1}'.format(type, format),
359             **url_params))
360
361     data, errors = conv.merge(
362         model.Poi.make_inputs_to_search_data(),
363         conv.struct(
364             dict(
365                 accept = conv.test(lambda value: not inputs['submit'],
366                     error = N_(u"You must accept license to be allowed to download data."),
367                     handle_none_value = True,
368                     ),
369                 ),
370             default = 'drop',
371             keep_none_values = True,
372             ),
373         )(inputs, state = ctx)
374     return templates.render(ctx, '/export-accept-license.mako',
375         export_title = ctx._(u"Directory Export in GeoJSON Format"),
376         errors = errors,
377         format = format,
378         inputs = inputs,
379         mode = mode,
380         type = type,
381         **model.Poi.extract_non_territorial_search_data(ctx, data))
382
383
384 @wsgihelpers.wsgify
385 @ramdb.ramdb_based
386 def export_directory_kml(req):
387     ctx = contexts.Ctx(req)
388
389     if conf['hide_export']:
390         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
391
392     params = req.GET
393     inputs = init_base(ctx, params)
394     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
395     inputs.update(dict(
396         accept = params.get('accept'),
397         submit = params.get('submit'),
398         ))
399
400     format = u'kml'
401     mode = u'export'
402     type = u'annuaire'
403
404     accept, error = conv.pipe(conv.guess_bool, conv.default(False), conv.test_is(True))(inputs['accept'], state = ctx)
405     if error is None:
406         url_params = dict(
407             (model.Poi.rename_input_to_param(input_name), value)
408             for input_name, value in inputs.iteritems()
409             )
410         del url_params['accept']
411         del url_params['submit']
412         return wsgihelpers.redirect(ctx, location = urls.get_url(ctx, u'api/v1/{0}/{1}'.format(type, format),
413             **url_params))
414
415     data, errors = conv.merge(
416         model.Poi.make_inputs_to_search_data(),
417         conv.struct(
418             dict(
419                 accept = conv.test(lambda value: not inputs['submit'],
420                     error = N_(u"You must accept license to be allowed to download data."),
421                     handle_none_value = True,
422                     ),
423                 ),
424             default = 'drop',
425             keep_none_values = True,
426             ),
427         )(inputs, state = ctx)
428     return templates.render(ctx, '/export-accept-license.mako',
429         export_title = ctx._(u"Directory Export in KML Format"),
430         errors = errors,
431         format = format,
432         inputs = inputs,
433         mode = mode,
434         type = type,
435         **model.Poi.extract_non_territorial_search_data(ctx, data))
436
437
438 @wsgihelpers.wsgify
439 @ramdb.ramdb_based
440 def export_geographical_coverage_csv(req):
441     ctx = contexts.Ctx(req)
442
443     if conf['hide_export']:
444         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
445
446     params = req.GET
447     inputs = init_base(ctx, params)
448     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
449     inputs.update(dict(
450         accept = params.get('accept'),
451         submit = params.get('submit'),
452         ))
453
454     format = u'csv'
455     mode = u'export'
456     type = u'couverture'
457
458     accept, error = conv.pipe(conv.guess_bool, conv.default(False), conv.test_is(True))(inputs['accept'], state = ctx)
459     if error is None:
460         url_params = dict(
461             (model.Poi.rename_input_to_param(input_name), value)
462             for input_name, value in inputs.iteritems()
463             )
464         del url_params['accept']
465         del url_params['submit']
466         return wsgihelpers.redirect(ctx, location = urls.get_url(ctx, u'api/v1/{0}/{1}'.format(type, format),
467             **url_params))
468
469     data, errors = conv.merge(
470         model.Poi.make_inputs_to_search_data(),
471         conv.struct(
472             dict(
473                 accept = conv.test(lambda value: not inputs['submit'],
474                     error = N_(u"You must accept license to be allowed to download data."),
475                     handle_none_value = True,
476                     ),
477                 ),
478             default = 'drop',
479             keep_none_values = True,
480             ),
481         )(inputs, state = ctx)
482     return templates.render(ctx, '/export-accept-license.mako',
483         export_title = ctx._(u"Geographical Coverage Export in CSV Format"),
484         errors = errors,
485         format = format,
486         inputs = inputs,
487         mode = mode,
488         type = type,
489         **model.Poi.extract_non_territorial_search_data(ctx, data))
490
491
492 @wsgihelpers.wsgify
493 @ramdb.ramdb_based
494 def export_geographical_coverage_excel(req):
495     ctx = contexts.Ctx(req)
496
497     if conf['hide_export']:
498         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
499
500     params = req.GET
501     inputs = init_base(ctx, params)
502     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
503     inputs.update(dict(
504         accept = params.get('accept'),
505         submit = params.get('submit'),
506         ))
507
508     format = u'excel'
509     mode = u'export'
510     type = u'couverture'
511
512     accept, error = conv.pipe(conv.guess_bool, conv.default(False), conv.test_is(True))(inputs['accept'], state = ctx)
513     if error is None:
514         url_params = dict(
515             (model.Poi.rename_input_to_param(input_name), value)
516             for input_name, value in inputs.iteritems()
517             )
518         del url_params['accept']
519         del url_params['submit']
520         return wsgihelpers.redirect(ctx, location = urls.get_url(ctx, u'api/v1/{0}/{1}'.format(type, format),
521             **url_params))
522
523     data, errors = conv.merge(
524         model.Poi.make_inputs_to_search_data(),
525         conv.struct(
526             dict(
527                 accept = conv.test(lambda value: not inputs['submit'],
528                     error = N_(u"You must accept license to be allowed to download data."),
529                     handle_none_value = True,
530                     ),
531                 ),
532             default = 'drop',
533             keep_none_values = True,
534             ),
535         )(inputs, state = ctx)
536     return templates.render(ctx, '/export-accept-license.mako',
537         export_title = ctx._(u"Geographical Coverage Export in Excel Format"),
538         errors = errors,
539         format = format,
540         inputs = inputs,
541         mode = mode,
542         type = type,
543         **model.Poi.extract_non_territorial_search_data(ctx, data))
544
545
546 @wsgihelpers.wsgify
547 @ramdb.ramdb_based
548 def geographical_coverage_csv(req):
549     ctx = contexts.Ctx(req)
550
551     if conf['hide_export']:
552         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
553
554     params = req.GET
555     inputs = init_base(ctx, params)
556     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
557
558     csv_bytes_by_name, errors = conv.pipe(
559         conv.inputs_to_geographical_coverage_csv_infos,
560         conv.csv_infos_to_csv_bytes,
561         )(inputs, state = ctx)
562     if errors is not None:
563         return wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
564     if not csv_bytes_by_name:
565         return wsgihelpers.no_content(ctx)
566     if len(csv_bytes_by_name) == 1:
567         csv_filename, csv_bytes = csv_bytes_by_name.items()[0]
568         req.response.content_type = 'text/csv; charset=utf-8'
569         req.response.content_disposition = 'attachment;filename={0}'.format(csv_filename)
570         return csv_bytes
571     zip_file = StringIO()
572     with zipfile.ZipFile(zip_file, 'w') as zip_archive:
573         for csv_filename, csv_bytes in csv_bytes_by_name.iteritems():
574             zip_archive.writestr(csv_filename, csv_bytes)
575     req.response.content_type = 'application/zip'
576     req.response.content_disposition = 'attachment;filename=export.zip'
577     return zip_file.getvalue()
578
579
580 @wsgihelpers.wsgify
581 @ramdb.ramdb_based
582 def geographical_coverage_excel(req):
583     ctx = contexts.Ctx(req)
584
585     if conf['hide_export']:
586         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
587
588     params = req.GET
589     inputs = init_base(ctx, params)
590     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
591
592     excel_bytes, errors = conv.pipe(
593         conv.inputs_to_geographical_coverage_csv_infos,
594         conv.csv_infos_to_excel_bytes,
595         )(inputs, state = ctx)
596     if errors is not None:
597         return wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
598     if not excel_bytes:
599         raise wsgihelpers.no_content(ctx)
600     req.response.content_type = 'application/vnd.ms-excel'
601     req.response.content_disposition = 'attachment;filename=export.xls'
602     return excel_bytes
603
604
605 @wsgihelpers.wsgify
606 @ramdb.ramdb_based
607 def geojson(req):
608     ctx = contexts.Ctx(req)
609
610     params = req.GET
611     inputs = init_base(ctx, params)
612     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
613     inputs.update(dict(
614         bbox = params.get('bbox'),
615         context = params.get('context'),
616         current = params.get('current'),
617         jsonp = params.get('jsonp'),
618         ))
619
620     data, errors = conv.pipe(
621         conv.inputs_to_pois_layer_data,
622         conv.default_pois_layer_data_bbox,
623         )(inputs, state = ctx)
624     if errors is not None:
625         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
626     clusters, errors = conv.layer_data_to_clusters(data, state = ctx)
627     if errors is not None:
628         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
629
630     geojson = {
631         'type': 'FeatureCollection',
632         'properties': {
633             'context': inputs['context'],  # Parameter given in request that is returned as is.
634             'date': unicode(datetime.datetime.utcnow()),
635         },
636         'features': [
637             {
638                 'type': 'Feature',
639                 'bbox': [
640                     cluster.left,
641                     cluster.bottom,
642                     cluster.right,
643                     cluster.top,
644                     ] if cluster.count > 1 else None,
645                 'geometry': {
646                     'type': 'Point',
647                     'coordinates': [cluster.center_longitude, cluster.center_latitude],
648                     },
649                 'properties': {
650                     'competent': cluster.competent,
651                     'count': cluster.count,
652                     'id': str(cluster.center_pois[0]._id),
653                     'centerPois': [
654                         {
655                             'id': str(poi._id),
656                             'name': poi.name,
657                             'postalDistribution': poi.postal_distribution_str,
658                             'slug': poi.slug,
659                             'streetAddress': poi.street_address,
660                             }
661                         for poi in cluster.center_pois
662                         ],
663                     },
664                 }
665             for cluster in clusters
666             ],
667         }
668     territory = data['territory']
669     if territory is not None:
670         geojson['features'].insert(0, {
671             'type': 'Feature',
672             'geometry': {
673                 'type': 'Point',
674                 'coordinates': [territory.geo[1], territory.geo[0]],
675                 },
676             'properties': {
677                 'home': True,
678                 'id': str(territory._id),
679                 },
680             })
681
682     response = json.dumps(
683         geojson,
684         encoding = 'utf-8',
685         ensure_ascii = False,
686         )
687     if inputs['jsonp']:
688         req.response.content_type = 'application/javascript; charset=utf-8'
689         return u'{0}({1})'.format(inputs['jsonp'], response)
690     else:
691         req.response.content_type = 'application/json; charset=utf-8'
692         return response
693
694
695 @wsgihelpers.wsgify
696 @ramdb.ramdb_based
697 def index(req):
698     ctx = contexts.Ctx(req)
699
700     params = req.params
701     init_base(ctx, params)
702
703     # Redirect to another page.
704     enabled_tabs = [
705         tab_name
706         for tab_key, tab_name in (
707             (u'map', u'carte'),
708             (u'list', u'liste'),
709             (u'directory', u'annuaire'),
710             (u'gadget', u'partage'),
711             (u'export', u'export'),
712             )
713         if not getattr(ctx, "hide_{0}".format(tab_key))
714         ]
715     if not len(enabled_tabs):
716         enabled_tabs = [u'carte']  # Ensure there is at least one visible tab
717     url_args = (conf['default_tab'] if conf['default_tab'] in enabled_tabs else enabled_tabs[0],)
718     url_kwargs = dict(params)
719     if ctx.container_base_url is None or ctx.gadget_id is None:
720         raise wsgihelpers.redirect(ctx, location = urls.get_url(ctx, *url_args, **url_kwargs))
721     else:
722         return templates.render(ctx, '/http-simulated-redirect.mako',
723             url_args = url_args,
724             url_kwargs = url_kwargs,
725             )
726
727
728 @wsgihelpers.wsgify
729 @ramdb.ramdb_based
730 def index_directory(req):
731     ctx = contexts.Ctx(req)
732
733     if conf['hide_directory']:
734         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Directory page disabled by configuration'))
735
736     params = req.GET
737     inputs = init_base(ctx, params)
738     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
739     mode = u'annuaire'
740
741     data, errors = conv.inputs_to_pois_directory_data(inputs, state = ctx)
742     if errors is not None:
743         directory = None
744         territory = None
745         if errors.get('territory') and errors['territory'] == ctx._(u'In "directory" mode, a commune is required'):
746             del errors['territory']
747     else:
748         territory = data['territory']
749         related_territories_id = ramdb.get_territory_related_territories_id(territory)
750         filter = data['filter']
751         if territory.__class__.__name__ not in model.communes_kinds:
752             filter = 'presence'
753         if filter == 'competence':
754             competence_territories_id = related_territories_id
755             presence_territory = None
756         elif filter == 'presence':
757             competence_territories_id = None
758             presence_territory = territory
759         else:
760             competence_territories_id = None
761             presence_territory = None
762         pois_id_iter = model.Poi.iter_ids(ctx,
763             competence_territories_id = competence_territories_id or (ramdb.get_territory_related_territories_id(
764                 data['base_territory'],
765                 ) if data.get('base_territory') is not None else None),
766             presence_territory = presence_territory,
767             **model.Poi.extract_non_territorial_search_data(ctx, data))
768         pois = set(
769             poi
770             for poi in (
771                 model.Poi.instance_by_id.get(poi_id)
772                 for poi_id in pois_id_iter
773                 )
774             if poi is not None
775             )
776         territory_latitude_cos = math.cos(math.radians(territory.geo[0]))
777         territory_latitude_sin = math.sin(math.radians(territory.geo[0]))
778         distance_and_poi_couples = sorted(
779             (
780                 (
781                     6372.8 * math.acos(
782                         round(
783                             math.sin(math.radians(poi.geo[0])) * territory_latitude_sin
784                             + math.cos(math.radians(poi.geo[0])) * territory_latitude_cos
785                                 * math.cos(math.radians(poi.geo[1] - territory.geo[1])),
786                             13,
787                             )
788                         ),
789                     poi,
790                     )
791                 for poi in pois
792                 if poi.geo is not None
793                 ),
794             key = lambda distance_and_poi: distance_and_poi[0],
795             )
796         directory = {}
797         for distance, poi in distance_and_poi_couples:
798             if poi.theme_slug is None:
799                 continue
800             theme_pois = directory.get(poi.theme_slug)
801             if theme_pois is not None and len(theme_pois) >= 3 and territory.__class__.__name__ in model.communes_kinds:
802                 continue
803             if filter is None:
804                 if poi.competence_territories_id is None:
805                     # When no filter is given, when a POI has no notion of competence territory, only show it when it is
806                     # not too far away from center territory.
807                     if distance > ctx.distance:
808                         continue
809                 elif related_territories_id.isdisjoint(poi.competence_territories_id):
810                     # In directory mode without filter, the incompetent organisms must not be shown.
811                     continue
812             if theme_pois is None:
813                 directory[poi.theme_slug] = [poi]
814             else:
815                 theme_pois.append(poi)
816     return templates.render(ctx, '/directory.mako',
817         directory = directory,
818         errors = errors,
819         inputs = inputs,
820         mode = mode,
821         territory = territory,
822         **model.Poi.extract_non_territorial_search_data(ctx, data))
823
824
825 @wsgihelpers.wsgify
826 @ramdb.ramdb_based
827 def index_export(req):
828     ctx = contexts.Ctx(req)
829
830     if conf['hide_export']:
831         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Export disabled by configuration'))
832
833     params = req.GET
834     inputs = init_base(ctx, params)
835     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
836     inputs.update(dict(
837         submit = params.get('submit'),
838         type_and_format = params.get('type_and_format'),
839         ))
840     mode = u'export'
841
842     data, errors = conv.merge(
843         model.Poi.make_inputs_to_search_data(),
844         conv.struct(
845             dict(
846                 type_and_format = conv.pipe(
847                     conv.input_to_slug,
848                     conv.test_in([
849                         'annuaire-csv',
850                         'annuaire-excel',
851                         'annuaire-geojson',
852                         'annuaire-kml',
853                         'couverture-csv',
854                         'couverture-excel',
855                         ]),
856                     ),
857                 ),
858             default = 'drop',
859             keep_none_values = True,
860             ),
861         )(inputs, state = ctx)
862     if errors is None:
863         if inputs['submit']:
864             if data['type_and_format'] is not None:
865                 type, format = data['type_and_format'].rsplit(u'-', 1)
866
867                 # Form submitted. Redirect to another page.
868                 url_args = ('export', type, format)
869                 search_params_name = model.Poi.get_search_params_name(ctx)
870                 url_kwargs = dict(
871                     (param_name, value)
872                     for param_name, value in (
873                         (model.Poi.rename_input_to_param(input_name), value)
874                         for input_name, value in inputs.iteritems()
875                         )
876                     if param_name in search_params_name
877                     )
878                 if ctx.container_base_url is None or ctx.gadget_id is None:
879                     raise wsgihelpers.redirect(ctx, location = urls.get_url(ctx, *url_args, **url_kwargs))
880                 else:
881                     return templates.render(ctx, '/http-simulated-redirect.mako',
882                         url_args = url_args,
883                         url_kwargs = url_kwargs,
884                         )
885             errors = dict(
886                 type_and_format = ctx._(u'Missing value'),
887                 )
888     return templates.render(ctx, '/export.mako',
889         errors = errors,
890         inputs = inputs,
891         mode = mode,
892         **model.Poi.extract_non_territorial_search_data(ctx, data))
893
894
895 @wsgihelpers.wsgify
896 @ramdb.ramdb_based
897 def index_gadget(req):
898     ctx = contexts.Ctx(req)
899
900     if conf['hide_gadget']:
901         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Gadget page disabled by configuration'))
902
903     params = req.GET
904     inputs = init_base(ctx, params)
905     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
906     mode = u'gadget'
907
908     data, errors = conv.inputs_to_pois_list_data(inputs, state = ctx)
909
910     return templates.render(ctx, '/gadget.mako',
911         errors = errors,
912         inputs = inputs,
913         mode = mode,
914         **data)
915
916
917 @wsgihelpers.wsgify
918 @ramdb.ramdb_based
919 def index_list(req):
920     ctx = contexts.Ctx(req)
921
922     params = req.GET
923     inputs = init_base(ctx, params)
924     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
925     inputs.update(dict(
926         page = params.get('page'),
927         sort_key = params.get('sort_key'),
928         ))
929     mode = u'liste'
930
931     data, errors = conv.inputs_to_pois_list_data(inputs, state = ctx)
932     non_territorial_search_data = model.Poi.extract_non_territorial_search_data(ctx, data)
933     if errors is not None:
934         pager = None
935     else:
936         filter = data['filter']
937         territory = data['territory']
938         related_territories_id = ramdb.get_territory_related_territories_id(territory) \
939             if territory is not None else None
940         if filter == 'competence':
941             competence_territories_id = related_territories_id
942             presence_territory = None
943         elif filter == 'presence':
944             competence_territories_id = None
945             presence_territory = territory
946         else:
947             competence_territories_id = None
948             presence_territory = None
949         pois_id_iter = model.Poi.iter_ids(ctx,
950             competence_territories_id = competence_territories_id or (ramdb.get_territory_related_territories_id(
951                 data['base_territory'],
952                 ) if data.get('base_territory') is not None else None),
953             presence_territory = presence_territory,
954             **non_territorial_search_data)
955         poi_by_id = dict(
956             (poi._id, poi)
957             for poi in (
958                 model.Poi.instance_by_id.get(poi_id)
959                 for poi_id in pois_id_iter
960                 )
961             if poi is not None
962             )
963         pager = pagers.Pager(item_count = len(poi_by_id), page_number = data['page_number'])
964         pager.items = model.Poi.sort_and_paginate_pois_list(
965             ctx,
966             pager,
967             poi_by_id,
968             related_territories_id = related_territories_id,
969             territory = territory,
970             sort_key = data['sort_key'],
971             **non_territorial_search_data
972             )
973     return templates.render(ctx, '/list.mako',
974         errors = errors,
975         inputs = inputs,
976         mode = mode,
977         pager = pager,
978         **non_territorial_search_data)
979
980
981 @wsgihelpers.wsgify
982 @ramdb.ramdb_based
983 def index_map(req):
984     ctx = contexts.Ctx(req)
985
986     if conf['hide_map']:
987         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Map page disabled by configuration'))
988
989     params = req.GET
990     inputs = init_base(ctx, params)
991     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
992     inputs.update(dict(
993         bbox = params.get('bbox'),
994         ))
995     mode = u'carte'
996
997     data, errors = conv.pipe(
998         conv.inputs_to_pois_layer_data,
999         conv.default_pois_layer_data_bbox,
1000         )(inputs, state = ctx)
1001
1002     if errors is None:
1003         bbox = data['bbox']
1004         territory = data['territory']
1005     else:
1006         bbox = None
1007         territory = None
1008     return templates.render(ctx, '/map.mako',
1009         bbox = bbox,
1010         errors = errors,
1011         inputs = inputs,
1012         mode = mode,
1013         territory = territory,
1014         **model.Poi.extract_non_territorial_search_data(ctx, data))
1015
1016
1017 def init_base(ctx, params):
1018     inputs = dict(
1019         base_category = params.getall('base_category'),
1020         base_territory = params.get('base_territory'),
1021         category_tag = params.getall('category_tag'),
1022         container_base_url = params.get('container_base_url'),
1023         distance = params.get('distance'),
1024         gadget = params.get('gadget'),
1025         territory_kind = params.getall('territory_kind'),
1026         )
1027
1028     for param_visibility_name in model.Poi.get_visibility_params_names(ctx):
1029         inputs[param_visibility_name] = conf.get(param_visibility_name) or params.get(param_visibility_name)
1030         param_visibility, error = conv.pipe(
1031             conv.guess_bool,
1032             conv.default(False),
1033             )(inputs[param_visibility_name], state = ctx)
1034         if error is not None:
1035             raise wsgihelpers.bad_request(ctx, explanation = ctx._('Error for "{0}" parameter: {1}').format(
1036                 param_visibility_name, error))
1037         setattr(ctx, param_visibility_name, param_visibility)
1038
1039     ctx.base_categories_slug, error = conv.uniform_sequence(
1040         conv.input_to_tag_slug,
1041         )(inputs['base_category'], state = ctx)
1042     if error is not None:
1043         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Base Categories Error: {0}').format(error))
1044
1045     ctx.category_tags_slug, error = conv.uniform_sequence(
1046         conv.input_to_tag_slug,
1047         )(inputs['category_tag'], state = ctx)
1048     if error is not None:
1049         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Category Tags Error: {0}').format(error))
1050
1051     container_base_url = inputs['container_base_url'] or None
1052 #    if container_base_url is None:
1053 #        container_hostname = None
1054 #    else:
1055 #        container_hostname = urlparse.urlsplit(container_base_url).hostname or None
1056     try:
1057         gadget_id = int(inputs['gadget'])
1058     except (TypeError, ValueError):
1059         gadget_id = None
1060     if gadget_id is None:
1061         if container_base_url is not None:
1062             # Ignore container site when no gadget ID is given.
1063             container_base_url = None
1064 #             container_hostname = None
1065     elif conf['require_subscription']:
1066         subscriber = model.Subscriber.find_one({'sites.subscriptions.id': gadget_id})
1067         if subscriber is None:
1068             raise wsgihelpers.bad_request(ctx,
1069                 comment = markupsafe.Markup(u'{0}<a href="{1}">{2}</a>{3}').format(
1070                     ctx._('Connect to '),
1071                     conf['brand_url'],
1072                     conf['brand_name'],
1073                     ctx._(', rebuild component and copy the generated JavaScript into your website.'),
1074                     ),
1075                 explanation = ctx._('''The gadget ID "{0}" doesn't exist.'''), title = ctx._('Invalid Gadget ID'))
1076         for site in subscriber.sites or []:
1077             for subscription in (site.subscriptions or []):
1078                 if subscription.id == gadget_id and subscription.type == u'etalage':
1079                     break
1080             else:
1081                 continue
1082             break
1083         else:
1084             raise wsgihelpers.bad_request(ctx,
1085                 comment = markupsafe.Markup(u'{0}<a href="{1}">{2}</a>{3}').format(
1086                     ctx._('Connect to '),
1087                     conf['brand_url'],
1088                     conf['brand_name'],
1089                     ctx._(', rebuild component and copy the generated JavaScript into your website.'),
1090                     ),
1091                 explanation = ctx._('''The gadget ID "{0}" is used by another component.'''),
1092                 title = ctx._('Invalid Gadget ID'))
1093         ctx.subscriber = subscriber
1094         if gadget_id is not None and container_base_url is None and subscription.url is not None:
1095             # When in gadget mode but without a container_base_url, we are accessed through the noscript iframe or by a
1096             # search engine. We need to retrieve the URL of page containing gadget to do a JavaScript redirection (in
1097             # publication.mako).
1098             container_base_url = subscription.url or None
1099 #             container_hostname = urlparse.urlsplit(container_base_url).hostname or None
1100     ctx.container_base_url = container_base_url
1101     ctx.gadget_id = gadget_id
1102
1103     ctx.base_territory, error = conv.input_to_postal_distribution_to_geolocated_territory(
1104         inputs['base_territory'],
1105         state = ctx,
1106         )
1107     if error is not None:
1108         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Base Territory Error: {0}').format(error))
1109     if ctx.subscriber is not None:
1110         subscriber_territory = ctx.subscriber.territory
1111         if subscriber_territory._id not in ctx.base_territory.ancestors_id:
1112             raise wsgihelpers.not_found(ctx, body = htmlhelpers.modify_html(ctx, templates.render(ctx,
1113                 '/error-invalid-territory.mako', parent_territory = subscriber_territory,
1114                 territory = ctx.base_territory)))
1115     if ctx.base_territory is None and ctx.subscriber is not None and ctx.subscriber.territory is not None:
1116         ctx.base_territory = Territory.get_variant_class(
1117             ctx.subscriber.territory['kind']).get(ctx.subscriber.territory['code']
1118             )
1119         if ctx.base_territory is None:
1120             raise wsgihelpers.not_found(ctx, body = htmlhelpers.modify_html(ctx, templates.render(ctx,
1121                 '/error-unknown-territory.mako', territory_code = ctx.subscriber.territory['code'],
1122                 territory_kind = ctx.subscriber.territory['kind'])))
1123
1124     ctx.distance, error = conv.pipe(
1125         conv.input_to_float,
1126         conv.test_between(0.0, 40075.16),
1127         conv.default(20.0),
1128         )(inputs['distance'], state = ctx)
1129     if error is not None:
1130         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Distance Error: {0}').format(error))
1131
1132     ctx.autocompleter_territories_kinds, errors = conv.uniform_sequence(
1133         conv.test_in(conf['autocompleter_territories_kinds'])
1134         )(inputs['territory_kind'], state = ctx)
1135     if errors is not None:
1136         autocompleter_territories_kinds = [
1137             territory_kind
1138             for index, territory_kind in enumerate(ctx.autocompleter_territories_kinds)
1139             if index not in errors
1140             ]
1141         ctx.autocompleter_territories_kinds = autocompleter_territories_kinds or conf['autocompleter_territories_kinds']
1142     return inputs
1143
1144
1145 @wsgihelpers.wsgify
1146 @ramdb.ramdb_based
1147 def kml(req):
1148     ctx = contexts.Ctx(req)
1149
1150     params = req.GET
1151     inputs = init_base(ctx, params)
1152     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
1153     inputs.update(dict(
1154         bbox = params.get('bbox'),
1155         context = params.get('context'),
1156         current = params.get('current'),
1157         ))
1158
1159     clusters, errors = conv.pipe(
1160         conv.inputs_to_pois_layer_data,
1161         conv.default_pois_layer_data_bbox,
1162         conv.layer_data_to_clusters,
1163         )(inputs, state = ctx)
1164     if errors is not None:
1165         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors))
1166
1167     req.response.content_type = 'application/vnd.google-earth.kml+xml; charset=utf-8'
1168     return templates.render(ctx, '/kml.mako',
1169         clusters = clusters,
1170         inputs = inputs,
1171         )
1172
1173
1174 def make_router():
1175     """Return a WSGI application that dispatches requests to controllers """
1176     return urls.make_router(
1177         ('GET', '^/?$', index),
1178         ('GET', '^/a-propos/?$', about),
1179         ('GET', '^/annuaire/?$', index_directory),
1180         ('GET', '^/api/v1/annuaire/csv/?$', csv),
1181         ('GET', '^/api/v1/annuaire/excel/?$', excel),
1182         ('GET', '^/api/v1/annuaire/geojson/?$', geojson),
1183         ('GET', '^/api/v1/annuaire/kml/?$', kml),
1184         ('GET', '^/api/v1/categories/autocomplete/?$', autocomplete_category),
1185         ('GET', '^/api/v1/couverture/csv/?$', geographical_coverage_csv),
1186         ('GET', '^/api/v1/couverture/excel/?$', geographical_coverage_excel),
1187         ('GET', '^/carte/?$', index_map),
1188         ('GET', '^/export/?$', index_export),
1189         ('GET', '^/export/annuaire/csv/?$', export_directory_csv),
1190         ('GET', '^/export/annuaire/excel/?$', export_directory_excel),
1191         ('GET', '^/export/annuaire/geojson/?$', export_directory_geojson),
1192         ('GET', '^/export/annuaire/kml/?$', export_directory_kml),
1193         ('GET', '^/export/couverture/csv/?$', export_geographical_coverage_csv),
1194         ('GET', '^/export/couverture/excel/?$', export_geographical_coverage_excel),
1195         ('GET', '^/fragment/organismes/(?P<poi_id>[a-z0-9]{24})/?$', poi_embedded),
1196         ('GET', '^/fragment/organismes/(?P<slug>[^/]+)/(?P<poi_id>[a-z0-9]{24})/?$', poi_embedded),
1197         ('GET', '^/gadget/?$', index_gadget),
1198         ('GET', '^/liste/?$', index_list),
1199         ('GET', '^/minisite/organismes/(?P<poi_id>[a-z0-9]{24})/?$', minisite),
1200         ('GET', '^/minisite/organismes/(?P<slug>[^/]+)/(?P<poi_id>[a-z0-9]{24})/?$', minisite),
1201         ('GET', '^/organismes/(?P<poi_id>[a-z0-9]{24})/?$', poi),
1202         ('GET', '^/organismes/(?P<slug>[^/]+)/(?P<poi_id>[a-z0-9]{24})/?$', poi),
1203         )
1204
1205
1206 @wsgihelpers.wsgify
1207 @ramdb.ramdb_based
1208 def minisite(req):
1209     ctx = contexts.Ctx(req)
1210
1211     if conf['hide_minisite']:
1212         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Minisite disabled by configuration'))
1213
1214     params = req.params
1215     inputs = init_base(ctx, params)
1216     inputs.update(dict(
1217         encoding = params.get('encoding') or u'',
1218         poi_id = req.urlvars.get('poi_id'),
1219         slug = req.urlvars.get('slug'),
1220         ))
1221
1222     data, errors = conv.pipe(
1223         conv.struct(
1224             dict(
1225                 poi_id = conv.pipe(
1226                     conv.input_to_object_id,
1227                     conv.id_to_poi,
1228                     conv.not_none,
1229                     ),
1230                 encoding = conv.pipe(
1231                     conv.input_to_slug,
1232                     conv.translate({u'utf-8': None}),
1233                     conv.test_in([u'cp1252', u'iso-8859-1', u'iso-8859-15']),
1234                     ),
1235                 ),
1236             default = 'drop',
1237             keep_none_values = True,
1238             ),
1239         conv.rename_item('poi_id', 'poi'),
1240         )(inputs, state = ctx)
1241
1242     if errors is not None and errors.get('poi_id'):
1243         return wsgihelpers.bad_request(ctx, explanation = ctx._('Error: {0}').format(errors['poi_id']))
1244
1245     data['url'] = url = urls.get_full_url(ctx, 'fragment', 'organismes', data['poi'].slug, data['poi']._id,
1246         encoding = data['encoding'])
1247     try:
1248         fragment = urllib2.urlopen(url).read().decode(data['encoding'] or 'utf-8')
1249     except:
1250         errors = dict(fragment = ctx._('Access to organism failed'))
1251     else:
1252         data['fragment'] = fragment
1253     return templates.render(ctx, '/minisite.mako', errors = errors, inputs = inputs, **data)
1254
1255
1256 @wsgihelpers.wsgify
1257 @ramdb.ramdb_based
1258 def poi(req):
1259     ctx = contexts.Ctx(req)
1260
1261     params = req.params
1262     inputs = init_base(ctx, params)
1263     inputs.update(dict(
1264         poi_id = req.urlvars.get('poi_id'),
1265         slug = req.urlvars.get('slug'),
1266         ))
1267
1268     poi, error = conv.pipe(
1269         conv.input_to_object_id,
1270         conv.id_to_poi,
1271         conv.not_none,
1272         )(inputs['poi_id'], state = ctx)
1273     if error is not None:
1274         raise wsgihelpers.bad_request(ctx, explanation = ctx._('POI ID Error: {0}').format(error))
1275
1276     slug = poi.slug
1277     if inputs['slug'] != slug:
1278         if ctx.container_base_url is None or ctx.gadget_id is None:
1279             raise wsgihelpers.redirect(ctx, location = urls.get_url(ctx, 'organismes', slug, poi._id))
1280         # In gadget mode, there is no need to redirect.
1281
1282     return templates.render(ctx, '/poi.mako', poi = poi)
1283
1284
1285 @wsgihelpers.wsgify
1286 @ramdb.ramdb_based
1287 def poi_embedded(req):
1288     ctx = contexts.Ctx(req)
1289
1290     if conf['hide_minisite']:
1291         return wsgihelpers.not_found(ctx, explanation = ctx._(u'Minisite disabled by configuration'))
1292
1293     params = req.params
1294     inputs = init_base(ctx, params)
1295     inputs.update(model.Poi.extract_search_inputs_from_params(ctx, params))
1296     inputs.update(dict(
1297         encoding = params.get('encoding') or u'',
1298         poi_id = req.urlvars.get('poi_id'),
1299         slug = req.urlvars.get('slug'),
1300         ))
1301
1302     poi, error = conv.pipe(
1303         conv.input_to_object_id,
1304         conv.id_to_poi,
1305         conv.not_none,
1306         )(inputs['poi_id'], state = ctx)
1307     if error is not None:
1308         raise wsgihelpers.bad_request(ctx, explanation = ctx._('POI ID Error: {0}').format(error))
1309
1310     encoding, error = conv.pipe(
1311         conv.input_to_slug,
1312         conv.translate({u'utf-8': None}),
1313         conv.test_in([u'cp1252', u'iso-8859-1', u'iso-8859-15']),
1314         )(inputs['encoding'], state = ctx)
1315     if error is not None:
1316         raise wsgihelpers.bad_request(ctx, explanation = ctx._('Encoding Error: {0}').format(error))
1317
1318     text = templates.render(ctx, '/poi-embedded.mako', poi = poi)
1319     if encoding is None:
1320         req.response.content_type = 'text/plain; charset=utf-8'
1321         return text
1322     else:
1323         req.response.content_type = 'text/plain; charset={0}'.format(encoding)
1324         return text.encode(encoding, errors = 'xmlcharrefreplace')