]> git.ipfire.org Git - ipfire.org.git/blame - src/web/__init__.py
location: Fix links
[ipfire.org.git] / src / web / __init__.py
CommitLineData
81675874 1#/usr/bin/python
2
feb02477 3import logging
81675874 4import os.path
c01ad253 5import phonenumbers
e96e445b 6import phonenumbers.geocoder
81675874 7import tornado.locale
a49b5422 8import tornado.options
81675874 9import tornado.web
10
a95c2f97 11import ipfire
574a88c7 12import ipfire.countries
440aba92 13from .. import util
feb02477 14
11347e46 15from .handlers import *
81675874 16
672be316 17from . import analytics
08df6527 18from . import auth
12e5de7e 19from . import blog
f301d952 20from . import boot
1958a22b 21from . import docs
c7bcb9ca 22from . import donate
aec63a26 23from . import downloads
96c9bb79 24from . import fireinfo
699a0911 25from . import iuse
97e15cf6 26from . import lists
f5b01fc2 27from . import location
a41085fb 28from . import nopaste
df6180a5 29from . import ui_modules
b01a1ee3 30from . import users
4235ba55 31from . import voip
12e5de7e 32
81675874 33class Application(tornado.web.Application):
3e7c6ccd
MT
34 def __init__(self, config, **kwargs):
35 # Initialize backend
a95c2f97 36 self.backend = ipfire.Backend(config)
a6dc0bad 37
9ed02e3b
MT
38 settings = {
39 # Do not compress responses
40 "gzip" : False,
41
42 # Enable XSRF cookies
43 "xsrf_cookies" : True,
a510f87d
MT
44 "xsrf_cookie_kwargs" : {
45 "secure" : True,
46 },
9ed02e3b
MT
47
48 # Login
49 "login_url" : "/login",
50
51 # Setup directory structure
52 "static_path" : self.backend.config.get("global", "static_dir"),
53 "template_path" : self.backend.config.get("global", "templates_dir"),
54
eabe6b8d 55 # UI Methods
9ed02e3b 56 "ui_methods" : {
574a88c7 57 "format_country_name" : self.format_country_name,
6eddfb50 58 "format_language_name" : self.format_language_name,
e96e445b
MT
59 "format_month_name" : self.format_month_name,
60 "format_phone_number" : self.format_phone_number,
61 "format_phone_number_to_e164" : self.format_phone_number_to_e164,
62 "format_phone_number_location" : self.format_phone_number_location,
cc3b928d 63 },
eabe6b8d
MT
64
65 # UI Modules
9ed02e3b 66 "ui_modules" : {
672be316
MT
67 # Analytics
68 "AnalyticsSummary" : analytics.SummaryModule,
69
5806d6fc
MT
70 # Auth
71 "Password" : auth.PasswordModule,
72
f5b01fc2 73 # Blog
7e64f6a3
MT
74 "BlogHistoryNavigation": blog.HistoryNavigationModule,
75 "BlogList" : blog.ListModule,
f91dfcc7 76
93feb275
MT
77 # Boot
78 "BootMenuConfig" : boot.MenuConfigModule,
79 "BootMenuHeader" : boot.MenuHeaderModule,
80 "BootMenuSeparator" : boot.MenuSeparatorModule,
81
cf59466c 82 # Docs
739fff76 83 "DocsDiff" : docs.DiffModule,
cf59466c 84 "DocsHeader" : docs.HeaderModule,
d25f886f 85 "DocsList" : docs.ListModule,
cf59466c 86
e1814f16
MT
87 # Nopaste
88 "Code" : nopaste.CodeModule,
89
23f0179e 90 # Fireinfo
96c9bb79 91 "FireinfoDeviceTable" : fireinfo.DeviceTableModule,
eabe6b8d
MT
92 "FireinfoDeviceAndGroupsTable"
93 : fireinfo.DeviceAndGroupsTableModule,
94
beb13102
MT
95 # Users
96 "UsersList" : users.ListModule,
97
4235ba55 98 # VoIP
214a68a0 99 "VoIPConferences" : voip.ConferencesModule,
00465786
MT
100 "VoIPOutboundRegistrations" :
101 voip.OutboundRegistrationsModule,
8e93325b 102 "VoIPQueues" : voip.QueuesModule,
4235ba55
MT
103 "VoIPRegistrations" : voip.RegistrationsModule,
104
eabe6b8d 105 # Misc
6c6de80a 106 "IPFireLogo" : ui_modules.IPFireLogoModule,
1c4522dc 107 "Markdown" : ui_modules.MarkdownModule,
eabe6b8d
MT
108 "Map" : ui_modules.MapModule,
109 "ProgressBar" : ui_modules.ProgressBarModule,
81675874 110 },
3403dc5e
MT
111
112 # Call this when a page wasn't found
b22bc8e8 113 "default_handler_class" : base.NotFoundHandler,
9ed02e3b 114 }
9068dba1 115 settings.update(kwargs)
5cf160e0 116
ae0228e1
MT
117 tornado.web.Application.__init__(self, **settings)
118
66862195 119 authentication_handlers = [
08df6527
MT
120 (r"/login", auth.LoginHandler),
121 (r"/logout", auth.LogoutHandler),
66862195
MT
122 ]
123
42e745e9 124 self.add_handlers(r"www\.([a-z]+\.dev\.)?ipfire\.org", [
940227cb
MT
125 # Entry site that lead the user to index
126 (r"/", IndexHandler),
940227cb 127
35ab4b94
MT
128 # Analytics
129 (r"/analytics", analytics.IndexHandler),
55ed268d 130 (r"/analytics/docs", analytics.DocsHandler),
35ab4b94 131
8505c8cd 132 # Authentication
268a972b 133 (r"/join", auth.JoinHandler),
8505c8cd
MT
134 (r"/login", auth.LoginHandler),
135 (r"/logout", auth.LogoutHandler),
42c7cc66 136 (r"/activate/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.ActivateHandler),
8505c8cd 137
34472923
MT
138 # Blog
139 (r"/blog", blog.IndexHandler),
34472923
MT
140 (r"/blog/drafts", blog.DraftsHandler),
141 (r"/blog/feed.xml", blog.FeedHandler),
4d657f4f 142 (r"/blog/write", blog.WriteHandler),
feb245e0 143 (r"/blog/years/([0-9]{4})", blog.YearHandler),
34472923
MT
144 (r"/blog/([0-9a-z\-\._]+)", blog.PostHandler),
145 (r"/blog/([0-9a-z\-\._]+)/delete", blog.DeleteHandler),
146 (r"/blog/([0-9a-z\-\._]+)/edit", blog.EditHandler),
147 (r"/blog/([0-9a-z\-\._]+)/publish", blog.PublishHandler),
56801afb 148 (r"/blog/([0-9a-z\-\._]+)/debug/email", blog.DebugEmailHandler),
34472923 149
1958a22b 150 # Docs
5f0d294e 151 (r"/docs/recent\-changes", docs.RecentChangesHandler),
0ce8cc32 152 (r"/docs/search", docs.SearchHandler),
350f391e 153 (r"/docs/tree", docs.TreeHandler),
16619e57 154 (r"/docs/watchlist", docs.WatchlistHandler),
bb998aca 155 (r"/docs/_restore", docs.RestoreHandler),
26ade731 156 (r"/docs/_upload", docs.UploadHandler),
3cfea281
MT
157 (r"/docs(/[A-Za-z0-9\-_\/]+)?/_edit", docs.EditHandler),
158 (r"/docs(/[A-Za-z0-9\-_\/]+)?/_render", docs.RenderHandler),
159 (r"/docs(/[A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", docs.WatchHandler),
107881bb 160 (r"/docs(/[A-Za-z0-9\-_\/]+)?/_files", docs.FilesHandler),
3cfea281
MT
161 (r"/docs(/[A-Za-z0-9\-_\/]+(?:.*)\.(?:\w+))/_delete", docs.DeleteFileHandler),
162 (r"/docs(/[A-Za-z0-9\-_\/]+(?:.*)\.(?:\w+))$", docs.FileHandler),
803f1cc2 163 (r"/docs(/[A-Za-z0-9\-_\/]*)?", docs.PageHandler),
1958a22b 164
aec63a26
MT
165 # Downloads
166 (r"/downloads", downloads.IndexHandler),
930647fc 167 (r"/downloads/cloud", StaticHandler, { "template" : "downloads/cloud.html" }),
aec63a26
MT
168 (r"/downloads/mirrors", downloads.MirrorsHandler),
169 (r"/downloads/thank-you", downloads.ThankYouHandler),
170 (r"/downloads/([0-9a-z\-\.]+)", downloads.ReleaseHandler),
171
e64ce07e 172 # Donate
c7bcb9ca
MT
173 (r"/donate", donate.DonateHandler),
174 (r"/donate/thank-you", donate.ThankYouHandler),
175 (r"/donate/error", donate.ErrorHandler),
d37d6cd2 176 (r"/donate/check-vat-number", donate.CheckVATNumberHandler),
8d48f4ef 177
a4422469
RH
178 # Fireinfo
179 (r"/fireinfo", fireinfo.IndexHandler),
180 (r"/fireinfo/admin", fireinfo.AdminIndexHandler),
181 (r"/fireinfo/vendors", fireinfo.VendorsHandler),
182 (r"/fireinfo/vendors/(pci|usb)/([0-9a-f]{4})", fireinfo.VendorHandler),
183 (r"/fireinfo/drivers/(.*)", fireinfo.DriverDetail),
184 (r"/fireinfo/profile/random", fireinfo.RandomProfileHandler),
185 (r"/fireinfo/profile/([a-z0-9]{40})", fireinfo.ProfileHandler),
186 (r"/fireinfo/processors", fireinfo.ProcessorsHandler),
187 (r"/fireinfo/releases", fireinfo.ReleasesHandler),
a4422469 188
97e15cf6
MT
189 # Lists
190 (r"/lists", lists.IndexHandler),
191
8066a1e7
MT
192 # Password Reset
193 (r"/password\-reset", auth.PasswordResetInitiationHandler),
194 (r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler),
69a212f3 195 (r"/.well-known/change-password", auth.WellKnownChangePasswordHandler),
8066a1e7 196
d9f7b6e5 197 # Projects
a4422469
RH
198 (r"/location/?", location.IndexHandler),
199 (r"/location/download", StaticHandler, { "template" : "location/download.html" }),
200 (r"/location/how\-to\-use", StaticHandler, { "template" : "location/how-to-use.html" }),
201 (r"/location/lookup/(.+)", location.LookupHandler),
d9f7b6e5 202
2fe1d960
MT
203 # Single-Sign-On for Discourse
204 (r"/sso/discourse", auth.SSODiscourse),
205
f2c38da0
MT
206 # User Groups
207 (r"/users/groups", users.GroupIndexHandler),
208 (r"/users/groups/([a-z_][a-z0-9_-]{0,31})", users.GroupShowHandler),
209
b01a1ee3 210 # Users
beb13102 211 (r"/users", users.IndexHandler),
53b2117f
MT
212 (r"/users/([a-z_][a-z0-9_-]{0,31})", users.ShowHandler),
213 (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", users.AvatarHandler),
1cd4d7d3 214 (r"/users/([a-z_][a-z0-9_-]{0,31})/delete", users.DeleteHandler),
3c986f14 215 (r"/users/([a-z_][a-z0-9_-]{0,31})/edit", users.EditHandler),
e4d2f51f 216 (r"/users/([a-z_][a-z0-9_-]{0,31})/passwd", users.PasswdHandler),
b01a1ee3 217
bb440bad
MT
218 # Promotional Consent Stuff
219 (r"/subscribe", users.SubscribeHandler),
220 (r"/unsubscribe", users.UnsubscribeHandler),
221
4235ba55
MT
222 # VoIP
223 (r"/voip", voip.IndexHandler),
224
45592df5 225 # Static Pages
930647fc
MT
226 (r"/about", StaticHandler, { "template" : "static/about.html" }),
227 (r"/legal", StaticHandler, { "template" : "static/legal.html" }),
228 (r"/help", StaticHandler, { "template" : "static/help.html" }),
8f9e394f 229 (r"/partners", StaticHandler, { "template" : "static/partners.html" }),
930647fc 230 (r"/sitemap", StaticHandler, { "template" : "static/sitemap.html" }),
45592df5 231
86d86819
MT
232 # API
233 (r"/api/check/email", auth.APICheckEmail),
234 (r"/api/check/uid", auth.APICheckUID),
235
14cd4fa8 236 # Handle old pages that have moved elsewhere
1f153396 237 (r"/blog/authors/(\w+)", tornado.web.RedirectHandler, { "url" : "/users/{0}" }),
7fa7d38d
MT
238 (r"/donation", tornado.web.RedirectHandler, { "url" : "/donate" }),
239 (r"/download", tornado.web.RedirectHandler, { "url" : "/downloads" }),
240 (r"/download/([0-9a-z\-\.]+)", tornado.web.RedirectHandler, { "url" : "/downloads/{0}" }),
85c977e2 241 (r"/features", tornado.web.RedirectHandler, { "url" : "/about" }),
14cd4fa8 242 (r"/imprint", tornado.web.RedirectHandler, { "url" : "/legal" }),
7fa7d38d
MT
243 (r"/news.rss", tornado.web.RedirectHandler, { "url" : "/blog/feed.xml" }),
244 (r"/news/(.*)", tornado.web.RedirectHandler, { "url" : "/blog/{0}" }),
d2000918 245 (r"/support", tornado.web.RedirectHandler, { "url" : "/help"}),
7fa7d38d 246 (r"/(de|en)/(.*)", tornado.web.RedirectHandler, { "url" : "/{0}"}),
37ed7c3c
MT
247
248 # Export arbitrary error pages
b22bc8e8 249 (r"/error/([45][0-9]{2})", base.ErrorHandler),
0ed3ea5c
MT
250
251 # Serve any static files
252 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
940227cb
MT
253 ])
254
34472923 255 # blog.ipfire.org - LEGACY REDIRECTION
c85f80e2 256 self.add_handlers(r"blog\.([a-z]+\.dev\.)?ipfire\.org", [
34472923
MT
257 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog" }),
258 (r"/authors/(\w+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/authors/{0}" }),
259 (r"/post/([0-9a-z\-\._]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/{0}" }),
260 (r"/tags/([0-9a-z\-\.]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/tags/{0}" }),
f0714277
MT
261
262 # RSS Feed
34472923
MT
263 (r"/feed.xml", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/feed.xml" }),
264 ])
12e5de7e 265
940227cb 266 # downloads.ipfire.org
c85f80e2 267 self.add_handlers(r"downloads\.([a-z]+\.dev\.)?ipfire\.org", [
82d4e789 268 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download" }),
450972a3 269 (r"/release/(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/{0}" }),
aec63a26 270 (r"/(.*)", downloads.FileHandler),
54af860e 271 ])
940227cb
MT
272
273 # mirrors.ipfire.org
c85f80e2 274 self.add_handlers(r"mirrors\.([a-z]+\.dev\.)?ipfire\.org", [
278b3118
MT
275 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/mirrors" }),
276 (r"/mirrors/(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/mirrors/{0}" }),
3808b871 277 ])
940227cb 278
d0d074e0 279 # planet.ipfire.org
c85f80e2 280 self.add_handlers(r"planet\.([a-z]+\.dev\.)?ipfire\.org", [
3d4ce901 281 (r"/", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/" }),
7fa7d38d
MT
282 (r"/post/([A-Za-z0-9_-]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/{0}" }),
283 (r"/user/([a-z0-9_-]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/authors/{0}" }),
bcc3ed4d
MT
284
285 # RSS
f0714277 286 (r"/rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
d76ec66e 287 (r"/user/([a-z0-9_-]+)/rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
f0714277 288 (r"/news.rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
3808b871 289 ])
d0d074e0 290
945aff38 291 # fireinfo.ipfire.org
c85f80e2 292 self.add_handlers(r"fireinfo\.([a-z]+\.dev\.)?ipfire\.org", [
945aff38 293 # Handle profiles
f784406c
MT
294 (r"/fireinfo/send/([a-z0-9]+)", fireinfo.ProfileSendHandler),
295
945aff38
MT
296 # Redirect anything else
297 (r"(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/fireinfo{0}" }),
298 ])
5cf160e0 299
c37ec602 300 # i-use.ipfire.org
c85f80e2 301 self.add_handlers(r"i-use\.([a-z]+\.dev\.)?ipfire\.org", [
e2f2865d 302 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/" }),
395c1ac0 303 (r"/profile/([a-f0-9]{40})/([0-9]+).png", iuse.ImageHandler),
c37ec602
MT
304 ])
305
8e2e1261
MT
306 # boot.ipfire.org
307 BOOT_STATIC_PATH = os.path.join(self.settings["static_path"], "netboot")
c85f80e2 308 self.add_handlers(r"boot\.([a-z]+\.dev\.)?ipfire\.org", [
f301d952 309 (r"/", tornado.web.RedirectHandler, { "url" : "https://wiki.ipfire.org/installation/pxe" }),
8e2e1261
MT
310
311 # Configurations
f301d952
MT
312 (r"/premenu.cfg", boot.PremenuCfgHandler),
313 (r"/menu.gpxe", boot.MenuGPXEHandler),
314 (r"/menu.cfg", boot.MenuCfgHandler),
8e2e1261
MT
315
316 # Static files
37b5c0cf 317 (r"/(boot\.png|pxelinux\.0|menu\.c32|vesamenu\.c32)",
8e2e1261
MT
318 tornado.web.StaticFileHandler, { "path" : BOOT_STATIC_PATH }),
319 ])
320
60024cc8 321 # nopaste.ipfire.org
c85f80e2 322 self.add_handlers(r"nopaste\.([a-z]+\.dev\.)?ipfire\.org", [
a41085fb 323 (r"/", nopaste.CreateHandler),
0ad49bfe
MT
324 (r"/upload", nopaste.UploadHandler),
325
326 # View
a41085fb
MT
327 (r"/raw/(.*)", nopaste.RawHandler),
328 (r"/view/(.*)", nopaste.ViewHandler),
0ed3ea5c
MT
329
330 # Serve any static files
331 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
fbac12cb 332 ] + authentication_handlers)
60024cc8 333
f5b01fc2 334 # location.ipfire.org
c85f80e2 335 self.add_handlers(r"location\.([a-z]+\.dev\.)?ipfire\.org", [
42e745e9 336 (r"(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/projects/location{0}" }),
f5b01fc2
MT
337 ])
338
9068dba1 339 # geoip.ipfire.org
c85f80e2 340 self.add_handlers(r"geoip\.([a-z]+\.dev\.)?ipfire\.org", [
f5b01fc2 341 (r"/", tornado.web.RedirectHandler, { "url" : "https://location.ipfire.org/" }),
3808b871 342 ])
9068dba1 343
66862195 344 # talk.ipfire.org
c85f80e2 345 self.add_handlers(r"talk\.([a-z]+\.dev\.)?ipfire\.org", [
786e9ca8
MT
346 (r"/", tornado.web.RedirectHandler, { "url" : "https://people.ipfire.org/" }),
347 ])
66862195 348
03706893 349 # people.ipfire.org
c85f80e2 350 self.add_handlers(r"people\.([a-z]+\.dev\.)?ipfire\.org", [
b6026905 351 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users" }),
268a972b 352 (r"/register", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/join" }),
b6026905
MT
353 (r"/users", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users" }),
354 (r"/users/([a-z_][a-z0-9_-]{0,31})", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users/{0}" }),
355 (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/users/{0}.jpg" }),
356 ])
2cd9af74 357
181d08f3 358 # wiki.ipfire.org
9741abf5 359 self.add_handlers(r"wiki\.([a-z]+\.dev\.)?ipfire\.org", [
42e745e9 360 (r"(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/docs{0}" }),
181d08f3
MT
361 ])
362
ae0228e1 363 # ipfire.org
42e745e9
MT
364 self.add_handlers(r"([a-z]+\.dev\.)?ipfire\.org", [
365 (r".*", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/" })
5cf160e0 366 ])
3add293a 367
feb02477
MT
368 logging.info("Successfully initialied application")
369
574a88c7 370 def format_country_name(self, handler, country_code):
e929ed92 371 return self.backend.get_country_name(country_code)
574a88c7 372
6eddfb50
MT
373 def format_language_name(self, handler, language):
374 _ = handler.locale.translate
375
376 if language == "de":
377 return _("German")
378 elif language == "en":
379 return _("English")
380 elif language == "es":
381 return _("Spanish")
382 elif language == "fr":
383 return _("French")
384 elif language == "it":
385 return _("Italian")
386 elif language == "nl":
387 return _("Dutch")
388 elif language == "pl":
389 return _("Polish")
390 elif language == "pt":
391 return _("Portuguese")
392 elif language == "ru":
393 return _("Russian")
394 elif language == "tr":
395 return _("Turkish")
396
397 return language
398
cc3b928d
MT
399 def format_month_name(self, handler, month):
400 _ = handler.locale.translate
401
402 if month == 1:
403 return _("January")
404 elif month == 2:
405 return _("February")
406 elif month == 3:
407 return _("March")
408 elif month == 4:
409 return _("April")
410 elif month == 5:
411 return _("May")
412 elif month == 6:
413 return _("June")
414 elif month == 7:
415 return _("July")
416 elif month == 8:
417 return _("August")
418 elif month == 9:
419 return _("September")
420 elif month == 10:
421 return _("October")
422 elif month == 11:
423 return _("November")
424 elif month == 12:
425 return _("December")
426
427 return month
401827c2 428
e96e445b
MT
429 def format_phone_number(self, handler, number):
430 if not isinstance(number, phonenumbers.PhoneNumber):
431 try:
01e73b0e 432 number = phonenumbers.parse(number, None)
e96e445b
MT
433 except phonenumbers.phonenumberutil.NumberParseException:
434 return number
c01ad253
MT
435
436 return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
437
e96e445b
MT
438 def format_phone_number_to_e164(self, handler, number):
439 return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
440
441 def format_phone_number_location(self, handler, number):
442 s = [
443 phonenumbers.geocoder.description_for_number(number, handler.locale.code),
444 phonenumbers.region_code_for_number(number),
445 ]
446
447 return ", ".join((e for e in s if e))