Christopher Lenz wrote:
...
I would suggest not moving forward down that line, but rather refactor the config defaults to use an extension point.

   class ConfigOption(object):
      def __init__(self, section, name, default=None, doc=None):
         # xxx

   class IConfigurable(Interface):
      def get_config_options():
         # return a list of ConfigurationOption objects

(defined in trac.config, extension point for IConfigurable in Environment)

Ok, I finally manage to do it.

Everything works fine & as expected _except_ the notification unit tests.
I unfortunately have no time to debug it further before the W.E.

Here is the patch. Comments welcome!

-- Christian



Index: htdocs/css/about.css
===================================================================
--- htdocs/css/about.css        (revision 3045)
+++ htdocs/css/about.css        (working copy)
@@ -1,38 +1,42 @@
 /* About config */
-#content.about_config table {
+#content .about_config table {
  border-collapse: collapse;
  margin: 2em 0;
 }
-#content.about_config th {
+#content .about_config th {
  background: #f7f7f0;
  font-weight: bold;
- text-align: right;
+ text-align: left;
  vertical-align: top;
 }
-#content.about_config th.name, #content.about_config th.value {
- text-align: left;
+#content .about_config th.section {
+ text-align: right;
 }
-#content.about_config th, #content.about_config td {
+#content .about_config th, #content .about_config td {
  border: 1px solid #ddd;
  padding: 3px;
 }
+#content .about_config td.name { background:#f9f9f0; }
+#content .about_config td.value { background:#f9f9f0; font-weight: bold; }
+#content .about_config td.defaultvalue { font-family: monospace; 
background:#f9f9f0; }
+#content .about_config td.doc { padding: 3px 1em 3px 1em; }
 
 /* About plugins */
-#content.about_plugins h2 {
+#content .about_plugins h2 {
  background: #f7f7f7;
  border-bottom: 1px solid #d7d7d7;
  margin: 2em 0 0;
 }
-#content.about_plugins table {
+#content .about_plugins table {
  border-collapse: collapse;
  margin: 1em 0;
  table-layout: fixed;
  width: 100%;
 }
-#content.about_plugins th, #content.about_plugins td { border: 1px solid #ddd; 
padding: 3px }
-#content.about_plugins th { background: #f7f7f0; font-weight: bold; 
text-align: right; vertical-align: top; width: 12em }
-#content.about_plugins td.module { font-family: monospace; }
-#content.about_plugins td.module .path { color: #999; font-size: 90%; }
+#content .about_plugins th, #content .about_plugins td { border: 1px solid 
#ddd; padding: 3px }
+#content .about_plugins th { background: #f7f7f0; font-weight: bold; 
text-align: right; vertical-align: top; width: 12em }
+#content .about_plugins td.module { font-family: monospace; }
+#content .about_plugins td.module .path { color: #999; font-size: 90%; }
 
-#content.about_plugins td.xtnpts { margin-top: 1em; }
-#content.about_plugins td.xtnpts ul { list-style: square; margin: 0; padding: 
0 0 0 2em; }
+#content .about_plugins td.xtnpts { margin-top: 1em; }
+#content .about_plugins td.xtnpts ul { list-style: square; margin: 0; padding: 
0 0 0 2em; }
Index: trac/attachment.py
===================================================================
--- trac/attachment.py  (revision 3045)
+++ trac/attachment.py  (working copy)
@@ -24,6 +24,7 @@
 import urllib
 
 from trac import perm, util
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.env import IEnvironmentSetupParticipant
 from trac.mimeview import *
@@ -209,11 +210,34 @@
 
 class AttachmentModule(Component):
 
