comparison roundup/configuration.py @ 6004:55f5060e0508

flake8 formatting fixes; use isinstance rather than type equality.
author John Rouillard <rouilj@ieee.org>
date Sat, 28 Dec 2019 13:01:14 -0500
parents 71c68961d9f4
children 380dec305c28
comparison
equal deleted inserted replaced
6003:48a1f919f894 6004:55f5060e0508
6 # Python 2: <https://pypi.org/project/configparser/>. That breaks 6 # Python 2: <https://pypi.org/project/configparser/>. That breaks
7 # Roundup if used with Python 2 because it generates unicode objects 7 # Roundup if used with Python 2 because it generates unicode objects
8 # where not expected by the Python code. Thus, a version check is 8 # where not expected by the Python code. Thus, a version check is
9 # used here instead of try/except. 9 # used here instead of try/except.
10 import sys 10 import sys
11 if sys.version_info[0] > 2:
12 import configparser # Python 3
13 else:
14 import ConfigParser as configparser # Python 2
15
16 import getopt 11 import getopt
17 import imp 12 import imp
18 import logging, logging.config 13 import logging, logging.config
19 import os 14 import os
20 import re 15 import re
27 import roundup.anypy.random_ as random_ 22 import roundup.anypy.random_ as random_
28 import binascii 23 import binascii
29 24
30 from roundup.backends import list_backends 25 from roundup.backends import list_backends
31 26
27 if sys.version_info[0] > 2:
28 import configparser # Python 3
29 else:
30 import ConfigParser as configparser # Python 2
31
32 # XXX i don't think this module needs string translation, does it? 32 # XXX i don't think this module needs string translation, does it?
33 33
34 ### Exceptions 34 ### Exceptions
35
35 36
36 class ConfigurationError(BaseException): 37 class ConfigurationError(BaseException):
37 pass 38 pass
39
38 40
39 class NoConfigError(ConfigurationError): 41 class NoConfigError(ConfigurationError):
40 42
41 """Raised when configuration loading fails 43 """Raised when configuration loading fails
42 44
45 """ 47 """
46 48
47 def __str__(self): 49 def __str__(self):
48 return "No valid configuration files found in directory %s" \ 50 return "No valid configuration files found in directory %s" \
49 % self.args[0] 51 % self.args[0]
52
50 53
51 class InvalidOptionError(ConfigurationError, KeyError, AttributeError): 54 class InvalidOptionError(ConfigurationError, KeyError, AttributeError):
52 55
53 """Attempted access to non-existing configuration option 56 """Attempted access to non-existing configuration option
54 57
61 64
62 """ 65 """
63 66
64 def __str__(self): 67 def __str__(self):
65 return "Unsupported configuration option: %s" % self.args[0] 68 return "Unsupported configuration option: %s" % self.args[0]
69
66 70
67 class OptionValueError(ConfigurationError, ValueError): 71 class OptionValueError(ConfigurationError, ValueError):
68 72
69 """Raised upon attempt to assign an invalid value to config option 73 """Raised upon attempt to assign an invalid value to config option
70 74
79 "option": _args[0].name, "value": _args[1]} 83 "option": _args[0].name, "value": _args[1]}
80 if len(_args) > 2: 84 if len(_args) > 2:
81 _rv += "\n".join(("",) + _args[2:]) 85 _rv += "\n".join(("",) + _args[2:])
82 return _rv 86 return _rv
83 87
88
84 class OptionUnsetError(ConfigurationError): 89 class OptionUnsetError(ConfigurationError):
85 90
86 """Raised when no Option value is available - neither set, nor default 91 """Raised when no Option value is available - neither set, nor default
87 92
88 Constructor parameters: Option instance. 93 Constructor parameters: Option instance.
90 """ 95 """
91 96
92 def __str__(self): 97 def __str__(self):
93 return "%s is not set and has no default" % self.args[0].name 98 return "%s is not set and has no default" % self.args[0].name
94 99
100
95 class UnsetDefaultValue: 101 class UnsetDefaultValue:
96 102
97 """Special object meaning that default value for Option is not specified""" 103 """Special object meaning that default value for Option is not specified"""
98 104
99 def __str__(self): 105 def __str__(self):
100 return "NO DEFAULT" 106 return "NO DEFAULT"
101 107
108
102 NODEFAULT = UnsetDefaultValue() 109 NODEFAULT = UnsetDefaultValue()
110
103 111
104 def create_token(size=32): 112 def create_token(size=32):
105 return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip()) 113 return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip())
106 114
107 ### Option classes 115 ### Option classes
116
108 117
109 class Option: 118 class Option:
110 119
111 """Single configuration option. 120 """Single configuration option.
112 121
139 """ 148 """
140 149
141 class_description = None 150 class_description = None
142 151
143 def __init__(self, config, section, setting, 152 def __init__(self, config, section, setting,
144 default=NODEFAULT, description=None, aliases=None 153 default=NODEFAULT, description=None, aliases=None):
145 ):
146 self.config = config 154 self.config = config
147 self.section = section 155 self.section = section
148 self.setting = setting.lower() 156 self.setting = setting.lower()
149 self.default = default 157 self.default = default
150 self.description = description 158 self.description = description
259 for _name in self.aliases: 267 for _name in self.aliases:
260 if hasattr(config, _name): 268 if hasattr(config, _name):
261 self.set(getattr(config, _name)) 269 self.set(getattr(config, _name))
262 break 270 break
263 271
272
264 class BooleanOption(Option): 273 class BooleanOption(Option):
265 274
266 """Boolean option: yes or no""" 275 """Boolean option: yes or no"""
267 276
268 class_description = "Allowed values: yes, no" 277 class_description = "Allowed values: yes, no"
272 return "yes" 281 return "yes"
273 else: 282 else:
274 return "no" 283 return "no"
275 284
276 def str2value(self, value): 285 def str2value(self, value):
277 if type(value) == type(""): 286 if isinstance(value, type("")):
278 _val = value.lower() 287 _val = value.lower()
279 if _val in ("yes", "true", "on", "1"): 288 if _val in ("yes", "true", "on", "1"):
280 _val = 1 289 _val = 1
281 elif _val in ("no", "false", "off", "0"): 290 elif _val in ("no", "false", "off", "0"):
282 _val = 0 291 _val = 0
284 raise OptionValueError(self, value, self.class_description) 293 raise OptionValueError(self, value, self.class_description)
285 else: 294 else:
286 _val = value and 1 or 0 295 _val = value and 1 or 0
287 return _val 296 return _val
288 297
298
289 class WordListOption(Option): 299 class WordListOption(Option):
290 300
291 """List of strings""" 301 """List of strings"""
292 302
293 class_description = "Allowed values: comma-separated list of words" 303 class_description = "Allowed values: comma-separated list of words"
295 def _value2str(self, value): 305 def _value2str(self, value):
296 return ','.join(value) 306 return ','.join(value)
297 307
298 def str2value(self, value): 308 def str2value(self, value):
299 return value.split(',') 309 return value.split(',')
310
300 311
301 class RunDetectorOption(Option): 312 class RunDetectorOption(Option):
302 313
303 """When a detector is run: always, never or for new items only""" 314 """When a detector is run: always, never or for new items only"""
304 315
309 if _val in ("yes", "no", "new"): 320 if _val in ("yes", "no", "new"):
310 return _val 321 return _val
311 else: 322 else:
312 raise OptionValueError(self, value, self.class_description) 323 raise OptionValueError(self, value, self.class_description)
313 324
325
314 class CsrfSettingOption(Option): 326 class CsrfSettingOption(Option):
315 327
316 """How should a csrf measure be enforced: required, yes, logfailure, no""" 328 """How should a csrf measure be enforced: required, yes, logfailure, no"""
317 329
318 class_description = "Allowed values: required, yes, logfailure, no" 330 class_description = "Allowed values: required, yes, logfailure, no"
322 if _val in ("required", "yes", "logfailure", "no"): 334 if _val in ("required", "yes", "logfailure", "no"):
323 return _val 335 return _val
324 else: 336 else:
325 raise OptionValueError(self, value, self.class_description) 337 raise OptionValueError(self, value, self.class_description)
326 338
339
327 class SameSiteSettingOption(Option): 340 class SameSiteSettingOption(Option):
328 341
329 """How should the SameSite cookie setting be set: strict, lax 342 """How should the SameSite cookie setting be set: strict, lax
330 or should it not be added (none)""" 343 or should it not be added (none)"""
331 344
335 _val = value.lower() 348 _val = value.lower()
336 if _val in ("strict", "lax", "none"): 349 if _val in ("strict", "lax", "none"):
337 return _val.capitalize() 350 return _val.capitalize()
338 else: 351 else:
339 raise OptionValueError(self, value, self.class_description) 352 raise OptionValueError(self, value, self.class_description)
340 353
354
341 class DatabaseBackend(Option): 355 class DatabaseBackend(Option):
342 """handle exact text of backend and make sure it's available""" 356 """handle exact text of backend and make sure it's available"""
343 class_description = "Available backends: %s"%", ".join(list_backends()) 357 class_description = "Available backends: %s" % ", ".join(list_backends())
344 358
345 def str2value(self, value): 359 def str2value(self, value):
346 _val = value.lower() 360 _val = value.lower()
347 if _val in list_backends(): 361 if _val in list_backends():
348 return _val 362 return _val
349 else: 363 else:
350 raise OptionValueError(self, value, self.class_description) 364 raise OptionValueError(self, value, self.class_description)
351 365
366
352 class HtmlToTextOption(Option): 367 class HtmlToTextOption(Option):
353 368
354 """What module should be used to convert emails with only text/html parts into text for display in roundup. Choose from beautifulsoup 4, dehtml - the internal code or none to disable html to text conversion. If beautifulsoup chosen but not available, dehtml will be used.""" 369 """What module should be used to convert emails with only text/html
370 parts into text for display in roundup. Choose from beautifulsoup
371 4, dehtml - the internal code or none to disable html to text
372 conversion. If beautifulsoup chosen but not available, dehtml will
373 be used.
374
375 """
355 376
356 class_description = "Allowed values: beautifulsoup, dehtml, none" 377 class_description = "Allowed values: beautifulsoup, dehtml, none"
357 378
358 def str2value(self, value): 379 def str2value(self, value):
359 _val = value.lower() 380 _val = value.lower()
360 if _val in ("beautifulsoup", "dehtml", "none"): 381 if _val in ("beautifulsoup", "dehtml", "none"):
361 return _val 382 return _val
362 else: 383 else:
363 raise OptionValueError(self, value, self.class_description) 384 raise OptionValueError(self, value, self.class_description)
364 385
386
365 class EmailBodyOption(Option): 387 class EmailBodyOption(Option):
366 388
367 """When to replace message body or strip quoting: always, never or for new items only""" 389 """When to replace message body or strip quoting: always, never
390 or for new items only"""
368 391
369 class_description = "Allowed values: yes, no, new" 392 class_description = "Allowed values: yes, no, new"
370 393
371 def str2value(self, value): 394 def str2value(self, value):
372 _val = value.lower() 395 _val = value.lower()
373 if _val in ("yes", "no", "new"): 396 if _val in ("yes", "no", "new"):
374 return _val 397 return _val
375 else: 398 else:
376 raise OptionValueError(self, value, self.class_description) 399 raise OptionValueError(self, value, self.class_description)
377 400
401
378 class IsolationOption(Option): 402 class IsolationOption(Option):
379 """Database isolation levels""" 403 """Database isolation levels"""
380 404
381 allowed = ['read uncommitted', 'read committed', 'repeatable read', 405 allowed = ['read uncommitted', 'read committed', 'repeatable read',
382 'serializable'] 406 'serializable']
383 class_description = "Allowed values: %s" % ', '.join ("'%s'" % a 407 class_description = "Allowed values: %s" % ', '.join("'%s'" % a
384 for a in allowed) 408 for a in allowed)
385 409
386 def str2value(self, value): 410 def str2value(self, value):
387 _val = value.lower() 411 _val = value.lower()
388 if _val in self.allowed: 412 if _val in self.allowed:
389 return _val 413 return _val
390 raise OptionValueError(self, value, self.class_description) 414 raise OptionValueError(self, value, self.class_description)
391 415
416
392 class MailAddressOption(Option): 417 class MailAddressOption(Option):
393 418
394 """Email address 419 """Email address
395 420
396 Email addresses may be either fully qualified or local. 421 Email addresses may be either fully qualified or local.
402 _val = Option.get(self) 427 _val = Option.get(self)
403 if "@" not in _val: 428 if "@" not in _val:
404 _val = "@".join((_val, self.config["MAIL_DOMAIN"])) 429 _val = "@".join((_val, self.config["MAIL_DOMAIN"]))
405 return _val 430 return _val
406 431
432
407 class FilePathOption(Option): 433 class FilePathOption(Option):
408 434
409 """File or directory path name 435 """File or directory path name
410 436
411 Paths may be either absolute or relative to the HOME. 437 Paths may be either absolute or relative to the HOME.
419 _val = Option.get(self) 445 _val = Option.get(self)
420 if _val and not os.path.isabs(_val): 446 if _val and not os.path.isabs(_val):
421 _val = os.path.join(self.config["HOME"], _val) 447 _val = os.path.join(self.config["HOME"], _val)
422 return _val 448 return _val
423 449
450
424 class MultiFilePathOption(Option): 451 class MultiFilePathOption(Option):
425 452
426 """List of space seperated File or directory path name 453 """List of space seperated File or directory path name
427 454
428 Paths may be either absolute or relative to the HOME. None 455 Paths may be either absolute or relative to the HOME. None
429 is returned if there are no elements. 456 is returned if there are no elements.
430 457
431 """ 458 """
432 459
433 class_description = "The space separated paths may be either absolute or\n" \ 460 class_description = "The space separated paths may be either absolute\n" \
434 "relative to the directory containing this config file." 461 "or relative to the directory containing this config file."
435 462
436 def get(self): 463 def get(self):
437 pathlist = [] 464 pathlist = []
438 _val = Option.get(self) 465 _val = Option.get(self)
439 for elem in _val.split(): 466 for elem in _val.split():
444 if pathlist: 471 if pathlist:
445 return pathlist 472 return pathlist
446 else: 473 else:
447 return None 474 return None
448 475
476
449 class FloatNumberOption(Option): 477 class FloatNumberOption(Option):
450 478
451 """Floating point numbers""" 479 """Floating point numbers"""
452 480
453 def str2value(self, value): 481 def str2value(self, value):
454 try: 482 try:
455 return float(value) 483 return float(value)
456 except ValueError: 484 except ValueError:
457 raise OptionValueError(self, value, 485 raise OptionValueError(self, value,
458 "Floating point number required") 486 "Floating point number required")
459 487
460 def _value2str(self, value): 488 def _value2str(self, value):
461 _val = str(value) 489 _val = str(value)
462 # strip fraction part from integer numbers 490 # strip fraction part from integer numbers
463 if _val.endswith(".0"): 491 if _val.endswith(".0"):
464 _val = _val[:-2] 492 _val = _val[:-2]
465 return _val 493 return _val
466 494
495
467 class IntegerNumberOption(Option): 496 class IntegerNumberOption(Option):
468 497
469 """Integer numbers""" 498 """Integer numbers"""
470 499
471 def str2value(self, value): 500 def str2value(self, value):
472 try: 501 try:
473 return int(value) 502 return int(value)
474 except ValueError: 503 except ValueError:
475 raise OptionValueError(self, value, "Integer number required") 504 raise OptionValueError(self, value, "Integer number required")
505
476 506
477 class IntegerNumberGeqZeroOption(Option): 507 class IntegerNumberGeqZeroOption(Option):
478 508
479 """Integer numbers greater than or equal to zero.""" 509 """Integer numbers greater than or equal to zero."""
480 510
484 if v < 0: 514 if v < 0:
485 raise OptionValueError(self, value, 515 raise OptionValueError(self, value,
486 "Integer number greater than or equal to zero required") 516 "Integer number greater than or equal to zero required")
487 return v 517 return v
488 except OptionValueError: 518 except OptionValueError:
489 raise # pass through subclass 519 raise # pass through subclass
490 except ValueError: 520 except ValueError:
491 raise OptionValueError(self, value, "Integer number required") 521 raise OptionValueError(self, value, "Integer number required")
522
492 523
493 class OctalNumberOption(Option): 524 class OctalNumberOption(Option):
494 525
495 """Octal Integer numbers""" 526 """Octal Integer numbers"""
496 527
497 def str2value(self, value): 528 def str2value(self, value):
498 try: 529 try:
499 return int(value, 8) 530 return int(value, 8)
500 except ValueError: 531 except ValueError:
501 raise OptionValueError(self, value, "Octal Integer number required") 532 raise OptionValueError(self, value,
533 "Octal Integer number required")
502 534
503 def _value2str(self, value): 535 def _value2str(self, value):
504 return oct(value) 536 return oct(value)
537
505 538
506 class MandatoryOption(Option): 539 class MandatoryOption(Option):
507 """Option must not be empty""" 540 """Option must not be empty"""
508 def str2value(self, value): 541 def str2value(self, value):
509 if not value: 542 if not value:
510 raise OptionValueError(self,value,"Value must not be empty.") 543 raise OptionValueError(self, value, "Value must not be empty.")
511 else: 544 else:
512 return value 545 return value
546
513 547
514 class WebUrlOption(Option): 548 class WebUrlOption(Option):
515 """URL MUST start with http/https scheme and end with '/'""" 549 """URL MUST start with http/https scheme and end with '/'"""
516 550
517 def str2value(self, value): 551 def str2value(self, value):
518 if not value: 552 if not value:
519 raise OptionValueError(self,value,"Value must not be empty.") 553 raise OptionValueError(self, value, "Value must not be empty.")
520 554
521 error_msg = '' 555 error_msg = ''
522 if not value.startswith(('http://', 'https://')): 556 if not value.startswith(('http://', 'https://')):
523 error_msg = "Value must start with http:// or https://.\n" 557 error_msg = "Value must start with http:// or https://.\n"
524 558
525 if not value.endswith('/'): 559 if not value.endswith('/'):
526 error_msg += "Value must end with /." 560 error_msg += "Value must end with /."
527 561
528 if error_msg: 562 if error_msg:
529 raise OptionValueError(self,value,error_msg) 563 raise OptionValueError(self, value, error_msg)
530 else: 564 else:
531 return value 565 return value
566
532 567
533 class NullableOption(Option): 568 class NullableOption(Option):
534 569
535 """Option that is set to None if its string value is one of NULL strings 570 """Option that is set to None if its string value is one of NULL strings
536 571
543 """ 578 """
544 579
545 NULL_STRINGS = ("",) 580 NULL_STRINGS = ("",)
546 581
547 def __init__(self, config, section, setting, 582 def __init__(self, config, section, setting,
548 default=NODEFAULT, description=None, aliases=None, 583 default=NODEFAULT, description=None, aliases=None,
549 null_strings=NULL_STRINGS 584 null_strings=NULL_STRINGS):
550 ):
551 self.null_strings = list(null_strings) 585 self.null_strings = list(null_strings)
552 Option.__init__(self, config, section, setting, default, 586 Option.__init__(self, config, section, setting, default,
553 description, aliases) 587 description, aliases)
554 588
555 def str2value(self, value): 589 def str2value(self, value):
556 if value in self.null_strings: 590 if value in self.null_strings:
557 return None 591 return None
558 else: 592 else:
561 def _value2str(self, value): 595 def _value2str(self, value):
562 if value is None: 596 if value is None:
563 return self.null_strings[0] 597 return self.null_strings[0]
564 else: 598 else:
565 return value 599 return value
600
566 601
567 class NullableFilePathOption(NullableOption, FilePathOption): 602 class NullableFilePathOption(NullableOption, FilePathOption):
568 603
569 # .get() and class_description are from FilePathOption, 604 # .get() and class_description are from FilePathOption,
570 get = FilePathOption.get 605 get = FilePathOption.get
571 class_description = FilePathOption.class_description 606 class_description = FilePathOption.class_description
572 # everything else taken from NullableOption (inheritance order) 607 # everything else taken from NullableOption (inheritance order)
608
573 609
574 class TimezoneOption(Option): 610 class TimezoneOption(Option):
575 611
576 class_description = \ 612 class_description = \
577 "If pytz module is installed, value may be any valid\n" \ 613 "If pytz module is installed, value may be any valid\n" \
593 except KeyError: 629 except KeyError:
594 raise OptionValueError(self, value, 630 raise OptionValueError(self, value,
595 "Timezone name or numeric hour offset required") 631 "Timezone name or numeric hour offset required")
596 return value 632 return value
597 633
598 634
599 class RegExpOption(Option): 635 class RegExpOption(Option):
600 636
601 """Regular Expression option (value is Regular Expression Object)""" 637 """Regular Expression option (value is Regular Expression Object)"""
602 638
603 class_description = "Value is Python Regular Expression (UTF8-encoded)." 639 class_description = "Value is Python Regular Expression (UTF8-encoded)."
604 640
605 RE_TYPE = type(re.compile("")) 641 RE_TYPE = type(re.compile(""))
606 642
607 def __init__(self, config, section, setting, 643 def __init__(self, config, section, setting,
608 default=NODEFAULT, description=None, aliases=None, 644 default=NODEFAULT, description=None, aliases=None,
609 flags=0, 645 flags=0):
610 ):
611 self.flags = flags 646 self.flags = flags
612 Option.__init__(self, config, section, setting, default, 647 Option.__init__(self, config, section, setting, default,
613 description, aliases) 648 description, aliases)
614 649
615 def _value2str(self, value): 650 def _value2str(self, value):
616 assert isinstance(value, self.RE_TYPE) 651 assert isinstance(value, self.RE_TYPE)
617 return value.pattern 652 return value.pattern
618 653
635 # is a sequence containing class name and constructor 670 # is a sequence containing class name and constructor
636 # parameters, starting from the setting name: 671 # parameters, starting from the setting name:
637 # setting, default, [description, [aliases]] 672 # setting, default, [description, [aliases]]
638 # Note: aliases should only exist in historical options for backwards 673 # Note: aliases should only exist in historical options for backwards
639 # compatibility - new options should *not* have aliases! 674 # compatibility - new options should *not* have aliases!
675
676
640 SETTINGS = ( 677 SETTINGS = (
641 ("main", ( 678 ("main", (
642 (FilePathOption, "database", "db", "Database directory path."), 679 (FilePathOption, "database", "db", "Database directory path."),
643 (Option, "template_engine", "zopetal", 680 (Option, "template_engine", "zopetal",
644 "Templating engine to use.\n" 681 "Templating engine to use.\n"
682 "Roles that a user gets when they register" 719 "Roles that a user gets when they register"
683 " with Email Gateway.\n" 720 " with Email Gateway.\n"
684 "This is a comma-separated string of role names" 721 "This is a comma-separated string of role names"
685 " (e.g. 'Admin,User')."), 722 " (e.g. 'Admin,User')."),
686 (Option, "obsolete_history_roles", "Admin", 723 (Option, "obsolete_history_roles", "Admin",
687 "On schema changes, properties or classes in the history may\n" 724 "On schema changes, properties or classes in the history may\n"
688 "become obsolete. Since normal access permissions do not apply\n" 725 "become obsolete. Since normal access permissions do not apply\n"
689 "(we don't know if a user should see such a property or class)\n" 726 "(we don't know if a user should see such a property or class)\n"
690 "a list of roles is specified here that are allowed to see\n" 727 "a list of roles is specified here that are allowed to see\n"
691 "these obsolete properties in the history. By default only the\n" 728 "these obsolete properties in the history. By default only the\n"
692 "admin role may see these history entries, you can make them\n" 729 "admin role may see these history entries, you can make them\n"
693 "visible to all users by adding, e.g., the 'User' role here."), 730 "visible to all users by adding, e.g., the 'User' role here."),
694 (Option, "error_messages_to", "user", 731 (Option, "error_messages_to", "user",
695 # XXX This description needs better wording, 732 # XXX This description needs better wording,
696 # with explicit allowed values list. 733 # with explicit allowed values list.
697 "Send error message emails to the dispatcher, user, or both?\n" 734 "Send error message emails to the dispatcher, user, or both?\n"
698 "The dispatcher is configured using the DISPATCHER_EMAIL" 735 "The dispatcher is configured using the DISPATCHER_EMAIL"
769 "address. This allows email exchanges to occur outside of\n" 806 "address. This allows email exchanges to occur outside of\n"
770 "the view of roundup and exposes the address of the person\n" 807 "the view of roundup and exposes the address of the person\n"
771 "who updated the issue, but it could be useful in some\n" 808 "who updated the issue, but it could be useful in some\n"
772 "unusual circumstances.\n" 809 "unusual circumstances.\n"
773 "If set to some other value, the value is used as the reply-to\n" 810 "If set to some other value, the value is used as the reply-to\n"
774 "address. It must be a valid RFC2822 address or people will not be\n" 811 "address. It must be a valid RFC2822 address or people will not\n"
775 "able to reply."), 812 "be able to reply."),
776 (NullableOption, "language", "", 813 (NullableOption, "language", "",
777 "Default locale name for this tracker.\n" 814 "Default locale name for this tracker.\n"
778 "If this option is not set, the language is determined\n" 815 "If this option is not set, the language is determined\n"
779 "by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,\n" 816 "by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,\n"
780 "or LANG, in that order of preference."), 817 "or LANG, in that order of preference."),
806 (BooleanOption, 'registration_prevalidate_username', "no", 843 (BooleanOption, 'registration_prevalidate_username', "no",
807 "When registering a user, check that the username\n" 844 "When registering a user, check that the username\n"
808 "is available before sending confirmation email.\n" 845 "is available before sending confirmation email.\n"
809 "Usually a username conflict is detected when\n" 846 "Usually a username conflict is detected when\n"
810 "confirming the registration. Disabled by default as\n" 847 "confirming the registration. Disabled by default as\n"
811 "it can be used for guessing existing usernames.\n" ), 848 "it can be used for guessing existing usernames.\n"),
812 (SameSiteSettingOption, 'samesite_cookie_setting', "Lax", 849 (SameSiteSettingOption, 'samesite_cookie_setting', "Lax",
813 """Set the mode of the SameSite cookie option for 850 """Set the mode of the SameSite cookie option for
814 the session cookie. Choices are 'Lax' or 851 the session cookie. Choices are 'Lax' or
815 'Strict'. 'None' can be used to suppress the 852 'Strict'. 'None' can be used to suppress the
816 option. Strict mode provides additional security 853 option. Strict mode provides additional security
830 additional REST-API parameters after the roundup web url configured in 867 additional REST-API parameters after the roundup web url configured in
831 the tracker section. If this variable is set to 'no', the rest path has 868 the tracker section. If this variable is set to 'no', the rest path has
832 no special meaning and will yield an error message."""), 869 no special meaning and will yield an error message."""),
833 (IntegerNumberGeqZeroOption, 'api_calls_per_interval', "0", 870 (IntegerNumberGeqZeroOption, 'api_calls_per_interval', "0",
834 "Limit API calls per api_interval_in_sec seconds to\n" 871 "Limit API calls per api_interval_in_sec seconds to\n"
835 "this number.\n" 872 "this number.\n"
836 "Determines the burst rate and the rate that new api\n" 873 "Determines the burst rate and the rate that new api\n"
837 "calls will be made available. If set to 360 and\n" 874 "calls will be made available. If set to 360 and\n"
838 "api_intervals_in_sec is set to 3600, the 361st call in\n" 875 "api_intervals_in_sec is set to 3600, the 361st call in\n"
839 "10 seconds results in a 429 error to the caller. It\n" 876 "10 seconds results in a 429 error to the caller. It\n"
840 "tells them to wait 10 seconds (360/3600) before making\n" 877 "tells them to wait 10 seconds (360/3600) before making\n"
966 "This is used to generate/validate json web tokens (jwt).\n" 1003 "This is used to generate/validate json web tokens (jwt).\n"
967 "Even if you don't use jwts it must not be empty.\n" 1004 "Even if you don't use jwts it must not be empty.\n"
968 "If less than 256 bits (32 characters) in length it will\n" 1005 "If less than 256 bits (32 characters) in length it will\n"
969 "disable use of jwt. Changing this invalidates all jwts\n" 1006 "disable use of jwt. Changing this invalidates all jwts\n"
970 "issued by the roundup instance requiring *all* users to\n" 1007 "issued by the roundup instance requiring *all* users to\n"
971 "generate new jwts. This is experimental and disabled by default.\n" 1008 "generate new jwts. This is experimental and disabled by\n"
972 "It must be persistent across application restarts.\n"), 1009 "default. It must be persistent across application restarts.\n"),
973 )), 1010 )),
974 ("rdbms", ( 1011 ("rdbms", (
975 (DatabaseBackend, 'backend', NODEFAULT, 1012 (DatabaseBackend, 'backend', NODEFAULT,
976 "Database backend."), 1013 "Database backend."),
977 (Option, 'name', 'roundup', 1014 (Option, 'name', 'roundup',
1095 "Add the mail address of the author to the author information at\n" 1132 "Add the mail address of the author to the author information at\n"
1096 "the top of all messages.\n" 1133 "the top of all messages.\n"
1097 "If this is false but add_authorinfo is true, only the name\n" 1134 "If this is false but add_authorinfo is true, only the name\n"
1098 "of the actor is added which protects the mail address of the\n" 1135 "of the actor is added which protects the mail address of the\n"
1099 "actor from being exposed at mail archives, etc."), 1136 "actor from being exposed at mail archives, etc."),
1100 ), "Outgoing email options.\nUsed for nosy messages and approval requests"), 1137 ), "Outgoing email options.\n"
1138 "Used for nosy messages and approval requests"),
1101 ("mailgw", ( 1139 ("mailgw", (
1102 (EmailBodyOption, "keep_quoted_text", "yes", 1140 (EmailBodyOption, "keep_quoted_text", "yes",
1103 "Keep email citations when accepting messages.\n" 1141 "Keep email citations when accepting messages.\n"
1104 "Setting this to \"no\" strips out \"quoted\" text\n" 1142 "Setting this to \"no\" strips out \"quoted\" text\n"
1105 "from the message. Setting this to \"new\" keeps quoted\n" 1143 "from the message. Setting this to \"new\" keeps quoted\n"
1265 ), "Nosy messages sending"), 1303 ), "Nosy messages sending"),
1266 ) 1304 )
1267 1305
1268 ### Configuration classes 1306 ### Configuration classes
1269 1307
1308
1270 class Config: 1309 class Config:
1271 1310
1272 """Base class for configuration objects. 1311 """Base class for configuration objects.
1273 1312
1274 Configuration options may be accessed as attributes or items 1313 Configuration options may be accessed as attributes or items
1364 # make the option known under all of its A.K.A.s 1403 # make the option known under all of its A.K.A.s
1365 for _name in option.aliases: 1404 for _name in option.aliases:
1366 self.options[_name] = option 1405 self.options[_name] = option
1367 1406
1368 def update_option(self, name, klass, 1407 def update_option(self, name, klass,
1369 default=NODEFAULT, description=None 1408 default=NODEFAULT, description=None):
1370 ):
1371 """Override behaviour of early created option. 1409 """Override behaviour of early created option.
1372 1410
1373 Parameters: 1411 Parameters:
1374 name: 1412 name:
1375 option name 1413 option name
1395 if description is None: 1433 if description is None:
1396 description = option.description 1434 description = option.description
1397 value = option.value2str(current=1) 1435 value = option.value2str(current=1)
1398 # resurrect the option 1436 # resurrect the option
1399 option = klass(self, option.section, option.setting, 1437 option = klass(self, option.section, option.setting,
1400 default=default, description=description) 1438 default=default, description=description)
1401 # apply the value 1439 # apply the value
1402 option.set(value) 1440 option.set(value)
1403 # incorporate new option 1441 # incorporate new option
1404 del self[name] 1442 del self[name]
1405 self.add_option(option) 1443 self.add_option(option)
1411 1449
1412 # Meant for commandline tools. 1450 # Meant for commandline tools.
1413 # Allows automatic creation of configuration files like this: 1451 # Allows automatic creation of configuration files like this:
1414 # roundup-server -p 8017 -u roundup --save-config 1452 # roundup-server -p 8017 -u roundup --save-config
1415 def getopt(self, args, short_options="", long_options=(), 1453 def getopt(self, args, short_options="", long_options=(),
1416 config_load_options=("C", "config"), **options 1454 config_load_options=("C", "config"), **options):
1417 ):
1418 """Apply options specified in command line arguments. 1455 """Apply options specified in command line arguments.
1419 1456
1420 Parameters: 1457 Parameters:
1421 args: 1458 args:
1422 command line to parse (sys.argv[1:]) 1459 command line to parse (sys.argv[1:])
1481 optlist.remove(option) 1518 optlist.remove(option)
1482 break 1519 break
1483 # apply options 1520 # apply options
1484 extra_options = [] 1521 extra_options = []
1485 for (opt, arg) in optlist: 1522 for (opt, arg) in optlist:
1486 if (opt in booleans): # and not arg 1523 if (opt in booleans): # and not arg
1487 arg = "yes" 1524 arg = "yes"
1488 try: 1525 try:
1489 name = cfg_names[opt] 1526 name = cfg_names[opt]
1490 except KeyError: 1527 except KeyError:
1491 extra_options.append((opt, arg)) 1528 extra_options.append((opt, arg))
1592 for section, options in need_set.items(): 1629 for section, options in need_set.items():
1593 _fp.write("# [%s]: %s\n" % (section, ", ".join(options))) 1630 _fp.write("# [%s]: %s\n" % (section, ", ".join(options)))
1594 for section in self.sections: 1631 for section in self.sections:
1595 comment = self.section_descriptions.get(section, None) 1632 comment = self.section_descriptions.get(section, None)
1596 if comment: 1633 if comment:
1597 _fp.write("\n# ".join([""] + comment.split("\n")) +"\n") 1634 _fp.write("\n# ".join([""] + comment.split("\n")) + "\n")
1598 else: 1635 else:
1599 # no section comment - just leave a blank line between sections 1636 # no section comment - just leave a blank line between sections
1600 _fp.write("\n") 1637 _fp.write("\n")
1601 _fp.write("[%s]\n" % section) 1638 _fp.write("[%s]\n" % section)
1602 for option in self._get_section_options(section): 1639 for option in self._get_section_options(section):
1641 because it is builtin pseudo-option, not a real Option 1678 because it is builtin pseudo-option, not a real Option
1642 object loaded from or saved to .ini file. 1679 object loaded from or saved to .ini file.
1643 1680
1644 """ 1681 """
1645 return [self.options[(_section, _name)] 1682 return [self.options[(_section, _name)]
1646 for _section in self.sections 1683 for _section in self.sections
1647 for _name in self._get_section_options(_section) 1684 for _name in self._get_section_options(_section)]
1648 ]
1649 1685
1650 def keys(self): 1686 def keys(self):
1651 """Return the list of "canonical" names of the options 1687 """Return the list of "canonical" names of the options
1652 1688
1653 Unlike .items(), this list also includes HOME 1689 Unlike .items(), this list also includes HOME
1668 1704
1669 # Note: __getattr__ is not symmetric to __setattr__: 1705 # Note: __getattr__ is not symmetric to __setattr__:
1670 # self.__dict__ lookup is done before calling this method 1706 # self.__dict__ lookup is done before calling this method
1671 def __getattr__(self, name): 1707 def __getattr__(self, name):
1672 return self[name] 1708 return self[name]
1709
1673 1710
1674 class UserConfig(Config): 1711 class UserConfig(Config):
1675 1712
1676 """Configuration for user extensions. 1713 """Configuration for user extensions.
1677 1714
1688 # see what options are already defined and add missing ones 1725 # see what options are already defined and add missing ones
1689 preset = [(option.section, option.setting) for option in self.items()] 1726 preset = [(option.section, option.setting) for option in self.items()]
1690 for section in config.sections(): 1727 for section in config.sections():
1691 for name in config.options(section): 1728 for name in config.options(section):
1692 if ((section, name) not in preset) \ 1729 if ((section, name) not in preset) \
1693 and (name not in defaults): 1730 and (name not in defaults):
1694 self.add_option(Option(self, section, name)) 1731 self.add_option(Option(self, section, name))
1732
1695 1733
1696 class CoreConfig(Config): 1734 class CoreConfig(Config):
1697 1735
1698 """Roundup instance configuration. 1736 """Roundup instance configuration.
1699 1737

Roundup Issue Tracker: http://roundup-tracker.org/