"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "roundup/configuration.py" between
roundup-1.6.1.tar.gz and roundup-2.0.0.tar.gz

About: Roundup is an highly customisable issue-tracking system with command-line, web and e-mail interfaces (written in Python).

configuration.py  (roundup-1.6.1):configuration.py  (roundup-2.0.0)
# Roundup Issue Tracker configuration support # Roundup Issue Tracker configuration support
# #
__docformat__ = "restructuredtext" __docformat__ = "restructuredtext"
# Some systems have a backport of the Python 3 configparser module to # Some systems have a backport of the Python 3 configparser module to
# Python 2: <https://pypi.org/project/configparser/>. That breaks # Python 2: <https://pypi.org/project/configparser/>. That breaks
# Roundup if used with Python 2 because it generates unicode objects # Roundup if used with Python 2 because it generates unicode objects
# where not expected by the Python code. Thus, a version check is # where not expected by the Python code. Thus, a version check is
# used here instead of try/except. # used here instead of try/except.
import sys import sys
if sys.version_info[0] > 2:
import configparser # Python 3
else:
import ConfigParser as configparser # Python 2
import getopt import getopt
import imp import imp
import logging, logging.config import logging, logging.config
import os import os
import re import re
import time import time
import smtplib import smtplib
import roundup.date import roundup.date
from roundup.anypy.strings import b2s
import roundup.anypy.random_ as random_
import binascii
from roundup.backends import list_backends
if sys.version_info[0] > 2:
import configparser # Python 3
else:
import ConfigParser as configparser # Python 2
from roundup.exceptions import RoundupException
# XXX i don't think this module needs string translation, does it? # XXX i don't think this module needs string translation, does it?
### Exceptions ### Exceptions
class ConfigurationError(BaseException): class ConfigurationError(RoundupException):
pass pass
class NoConfigError(ConfigurationError): class NoConfigError(ConfigurationError):
"""Raised when configuration loading fails """Raised when configuration loading fails
Constructor parameters: path to the directory that was used as HOME Constructor parameters: path to the directory that was used as HOME
""" """
skipping to change at line 98 skipping to change at line 106
class UnsetDefaultValue: class UnsetDefaultValue:
"""Special object meaning that default value for Option is not specified""" """Special object meaning that default value for Option is not specified"""
def __str__(self): def __str__(self):
return "NO DEFAULT" return "NO DEFAULT"
NODEFAULT = UnsetDefaultValue() NODEFAULT = UnsetDefaultValue()
def create_token(size=32):
return b2s(binascii.b2a_base64(random_.token_bytes(size)).strip())
### Option classes ### Option classes
class Option: class Option:
"""Single configuration option. """Single configuration option.
Options have following attributes: Options have following attributes:
config config
reference to the containing Config object reference to the containing Config object
skipping to change at line 135 skipping to change at line 146
"Canonical name" is also included. "Canonical name" is also included.
The name and aliases are forced to be uppercase. The name and aliases are forced to be uppercase.
The setting name is forced to lowercase. The setting name is forced to lowercase.
""" """
class_description = None class_description = None
def __init__(self, config, section, setting, def __init__(self, config, section, setting,
default=NODEFAULT, description=None, aliases=None default=NODEFAULT, description=None, aliases=None):
):
self.config = config self.config = config
self.section = section self.section = section
self.setting = setting.lower() self.setting = setting.lower()
self.default = default self.default = default
self.description = description self.description = description
self.name = setting.upper() self.name = setting.upper()
if section != "main": if section != "main":
self.name = "_".join((section.upper(), self.name)) self.name = "_".join((section.upper(), self.name))
if aliases: if aliases:
self.aliases = [alias.upper() for alias in list(aliases)] self.aliases = [alias.upper() for alias in list(aliases)]
skipping to change at line 268 skipping to change at line 278
class_description = "Allowed values: yes, no" class_description = "Allowed values: yes, no"
def _value2str(self, value): def _value2str(self, value):
if value: if value:
return "yes" return "yes"
else: else:
return "no" return "no"
def str2value(self, value): def str2value(self, value):
if type(value) == type(""): if isinstance(value, type("")):
_val = value.lower() _val = value.lower()
if _val in ("yes", "true", "on", "1"): if _val in ("yes", "true", "on", "1"):
_val = 1 _val = 1
elif _val in ("no", "false", "off", "0"): elif _val in ("no", "false", "off", "0"):
_val = 0 _val = 0
else: else:
raise OptionValueError(self, value, self.class_description) raise OptionValueError(self, value, self.class_description)
else: else:
_val = value and 1 or 0 _val = value and 1 or 0
return _val return _val
skipping to change at line 332 skipping to change at line 342
class_description = "Allowed values: Strict, Lax, None" class_description = "Allowed values: Strict, Lax, None"
def str2value(self, value): def str2value(self, value):
_val = value.lower() _val = value.lower()
if _val in ("strict", "lax", "none"): if _val in ("strict", "lax", "none"):
return _val.capitalize() return _val.capitalize()
else: else:
raise OptionValueError(self, value, self.class_description) raise OptionValueError(self, value, self.class_description)
class DatabaseBackend(Option):
"""handle exact text of backend and make sure it's available"""
class_description = "Available backends: %s" % ", ".join(list_backends())
def str2value(self, value):
_val = value.lower()
if _val in list_backends():
return _val
else:
raise OptionValueError(self, value, self.class_description)
class HtmlToTextOption(Option): class HtmlToTextOption(Option):
"""What module should be used to convert emails with only text/html parts in """What module should be used to convert emails with only text/html
to text for display in roundup. Choose from beautifulsoup 4, dehtml - the intern parts into text for display in roundup. Choose from beautifulsoup
al code or none to disable html to text conversion. If beautifulsoup chosen but 4, dehtml - the internal code or none to disable html to text
not available, dehtml will be used.""" conversion. If beautifulsoup chosen but not available, dehtml will
be used.
"""
class_description = "Allowed values: beautifulsoup, dehtml, none" class_description = "Allowed values: beautifulsoup, dehtml, none"
def str2value(self, value): def str2value(self, value):
_val = value.lower() _val = value.lower()
if _val in ("beautifulsoup", "dehtml", "none"): if _val in ("beautifulsoup", "dehtml", "none"):
return _val return _val
else: else:
raise OptionValueError(self, value, self.class_description) raise OptionValueError(self, value, self.class_description)
class EmailBodyOption(Option): class EmailBodyOption(Option):
"""When to replace message body or strip quoting: always, never or for new i """When to replace message body or strip quoting: always, never
tems only""" or for new items only"""
class_description = "Allowed values: yes, no, new" class_description = "Allowed values: yes, no, new"
def str2value(self, value): def str2value(self, value):
_val = value.lower() _val = value.lower()
if _val in ("yes", "no", "new"): if _val in ("yes", "no", "new"):
return _val return _val
else: else:
raise OptionValueError(self, value, self.class_description) raise OptionValueError(self, value, self.class_description)
class IsolationOption(Option): class IsolationOption(Option):
"""Database isolation levels""" """Database isolation levels"""
allowed = ['read uncommitted', 'read committed', 'repeatable read', allowed = ['read uncommitted', 'read committed', 'repeatable read',
'serializable'] 'serializable']
class_description = "Allowed values: %s" % ', '.join ("'%s'" % a class_description = "Allowed values: %s" % ', '.join("'%s'" % a
for a in allowed) for a in allowed)
def str2value(self, value): def str2value(self, value):
_val = value.lower() _val = value.lower()
if _val in self.allowed: if _val in self.allowed:
return _val return _val
raise OptionValueError(self, value, self.class_description) raise OptionValueError(self, value, self.class_description)
class MailAddressOption(Option): class MailAddressOption(Option):
"""Email address """Email address
skipping to change at line 413 skipping to change at line 441
class MultiFilePathOption(Option): class MultiFilePathOption(Option):
"""List of space seperated File or directory path name """List of space seperated File or directory path name
Paths may be either absolute or relative to the HOME. None Paths may be either absolute or relative to the HOME. None
is returned if there are no elements. is returned if there are no elements.
""" """
class_description = "The space separated paths may be either absolute or\n" class_description = "The space separated paths may be either absolute\n" \
\ "or relative to the directory containing this config file."
"relative to the directory containing this config file."
def get(self): def get(self):
pathlist = [] pathlist = []
_val = Option.get(self) _val = Option.get(self)
for elem in _val.split(): for elem in _val.split():
if elem and not os.path.isabs(elem): if elem and not os.path.isabs(elem):
pathlist.append(os.path.join(self.config["HOME"], elem)) pathlist.append(os.path.join(self.config["HOME"], elem))
else: else:
pathlist.append(elem) pathlist.append(elem)
if pathlist: if pathlist:
skipping to change at line 438 skipping to change at line 466
class FloatNumberOption(Option): class FloatNumberOption(Option):
"""Floating point numbers""" """Floating point numbers"""
def str2value(self, value): def str2value(self, value):
try: try:
return float(value) return float(value)
except ValueError: except ValueError:
raise OptionValueError(self, value, raise OptionValueError(self, value,
"Floating point number required") "Floating point number required")
def _value2str(self, value): def _value2str(self, value):
_val = str(value) _val = str(value)
# strip fraction part from integer numbers # strip fraction part from integer numbers
if _val.endswith(".0"): if _val.endswith(".0"):
_val = _val[:-2] _val = _val[:-2]
return _val return _val
class IntegerNumberOption(Option): class IntegerNumberOption(Option):
"""Integer numbers""" """Integer numbers"""
def str2value(self, value): def str2value(self, value):
try: try:
return int(value) return int(value)
except ValueError: except ValueError:
raise OptionValueError(self, value, "Integer number required") raise OptionValueError(self, value, "Integer number required")
class IntegerNumberGeqZeroOption(Option):
"""Integer numbers greater than or equal to zero."""
def str2value(self, value):
try:
v = int(value)
if v < 0:
raise OptionValueError(self, value,
"Integer number greater than or equal to zero required")
return v
except OptionValueError:
raise # pass through subclass
except ValueError:
raise OptionValueError(self, value, "Integer number required")
class OctalNumberOption(Option): class OctalNumberOption(Option):
"""Octal Integer numbers""" """Octal Integer numbers"""
def str2value(self, value): def str2value(self, value):
try: try:
return int(value, 8) return int(value, 8)
except ValueError: except ValueError:
raise OptionValueError(self, value, "Octal Integer number required") raise OptionValueError(self, value,
"Octal Integer number required")
def _value2str(self, value): def _value2str(self, value):
return oct(value) return oct(value)
class MandatoryOption(Option):
"""Option must not be empty"""
def str2value(self, value):
if not value:
raise OptionValueError(self, value, "Value must not be empty.")
else:
return value
class WebUrlOption(Option):
"""URL MUST start with http/https scheme and end with '/'"""
def str2value(self, value):
if not value:
raise OptionValueError(self, value, "Value must not be empty.")
error_msg = ''
if not value.startswith(('http://', 'https://')):
error_msg = "Value must start with http:// or https://.\n"
if not value.endswith('/'):
error_msg += "Value must end with /."
if error_msg:
raise OptionValueError(self, value, error_msg)
else:
return value
class NullableOption(Option): class NullableOption(Option):
"""Option that is set to None if its string value is one of NULL strings """Option that is set to None if its string value is one of NULL strings
Default nullable strings list contains empty string only. Default nullable strings list contains empty string only.
There is constructor parameter allowing to specify different nullables. There is constructor parameter allowing to specify different nullables.
Conversion to external representation returns the first of the NULL Conversion to external representation returns the first of the NULL
strings list when the value is None. strings list when the value is None.
""" """
NULL_STRINGS = ("",) NULL_STRINGS = ("",)
def __init__(self, config, section, setting, def __init__(self, config, section, setting,
default=NODEFAULT, description=None, aliases=None, default=NODEFAULT, description=None, aliases=None,
null_strings=NULL_STRINGS null_strings=NULL_STRINGS):
):
self.null_strings = list(null_strings) self.null_strings = list(null_strings)
Option.__init__(self, config, section, setting, default, Option.__init__(self, config, section, setting, default,
description, aliases) description, aliases)
def str2value(self, value): def str2value(self, value):
if value in self.null_strings: if value in self.null_strings:
return None return None
else: else:
return value return value
def _value2str(self, value): def _value2str(self, value):
if value is None: if value is None:
return self.null_strings[0] return self.null_strings[0]
skipping to change at line 519 skipping to change at line 590
# everything else taken from NullableOption (inheritance order) # everything else taken from NullableOption (inheritance order)
class TimezoneOption(Option): class TimezoneOption(Option):
class_description = \ class_description = \
"If pytz module is installed, value may be any valid\n" \ "If pytz module is installed, value may be any valid\n" \
"timezone specification (e.g. EET or Europe/Warsaw).\n" \ "timezone specification (e.g. EET or Europe/Warsaw).\n" \
"If pytz is not installed, value must be integer number\n" \ "If pytz is not installed, value must be integer number\n" \
"giving local timezone offset from UTC in hours." "giving local timezone offset from UTC in hours."
# fix issue2551030, default value for timezone
# Must be 0 if no pytz can be UTC if pytz.
try:
import pytz
defaulttz = "UTC"
except ImportError:
defaulttz = "0"
def str2value(self, value): def str2value(self, value):
try: try:
roundup.date.get_timezone(value) roundup.date.get_timezone(value)
except KeyError: except KeyError:
raise OptionValueError(self, value, raise OptionValueError(self, value,
"Timezone name or numeric hour offset required") "Timezone name or numeric hour offset required")
return value return value
class RegExpOption(Option): class RegExpOption(Option):
"""Regular Expression option (value is Regular Expression Object)""" """Regular Expression option (value is Regular Expression Object)"""
class_description = "Value is Python Regular Expression (UTF8-encoded)." class_description = "Value is Python Regular Expression (UTF8-encoded)."
RE_TYPE = type(re.compile("")) RE_TYPE = type(re.compile(""))
def __init__(self, config, section, setting, def __init__(self, config, section, setting,
default=NODEFAULT, description=None, aliases=None, default=NODEFAULT, description=None, aliases=None,
flags=0, flags=0):
):
self.flags = flags self.flags = flags
Option.__init__(self, config, section, setting, default, Option.__init__(self, config, section, setting, default,
description, aliases) description, aliases)
def _value2str(self, value): def _value2str(self, value):
assert isinstance(value, self.RE_TYPE) assert isinstance(value, self.RE_TYPE)
return value.pattern return value.pattern
def str2value(self, value): def str2value(self, value):
if not isinstance(value, unicode): if not isinstance(value, type(u'')):
value = str(value) value = str(value)
if not isinstance(value, type(u'')):
# if it is 7-bit ascii, use it as string, # if it is 7-bit ascii, use it as string,
# otherwise convert to unicode. # otherwise convert to unicode.
try: try:
value.decode("ascii") value.decode("ascii")
except UnicodeError: except UnicodeError:
value = value.decode("utf-8") value = value.decode("utf-8")
return re.compile(value, self.flags) return re.compile(value, self.flags)
### Main configuration layout. ### Main configuration layout.
# Config is described as a sequence of sections, # Config is described as a sequence of sections,
# where each section name is followed by a sequence # where each section name is followed by a sequence
# of Option definitions. Each Option definition # of Option definitions. Each Option definition
# is a sequence containing class name and constructor # is a sequence containing class name and constructor
# parameters, starting from the setting name: # parameters, starting from the setting name:
# setting, default, [description, [aliases]] # setting, default, [description, [aliases]]
# Note: aliases should only exist in historical options for backwards # Note: aliases should only exist in historical options for backwards
# compatibility - new options should *not* have aliases! # compatibility - new options should *not* have aliases!
SETTINGS = ( SETTINGS = (
("main", ( ("main", (
(FilePathOption, "database", "db", "Database directory path."), (FilePathOption, "database", "db", "Database directory path."),
(Option, "template_engine", "zopetal", (Option, "template_engine", "zopetal",
"Templating engine to use.\n" "Templating engine to use.\n"
"Possible values are 'zopetal' for the old TAL engine\n" "Possible values are 'zopetal' for the old TAL engine\n"
"ported from Zope, or 'chameleon' for Chameleon."), "ported from Zope, or 'chameleon' for Chameleon."),
(FilePathOption, "templates", "html", (FilePathOption, "templates", "html",
"Path to the HTML templates directory."), "Path to the HTML templates directory."),
(MultiFilePathOption, "static_files", "", (MultiFilePathOption, "static_files", "",
skipping to change at line 614 skipping to change at line 694
"Roles that a user gets when they register" "Roles that a user gets when they register"
" with Web User Interface.\n" " with Web User Interface.\n"
"This is a comma-separated string of role names" "This is a comma-separated string of role names"
" (e.g. 'Admin,User')."), " (e.g. 'Admin,User')."),
(Option, "new_email_user_roles", "User", (Option, "new_email_user_roles", "User",
"Roles that a user gets when they register" "Roles that a user gets when they register"
" with Email Gateway.\n" " with Email Gateway.\n"
"This is a comma-separated string of role names" "This is a comma-separated string of role names"
" (e.g. 'Admin,User')."), " (e.g. 'Admin,User')."),
(Option, "obsolete_history_roles", "Admin", (Option, "obsolete_history_roles", "Admin",
"On schema changes, properties or classes in the history may\n" "On schema changes, properties or classes in the history may\n"
"become obsolete. Since normal access permissions do not apply\n" "become obsolete. Since normal access permissions do not apply\n"
"(we don't know if a user should see such a property or class)\n" "(we don't know if a user should see such a property or class)\n"
"a list of roles is specified here that are allowed to see\n" "a list of roles is specified here that are allowed to see\n"
"these obsolete properties in the history. By default only the\n" "these obsolete properties in the history. By default only the\n"
"admin role may see these history entries, you can make them\n" "admin role may see these history entries, you can make them\n"
"visible to all users by adding, e.g., the 'User' role here."), "visible to all users by adding, e.g., the 'User' role here."),
(Option, "error_messages_to", "user", (Option, "error_messages_to", "user",
# XXX This description needs better wording, 'Send error message emails to the "dispatcher", "user", '
# with explicit allowed values list. 'or "both" (these are the allowed values)?\n'
"Send error message emails to the dispatcher, user, or both?\n" 'The dispatcher is configured using the DISPATCHER_EMAIL'
"The dispatcher is configured using the DISPATCHER_EMAIL" ' setting.'),
" setting."),
(Option, "html_version", "html4", (Option, "html_version", "html4",
"HTML version to generate. The templates are html4 by default.\n" "HTML version to generate. The templates are html4 by default.\n"
"If you wish to make them xhtml, then you'll need to change this\n" "If you wish to make them xhtml, then you'll need to change this\n"
"var to 'xhtml' too so all auto-generated HTML is compliant.\n" "var to 'xhtml' too so all auto-generated HTML is compliant.\n"
"Allowed values: html4, xhtml"), "Allowed values: html4, xhtml"),
(TimezoneOption, "timezone", "UTC", "Default timezone offset," (TimezoneOption, "timezone", TimezoneOption.defaulttz,
"Default timezone offset,"
" applied when user's timezone is not set.", " applied when user's timezone is not set.",
["DEFAULT_TIMEZONE"]), ["DEFAULT_TIMEZONE"]),
(BooleanOption, "instant_registration", "no", (BooleanOption, "instant_registration", "no",
"Register new users instantly, or require confirmation via\n" "Register new users instantly, or require confirmation via\n"
"email?"), "email?"),
(BooleanOption, "email_registration_confirmation", "yes", (BooleanOption, "email_registration_confirmation", "yes",
"Offer registration confirmation by email or only through the web?") , "Offer registration confirmation by email or only through the web?") ,
(Option, "indexer", "", (Option, "indexer", "",
"Force Roundup to use a particular text indexer.\n" "Force Roundup to use a particular text indexer.\n"
"If no indexer is supplied, the first available indexer\n" "If no indexer is supplied, the first available indexer\n"
"will be used in the following order:\n" "will be used in the following order:\n"
"Possible values: xapian, whoosh, native (internal)."), "Possible values: xapian, whoosh, native (internal)."),
(WordListOption, "indexer_stopwords", "", (WordListOption, "indexer_stopwords", "",
"Additional stop-words for the full-text indexer specific to\n" "Additional stop-words for the full-text indexer specific to\n"
"your tracker. See the indexer source for the default list of\n" "your tracker. See the indexer source for the default list of\n"
"stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"), "stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"),
(OctalNumberOption, "umask", "02", (OctalNumberOption, "umask", "0o002",
"Defines the file creation mode mask."), "Defines the file creation mode mask."),
(IntegerNumberOption, 'csv_field_size', '131072', (IntegerNumberGeqZeroOption, 'csv_field_size', '131072',
"Maximum size of a csv-field during import. Roundups export\n" "Maximum size of a csv-field during import. Roundups export\n"
"format is a csv (comma separated values) variant. The csv\n" "format is a csv (comma separated values) variant. The csv\n"
"reader has a limit on the size of individual fields\n" "reader has a limit on the size of individual fields\n"
"starting with python 2.5. Set this to a higher value if you\n" "starting with python 2.5. Set this to a higher value if you\n"
"get the error 'Error: field larger than field limit' during\n" "get the error 'Error: field larger than field limit' during\n"
"import."), "import."),
(IntegerNumberOption, 'password_pbkdf2_default_rounds', '10000', (IntegerNumberGeqZeroOption, 'password_pbkdf2_default_rounds', '10000',
"Sets the default number of rounds used when encoding passwords\n" "Sets the default number of rounds used when encoding passwords\n"
"using the PBKDF2 scheme. Set this to a higher value on faster\n" "using the PBKDF2 scheme. Set this to a higher value on faster\n"
"systems which want more security.\n" "systems which want more security.\n"
"PBKDF2 (Password-Based Key Derivation Function) is a\n" "PBKDF2 (Password-Based Key Derivation Function) is a\n"
"password hashing mechanism that derives hash from the\n" "password hashing mechanism that derives hash from the\n"
"password and a random salt. For authentication this process\n" "password and a random salt. For authentication this process\n"
"is repeated with the same salt as in the stored hash.\n" "is repeated with the same salt as in the stored hash.\n"
"If both hashes match, the authentication succeeds.\n" "If both hashes match, the authentication succeeds.\n"
"PBKDF2 supports a variable 'rounds' parameter which varies\n" "PBKDF2 supports a variable 'rounds' parameter which varies\n"
"the time-cost of calculating the hash - doubling the number\n" "the time-cost of calculating the hash - doubling the number\n"
"of rounds doubles the cpu time required to calculate it. The\n" "of rounds doubles the cpu time required to calculate it. The\n"
"purpose of this is to periodically adjust the rounds as CPUs\n" "purpose of this is to periodically adjust the rounds as CPUs\n"
"become faster. The currently enforced minimum number of\n" "become faster. The currently enforced minimum number of\n"
"rounds is 1000.\n" "rounds is 1000.\n"
"See: http://en.wikipedia.org/wiki/PBKDF2 and RFC2898"), "See: http://en.wikipedia.org/wiki/PBKDF2 and RFC2898"),
)), )),
("tracker", ( ("tracker", (
(Option, "name", "Roundup issue tracker", (Option, "name", "Roundup issue tracker",
"A descriptive name for your roundup instance."), "A descriptive name for your roundup instance."),
(Option, "web", NODEFAULT, (WebUrlOption, "web", NODEFAULT,
"The web address that the tracker is viewable at.\n" "The web address that the tracker is viewable at.\n"
"This will be included in information" "This will be included in information"
" sent to users of the tracker.\n" " sent to users of the tracker.\n"
"The URL MUST include the cgi-bin part or anything else\n" "The URL MUST include the cgi-bin part or anything else\n"
"that is required to get to the home page of the tracker.\n" "that is required to get to the home page of the tracker.\n"
"You MUST include a trailing '/' in the URL."), "URL MUST start with http/https scheme and end with '/'"),
(MailAddressOption, "email", "issue_tracker", (MailAddressOption, "email", "issue_tracker",
"Email address that mail to roundup should go to.\n" "Email address that mail to roundup should go to.\n"
"If no domain is specified then mail_domain is added."), "If no domain is specified then mail_domain is added."),
(Option, "replyto_address", "", (Option, "replyto_address", "",
"Controls the reply-to header address used when sending\n" "Controls the reply-to header address used when sending\n"
"nosy messages.\n" "nosy messages.\n"
"If the value is unset (default) the roundup tracker's\n" "If the value is unset (default) the roundup tracker's\n"
"email address (above) is used.\n" "email address (above) is used.\n"
"If set to \"AUTHOR\" then the primary email address of the\n" "If set to \"AUTHOR\" then the primary email address of the\n"
"author of the change will be used as the reply-to\n" "author of the change will be used as the reply-to\n"
"address. This allows email exchanges to occur outside of\n" "address. This allows email exchanges to occur outside of\n"
"the view of roundup and exposes the address of the person\n" "the view of roundup and exposes the address of the person\n"
"who updated the issue, but it could be useful in some\n" "who updated the issue, but it could be useful in some\n"
"unusual circumstances.\n" "unusual circumstances.\n"
"If set to some other value, the value is used as the reply-to\n" "If set to some other value, the value is used as the reply-to\n"
"address. It must be a valid RFC2822 address or people will not be\n "address. It must be a valid RFC2822 address or people will not\n"
" "be able to reply."),
"able to reply."),
(NullableOption, "language", "", (NullableOption, "language", "",
"Default locale name for this tracker.\n" "Default locale name for this tracker.\n"
"If this option is not set, the language is determined\n" "If this option is not set, the language is determined\n"
"by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,\n" "by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,\n"
"or LANG, in that order of preference."), "or LANG, in that order of preference."),
)), )),
("web", ( ("web", (
(BooleanOption, "allow_html_file", "no", (BooleanOption, "allow_html_file", "no",
"Setting this option enables Roundup to serve uploaded HTML\n" "Setting this option enables Roundup to serve uploaded HTML\n"
"file content *as HTML*. This is a potential security risk\n" "file content *as HTML*. This is a potential security risk\n"
"and is therefore disabled by default. Set to 'yes' if you\n" "and is therefore disabled by default. Set to 'yes' if you\n"
"trust *all* users uploading content to your tracker."), "trust *all* users uploading content to your tracker."),
(BooleanOption, 'http_auth', "yes", (BooleanOption, 'http_auth', "yes",
"Whether to use HTTP Basic Authentication, if present.\n" "Whether to use HTTP Basic Authentication, if present.\n"
"Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION\n" "Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION\n"
"variables supplied by your web server (in that order).\n" "variables supplied by your web server (in that order).\n"
"Set this option to 'no' if you do not wish to use HTTP Basic\n" "Set this option to 'no' if you do not wish to use HTTP Basic\n"
"Authentication in your web interface."), "Authentication in your web interface."),
(BooleanOption, 'http_auth_convert_realm_to_lowercase', "no",
"If usernames consist of a name and a domain/realm part of\n"
"the form user@realm and we're using REMOTE_USER for\n"
"authentication (e.g. via Kerberos), convert the realm part\n"
"of the incoming REMOTE_USER to lowercase before matching\n"
"against the roundup username. This allows roundup usernames\n"
"to be lowercase (including the realm) and still follow the\n"
"Kerberos convention of using an uppercase realm. In\n"
"addition this is compatible with Active Directory which\n"
"stores the username with realm as UserPrincipalName in\n"
"lowercase."),
(BooleanOption, 'cookie_takes_precedence', "no",
"If the http_auth option is in effect (see above)\n"
"we're accepting a REMOTE_USER variable resulting from\n"
"an authentication mechanism implemented in the web-server,\n"
"e.g., Kerberos login or similar. To override the mechanism\n"
"provided by the web-server (e.g. for enabling sub-login as\n"
"another user) we tell roundup that the cookie takes\n"
"precedence over a REMOTE_USER or HTTP_AUTHORIZATION\n"
"variable. So if both, a cookie and a REMOTE_USER is\n"
"present, the cookie wins.\n"),
(IntegerNumberGeqZeroOption, 'login_attempts_min', "3",
"Limit login attempts per user per minute to this number.\n"
"By default the 4th login attempt in a minute will notify\n"
"the user that they need to wait 20 seconds before trying to\n"
"log in again. This limits password guessing attacks and\n"
"shouldn't need to be changed. Rate limiting on login can\n"
"be disabled by setting the value to 0."),
(IntegerNumberGeqZeroOption, 'registration_delay', "4",
"The number of seconds needed to complete the new user\n"
"registration form. This limits the rate at which bots\n"
"can attempt to sign up. Limit can be disabled by setting\n"
"the value to 0."),
(BooleanOption, 'registration_prevalidate_username', "no",
"When registering a user, check that the username\n"
"is available before sending confirmation email.\n"
"Usually a username conflict is detected when\n"
"confirming the registration. Disabled by default as\n"
"it can be used for guessing existing usernames.\n"),
(SameSiteSettingOption, 'samesite_cookie_setting', "Lax", (SameSiteSettingOption, 'samesite_cookie_setting', "Lax",
"""Set the mode of the SameSite cookie option for """Set the mode of the SameSite cookie option for
the session cookie. Choices are 'Lax' or the session cookie. Choices are 'Lax' or
'Strict'. 'None' can be used to suppress the 'Strict'. 'None' can be used to suppress the
option. Strict mode provides additional security option. Strict mode provides additional security
against CSRF attacks, but may confuse users who against CSRF attacks, but may confuse users who
are logged into roundup and open a roundup link are logged into roundup and open a roundup link
from a source other than roundup (e.g. link in from a source other than roundup (e.g. link in
email)."""), email)."""),
(BooleanOption, 'enable_xmlrpc', "yes",
"""Whether to enable the XMLRPC API in the roundup web
interface. By default the XMLRPC endpoint is the string 'xmlrpc'
after the roundup web url configured in the 'tracker' section.
If this variable is set to 'no', the xmlrpc path has no special meaning
and will yield an error message."""),
(BooleanOption, 'enable_rest', "yes",
"""Whether to enable the REST API in the roundup web
interface. By default the REST endpoint is the string 'rest' plus any
additional REST-API parameters after the roundup web url configured in
the tracker section. If this variable is set to 'no', the rest path has
no special meaning and will yield an error message."""),
(IntegerNumberGeqZeroOption, 'api_calls_per_interval', "0",
"Limit API calls per api_interval_in_sec seconds to\n"
"this number.\n"
"Determines the burst rate and the rate that new api\n"
"calls will be made available. If set to 360 and\n"
"api_intervals_in_sec is set to 3600, the 361st call in\n"
"10 seconds results in a 429 error to the caller. It\n"
"tells them to wait 10 seconds (360/3600) before making\n"
"another api request. A value of 0 turns off rate\n"
"limiting in the API. Tune this as needed. See rest\n"
"documentation for more info.\n"),
(IntegerNumberGeqZeroOption, 'api_interval_in_sec', "3600",
"Defines the interval in seconds over which an api client can\n"
"make api_calls_per_interval api calls. Tune this as needed.\n"),
(CsrfSettingOption, 'csrf_enforce_token', "yes", (CsrfSettingOption, 'csrf_enforce_token', "yes",
"""How do we deal with @csrf fields in posted forms. """How do we deal with @csrf fields in posted forms.
Set this to 'required' to block the post and notify Set this to 'required' to block the post and notify
the user if the field is missing or invalid. the user if the field is missing or invalid.
Set this to 'yes' to block the post and notify the user Set this to 'yes' to block the post and notify the user
if the token is invalid, but accept the form if if the token is invalid, but accept the form if
the field is missing. the field is missing.
Set this to 'logfailure' to log a notice to the roundup Set this to 'logfailure' to log a notice to the roundup
log if the field is invalid or missing, but accept log if the field is invalid or missing, but accept
the post. the post.
Set this to 'no' to ignore the field and accept the post. Set this to 'no' to ignore the field and accept the post.
"""), """),
(IntegerNumberOption, 'csrf_token_lifetime', "20160", (IntegerNumberGeqZeroOption, 'csrf_token_lifetime', "20160",
"""csrf_tokens have a limited lifetime. If they are not """csrf_tokens have a limited lifetime. If they are not
used they are purged from the database after this used they are purged from the database after this
number of minutes. Default (20160) is 2 weeks."""), number of minutes. Default (20160) is 2 weeks."""),
(CsrfSettingOption, 'csrf_enforce_token', "yes", (CsrfSettingOption, 'csrf_enforce_token', "yes",
"""How do we deal with @csrf fields in posted forms. """How do we deal with @csrf fields in posted forms.
Set this to 'required' to block the post and notify Set this to 'required' to block the post and notify
the user if the field is missing or invalid. the user if the field is missing or invalid.
Set this to 'yes' to block the post and notify the user Set this to 'yes' to block the post and notify the user
if the token is invalid, but accept the form if if the token is invalid, but accept the form if
the field is missing. the field is missing.
Set this to 'logfailure' to log a notice to the roundup Set this to 'logfailure' to log a notice to the roundup
log if the field is invalid or missing, but accept log if the field is invalid or missing, but accept
the post. the post.
Set this to 'no' to ignore the field and accept the post. Set this to 'no' to ignore the field and accept the post.
"""), """),
(CsrfSettingOption, 'csrf_enforce_header_X-REQUESTED-WITH', "yes", (CsrfSettingOption, 'csrf_enforce_header_X-REQUESTED-WITH', "yes",
"""This is only used for xmlrpc requests. This test is """This is only used for xmlrpc and rest requests. This test is
done after Origin and Referer headers are checked. It only done after Origin and Referer headers are checked. It only
verifies that the X-Requested-With header exists. The value verifies that the X-Requested-With header exists. The value
is ignored. is ignored.
Set this to 'required' to block the post and notify Set this to 'required' to block the post and notify
the user if the header is missing or invalid. the user if the header is missing or invalid.
Set this to 'yes' is the same as required. Set this to 'yes' is the same as required.
Set this to 'logfailure' is the same as 'no'. Set this to 'logfailure' is the same as 'no'.
Set this to 'no' to ignore the header and accept the post."""), Set this to 'no' to ignore the header and accept the post."""),
(CsrfSettingOption, 'csrf_enforce_header_referer', "yes", (CsrfSettingOption, 'csrf_enforce_header_referer', "yes",
"""Verify that the Referer http header matches the """Verify that the Referer http header matches the
skipping to change at line 816 skipping to change at line 961
tracker.web setting in config.ini. tracker.web setting in config.ini.
Set this to 'required' to block the post and notify Set this to 'required' to block the post and notify
the user if the header is missing or invalid. the user if the header is missing or invalid.
Set this to 'yes' to block the post and notify the user Set this to 'yes' to block the post and notify the user
if the header is invalid, but accept the form if if the header is invalid, but accept the form if
the field is missing. the field is missing.
Set this to 'logfailure' to log a notice to the roundup Set this to 'logfailure' to log a notice to the roundup
log if the header is invalid or missing, but accept log if the header is invalid or missing, but accept
the post. the post.
Set this to 'no' to ignore the header and accept the post."""), Set this to 'no' to ignore the header and accept the post."""),
(IntegerNumberOption, 'csrf_header_min_count', "1", (IntegerNumberGeqZeroOption, 'csrf_header_min_count', "1",
"""Minimum number of header checks that must pass """Minimum number of header checks that must pass
to accept the request. Set to 0 to accept post to accept the request. Set to 0 to accept post
even if no header checks pass. Usually the Host header check even if no header checks pass. Usually the Host header check
always passes, so setting it less than 1 is not recommended."""), always passes, so setting it less than 1 is not recommended."""),
(BooleanOption, 'use_browser_language', "yes", (BooleanOption, 'use_browser_language', "yes",
"Whether to use HTTP Accept-Language, if present.\n" "Whether to use HTTP Accept-Language, if present.\n"
"Browsers send a language-region preference list.\n" "Browsers send a language-region preference list.\n"
"It's usually set in the client's browser or in their\n" "It's usually set in the client's browser or in their\n"
"Operating System.\n" "Operating System.\n"
"Set this option to 'no' if you want to ignore it."), "Set this option to 'no' if you want to ignore it."),
(BooleanOption, "debug", "no", (BooleanOption, "debug", "no",
"Setting this option makes Roundup display error tracebacks\n" "Setting this option makes Roundup display error tracebacks\n"
"in the user's browser rather than emailing them to the\n" "in the user's browser rather than emailing them to the\n"
"tracker admin."), "tracker admin."),
(BooleanOption, "migrate_passwords", "yes", (BooleanOption, "migrate_passwords", "yes",
"Setting this option makes Roundup migrate passwords with\n" "Setting this option makes Roundup migrate passwords with\n"
"an insecure password-scheme to a more secure scheme\n" "an insecure password-scheme to a more secure scheme\n"
"when the user logs in via the web-interface."), "when the user logs in via the web-interface."),
(MandatoryOption, "secret_key", create_token(),
"A per tracker secret used in etag calculations for\n"
"an object. It must not be empty.\n"
"It prevents reverse engineering hidden data in an object\n"
"by calculating the etag for a sample object. Then modifying\n"
"hidden properties until the sample object's etag matches\n"
"the one returned by roundup.\n"
"Changing this changes the etag and invalidates updates by\n"
"clients. It must be persistent across application restarts.\n"
"(Note the default value changes every time\n"
" roundup-admin updateconfig\n"
"is run, so it must be explicitly set to a non-empty string.\n"),
(MandatoryOption, "jwt_secret", "disabled",
"This is used to generate/validate json web tokens (jwt).\n"
"Even if you don't use jwts it must not be empty.\n"
"If less than 256 bits (32 characters) in length it will\n"
"disable use of jwt. Changing this invalidates all jwts\n"
"issued by the roundup instance requiring *all* users to\n"
"generate new jwts. This is experimental and disabled by\n"
"default. It must be persistent across application restarts.\n"),
)), )),
("rdbms", ( ("rdbms", (
(DatabaseBackend, 'backend', NODEFAULT,
"Database backend."),
(Option, 'name', 'roundup', (Option, 'name', 'roundup',
"Name of the database to use.", "Name of the database to use.",
['MYSQL_DBNAME']), ['MYSQL_DBNAME']),
(Option, 'backend', '',
"Database backend."),
(NullableOption, 'host', 'localhost', (NullableOption, 'host', 'localhost',
"Database server host.", "Database server host.",
['MYSQL_DBHOST']), ['MYSQL_DBHOST']),
(NullableOption, 'port', '', (NullableOption, 'port', '',
"TCP port number of the database server.\n" "TCP port number of the database server.\n"
"Postgresql usually resides on port 5432 (if any),\n" "Postgresql usually resides on port 5432 (if any),\n"
"for MySQL default port number is 3306.\n" "for MySQL default port number is 3306.\n"
"Leave this option empty to use backend default"), "Leave this option empty to use backend default"),
(NullableOption, 'user', 'roundup', (NullableOption, 'user', 'roundup',
"Database user name that Roundup should use.", "Database user name that Roundup should use.",
['MYSQL_DBUSER']), ['MYSQL_DBUSER']),
(NullableOption, 'password', 'roundup', (NullableOption, 'password', 'roundup',
"Database user password.", "Database user password.",
['MYSQL_DBPASSWORD']), ['MYSQL_DBPASSWORD']),
(NullableOption, 'read_default_file', '~/.my.cnf', (NullableOption, 'read_default_file', '~/.my.cnf',
"Name of the MySQL defaults file.\n" "Name of the MySQL defaults file.\n"
"Only used in MySQL connections."), "Only used in MySQL connections."),
(NullableOption, 'read_default_group', 'roundup', (NullableOption, 'read_default_group', 'roundup',
"Name of the group to use in the MySQL defaults file (.my.cnf).\n" "Name of the group to use in the MySQL defaults file (.my.cnf).\n"
"Only used in MySQL connections."), "Only used in MySQL connections."),
(IntegerNumberOption, 'sqlite_timeout', '30', (Option, 'mysql_charset', 'utf8',
"Charset to use for mysql connection,\n"
"use 'default' for the mysql default, no charset option\n"
"is used when creating the connection in that case.\n"
"Otherwise any permissible mysql charset is allowed here.\n"
"Only used in MySQL connections."),
(IntegerNumberGeqZeroOption, 'sqlite_timeout', '30',
"Number of seconds to wait when the SQLite database is locked\n" "Number of seconds to wait when the SQLite database is locked\n"
"Default: use a 30 second timeout (extraordinarily generous)\n" "Default: use a 30 second timeout (extraordinarily generous)\n"
"Only used in SQLite connections."), "Only used in SQLite connections."),
(IntegerNumberOption, 'cache_size', '100', (IntegerNumberGeqZeroOption, 'cache_size', '100',
"Size of the node cache (in elements)"), "Size of the node cache (in elements)"),
(BooleanOption, "allow_create", "yes", (BooleanOption, "allow_create", "yes",
"Setting this option to 'no' protects the database against table cre ations."), "Setting this option to 'no' protects the database against table cre ations."),
(BooleanOption, "allow_alter", "yes", (BooleanOption, "allow_alter", "yes",
"Setting this option to 'no' protects the database against table alt erations."), "Setting this option to 'no' protects the database against table alt erations."),
(BooleanOption, "allow_drop", "yes", (BooleanOption, "allow_drop", "yes",
"Setting this option to 'no' protects the database against table dro ps."), "Setting this option to 'no' protects the database against table dro ps."),
(NullableOption, 'template', '', (NullableOption, 'template', '',
"Name of the PostgreSQL template for database creation.\n" "Name of the PostgreSQL template for database creation.\n"
"For database creation the template used has to match\n" "For database creation the template used has to match\n"
skipping to change at line 890 skipping to change at line 1061
" new encoding (UTF8) is incompatible with the encoding of\n" " new encoding (UTF8) is incompatible with the encoding of\n"
" the template database (SQL_ASCII)\n" " the template database (SQL_ASCII)\n"
" HINT: Use the same encoding as in the template database,\n" " HINT: Use the same encoding as in the template database,\n"
" or use template0 as template.\n" " or use template0 as template.\n"
"then set this option to the template name given in the\n" "then set this option to the template name given in the\n"
"error message."), "error message."),
(IsolationOption, 'isolation_level', 'read committed', (IsolationOption, 'isolation_level', 'read committed',
"Database isolation level, currently supported for\n" "Database isolation level, currently supported for\n"
"PostgreSQL and mysql. See, e.g.,\n" "PostgreSQL and mysql. See, e.g.,\n"
"http://www.postgresql.org/docs/9.1/static/transaction-iso.html"), "http://www.postgresql.org/docs/9.1/static/transaction-iso.html"),
), "Settings in this section are used" ), "Settings in this section (except for backend) are used"
" by RDBMS backends only" " by RDBMS backends only."
), ),
("logging", ( ("logging", (
(FilePathOption, "config", "", (FilePathOption, "config", "",
"Path to configuration file for standard Python logging module.\n" "Path to configuration file for standard Python logging module.\n"
"If this option is set, logging configuration is loaded\n" "If this option is set, logging configuration is loaded\n"
"from specified file; options 'filename' and 'level'\n" "from specified file; options 'filename' and 'level'\n"
"in this section are ignored."), "in this section are ignored."),
(FilePathOption, "filename", "", (FilePathOption, "filename", "",
"Log file name for minimal logging facility built into Roundup.\n" "Log file name for minimal logging facility built into Roundup.\n"
"If no file name specified, log messages are written on stderr.\n" "If no file name specified, log messages are written on stderr.\n"
"If above 'config' option is set, this option has no effect."), "If above 'config' option is set, this option has no effect."),
(Option, "level", "ERROR", (Option, "level", "ERROR",
"Minimal severity level of messages written to log file.\n" "Minimal severity level of messages written to log file.\n"
"If above 'config' option is set, this option has no effect.\n" "If above 'config' option is set, this option has no effect.\n"
"Allowed values: DEBUG, INFO, WARNING, ERROR"), "Allowed values: DEBUG, INFO, WARNING, ERROR"),
(BooleanOption, "disable_loggers", "no",
"If set to yes, only the loggers configured in this section will\n"
"be used. Yes will disable gunicorn's --access-logfile.\n"),
)), )),
("mail", ( ("mail", (
(Option, "domain", NODEFAULT, (Option, "domain", NODEFAULT,
"The email domain that admin_email, issue_tracker and\n" "The email domain that admin_email, issue_tracker and\n"
"dispatcher_email belong to.\n" "dispatcher_email belong to.\n"
"This domain is added to those config items if they don't\n" "This domain is added to those config items if they don't\n"
"explicitly include a domain.\n" "explicitly include a domain.\n"
"Do not include the '@' symbol."), "Do not include the '@' symbol."),
(Option, "host", NODEFAULT, (Option, "host", NODEFAULT,
"SMTP mail host that roundup will use to send mail", "SMTP mail host that roundup will use to send mail",
["MAILHOST"],), ["MAILHOST"],),
(Option, "username", "", "SMTP login name.\n" (Option, "username", "", "SMTP login name.\n"
"Set this if your mail host requires authenticated access.\n" "Set this if your mail host requires authenticated access.\n"
"If username is not empty, password (below) MUST be set!"), "If username is not empty, password (below) MUST be set!"),
(Option, "password", NODEFAULT, "SMTP login password.\n" (Option, "password", NODEFAULT, "SMTP login password.\n"
"Set this if your mail host requires authenticated access."), "Set this if your mail host requires authenticated access."),
(IntegerNumberOption, "port", smtplib.SMTP_PORT, (IntegerNumberGeqZeroOption, "port", smtplib.SMTP_PORT,
"Default port to send SMTP on.\n" "Default port to send SMTP on.\n"
"Set this if your mail server runs on a different port."), "Set this if your mail server runs on a different port."),
(NullableOption, "local_hostname", '', (NullableOption, "local_hostname", '',
"The local hostname to use during SMTP transmission.\n" "The local hostname to use during SMTP transmission.\n"
"Set this if your mail server requires something specific."), "Set this if your mail server requires something specific."),
(BooleanOption, "tls", "no", (BooleanOption, "tls", "no",
"If your SMTP mail host provides or requires TLS\n" "If your SMTP mail host provides or requires TLS\n"
"(Transport Layer Security) then set this option to 'yes'."), "(Transport Layer Security) then set this option to 'yes'."),
(NullableFilePathOption, "tls_keyfile", "", (NullableFilePathOption, "tls_keyfile", "",
"If TLS is used, you may set this option to the name\n" "If TLS is used, you may set this option to the name\n"
skipping to change at line 959 skipping to change at line 1133
" SENDMAILDEBUG.\nEnvironment variable takes precedence."), " SENDMAILDEBUG.\nEnvironment variable takes precedence."),
(BooleanOption, "add_authorinfo", "yes", (BooleanOption, "add_authorinfo", "yes",
"Add a line with author information at top of all messages\n" "Add a line with author information at top of all messages\n"
"sent by roundup"), "sent by roundup"),
(BooleanOption, "add_authoremail", "yes", (BooleanOption, "add_authoremail", "yes",
"Add the mail address of the author to the author information at\n" "Add the mail address of the author to the author information at\n"
"the top of all messages.\n" "the top of all messages.\n"
"If this is false but add_authorinfo is true, only the name\n" "If this is false but add_authorinfo is true, only the name\n"
"of the actor is added which protects the mail address of the\n" "of the actor is added which protects the mail address of the\n"
"actor from being exposed at mail archives, etc."), "actor from being exposed at mail archives, etc."),
), "Outgoing email options.\nUsed for nosy messages and approval requests"), ), "Outgoing email options.\n"
"Used for nosy messages and approval requests"),
("mailgw", ( ("mailgw", (
(EmailBodyOption, "keep_quoted_text", "yes", (EmailBodyOption, "keep_quoted_text", "yes",
"Keep email citations when accepting messages.\n" "Keep email citations when accepting messages.\n"
"Setting this to \"no\" strips out \"quoted\" text\n" "Setting this to \"no\" strips out \"quoted\" text\n"
"from the message. Setting this to \"new\" keeps quoted\n" "from the message. Setting this to \"new\" keeps quoted\n"
"text only if a new issue is being created.\n" "text only if a new issue is being created.\n"
"Signatures are also stripped.", "Signatures are also stripped.",
["EMAIL_KEEP_QUOTED_TEXT"]), ["EMAIL_KEEP_QUOTED_TEXT"]),
(EmailBodyOption, "leave_body_unchanged", "no", (EmailBodyOption, "leave_body_unchanged", "no",
"Setting this to \"yes\" preserves the email body\n" "Setting this to \"yes\" preserves the email body\n"
skipping to change at line 1016 skipping to change at line 1191
"against issue titles in the case where there is no\n" "against issue titles in the case where there is no\n"
"designator [prefix]. \"never\" turns off matching.\n" "designator [prefix]. \"never\" turns off matching.\n"
"\"creation + interval\" or \"activity + interval\"\n" "\"creation + interval\" or \"activity + interval\"\n"
"will match an issue for the interval after the issue's\n" "will match an issue for the interval after the issue's\n"
"creation or last activity. The interval is a standard\n" "creation or last activity. The interval is a standard\n"
"Roundup interval."), "Roundup interval."),
(BooleanOption, "subject_updates_title", "yes", (BooleanOption, "subject_updates_title", "yes",
"Update issue title if incoming subject of email is different.\n" "Update issue title if incoming subject of email is different.\n"
"Setting this to \"no\" will ignore the title part of" "Setting this to \"no\" will ignore the title part of"
" the subject\nof incoming email messages.\n"), " the subject\nof incoming email messages.\n"),
(RegExpOption, "refwd_re", "(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+", (RegExpOption, "refwd_re", r"(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+",
"Regular expression matching a single reply or forward\n" "Regular expression matching a single reply or forward\n"
"prefix prepended by the mailer. This is explicitly\n" "prefix prepended by the mailer. This is explicitly\n"
"stripped from the subject during parsing."), "stripped from the subject during parsing."),
(RegExpOption, "origmsg_re", (RegExpOption, "origmsg_re",
"^[>|\s]*-----\s?Original Message\s?-----$", r"^[>|\s]*-----\s?Original Message\s?-----$",
"Regular expression matching start of an original message\n" "Regular expression matching start of an original message\n"
"if quoted the in body."), "if quoted the in body."),
(RegExpOption, "sign_re", "^[>|\s]*-- ?$", (RegExpOption, "sign_re", r"^[>|\s]*-- ?$",
"Regular expression matching the start of a signature\n" "Regular expression matching the start of a signature\n"
"in the message body."), "in the message body."),
(RegExpOption, "eol_re", r"[\r\n]+", (RegExpOption, "eol_re", r"[\r\n]+",
"Regular expression matching end of line."), "Regular expression matching end of line."),
(RegExpOption, "blankline_re", r"[\r\n]+\s*[\r\n]+", (RegExpOption, "blankline_re", r"[\r\n]+\s*[\r\n]+",
"Regular expression matching a blank line."), "Regular expression matching a blank line."),
(BooleanOption, "unpack_rfc822", "no", (BooleanOption, "unpack_rfc822", "no",
"Unpack attached messages (encoded as message/rfc822 in MIME)\n" "Unpack attached messages (encoded as message/rfc822 in MIME)\n"
"as multiple parts attached as files to the issue, if not\n" "as multiple parts attached as files to the issue, if not\n"
"set we handle message/rfc822 attachments as a single file."), "set we handle message/rfc822 attachments as a single file."),
skipping to change at line 1058 skipping to change at line 1233
"be used instead."), "be used instead."),
(BooleanOption, "keep_real_from", "no", (BooleanOption, "keep_real_from", "no",
"When handling emails ignore the Resent-From:-header\n" "When handling emails ignore the Resent-From:-header\n"
"and use the original senders From:-header instead.\n" "and use the original senders From:-header instead.\n"
"(This might be desirable in some situations where a moderator\n" "(This might be desirable in some situations where a moderator\n"
"reads incoming messages first before bouncing them to Roundup)", "reads incoming messages first before bouncing them to Roundup)",
["EMAIL_KEEP_REAL_FROM"]), ["EMAIL_KEEP_REAL_FROM"]),
), "Roundup Mail Gateway options"), ), "Roundup Mail Gateway options"),
("pgp", ( ("pgp", (
(BooleanOption, "enable", "no", (BooleanOption, "enable", "no",
"Enable PGP processing. Requires pyme. If you're planning\n" "Enable PGP processing. Requires gpg. If you're planning\n"
"to send encrypted PGP mail to the tracker, you should also\n" "to send encrypted PGP mail to the tracker, you should also\n"
"enable the encrypt-option below, otherwise mail received\n" "enable the encrypt-option below, otherwise mail received\n"
"encrypted might be sent unencrypted to another user."), "encrypted might be sent unencrypted to another user."),
(NullableOption, "roles", "", (NullableOption, "roles", "",
"If specified, a comma-separated list of roles to perform\n" "If specified, a comma-separated list of roles to perform\n"
"PGP processing on. If not specified, it happens for all\n" "PGP processing on. If not specified, it happens for all\n"
"users. Note that received PGP messages (signed and/or\n" "users. Note that received PGP messages (signed and/or\n"
"encrypted) will be processed with PGP even if the user\n" "encrypted) will be processed with PGP even if the user\n"
"doesn't have one of the PGP roles, you can use this to make\n" "doesn't have one of the PGP roles, you can use this to make\n"
"PGP processing completely optional by defining a role here\n" "PGP processing completely optional by defining a role here\n"
skipping to change at line 1120 skipping to change at line 1295
"nosy list? If 'new' is used, then the recipients will\n" "nosy list? If 'new' is used, then the recipients will\n"
"only be added when a message creates a new issue.\n" "only be added when a message creates a new issue.\n"
"If 'yes', then the recipients will be added on followups too.\n" "If 'yes', then the recipients will be added on followups too.\n"
"If 'no', they're never added to the nosy.\n", "If 'no', they're never added to the nosy.\n",
["ADD_RECIPIENTS_TO_NOSY"]), ["ADD_RECIPIENTS_TO_NOSY"]),
(Option, "email_sending", "single", (Option, "email_sending", "single",
"Controls the email sending from the nosy reactor. If\n" "Controls the email sending from the nosy reactor. If\n"
"\"multiple\" then a separate email is sent to each\n" "\"multiple\" then a separate email is sent to each\n"
"recipient. If \"single\" then a single email is sent with\n" "recipient. If \"single\" then a single email is sent with\n"
"each recipient as a CC address."), "each recipient as a CC address."),
(IntegerNumberOption, "max_attachment_size", sys.maxint, (IntegerNumberGeqZeroOption, "max_attachment_size", sys.maxsize,
"Attachments larger than the given number of bytes\n" "Attachments larger than the given number of bytes\n"
"won't be attached to nosy mails. They will be replaced by\n" "won't be attached to nosy mails. They will be replaced by\n"
"a link to the tracker's download page for the file.") "a link to the tracker's download page for the file.")
), "Nosy messages sending"), ), "Nosy messages sending"),
) )
### Configuration classes ### Configuration classes
class Config: class Config:
skipping to change at line 1228 skipping to change at line 1403
_options = self._get_section_options(_section) _options = self._get_section_options(_section)
if _name not in _options: if _name not in _options:
_options.append(_name) _options.append(_name)
# (section, name) key is used for writing .ini file # (section, name) key is used for writing .ini file
self.options[(_section, _name)] = option self.options[(_section, _name)] = option
# make the option known under all of its A.K.A.s # make the option known under all of its A.K.A.s
for _name in option.aliases: for _name in option.aliases:
self.options[_name] = option self.options[_name] = option
def update_option(self, name, klass, def update_option(self, name, klass,
default=NODEFAULT, description=None default=NODEFAULT, description=None):
):
"""Override behaviour of early created option. """Override behaviour of early created option.
Parameters: Parameters:
name: name:
option name option name
klass: klass:
one of the Option classes one of the Option classes
default: default:
optional default value for the option optional default value for the option
description: description:
skipping to change at line 1259 skipping to change at line 1433
# fetch current option # fetch current option
option = self._get_option(name) option = self._get_option(name)
# compute constructor parameters # compute constructor parameters
if default is NODEFAULT: if default is NODEFAULT:
default = option.default default = option.default
if description is None: if description is None:
description = option.description description = option.description
value = option.value2str(current=1) value = option.value2str(current=1)
# resurrect the option # resurrect the option
option = klass(self, option.section, option.setting, option = klass(self, option.section, option.setting,
default=default, description=description) default=default, description=description)
# apply the value # apply the value
option.set(value) option.set(value)
# incorporate new option # incorporate new option
del self[name] del self[name]
self.add_option(option) self.add_option(option)
def reset(self): def reset(self):
"""Set all options to their default values""" """Set all options to their default values"""
for _option in self.items(): for _option in self.items():
_option.reset() _option.reset()
# Meant for commandline tools. # Meant for commandline tools.
# Allows automatic creation of configuration files like this: # Allows automatic creation of configuration files like this:
# roundup-server -p 8017 -u roundup --save-config # roundup-server -p 8017 -u roundup --save-config
def getopt(self, args, short_options="", long_options=(), def getopt(self, args, short_options="", long_options=(),
config_load_options=("C", "config"), **options config_load_options=("C", "config"), **options):
):
"""Apply options specified in command line arguments. """Apply options specified in command line arguments.
Parameters: Parameters:
args: args:
command line to parse (sys.argv[1:]) command line to parse (sys.argv[1:])
short_options: short_options:
optional string of letters for command line options optional string of letters for command line options
that are not config options that are not config options
long_options: long_options:
optional list of names for long options optional list of names for long options
skipping to change at line 1345 skipping to change at line 1518
# load config file if requested # load config file if requested
if config_load_options: if config_load_options:
for option in optlist: for option in optlist:
if option[0] in config_load_options: if option[0] in config_load_options:
self.load_ini(option[1]) self.load_ini(option[1])
optlist.remove(option) optlist.remove(option)
break break
# apply options # apply options
extra_options = [] extra_options = []
for (opt, arg) in optlist: for (opt, arg) in optlist:
if (opt in booleans): # and not arg if (opt in booleans): # and not arg
arg = "yes" arg = "yes"
try: try:
name = cfg_names[opt] name = cfg_names[opt]
except KeyError: except KeyError:
extra_options.append((opt, arg)) extra_options.append((opt, arg))
else: else:
self[name] = arg self[name] = arg
return (extra_options, args) return (extra_options, args)
# option and section locators (used in option access methods) # option and section locators (used in option access methods)
skipping to change at line 1445 skipping to change at line 1618
If the file to write already exists, it is saved with '.bak' If the file to write already exists, it is saved with '.bak'
extension. extension.
""" """
if ini_file is None: if ini_file is None:
ini_file = self.filepath ini_file = self.filepath
_tmp_file = os.path.splitext(ini_file)[0] _tmp_file = os.path.splitext(ini_file)[0]
_bak_file = _tmp_file + ".bak" _bak_file = _tmp_file + ".bak"
_tmp_file = _tmp_file + ".tmp" _tmp_file = _tmp_file + ".tmp"
_fp = file(_tmp_file, "wt") _fp = open(_tmp_file, "wt")
_fp.write("# %s configuration file\n" % self._get_name()) _fp.write("# %s configuration file\n" % self._get_name())
_fp.write("# Autogenerated at %s\n" % time.asctime()) _fp.write("# Autogenerated at %s\n" % time.asctime())
need_set = self._get_unset_options() need_set = self._get_unset_options()
if need_set: if need_set:
_fp.write("\n# WARNING! Following options need adjustments:\n") _fp.write("\n# WARNING! Following options need adjustments:\n")
for section, options in need_set.items(): for section, options in need_set.items():
_fp.write("# [%s]: %s\n" % (section, ", ".join(options))) _fp.write("# [%s]: %s\n" % (section, ", ".join(options)))
for section in self.sections: for section in self.sections:
comment = self.section_descriptions.get(section, None) comment = self.section_descriptions.get(section, None)
if comment: if comment:
_fp.write("\n# ".join([""] + comment.split("\n")) +"\n") _fp.write("\n# ".join([""] + comment.split("\n")) + "\n")
else: else:
# no section comment - just leave a blank line between sections # no section comment - just leave a blank line between sections
_fp.write("\n") _fp.write("\n")
_fp.write("[%s]\n" % section) _fp.write("[%s]\n" % section)
for option in self._get_section_options(section): for option in self._get_section_options(section):
_fp.write("\n" + self.options[(section, option)].format()) _fp.write("\n" + self.options[(section, option)].format())
_fp.close() _fp.close()
if os.access(ini_file, os.F_OK): if os.access(ini_file, os.F_OK):
if os.access(_bak_file, os.F_OK): if os.access(_bak_file, os.F_OK):
os.remove(_bak_file) os.remove(_bak_file)
skipping to change at line 1505 skipping to change at line 1678
def items(self): def items(self):
"""Return the list of Option objects, in .ini file order """Return the list of Option objects, in .ini file order
Note that HOME is not included in this list Note that HOME is not included in this list
because it is builtin pseudo-option, not a real Option because it is builtin pseudo-option, not a real Option
object loaded from or saved to .ini file. object loaded from or saved to .ini file.
""" """
return [self.options[(_section, _name)] return [self.options[(_section, _name)]
for _section in self.sections for _section in self.sections
for _name in self._get_section_options(_section) for _name in self._get_section_options(_section)]
]
def keys(self): def keys(self):
"""Return the list of "canonical" names of the options """Return the list of "canonical" names of the options
Unlike .items(), this list also includes HOME Unlike .items(), this list also includes HOME
""" """
return ["HOME"] + [_option.name for _option in self.items()] return ["HOME"] + [_option.name for _option in self.items()]
# .values() is not implemented because i am not sure what should be # .values() is not implemented because i am not sure what should be
skipping to change at line 1546 skipping to change at line 1718
Instances of this class have no predefined configuration layout. Instances of this class have no predefined configuration layout.
Options are created on the fly for each setting present in the Options are created on the fly for each setting present in the
config file. config file.
""" """
def _adjust_options(self, config): def _adjust_options(self, config):
# config defaults appear in all sections. # config defaults appear in all sections.
# we'll need to filter them out. # we'll need to filter them out.
defaults = config.defaults().keys() defaults = list(config.defaults().keys())
# see what options are already defined and add missing ones # see what options are already defined and add missing ones
preset = [(option.section, option.setting) for option in self.items()] preset = [(option.section, option.setting) for option in self.items()]
for section in config.sections(): for section in config.sections():
for name in config.options(section): for name in config.options(section):
if ((section, name) not in preset) \ if ((section, name) not in preset) \
and (name not in defaults): and (name not in defaults):
self.add_option(Option(self, section, name)) self.add_option(Option(self, section, name))
class CoreConfig(Config): class CoreConfig(Config):
"""Roundup instance configuration. """Roundup instance configuration.
Core config has a predefined layout (see the SETTINGS structure), Core config has a predefined layout (see the SETTINGS structure),
supports loading of old-style pythonic configurations and holds supports loading of old-style pythonic configurations and holds
three additional attributes: three additional attributes:
detectors: detectors:
skipping to change at line 1614 skipping to change at line 1786
Config.reset(self) Config.reset(self)
if self.ext: if self.ext:
self.ext.reset() self.ext.reset()
if self.detectors: if self.detectors:
self.detectors.reset() self.detectors.reset()
self.init_logging() self.init_logging()
def init_logging(self): def init_logging(self):
_file = self["LOGGING_CONFIG"] _file = self["LOGGING_CONFIG"]
if _file and os.path.isfile(_file): if _file and os.path.isfile(_file):
logging.config.fileConfig(_file) logging.config.fileConfig(_file,
disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"])
return return
_file = self["LOGGING_FILENAME"] _file = self["LOGGING_FILENAME"]
# set file & level on the roundup logger # set file & level on the roundup logger
logger = logging.getLogger('roundup') logger = logging.getLogger('roundup')
if _file: if _file:
hdlr = logging.FileHandler(_file) hdlr = logging.FileHandler(_file)
else: else:
hdlr = logging.StreamHandler(sys.stdout) hdlr = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter( formatter = logging.Formatter(
'%(asctime)s %(levelname)s %(message)s') '%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter) hdlr.setFormatter(formatter)
# no logging API to remove all existing handlers!?! # no logging API to remove all existing handlers!?!
for h in logger.handlers: for h in logger.handlers:
h.close() h.close()
logger.removeHandler(hdlr) logger.removeHandler(hdlr)
logger.handlers = [hdlr] logger.handlers = [hdlr]
logger.setLevel(logging._levelNames[self["LOGGING_LEVEL"] or "ERROR"]) logger.setLevel(self["LOGGING_LEVEL"] or "ERROR")
def load(self, home_dir): def load(self, home_dir):
"""Load configuration from path designated by home_dir argument""" """Load configuration from path designated by home_dir argument"""
if os.path.isfile(os.path.join(home_dir, self.INI_FILE)): if os.path.isfile(os.path.join(home_dir, self.INI_FILE)):
self.load_ini(home_dir) self.load_ini(home_dir)
else: else:
self.load_pyconfig(home_dir) self.load_pyconfig(home_dir)
self.init_logging() self.init_logging()
self.ext = UserConfig(os.path.join(home_dir, "extensions")) self.ext = UserConfig(os.path.join(home_dir, "extensions"))
self.detectors = UserConfig(os.path.join(home_dir, "detectors")) self.detectors = UserConfig(os.path.join(home_dir, "detectors"))
 End of changes. 62 change blocks. 
84 lines changed or deleted 251 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)