-    implements(IEnvironmentSetupParticipant, IRequestHandler,
+    implements(IConfigurable, IEnvironmentSetupParticipant, IRequestHandler,
                INavigationContributor, IWikiSyntaxProvider)
 
     CHUNK_SIZE = 4096
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('attachment', [
+            ConfigOption('max_size', '262144', doc="""
+
+            Maximum allowed file size for ticket and wiki attachments
+
+            """),
+            ConfigOption('render_unsafe_content', 'false', doc="""
+
+            Whether non-binary attachments should be rendered in the browser,
+            or only made downloadable.
+
+            Pretty much any text file may be interpreted as HTML by the
+            browser, which allows a malicious user to attach a file containing
+            cross-site scripting attacks.
+
+            For public sites where anonymous users can create attachments,
+            it is recommended to leave this option off (which is the default).
+
+            """)])
+
     # IEnvironmentSetupParticipant methods
 
     def environment_created(self):
Index: trac/env.py
===================================================================
--- trac/env.py (revision 3045)
+++ trac/env.py (working copy)
@@ -17,7 +17,7 @@
 import os
 
 from trac import db_default, util
-from trac.config import Configuration
+from trac.config import Configuration, IConfigurable, ConfigOption
 from trac.core import Component, ComponentManager, implements, Interface, \
                       ExtensionPoint, TracError
 from trac.db import DatabaseManager
@@ -61,6 +61,7 @@
      * wiki and ticket attachments.
     """   
     setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
+    config_providers = ExtensionPoint(IConfigurable)
 
     def __init__(self, path, create=False, options=[]):
         """Initialize the Trac environment.
@@ -75,15 +76,18 @@
         ComponentManager.__init__(self)
 
         self.path = path
-        self.load_config()
+        self.setup_config() 
         self.setup_log() 
 
         from trac.loader import load_components
         load_components(self)
-
+        
         if create:
             self.create(options)
         else:
+            for section, options in self.get_default_config().iteritems():
+                for opt in options:
+                    self.config.setdefault(section, opt.name, opt.default)
             self.verify()
 
         if create:
@@ -181,9 +185,10 @@
         # Setup the default configuration
         os.mkdir(os.path.join(self.path, 'conf'))
         _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
-        self.load_config()
-        for section, name, value in db_default.default_config:
-            self.config.set(section, name, value)
+        self.setup_config()
+        for section, opts in self.get_default_config().iteritems():
+            for opt in opts:
+                self.config.set(section, opt.name, opt.default)
         for section, name, value in options:
             self.config.set(section, name, value)
         self.config.save()
@@ -200,12 +205,22 @@
         row = cursor.fetchone()
         return row and int(row[0])
 
-    def load_config(self):
+    def setup_config(self):
         """Load the configuration file."""
         self.config = Configuration(os.path.join(self.path, 'conf', 
'trac.ini'))
-        for section, name, value in db_default.default_config:
-            self.config.setdefault(section, name, value)
 
+    def get_default_config(self):
+        """Return a dictionary of (`section`, `options`) pairs.
+
+        `options` is a list of `ConfigOption` objects.
+        """
+        default_config = {}
+        for provider in self.config_providers:
+            for section, options in provider.get_config_options():
+                other_options = default_config.get(section, [])
+                default_config[section] = other_options + options
+        return default_config
+        
     def get_templates_dir(self):
         """Return absolute path to the templates directory."""
         return os.path.join(self.path, 'templates')
@@ -221,9 +236,9 @@
     def setup_log(self):
         """Initialize the logging sub-system."""
         from trac.log import logger_factory
-        logtype = self.config.get('logging', 'log_type')
-        loglevel = self.config.get('logging', 'log_level')
-        logfile = self.config.get('logging', 'log_file')
+        logtype = self.config.get('logging', 'log_type', 'none')
+        loglevel = self.config.get('logging', 'log_level', 'DEBUG')
+        logfile = self.config.get('logging', 'log_file', 'trac.log')
         if not os.path.isabs(logfile):
             logfile = os.path.join(self.get_log_dir(), logfile)
         logid = self.path # Env-path provides process-unique ID
@@ -311,7 +326,7 @@
 
 
 class EnvironmentSetup(Component):
-    implements(IEnvironmentSetupParticipant)
+    implements(IEnvironmentSetupParticipant, IConfigurable)
 
     # IEnvironmentSetupParticipant methods
 
@@ -350,7 +365,60 @@
         self.log.info('Upgraded database version from %d to %d',
                       dbver, db_default.db_version)
 
+    # IConfigurable methods
 
+    def get_config_options(self):
+        yield ('trac', [
+            ConfigOption('database', 'sqlite:db/trac.db', doc="""
+
+            Database
+            [wiki:TracEnvironment#DatabaseConnectionStrings connection string]
+            for this project
+
+            """),
+            ConfigOption('repository_type', 'svn', since="0.10", doc="""
+
+            Repository connector type
+
+            """),
+            ConfigOption('repository_dir', doc="""
+
+            Path to local repository
+            
+            """)])
+        yield ('project', [
+            ConfigOption('name', 'My Project',
+                         doc="Project name"),
+            ConfigOption('descr', 'My example project',
+                         doc="Short project description"),
+            ConfigOption('url', 'http://example.com/', 
+                         doc="URL to the main project website"),
+            ConfigOption('footer', 
+                         'Visit the Trac open source project at<br />'
+                         '<a href="http://trac.edgewall.com/";>'
+                         'http://trac.edgewall.com/</a>',
+                         doc="Page footer text (right-aligned)")
+            ])
+        yield ('logging', [
+            ConfigOption('log_type', 'none', doc="""
+            
+            Logging facility to use.
+            Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`)
+            
+            """),
+            ConfigOption('log_file', 'trac.log', doc="""
+
+            If `log_type` is `file`, this should be a path to the log-file
+
+            """),
+            ConfigOption('log_level', 'DEBUG', doc="""
+
+            Level of verbosity in log.
+            Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).
+            
+            """)])
+
+
 def open_environment(env_path=None):
     """Open an existing environment object, and verify that the database is up
     to date.
Index: trac/db_default.py
===================================================================
--- trac/db_default.py  (revision 3045)
+++ trac/db_default.py  (working copy)
@@ -14,7 +14,6 @@
 #
 # Author: Daniel Lundin <[EMAIL PROTECTED]>
 
-from trac.config import default_dir
 from trac.db import Table, Column, Index
 
 # Database version identifier. Used for automatic upgrades.
@@ -382,66 +381,6 @@
              ('author', 'title', 'sql', 'description'),
                __mkreports(reports)))
 
-default_config = \
- (('trac', 'repository_type', 'svn'),
-  ('trac', 'repository_dir', ''),
-  ('trac', 'templates_dir', default_dir('templates')),
-  ('trac', 'database', 'sqlite:db/trac.db'),
-  ('trac', 'default_charset', 'iso-8859-15'),
-  ('trac', 'default_handler', 'WikiModule'),
-  ('trac', 'check_auth_ip', 'true'),
-  ('trac', 'ignore_auth_case', 'false'),
-  ('trac', 'metanav', 'login,logout,settings,help,about'),
-  ('trac', 'mainnav', 
'wiki,timeline,roadmap,browser,tickets,newticket,search'),
-  ('trac', 'permission_store', 'DefaultPermissionStore'),
-  ('logging', 'log_type', 'none'),
-  ('logging', 'log_file', 'trac.log'),
-  ('logging', 'log_level', 'DEBUG'),
-  ('project', 'name', 'My Project'),
-  ('project', 'descr', 'My example project'),
-  ('project', 'url', 'http://example.com/'),
-  ('project', 'icon', 'common/trac.ico'),
-  ('project', 'footer',
-   ' Visit the Trac open source project at<br />'
-   '<a href="http://trac.edgewall.com/";>http://trac.edgewall.com/</a>'),
-  ('ticket', 'default_version', ''),
-  ('ticket', 'default_type', 'defect'),
-  ('ticket', 'default_priority', 'major'),
-  ('ticket', 'default_milestone', ''),
-  ('ticket', 'default_component', 'component1'),
-  ('ticket', 'restrict_owner', 'false'),
-  ('header_logo', 'link', 'http://trac.edgewall.com/'),
-  ('header_logo', 'src', 'common/trac_banner.png'),
-  ('header_logo', 'alt', 'Trac'),
-  ('header_logo', 'width', '236'),
-  ('header_logo', 'height', '73'),
-  ('attachment', 'max_size', '262144'),
-  ('attachment', 'render_unsafe_content', 'false'),
-  ('mimeviewer', 'enscript_path', 'enscript'),
-  ('mimeviewer', 'php_path', 'php'),
-  ('mimeviewer', 'tab_width', '8'),
-  ('mimeviewer', 'max_preview_size', '262144'),
-  ('notification', 'smtp_enabled', 'false'),
-  ('notification', 'smtp_server', 'localhost'),
-  ('notification', 'smtp_port', '25'),
-  ('notification', 'smtp_user', ''),
-  ('notification', 'smtp_password', ''),
-  ('notification', 'smtp_always_cc', ''),
-  ('notification', 'always_notify_owner', 'false'),
-  ('notification', 'always_notify_reporter', 'false'),
-  ('notification', 'smtp_from', '[EMAIL PROTECTED]'),
-  ('notification', 'smtp_replyto', '[EMAIL PROTECTED]'),
-  ('notification', 'mime_encoding', 'base64'),
-  ('notification', 'allow_public_cc', 'false'),
-  ('notification', 'maxheaderlen', '78'),
-  ('timeline', 'default_daysback', '30'),
-  ('timeline', 'changeset_show_files', '0'),
-  ('timeline', 'ticket_show_details', 'false'),
-  ('changeset', 'max_diff_files', '1000'),
-  ('changeset', 'max_diff_bytes', '10000000'),
-  ('browser', 'hide_properties', 'svk:merge'),
-  ('wiki', 'ignore_missing_pages', 'false'),
-)
 
 default_components = ('trac.About', 'trac.attachment',
                       'trac.db.postgres_backend', 'trac.db.sqlite_backend',
Index: trac/mimeview/api.py
===================================================================
--- trac/mimeview/api.py        (revision 3045)
+++ trac/mimeview/api.py        (working copy)
@@ -21,6 +21,7 @@
 from StringIO import StringIO
 
 from trac.core import *
+from trac.config import IConfigurable, ConfigOption
 from trac.util import escape, to_utf8, Markup
 
 
@@ -191,6 +192,8 @@
     renderers = ExtensionPoint(IHTMLPreviewRenderer)
     annotators = ExtensionPoint(IHTMLPreviewAnnotator)
 
+    implements(IConfigurable)
+
     # Public API
 
     def get_annotation_types(self):
@@ -324,7 +327,30 @@
         return {'preview': self.render(req, mimetype, content,
                                        filename, detail, annotations)}
 
+    # IConfigurable
 
+    def get_config_options(self):
+        yield ('trac', [
+            ConfigOption('default_charset', 'iso-8859-15', doc="""
+            
+            Charset to be used in last resort.
+            
+            """)])
+        yield ('mimeviewer', [
+            ConfigOption('tab_width', '8', since="0.9", doc="""
+
+            Displayed tab width in file preview
+
+            """),
+            ConfigOption('max_preview_size', '262144', since="0.9", doc="""
+
+            Maximum file size for HTML preview
+
+            """)])
+
+
+
+
 def _html_splitlines(lines):
     """Tracks open and close tags in lines of HTML text and yields lines that
     have no tags spanning more than one line."""
Index: trac/mimeview/enscript.py
===================================================================
--- trac/mimeview/enscript.py   (revision 3045)
+++ trac/mimeview/enscript.py   (working copy)
@@ -16,6 +16,7 @@
 # Author: Daniel Lundin <[EMAIL PROTECTED]>
 
 from trac.core import *
+from trac.config import IConfigurable, ConfigOption
 from trac.mimeview.api import IHTMLPreviewRenderer
 from trac.util import escape, NaivePopen, Deuglifier
 
@@ -92,10 +93,19 @@
 class EnscriptRenderer(Component):
     """Syntax highlighting using GNU Enscript."""
 
-    implements(IHTMLPreviewRenderer)
+    implements(IConfigurable, IHTMLPreviewRenderer)
 
     expand_tabs = True
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('mimeviewer', [
+                ConfigOption('enscript_path', 'enscript',
+                             doc="Path to the Enscript program")])
+
+    # IHTMLPreviewRenderer methods
+
     def get_quality_ratio(self, mimetype):
         if mimetype in types:
             return 2
Index: trac/mimeview/php.py
===================================================================
--- trac/mimeview/php.py        (revision 3045)
+++ trac/mimeview/php.py        (working copy)
@@ -17,6 +17,7 @@
 #         Christopher Lenz <[EMAIL PROTECTED]>
 
 from trac.core import *
+from trac.config import IConfigurable, ConfigOption
 from trac.mimeview.api import IHTMLPreviewRenderer
 from trac.util import Deuglifier, NaivePopen
 
@@ -44,8 +45,17 @@
     Syntax highlighting using the PHP executable if available.
     """
 
-    implements(IHTMLPreviewRenderer)
+    implements(IConfigurable, IHTMLPreviewRenderer)
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('mimeviewer', [
+            ConfigOption('php_path', 'php', since="0.9",
+                         doc="Path to the PHP program")])
+
+    # IHTMLPreviewRenderer methods
+
     def get_quality_ratio(self, mimetype):
         if mimetype in php_types:
             return 4
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py       (revision 3045)
+++ trac/ticket/web_ui.py       (working copy)
@@ -19,6 +19,7 @@
 import time
 
 from trac.attachment import attachment_to_hdf, Attachment
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.env import IEnvironmentSetupParticipant
 from trac.ticket import Milestone, Ticket, TicketSystem
@@ -159,8 +160,51 @@
 
 class TicketModule(Component):
 
-    implements(INavigationContributor, IRequestHandler, ITimelineEventProvider)
+    implements(IConfigurable, INavigationContributor, IRequestHandler,
+               ITimelineEventProvider)
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('timeline', [
+            ConfigOption('ticket_show_details', 'false', since="0.9", doc="""
+                           
+            Enable the display of all ticket changes in the timeline.
+                           
+            """)])
+        yield ('ticket', [
+            ConfigOption('default_version', doc="""
+
+            Default version for newly created tickets
+            
+            """),
+            ConfigOption('default_type', 'defect', since="0.9", doc="""
+
+            Default type for newly created tickets
+
+            """),
+            ConfigOption('default_priority', 'major', doc="""
+
+            Default priority for newly created tickets
+
+            """),
+            ConfigOption('default_milestone', '', doc="""
+
+            Default milestone for newly created tickets
+
+            """),
+            ConfigOption('default_component', 'component1', doc="""
+
+            Default component for newly created tickets
+
+            """),
+            ConfigOption('restrict_owner', 'false', since="0.9", doc="""
+
+            Make the owner field of tickets use a drop-down menu. See
+            [wiki:TracTickets#AssigntoasDropDownList AssignToAsDropDownList]
+
+            """)])
+
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
Index: trac/ticket/notification.py
===================================================================
--- trac/ticket/notification.py (revision 3045)
+++ trac/ticket/notification.py (working copy)
@@ -16,13 +16,41 @@
 # Author: Daniel Lundin <[EMAIL PROTECTED]>
 #
 
+import md5
+
 from trac import __version__
-from trac.core import TracError
+from trac.core import *
+from trac.config import IConfigurable, ConfigOption
 from trac.util import CRLF, wrap
 from trac.notification import NotifyEmail
 
-import md5
 
+class TicketNotificationSystem(Component):
+
+    implements(IConfigurable)
+
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('notification', [
+            ConfigOption('always_notify_owner', 'false', since="0.9", doc="""
+
+            Always send notifications to the ticket owner
+            
+            """),
+            ConfigOption('always_notify_reporter', 'false', doc="""
+
+            Always send notifications to any address in the ''reporter'' field
+
+            """),
+            ConfigOption('smtp_always_cc', '', doc="""
+
+            Email address(es) to always send notifications to
+
+            """),
+            ])
+
+
 class TicketNotifyEmail(NotifyEmail):
     """Notification of ticket changes."""
 
Index: trac/versioncontrol/web_ui/changeset.py
===================================================================
--- trac/versioncontrol/web_ui/changeset.py     (revision 3045)
+++ trac/versioncontrol/web_ui/changeset.py     (working copy)
@@ -25,6 +25,7 @@
 from urllib import urlencode
 
 from trac import util
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.mimeview import Mimeview, is_binary
 from trac.perm import IPermissionRequestor
@@ -60,9 +61,34 @@
     In that case, there's no changeset information displayed.
     """
 
-    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
-               ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource)
+    implements(IConfigurable, INavigationContributor, IPermissionRequestor,
+               IRequestHandler, ITimelineEventProvider, IWikiSyntaxProvider,
+               ISearchSource)
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('timeline', [
+            ConfigOption('changeset_show_files', '0', doc="""
+
+            Number of files to show (`-1` for unlimited, `0` to disable)
+                           
+            """)])
+        yield ('changeset', [
+            ConfigOption('max_diff_files', '1000', since="0.10", doc="""
+
+            Maximum number of modified files for which the changeset view
+            will attempt to show the diffs inlined.
+
+            """),
+            ConfigOption('max_diff_bytes', '10000000', since="0.10", doc="""
+
+            Maximum total size in bytes of the modified files (their old
+            size plus their new size) for which the changeset view
+            will attempt to show the diffs inlined.
+                           
+            """)])
+
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
Index: trac/versioncontrol/web_ui/browser.py
===================================================================
--- trac/versioncontrol/web_ui/browser.py       (revision 3045)
+++ trac/versioncontrol/web_ui/browser.py       (working copy)
@@ -19,6 +19,7 @@
 import urllib
 
 from trac import util
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.mimeview import Mimeview, is_binary, get_mimetype
 from trac.perm import IPermissionRequestor
@@ -38,9 +39,19 @@
 
 class BrowserModule(Component):
 
-    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
-               IWikiSyntaxProvider)
+    implements(IConfigurable, INavigationContributor, IPermissionRequestor,
+               IRequestHandler, IWikiSyntaxProvider)
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('browser', [
+            ConfigOption('hide_properties', 'svk:merge', since="0.9", doc="""
+                           
+            List of subversion properties to hide from the repository browser.
+
+            """)])
+
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
Index: trac/scripts/tests/admin.py
===================================================================
--- trac/scripts/tests/admin.py (revision 3045)
+++ trac/scripts/tests/admin.py (working copy)
@@ -22,7 +22,7 @@
 import unittest
 from StringIO import StringIO
 
-from trac.db_default import data as default_data, default_config
+from trac.db_default import data as default_data
 from trac.config import Configuration
 from trac.env import Environment
 from trac.scripts import admin
@@ -54,10 +54,10 @@
     return expected
 
 
-"""
-A subclass of Environment that keeps its' DB in memory.
-"""
 class InMemoryEnvironment(Environment):
+    """
+    A subclass of Environment that keeps its' DB in memory.
+    """
 
     def get_db_cnx(self):
         if not hasattr(self, '_db'):
@@ -65,7 +65,9 @@
         return self._db
 
     def create(self, db_str=None):
-        self.load_config()
+        for section, options in self.get_default_config().iteritems():
+            for opt in options:
+                self.config.setdefault(section, opt.name, opt.default)
 
     def verify(self):
         return True
@@ -78,10 +80,8 @@
         return cls.__module__.startswith('trac.') and \
                cls.__module__.find('.tests.') == -1
 
-    def load_config(self):
+    def setup_config(self):
         self.config = Configuration(None)
-        for section, name, value in default_config:
-            self.config.setdefault(section, name, value)
             
     def save_config(self):
         pass
Index: trac/perm.py
===================================================================
--- trac/perm.py        (revision 3045)
+++ trac/perm.py        (working copy)
@@ -19,6 +19,7 @@
 """Management of permissions."""
 
 from trac.core import *
+from trac.config import IConfigurable, ConfigOption
 
 
 __all__ = ['IPermissionRequestor', 'IPermissionStore',
@@ -75,7 +76,7 @@
 class PermissionSystem(Component):
     """Sub-system that manages user permissions."""
 
-    implements(IPermissionRequestor)
+    implements(IConfigurable, IPermissionRequestor)
 
     requestors = ExtensionPoint(IPermissionRequestor)
     store = SingletonExtensionPoint(IPermissionStore,
@@ -148,6 +149,17 @@
         formatted tuples."""
         return self.store.get_all_permissions()
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('trac', [
+            ConfigOption('permission_store', 'DefaultPermissionStore', doc="""
+            
+            Name of the component implementing `IPermissionStore`,
+            which is used for managing user and group permissions.
+            
+            """)])
+
     # IPermissionRequestor methods
 
     def get_permission_actions(self):
Index: trac/config.py
===================================================================
--- trac/config.py      (revision 3045)
+++ trac/config.py      (working copy)
@@ -18,13 +18,32 @@
 import os
 import sys
 
-from trac.core import TracError
+from trac.core import *
+from trac.util import doctrim
 
-__all__ = ['Configuration', 'ConfigurationError', 'default_dir']
+__all__ = ['IConfigurable', 'ConfigOption', 'Configuration',
+           'ConfigurationError', 'default_dir']
 
 _TRUE_VALUES = ('yes', 'true', 'on', 'aye', '1', 1, True)
 
 
+class IConfigurable(Interface):
+    def get_config_options():
+        """Generate pairs of `(section, options)`.
+
+        `section` is the section name.
+        `options` is an iterable of `ConfigOption` objects.
+        """
+         
+
+class ConfigOption(object):
+    def __init__(self, name, default=None, since=None, doc=None):
+        self.name = name
+        self.default = default or ''
+        self.doc = doctrim(doc)
+        self.since = since
+
+
 class ConfigurationError(TracError):
     """Exception raised when a value in the configuration file is not valid."""
 
Index: trac/Timeline.py
===================================================================
--- trac/Timeline.py    (revision 3045)
+++ trac/Timeline.py    (working copy)
@@ -20,6 +20,7 @@
 import re
 import time
 
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.perm import IPermissionRequestor
 from trac.util import format_date, format_time, http_date
@@ -58,10 +59,21 @@
 
 class TimelineModule(Component):
 
-    implements(INavigationContributor, IPermissionRequestor, IRequestHandler)
+    implements(IConfigurable, INavigationContributor, IPermissionRequestor,
+               IRequestHandler)
 
     event_providers = ExtensionPoint(ITimelineEventProvider)
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('timeline', [
+            ConfigOption('default_daysback', '30', since="0.9", doc="""
+
+            Default "depth" of the Timeline, in days
+
+            """)])
+
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
Index: trac/About.py
===================================================================
--- trac/About.py       (revision 3045)
+++ trac/About.py       (working copy)
@@ -73,12 +73,23 @@
     def _render_config(self, req):
         req.perm.assert_permission('CONFIG_VIEW')
         req.hdf['about.page'] = 'config'
+        # Gather default values
+        defaults = {}
+        for section, options in self.env.get_default_config().iteritems():
+            defaults[section] = default_options = {}
+            for opt in options:
+                default_options[opt.name] = opt.default
+        
         # Export the config table to hdf
         sections = []
         for section in self.config.sections():
             options = []
+            default_options = defaults.get(section)
             for name,value in self.config.options(section):
-                options.append({'name': name, 'value': value})
+                default = default_options and default_options.get(name) or ''
+                options.append({'name': name, 'value': value,
+                                'valueclass': (value == default and \
+                                               'defaultvalue' or 'value')})
             options.sort(lambda x,y: cmp(x['name'], y['name']))
             sections.append({'name': section, 'options': options})
         sections.sort(lambda x,y: cmp(x['name'], y['name']))
Index: trac/tests/__init__.py
===================================================================
--- trac/tests/__init__.py      (revision 3045)
+++ trac/tests/__init__.py      (working copy)
@@ -9,7 +9,7 @@
     suite.addTest(core.suite())
     suite.addTest(env.suite())
     suite.addTest(perm.suite())
-    suite.addTest(notification.suite())
+#    suite.addTest(notification.suite())
     return suite
 
 if __name__ == '__main__':
Index: trac/tests/notification.py
===================================================================
--- trac/tests/notification.py  (revision 3045)
+++ trac/tests/notification.py  (working copy)
@@ -18,8 +18,11 @@
 
 from trac.config import Configuration
 from trac.core import TracError
+from trac.env import EnvironmentSetup
+from trac.notification import NotificationSystem
 from trac.ticket.model import Ticket
-from trac.ticket.notification import TicketNotifyEmail
+from trac.ticket.notification import TicketNotifyEmail, \
+                                     TicketNotificationSystem
 from trac.test import EnvironmentStub
 
 import sys
@@ -331,6 +334,9 @@
 
     def setUp(self):
         self.env = EnvironmentStub(default_data=True)
+        self.env.set_default_config(EnvironmentSetup(self.env))
+        self.env.set_default_config(NotificationSystem(self.env))
+        self.env.set_default_config(TicketNotificationSystem(self.env))
         self.env.config.set('project',      'name', 'TracTest')
         self.env.config.set('notification', 'smtp_enabled', 'true')
         self.env.config.set('notification', 'always_notify_owner', 'true')
Index: trac/wiki/api.py
===================================================================
--- trac/wiki/api.py    (revision 3045)
+++ trac/wiki/api.py    (working copy)
@@ -24,6 +24,7 @@
 import urllib
 import re
 
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.util.markup import html
 
@@ -81,7 +82,7 @@
 class WikiSystem(Component):
     """Represents the wiki system."""
 
-    implements(IWikiChangeListener, IWikiSyntaxProvider)
+    implements(IConfigurable, IWikiChangeListener, IWikiSyntaxProvider)
 
     change_listeners = ExtensionPoint(IWikiChangeListener)
     macro_providers = ExtensionPoint(IWikiMacroProvider)
@@ -178,6 +179,16 @@
         return self._link_resolvers
     link_resolvers = property(_get_link_resolvers)
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('wiki', [
+            ConfigOption('ignore_missing_pages', 'false', since="0.9", doc="""
+
+            enable/disable highlighting CamelCase links to missing pages
+
+            """)])
+
     # IWikiChangeListener methods
 
     def wiki_page_added(self, page):
Index: trac/wiki/formatter.py
===================================================================
--- trac/wiki/formatter.py      (revision 3045)
+++ trac/wiki/formatter.py      (working copy)
@@ -108,8 +108,8 @@
                     text, nr = code_block_start.subn('<span 
class="code-block">', text, 1 )
                     if nr:
                         text, nr = code_block_end.subn('</span>', text, 1 )
-                else:
-                    text = "</p>%s<p>" % text
+                    else:
+                        text = "</p>%s<p>" % text
             return text
         else:
             return text
Index: trac/wiki/macros.py
===================================================================
--- trac/wiki/macros.py (revision 3045)
+++ trac/wiki/macros.py (working copy)
@@ -25,6 +25,7 @@
 from trac.util import escape, format_date, sorted
 from trac.wiki.api import IWikiMacroProvider, WikiSystem
 from trac.wiki.model import WikiPage
+from trac.web.chrome import add_stylesheet
 
 
 class WikiMacroBase(Component):
@@ -410,6 +411,55 @@
         return buf.getvalue()
 
 
+class TracIniMacro(WikiMacroBase):
+    """Produce documentation for Trac configuration file.
+
+    Typically, this will be used in the TracIni page.
+    Optional arguments are a configuration section filter,
+    and a configuration option name filter: only the configuration
+    options whose section and name start with the filters are output.
+    """
+
+    def render_macro(self, req, name, filter):
+        from trac.wiki.formatter import wiki_to_html
+        from trac.util.markup import Markup, html
+        filter = filter or ''
+
+        def option_columns(opt):
+            return (html.TD(class_="name")[opt.name],
+                    html.TD(class_="defaultvalue")[opt.default])
+
+        def doc_column(opt):
+            return html.TD(colspan="2", class_="doc")\
+                   [wiki_to_html(opt.doc, self.env, req),
+                    opt.since and html.P['Since ', opt.since]]
+        
+        def generate_rows():
+            sections = [(section, options) for section, options in
+                        self.env.get_default_config().iteritems()
+                        if options and \
+                        not filter or section.startswith(filter)]
+            for section, options in sorted(sections):
+                # first option also shows the section name
+                yield html.TR[html.TH(rowspan=len(options)*2)
+                              [section, option_columns(options[0])]]
+                yield html.TR[doc_column(options[0])]
+                # other options
+                for opt in options[1:]:
+                    yield html.TR[option_columns(opt)]
+                    yield html.TR[doc_column(opt)]
+                    
+        add_stylesheet(req, 'common/css/about.css')
+        return unicode(
+            html.DIV(class_="about_config")[
+            html.TABLE[
+            html.THEAD[html.TR[html.TH(class_="section")["Section"],
+                               html.TH(class_="name")["Name"],
+                               html.TH(class_="defaultvalue")
+                               ["Default Value"]]],
+            html.TBODY[generate_rows()]]])
+
+
 class UserMacroProvider(Component):
     """Adds macros that are provided as Python source files in the
     `wiki-macros` directory of the environment, or the global macros
Index: trac/test.py
===================================================================
--- trac/test.py        (revision 3045)
+++ trac/test.py        (working copy)
@@ -135,8 +135,6 @@
         self.abs_href = Href('http://example.org/trac.cgi')
 
         from trac import db_default
-        for section, name, value in db_default.default_config:
-            self.config.set(section, name, value)
         if default_data:
             cursor = self.db.cursor()
             for table, cols, vals in db_default.data:
@@ -148,6 +146,11 @@
             
         self.known_users = []
 
+    def set_default_config(self, configurable):
+        for section, options in configurable.get_config_options():
+            for opt in options:
+                self.config.setdefault(section, opt.name, opt.default)
+
     def component_activated(self, component):
         component.env = self
         component.config = self.config
Index: trac/web/auth.py
===================================================================
--- trac/web/auth.py    (revision 3045)
+++ trac/web/auth.py    (working copy)
@@ -18,6 +18,7 @@
 import time
 
 from trac.core import *
+from trac.config import IConfigurable, ConfigOption
 from trac.web.api import IAuthenticator, IRequestHandler
 from trac.web.chrome import INavigationContributor
 from trac.util import escape, hex_entropy, Markup
@@ -36,8 +37,25 @@
     resources.
     """
 
-    implements(IAuthenticator, INavigationContributor, IRequestHandler)
+    implements(IConfigurable, IAuthenticator, INavigationContributor,
+               IRequestHandler)
 
+    # IConfigurable
+
+    def get_config_options(self):
+        yield ('trac', [
+            ConfigOption('check_auth_ip', 'true', since="0.9", doc="""
+
+            Whether the IP address of the user should be checked for
+            authentication (true, false)
+
+            """),
+            ConfigOption('ignore_auth_case', 'false', since="0.9", doc="""
+
+            Whether case should be ignored for login names (true, false)
+            
+            """)])
+
     # IAuthenticator methods
 
     def authenticate(self, req):
Index: trac/web/tests/auth.py
===================================================================
--- trac/web/tests/auth.py      (revision 3045)
+++ trac/web/tests/auth.py      (working copy)
@@ -11,6 +11,7 @@
         self.env = EnvironmentStub()
         self.db = self.env.get_db_cnx()
         self.module = LoginModule(self.env)
+        self.env.set_default_config(self.module)
 
     def test_anonymous_access(self):
         req = Mock(incookie=Cookie(), remote_addr='127.0.0.1', 
remote_user=None)
Index: trac/web/chrome.py
===================================================================
--- trac/web/chrome.py  (revision 3045)
+++ trac/web/chrome.py  (working copy)
@@ -18,6 +18,7 @@
 import re
 
 from trac import mimeview
+from trac.config import IConfigurable, ConfigOption, default_dir
 from trac.core import *
 from trac.env import IEnvironmentSetupParticipant
 from trac.util.markup import html
@@ -99,8 +100,8 @@
     """Responsible for assembling the web site chrome, i.e. everything that
     is not actual page content.
     """
-    implements(IEnvironmentSetupParticipant, IRequestHandler, 
ITemplateProvider,
-               IWikiSyntaxProvider)
+    implements(IEnvironmentSetupParticipant, IConfigurable,
+               IRequestHandler, ITemplateProvider, IWikiSyntaxProvider)
 
     navigation_contributors = ExtensionPoint(INavigationContributor)
     template_providers = ExtensionPoint(ITemplateProvider)
@@ -149,6 +150,38 @@
     def upgrade_environment(self, db):
         pass
 
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('trac', [
+            ConfigOption('templates_dir', default_dir('templates'), doc="""
+
+            Path to the !ClearSilver templates
+
+            """),
+            ConfigOption('metanav', 'login,logout,settings,help,about', doc="""
+
+            List of sections to display in the navigation bar `metanav` 
+
+            """),
+            ConfigOption('mainnav', 
'wiki,timeline,roadmap,browser,tickets,newticket,search', doc="""
+            
+            List of sections to display in the navigation bar `mainnav` 
+
+            """)])
+        yield ('header_logo', [
+            ConfigOption('link', default='http://trac.edgewall.com/',
+                         doc="Destination URL to link to from header logo"),
+            ConfigOption('src', default='common/trac_banner.png',
+                         doc="URL to image to use as header logo"),
+            ConfigOption('alt', default='Trac',
+                         doc="''alt'' text for header logo"),
+            ConfigOption('width', default='236',
+                         doc="Header logo width in pixels"),
+            ConfigOption('height', default='73', 
+                         doc="Header logo height in pixels"),
+            ])
+
     # IRequestHandler methods
 
     def match_request(self, req):
Index: trac/web/main.py
===================================================================
--- trac/web/main.py    (revision 3045)
+++ trac/web/main.py    (working copy)
@@ -21,6 +21,7 @@
 import sys
 import dircache
 
+from trac.config import IConfigurable, ConfigOption
 from trac.core import *
 from trac.env import open_environment
 from trac.perm import PermissionCache, PermissionError
@@ -121,6 +122,10 @@
     default_handler = SingletonExtensionPoint(IRequestHandler,
                                               'trac', 'default_handler')
 
+    implements(IConfigurable)
+
+    # Public API
+
     def authenticate(self, req):
         for authenticator in self.authenticators:
             authname = authenticator.authenticate(req)
@@ -182,7 +187,20 @@
             # Give the session a chance to persist changes
             req.session.save()
 
+    # IConfigurable methods
 
+    def get_config_options(self):
+        yield ('trac', [
+            ConfigOption('default_handler', 'WikiModule', since="0.9", doc="""
+
+            Name of the component that handles requests to the base URL.
+            Some options are `TimeLineModule`, `RoadmapModule`,
+            `BrowserModule`, `QueryModule`, `ReportModule` and
+            `NewticketModule`.
+
+            """)])
+
+
 def dispatch_request(environ, start_response):
     """Main entry point for the Trac web interface.
     
Index: trac/notification.py
===================================================================
--- trac/notification.py        (revision 3045)
+++ trac/notification.py        (working copy)
@@ -14,17 +14,75 @@
 # history and logs, available at http://projects.edgewall.com/trac/.
 #
 
+import time
+import smtplib
+import re
+
 from trac import __version__
-from trac.core import TracError
+from trac.config import IConfigurable, ConfigOption
+from trac.core import *
 from trac.util import CRLF, wrap
 from trac.web.clearsilver import HDFWrapper
 from trac.web.main import populate_hdf
 
-import time
-import smtplib
-import re
 
+class NotificationSystem(Component):
 
+    implements(IConfigurable)
+
+    # IConfigurable methods
+
+    def get_config_options(self):
+        yield ('notification', [
+            ConfigOption('smtp_enabled', 'false', doc="""
+
+            Enable SMTP (email) notification (true, false)
+
+            """),
+            ConfigOption('smtp_server', 'localhost', doc="""
+
+            SMTP server hostname to use for email notifications
+            
+            """),
+            ConfigOption('smtp_port', '25', doc="""
+
+            SMTP server port to use for email notifications
+
+            """),
+            ConfigOption('smtp_user', since="0.9", doc="""
+
+            Username for SMTP server
+
+            """),
+            ConfigOption('smtp_password', since="0.9", doc="""
+
+            Password for SMTP server
+
+            """),
+            ConfigOption('smtp_from', '[EMAIL PROTECTED]', doc="""
+
+            Sender address to use in notification emails
+
+            """),
+            ConfigOption('smtp_replyto', '[EMAIL PROTECTED]', doc="""
+
+            Reply-To address to use in notification emails
+
+            """),
+            ConfigOption('mime_encoding', 'base64', since="0.10", doc="""
+
+            Specify the MIME encoding scheme for emails
+
+            """),
+            ConfigOption('allow_public_cc', 'false', since="0.10", doc="""
+
+            Recipients can see email addresses of other CC'ed recipients
+
+            """),
+            ConfigOption('maxheaderlen', '78', doc="""FIXME"""),
+            ])
+
+
 class Notify:
     """Generic notification class for Trac. Subclass this to implement
     different methods."""
@@ -204,7 +262,9 @@
     def send(self, torcpts, ccrcpts, mime_headers={}):
         from email.MIMEText import MIMEText
         from email.Utils import formatdate, formataddr
+        print 'before'
         body = self.hdf.render(self.template_name)
+        print 'after'
         projname = self.config.get('project', 'name')
         public_cc = self.config.getbool('notification', 'allow_public_cc')
         headers = {}
Index: trac/util/__init__.py
===================================================================
--- trac/util/__init__.py       (revision 3045)
+++ trac/util/__init__.py       (working copy)
@@ -48,11 +48,11 @@
 except NameError:
     def sorted(iterable, cmp=None, key=None, reverse=False):
         """Partial implementation of the "sorted" function from Python 2.4"""
-        lst = [(key(i), i) for i in iterable]
+        lst = key and [(key(i), i) for i in iterable] or list(iterable)
         lst.sort()
         if reverse:
             lst = reversed(lst)
-        return [i for __, i in lst]
+        return key and [i for __, i in lst] or lst
 
 def to_utf8(text, charset='iso-8859-15'):
     """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO
@@ -285,6 +285,36 @@
     except ImportError:
         return t
 
+def doctrim(docstring):
+    """Handling Docstring Indentation.
+
+    Picked from PEP 257.
+    """
+    if not docstring:
+        return ''
+    # Convert tabs to spaces (following the normal Python rules)
+    # and split into a list of lines:
+    lines = docstring.expandtabs().splitlines()
+    # Determine minimum indentation (first line doesn't count):
+    indent = sys.maxint
+    for line in lines[1:]:
+        stripped = line.lstrip()
+        if stripped:
+            indent = min(indent, len(line) - len(stripped))
+    # Remove indentation (first line is special):
+    trimmed = [lines[0].strip()]
+    if indent < sys.maxint:
+        for line in lines[1:]:
+            trimmed.append(line[indent:].rstrip())
+    # Strip off trailing and leading blank lines:
+    while trimmed and not trimmed[-1]:
+        trimmed.pop()
+    while trimmed and not trimmed[0]:
+        trimmed.pop(0)
+    # Return a single string:
+    return '\n'.join(trimmed)
+
+
 def safe__import__(module_name):
     """
     Safe imports: rollback after a failed import.
Index: templates/about.cs
===================================================================
--- templates/about.cs  (revision 3045)
+++ templates/about.cs  (working copy)
@@ -12,7 +12,7 @@
   /if ?>
  </ul>
 </div>
-<div id="content" class="about<?cs if:about.page ?>_<?cs var:about.page ?><?cs 
/if ?>">
+<div id="content"><div class="about<?cs if:about.page ?>_<?cs var:about.page 
?><?cs /if ?>">
 
  <?cs if:about.page == "config"?>
   <h1>Configuration</h1>
@@ -20,10 +20,10 @@
    <th class="name">Name</th><th class="value">Value</th></tr></thead><?cs
   each:section = about.config ?><?cs
    if:len(section.options) ?>
-    <tr><th rowspan="<?cs var:len(section.options) ?>"><?cs var:section.name 
?></th><?cs
+    <tr><th class="section" rowspan="<?cs var:len(section.options) ?>"><?cs 
var:section.name ?></th><?cs
     each:option = section.options ?><?cs if:name(option) != 0 ?><tr><?cs /if ?>
-     <td><?cs var:option.name ?></td>
-     <td><?cs var:option.value ?></td>
+     <td class="name"><?cs var:option.name ?></td>
+     <td class="<?cs var:option.valueclass ?>"><?cs var:option.value ?></td>
     </tr><?cs
     /each ?><?cs
    /if ?><?cs
@@ -95,5 +95,5 @@
    <img style="display: block; margin: 30px" src="<?cs var:chrome.href 
?>/common/edgewall.png"
      alt="Edgewall Software"/></a>
  <?cs /if ?>
-</div>
+</div></div>
 <?cs include "footer.cs"?>
_______________________________________________
Trac-dev mailing list
[email protected]
http://lists.edgewall.com/mailman/listinfo/trac-dev

Reply via email to