]> git.ipfire.org Git - ipfire.org.git/blame - src/web/__init__.py
voip: Show queues
[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
08df6527 17from . import auth
12e5de7e 18from . import blog
f301d952 19from . import boot
1958a22b 20from . import docs
c7bcb9ca 21from . import donate
aec63a26 22from . import downloads
96c9bb79 23from . import fireinfo
699a0911 24from . import iuse
f5b01fc2 25from . import location
a41085fb 26from . import nopaste
03706893 27from . import people
df6180a5 28from . import ui_modules
b01a1ee3 29from . import users
4235ba55 30from . import voip
181d08f3 31from . import wiki
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,
44
45 # Login
46 "login_url" : "/login",
47
48 # Setup directory structure
49 "static_path" : self.backend.config.get("global", "static_dir"),
50 "template_path" : self.backend.config.get("global", "templates_dir"),
51
eabe6b8d 52 # UI Methods
9ed02e3b 53 "ui_methods" : {
574a88c7 54 "format_country_name" : self.format_country_name,
6eddfb50 55 "format_language_name" : self.format_language_name,
e96e445b
MT
56 "format_month_name" : self.format_month_name,
57 "format_phone_number" : self.format_phone_number,
58 "format_phone_number_to_e164" : self.format_phone_number_to_e164,
59 "format_phone_number_location" : self.format_phone_number_location,
cc3b928d 60 },
eabe6b8d
MT
61
62 # UI Modules
9ed02e3b 63 "ui_modules" : {
f5b01fc2 64 # Blog
7e64f6a3
MT
65 "BlogHistoryNavigation": blog.HistoryNavigationModule,
66 "BlogList" : blog.ListModule,
f91dfcc7
MT
67 "BlogPost" : blog.PostModule,
68
93feb275
MT
69 # Boot
70 "BootMenuConfig" : boot.MenuConfigModule,
71 "BootMenuHeader" : boot.MenuHeaderModule,
72 "BootMenuSeparator" : boot.MenuSeparatorModule,
73
cf59466c
MT
74 # Docs
75 "DocsHeader" : docs.HeaderModule,
76
eabe6b8d 77 # People
c66f2152 78 "Agent" : people.AgentModule,
dbb0c109
MT
79 "CDR" : people.CDRModule,
80 "Channels" : people.ChannelsModule,
68ece434 81 "MOS" : people.MOSModule,
b5e2077f 82 "Password" : people.PasswordModule,
dbb0c109 83 "Registrations" : people.RegistrationsModule,
917434b8 84
e1814f16
MT
85 # Nopaste
86 "Code" : nopaste.CodeModule,
87
23f0179e 88 # Fireinfo
96c9bb79 89 "FireinfoDeviceTable" : fireinfo.DeviceTableModule,
eabe6b8d
MT
90 "FireinfoDeviceAndGroupsTable"
91 : fireinfo.DeviceAndGroupsTableModule,
92
beb13102
MT
93 # Users
94 "UsersList" : users.ListModule,
95
4235ba55 96 # VoIP
00465786
MT
97 "VoIPOutboundRegistrations" :
98 voip.OutboundRegistrationsModule,
8e93325b 99 "VoIPQueues" : voip.QueuesModule,
4235ba55
MT
100 "VoIPRegistrations" : voip.RegistrationsModule,
101
6ac7e934 102 # Wiki
c21ffadb 103 "WikiDiff" : wiki.WikiDiffModule,
f9db574a 104 "WikiList" : wiki.WikiListModule,
6ac7e934 105
eabe6b8d 106 # Misc
6563eb49 107 "ChristmasBanner" : ui_modules.ChristmasBannerModule,
1c4522dc 108 "Markdown" : ui_modules.MarkdownModule,
eabe6b8d
MT
109 "Map" : ui_modules.MapModule,
110 "ProgressBar" : ui_modules.ProgressBarModule,
81675874 111 },
3403dc5e
MT
112
113 # Call this when a page wasn't found
b22bc8e8 114 "default_handler_class" : base.NotFoundHandler,
9ed02e3b 115 }
9068dba1 116 settings.update(kwargs)
5cf160e0 117
ae0228e1
MT
118 tornado.web.Application.__init__(self, **settings)
119
66862195 120 authentication_handlers = [
08df6527
MT
121 (r"/login", auth.LoginHandler),
122 (r"/logout", auth.LogoutHandler),
66862195
MT
123 ]
124
c85f80e2 125 self.add_handlers(r"(www\.)?([a-z]+\.dev\.)?ipfire\.org", [
940227cb
MT
126 # Entry site that lead the user to index
127 (r"/", IndexHandler),
940227cb 128
8505c8cd
MT
129 # Authentication
130 (r"/login", auth.LoginHandler),
131 (r"/logout", auth.LogoutHandler),
132 (r"/register", auth.RegisterHandler),
133
34472923
MT
134 # Blog
135 (r"/blog", blog.IndexHandler),
136 (r"/blog/authors/(\w+)", blog.AuthorHandler),
137 (r"/blog/compose", blog.ComposeHandler),
138 (r"/blog/drafts", blog.DraftsHandler),
139 (r"/blog/feed.xml", blog.FeedHandler),
140 (r"/blog/search", blog.SearchHandler),
141 (r"/blog/tags/([0-9a-z\-\.]+)", blog.TagHandler),
feb245e0 142 (r"/blog/years/([0-9]{4})", blog.YearHandler),
34472923
MT
143 (r"/blog/([0-9a-z\-\._]+)", blog.PostHandler),
144 (r"/blog/([0-9a-z\-\._]+)/delete", blog.DeleteHandler),
145 (r"/blog/([0-9a-z\-\._]+)/edit", blog.EditHandler),
146 (r"/blog/([0-9a-z\-\._]+)/publish", blog.PublishHandler),
147
1958a22b 148 # Docs
4a1bfdd5 149 (r"/docs((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))$", docs.FileHandler),
1958a22b
MT
150 (r"/docs([A-Za-z0-9\-_\/]+)?", docs.PageHandler),
151
aec63a26
MT
152 # Downloads
153 (r"/downloads", downloads.IndexHandler),
154 (r"/downloads/mirrors", downloads.MirrorsHandler),
155 (r"/downloads/thank-you", downloads.ThankYouHandler),
156 (r"/downloads/([0-9a-z\-\.]+)", downloads.ReleaseHandler),
157
e64ce07e 158 # Donate
c7bcb9ca
MT
159 (r"/donate", donate.DonateHandler),
160 (r"/donate/thank-you", donate.ThankYouHandler),
161 (r"/donate/error", donate.ErrorHandler),
8d48f4ef 162
8066a1e7
MT
163 # Password Reset
164 (r"/password\-reset", auth.PasswordResetInitiationHandler),
165 (r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler),
166
2fe1d960
MT
167 # Single-Sign-On for Discourse
168 (r"/sso/discourse", auth.SSODiscourse),
169
f2c38da0
MT
170 # User Groups
171 (r"/users/groups", users.GroupIndexHandler),
172 (r"/users/groups/([a-z_][a-z0-9_-]{0,31})", users.GroupShowHandler),
173
b01a1ee3 174 # Users
beb13102 175 (r"/users", users.IndexHandler),
53b2117f
MT
176 (r"/users/([a-z_][a-z0-9_-]{0,31})", users.ShowHandler),
177 (r"/users/([a-z_][a-z0-9_-]{0,31})\.jpg", users.AvatarHandler),
1cd4d7d3 178 (r"/users/([a-z_][a-z0-9_-]{0,31})/delete", users.DeleteHandler),
3c986f14 179 (r"/users/([a-z_][a-z0-9_-]{0,31})/edit", users.EditHandler),
e4d2f51f 180 (r"/users/([a-z_][a-z0-9_-]{0,31})/passwd", users.PasswdHandler),
b01a1ee3 181
4235ba55
MT
182 # VoIP
183 (r"/voip", voip.IndexHandler),
184
45592df5 185 # Static Pages
8ea3eaa2 186 (r"/about", StaticHandler, { "template" : "about.html" }),
45592df5 187 (r"/legal", StaticHandler, { "template" : "legal.html" }),
d2000918 188 (r"/help", StaticHandler, { "template" : "help.html" }),
45592df5 189
14cd4fa8 190 # Handle old pages that have moved elsewhere
7fa7d38d
MT
191 (r"/donation", tornado.web.RedirectHandler, { "url" : "/donate" }),
192 (r"/download", tornado.web.RedirectHandler, { "url" : "/downloads" }),
193 (r"/download/([0-9a-z\-\.]+)", tornado.web.RedirectHandler, { "url" : "/downloads/{0}" }),
85c977e2 194 (r"/features", tornado.web.RedirectHandler, { "url" : "/about" }),
14cd4fa8 195 (r"/imprint", tornado.web.RedirectHandler, { "url" : "/legal" }),
7fa7d38d
MT
196 (r"/news.rss", tornado.web.RedirectHandler, { "url" : "/blog/feed.xml" }),
197 (r"/news/(.*)", tornado.web.RedirectHandler, { "url" : "/blog/{0}" }),
d2000918 198 (r"/support", tornado.web.RedirectHandler, { "url" : "/help"}),
7fa7d38d 199 (r"/(de|en)/(.*)", tornado.web.RedirectHandler, { "url" : "/{0}"}),
37ed7c3c
MT
200
201 # Export arbitrary error pages
b22bc8e8 202 (r"/error/([45][0-9]{2})", base.ErrorHandler),
0ed3ea5c
MT
203
204 # Serve any static files
205 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
940227cb
MT
206 ])
207
34472923 208 # blog.ipfire.org - LEGACY REDIRECTION
c85f80e2 209 self.add_handlers(r"blog\.([a-z]+\.dev\.)?ipfire\.org", [
34472923
MT
210 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog" }),
211 (r"/authors/(\w+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/authors/{0}" }),
212 (r"/post/([0-9a-z\-\._]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/{0}" }),
213 (r"/tags/([0-9a-z\-\.]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/tags/{0}" }),
f0714277
MT
214
215 # RSS Feed
34472923
MT
216 (r"/feed.xml", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/feed.xml" }),
217 ])
12e5de7e 218
940227cb 219 # downloads.ipfire.org
c85f80e2 220 self.add_handlers(r"downloads\.([a-z]+\.dev\.)?ipfire\.org", [
82d4e789 221 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download" }),
450972a3 222 (r"/release/(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/{0}" }),
aec63a26 223 (r"/(.*)", downloads.FileHandler),
54af860e 224 ])
940227cb
MT
225
226 # mirrors.ipfire.org
c85f80e2 227 self.add_handlers(r"mirrors\.([a-z]+\.dev\.)?ipfire\.org", [
278b3118
MT
228 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/mirrors" }),
229 (r"/mirrors/(.*)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/download/mirrors/{0}" }),
3808b871 230 ])
940227cb 231
d0d074e0 232 # planet.ipfire.org
c85f80e2 233 self.add_handlers(r"planet\.([a-z]+\.dev\.)?ipfire\.org", [
3d4ce901 234 (r"/", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/" }),
7fa7d38d
MT
235 (r"/post/([A-Za-z0-9_-]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/{0}" }),
236 (r"/user/([a-z0-9_-]+)", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/blog/authors/{0}" }),
bcc3ed4d
MT
237
238 # RSS
f0714277 239 (r"/rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
d76ec66e 240 (r"/user/([a-z0-9_-]+)/rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
f0714277 241 (r"/news.rss", tornado.web.RedirectHandler, { "url" : "https://blog.ipfire.org/feed.xml" }),
3808b871 242 ])
d0d074e0 243
66862195 244 # fireinfo.ipfire.org
c85f80e2 245 self.add_handlers(r"fireinfo\.([a-z]+\.dev\.)?ipfire\.org", [
96c9bb79 246 (r"/", fireinfo.IndexHandler),
8ab37e0b 247
dc96f754
MT
248 # Admin
249 (r"/admin", fireinfo.AdminIndexHandler),
250
8ab37e0b
MT
251 # Vendors
252 (r"/vendors", fireinfo.VendorsHandler),
253 (r"/vendors/(pci|usb)/([0-9a-f]{4})", fireinfo.VendorHandler),
66862195 254
1e3b2aad 255 # Driver
0cd21a36 256 (r"/drivers/(.*)", fireinfo.DriverDetail),
1e3b2aad 257
66862195 258 # Show profiles
96c9bb79 259 (r"/profile/random", fireinfo.RandomProfileHandler),
b84b407f 260 (r"/profile/([a-z0-9]{40})", fireinfo.ProfileHandler),
91a446f0 261
ed2e3c1f 262 # Stats
19518d6e 263 (r"/processors", fireinfo.ProcessorsHandler),
ed2e3c1f
MT
264 (r"/releases", fireinfo.ReleasesHandler),
265
19518d6e 266 # Send profiles
96c9bb79 267 (r"/send/([a-z0-9]+)", fireinfo.ProfileSendHandler),
0ed3ea5c
MT
268
269 # Serve any static files
270 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
dc96f754 271 ] + authentication_handlers)
5cf160e0 272
c37ec602 273 # i-use.ipfire.org
c85f80e2 274 self.add_handlers(r"i-use\.([a-z]+\.dev\.)?ipfire\.org", [
e2f2865d 275 (r"/", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org/" }),
395c1ac0 276 (r"/profile/([a-f0-9]{40})/([0-9]+).png", iuse.ImageHandler),
c37ec602
MT
277 ])
278
8e2e1261
MT
279 # boot.ipfire.org
280 BOOT_STATIC_PATH = os.path.join(self.settings["static_path"], "netboot")
c85f80e2 281 self.add_handlers(r"boot\.([a-z]+\.dev\.)?ipfire\.org", [
f301d952 282 (r"/", tornado.web.RedirectHandler, { "url" : "https://wiki.ipfire.org/installation/pxe" }),
8e2e1261
MT
283
284 # Configurations
f301d952
MT
285 (r"/premenu.cfg", boot.PremenuCfgHandler),
286 (r"/menu.gpxe", boot.MenuGPXEHandler),
287 (r"/menu.cfg", boot.MenuCfgHandler),
8e2e1261
MT
288
289 # Static files
37b5c0cf 290 (r"/(boot\.png|pxelinux\.0|menu\.c32|vesamenu\.c32)",
8e2e1261
MT
291 tornado.web.StaticFileHandler, { "path" : BOOT_STATIC_PATH }),
292 ])
293
60024cc8 294 # nopaste.ipfire.org
c85f80e2 295 self.add_handlers(r"nopaste\.([a-z]+\.dev\.)?ipfire\.org", [
a41085fb
MT
296 (r"/", nopaste.CreateHandler),
297 (r"/raw/(.*)", nopaste.RawHandler),
298 (r"/view/(.*)", nopaste.ViewHandler),
0ed3ea5c
MT
299
300 # Serve any static files
301 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
3808b871 302 ] + authentication_handlers)
60024cc8 303
f5b01fc2 304 # location.ipfire.org
c85f80e2 305 self.add_handlers(r"location\.([a-z]+\.dev\.)?ipfire\.org", [
f5b01fc2 306 (r"/", location.IndexHandler),
9c83876f 307 (r"/download", StaticHandler, { "template" : "../location/download.html" }),
55eea098 308 (r"/how\-to\-use", StaticHandler, { "template" : "../location/how-to-use.html" }),
2517822e 309 (r"/lookup/(.+)/blacklists", location.BlacklistsHandler),
f5b01fc2 310 (r"/lookup/(.+)", location.LookupHandler),
0ed3ea5c
MT
311
312 # Serve any static files
313 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
f5b01fc2
MT
314 ])
315
9068dba1 316 # geoip.ipfire.org
c85f80e2 317 self.add_handlers(r"geoip\.([a-z]+\.dev\.)?ipfire\.org", [
f5b01fc2 318 (r"/", tornado.web.RedirectHandler, { "url" : "https://location.ipfire.org/" }),
3808b871 319 ])
9068dba1 320
66862195 321 # talk.ipfire.org
c85f80e2 322 self.add_handlers(r"talk\.([a-z]+\.dev\.)?ipfire\.org", [
786e9ca8
MT
323 (r"/", tornado.web.RedirectHandler, { "url" : "https://people.ipfire.org/" }),
324 ])
66862195 325
03706893 326 # people.ipfire.org
c85f80e2 327 self.add_handlers(r"people\.([a-z]+\.dev\.)?ipfire\.org", [
786e9ca8 328 (r"/", people.IndexHandler),
2c65e17c 329 (r"/activate/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.ActivateHandler),
30aeccdb 330 (r"/conferences", people.ConferencesHandler),
f32dd17f 331 (r"/register", auth.RegisterHandler),
2c65e17c
MT
332 (r"/users/([a-z_][a-z0-9_-]{0,31})/calls/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", people.CallHandler),
333 (r"/users/([a-z_][a-z0-9_-]{0,31})/calls(?:/(\d{4}-\d{2}-\d{2}))?", people.CallsHandler),
2c65e17c 334 (r"/users/([a-z_][a-z0-9_-]{0,31})/sip", people.SIPHandler),
2dac7110 335
92c4b559
MT
336 # Promotional Consent Stuff
337 (r"/subscribe", people.SubscribeHandler),
338 (r"/unsubscribe", people.UnsubscribeHandler),
339
0ed3ea5c
MT
340 # Serve any static files
341 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
342
689effd0 343 # API
66181c96 344 (r"/api/check/email", auth.APICheckEmail),
689effd0 345 (r"/api/check/uid", auth.APICheckUID),
786e9ca8 346 ] + authentication_handlers)
2cd9af74 347
181d08f3 348 # wiki.ipfire.org
c85f80e2 349 self.add_handlers(r"wiki\.([a-z]+\.dev\.)?ipfire\.org",
181d08f3
MT
350 authentication_handlers + [
351
f2cfd873 352 # Actions
b26c705a 353 (r"((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))/_delete", wiki.ActionDeleteHandler),
40cb87a4 354 (r"([A-Za-z0-9\-_\/]+)?/_edit", wiki.ActionEditHandler),
2901b734 355 (r"([A-Za-z0-9\-_\/]+)?/_render", wiki.ActionRenderHandler),
9db2e89f 356 (r"([A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", wiki.ActionWatchHandler),
d4c68c5c 357 (r"/actions/restore", wiki.ActionRestoreHandler),
f2cfd873
MT
358 (r"/actions/upload", wiki.ActionUploadHandler),
359
f9db574a
MT
360 # Handlers
361 (r"/recent\-changes", wiki.RecentChangesHandler),
181d08f3 362 (r"/search", wiki.SearchHandler),
86368c12 363 (r"/tree", wiki.TreeHandler),
2f23c558 364 (r"/watchlist", wiki.WatchlistHandler),
f9db574a 365
f2cfd873 366 # Media
3b33319e 367 (r"([A-Za-z0-9\-_\/]+)?/_files", wiki.FilesHandler),
f2cfd873 368
0ed3ea5c
MT
369 # Serve any static files
370 (r"/static/(.*)", tornado.web.StaticFileHandler, { "path" : self.settings.get("static_path") }),
371
f9db574a 372 # Render pages
181d08f3
MT
373 (r"([A-Za-z0-9\-_\/]+)?", wiki.PageHandler),
374 ])
375
ae0228e1 376 # ipfire.org
e65b9a6d 377 self.add_handlers(r"(.*)ipfire\.org", [
ba43a892 378 (r".*", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org" })
5cf160e0 379 ])
3add293a 380
feb02477
MT
381 logging.info("Successfully initialied application")
382
574a88c7 383 def format_country_name(self, handler, country_code):
e929ed92 384 return self.backend.get_country_name(country_code)
574a88c7 385
6eddfb50
MT
386 def format_language_name(self, handler, language):
387 _ = handler.locale.translate
388
389 if language == "de":
390 return _("German")
391 elif language == "en":
392 return _("English")
393 elif language == "es":
394 return _("Spanish")
395 elif language == "fr":
396 return _("French")
397 elif language == "it":
398 return _("Italian")
399 elif language == "nl":
400 return _("Dutch")
401 elif language == "pl":
402 return _("Polish")
403 elif language == "pt":
404 return _("Portuguese")
405 elif language == "ru":
406 return _("Russian")
407 elif language == "tr":
408 return _("Turkish")
409
410 return language
411
cc3b928d
MT
412 def format_month_name(self, handler, month):
413 _ = handler.locale.translate
414
415 if month == 1:
416 return _("January")
417 elif month == 2:
418 return _("February")
419 elif month == 3:
420 return _("March")
421 elif month == 4:
422 return _("April")
423 elif month == 5:
424 return _("May")
425 elif month == 6:
426 return _("June")
427 elif month == 7:
428 return _("July")
429 elif month == 8:
430 return _("August")
431 elif month == 9:
432 return _("September")
433 elif month == 10:
434 return _("October")
435 elif month == 11:
436 return _("November")
437 elif month == 12:
438 return _("December")
439
440 return month
401827c2 441
e96e445b
MT
442 def format_phone_number(self, handler, number):
443 if not isinstance(number, phonenumbers.PhoneNumber):
444 try:
01e73b0e 445 number = phonenumbers.parse(number, None)
e96e445b
MT
446 except phonenumbers.phonenumberutil.NumberParseException:
447 return number
c01ad253
MT
448
449 return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
450
e96e445b
MT
451 def format_phone_number_to_e164(self, handler, number):
452 return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
453
454 def format_phone_number_location(self, handler, number):
455 s = [
456 phonenumbers.geocoder.description_for_number(number, handler.locale.code),
457 phonenumbers.region_code_for_number(number),
458 ]
459
460 return ", ".join((e for e in s if e))