Review Board

beta

Add dynamic site configuration to Review Board

Updated 4 months ago

Christian Hammond Reviewers
trunk reviewboard
None Review Board SVN
Up until now users have had to edit settings_local.py to customize their Review Board install. While we documented many of the options, it was still a pain for users as they'd have to log in and edit the file and then restart the server.

With the new djblets.siteconfig app, we can now move in a more modern direction for customization of Review Board. Aside from a few basic essential settings in settings_local.py (such as database configuration) the admin UI will now be the place to go to customize Review Board.

A migration script is provided to automatically migrate all settings into the new siteconfig database entry. The next time users run ./manage.py syncdb, the script will detect that the settings need to be migrated and handle it all. Everything we care about should be preserved, including authentication information.
I've tested this with brand new installs and with a few different migrated databases. I haven't hit any problems yet but I haven't actually tested the resulting settings for authentication. Users can specify NIS or LDAP servers and I know we save the information but before this goes in, I'll be testing on an actual install.
/trunk/reviewboard/manage.py
Revision 1421 New Change
1
#!/usr/bin/env python
1
#!/usr/bin/env python
2
2
3
import imp
3
import imp
4
import sys
4
import sys
5
import os
5
import os
6
from os.path import abspath, dirname
6
from os.path import abspath, dirname
7
7
8
from django.core.management import execute_manager
8
from django.core.management import execute_manager, setup_environ
9
from django.template.defaultfilters import striptags
9
10
10
# Add the parent directory of 'manage.py' to the python path, so manage.py can
11
# Add the parent directory of 'manage.py' to the python path, so manage.py can
11
# be run from any directory.  From http://www.djangosnippets.org/snippets/281/
12
# be run from any directory.  From http://www.djangosnippets.org/snippets/281/
12
sys.path.insert(0, dirname(dirname(abspath(__file__))))
13
sys.path.insert(0, dirname(dirname(abspath(__file__))))
13
14
14
try:
15
try:
15
    import settings # Assumed to be in the same directory.
16
    import settings # Assumed to be in the same directory.
16
except ImportError:
17
except ImportError:
17
    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
18
    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
18
    sys.exit(1)
19
    sys.exit(1)
19
20
21
from reviewboard.admin import checks
22
20
23
21
warnings_found = 0
24
warnings_found = 0
22
def check_dependencies():
25
def check_dependencies():
23
    from settings import dependency_error
26
    from settings import dependency_error
24
27
28
    # Some of our checks require access to django.conf.settings, so
29
    # tell Django about our settings.
30
    setup_environ(settings)
31
25
    # Python 2.4
32
    # Python 2.4
26
    if sys.version_info[0] < 2 or \
33
    if sys.version_info[0] < 2 or \
27
       (sys.version_info[0] == 2 and sys.version_info[1] < 4):
34
       (sys.version_info[0] == 2 and sys.version_info[1] < 4):
28
        dependency_error('Python 2.4 or newer is required.')
35
        dependency_error('Python 2.4 or newer is required.')
29
36
48
    except ImportError:
55
    except ImportError:
49
        dependency_error('The Python Imaging Library (PIL) is required.')
56
        dependency_error('The Python Imaging Library (PIL) is required.')
50
57
51
    import subprocess
58
    import subprocess
52
59
53
    # pygments
54
    if settings.DIFF_SYNTAX_HIGHLIGHTING:
55
        try:
56
            import pygments
57
            version = pygments.__version__.split(".")
58
            if version[0] == 0 and version[1] < 9:
59
                dependency_error('Pygments is installed, but is an old version. '
60
                                 'Versions prior to 0.9 are known to have '
61
                                 'serious problems.')
62
        except ImportError:
63
            dependency_error('The Pygments library is required when ' +
64
                             'DIFF_SYNTAX_HIGHLIGHTING is enabled.')
65
66
    # PyLucene
67
    if settings.ENABLE_SEARCH:
68
        try:
69
            import lucene
70
        except ImportError:
71
            dependency_error('PyLucene (with JCC) is required when '
72
                             'ENABLE_SEARCH is set.')
73
74
    # The following checks are non-fatal warnings, since these dependencies are
60
    # The following checks are non-fatal warnings, since these dependencies are
75
    # merely recommended, not required.
61
    # merely recommended, not required.
76
    def dependency_warning(string):
62
    def dependency_warning(string):
77
        sys.stderr.write('Warning: %s\n' % string)
63
        sys.stderr.write('Warning: %s\n' % string)
78
        global warnings_found
64
        global warnings_found
95
    try:
81
    try:
96
        imp.find_module('mercurial')
82
        imp.find_module('mercurial')
97
    except ImportError:
83
    except ImportError:
98
        dependency_warning('hg not found.  Mercurial integration will not work.')
84
        dependency_warning('hg not found.  Mercurial integration will not work.')
99
85
86
    for check_func in (checks.get_can_enable_search,
87
                       checks.get_can_enable_syntax_highlighting):
88
        success, reason = check_func()
89
90
        if not success:
91
            dependency_warning(striptags(reason))
92
100
    found = False
93
    found = False
101
    for dir in os.environ['PATH'].split(os.environ.get('IFS', ':')):
94
    for dir in os.environ['PATH'].split(os.environ.get('IFS', ':')):
102
        if os.path.exists(os.path.join(dir, 'git')):
95
        if os.path.exists(os.path.join(dir, 'git')):
103
            found = True
96
            found = True
104
            break
97
            break
/trunk/reviewboard/settings.py
Revision 1421 New Change
48
    'django.core.context_processors.auth',
48
    'django.core.context_processors.auth',
49
    'django.core.context_processors.debug',
49
    'django.core.context_processors.debug',
50
    'django.core.context_processors.i18n',
50
    'django.core.context_processors.i18n',
51
    'django.core.context_processors.media',
51
    'django.core.context_processors.media',
52
    'django.core.context_processors.request',
52
    'django.core.context_processors.request',
53
    'djblets.siteconfig.context_processors.siteconfig',
53
    'djblets.util.context_processors.settingsVars',
54
    'djblets.util.context_processors.settingsVars',
54
    'djblets.util.context_processors.siteRoot',
55
    'djblets.util.context_processors.siteRoot',
55
)
56
)
56
57
57
SITE_ROOT_URLCONF = 'reviewboard.urls'
58
SITE_ROOT_URLCONF = 'reviewboard.urls'
77
    'django.contrib.markup',
78
    'django.contrib.markup',
78
    'django.contrib.sites',
79
    'django.contrib.sites',
79
    'django.contrib.sessions',
80
    'django.contrib.sessions',
80
    'djblets.datagrid',
81
    'djblets.datagrid',
81
    'djblets.feedview',
82
    'djblets.feedview',
83
    'djblets.siteconfig',
82
    'djblets.util',
84
    'djblets.util',
83
    'djblets.webapi',
85
    'djblets.webapi',
84
    'reviewboard.accounts',
86
    'reviewboard.accounts',
85
    'reviewboard.admin',
87
    'reviewboard.admin',
86
    'reviewboard.diffviewer',
88
    'reviewboard.diffviewer',
95
WEB_API_ENCODERS = (
97
WEB_API_ENCODERS = (
96
    'djblets.webapi.core.BasicAPIEncoder',
98
    'djblets.webapi.core.BasicAPIEncoder',
97
    'reviewboard.webapi.json.ReviewBoardAPIEncoder',
99
    'reviewboard.webapi.json.ReviewBoardAPIEncoder',
98
)
100
)
99
101
100
# Whether to use django's built-in system for users.  This turns on certain
101
# features like the registration page and profile editing.  If you're tying
102
# reviewboard in to an existing authentication environment (such as NIS),
103
# this data will come in from outside.
104
BUILTIN_AUTH = True
105
AUTH_PROFILE_MODULE = "accounts.Profile"
102
AUTH_PROFILE_MODULE = "accounts.Profile"
106
103
107
# Default repository path to use for the source code.
108
DEFAULT_REPOSITORY_PATH = None
109
110
# Default expiration time for the cache.  Note that this has no effect unless
104
# Default expiration time for the cache.  Note that this has no effect unless
111
# CACHE_BACKEND is specified in settings_local.py
105
# CACHE_BACKEND is specified in settings_local.py
112
CACHE_EXPIRATION_TIME = 60 * 60 * 24 * 30 # 1 month
106
CACHE_EXPIRATION_TIME = 60 * 60 * 24 * 30 # 1 month
113
107
114
# Custom test runner, which uses nose to find tests and execute them.  This
108
# Custom test runner, which uses nose to find tests and execute them.  This
115
# gives us a somewhat more comprehensive test execution than django's built-in
109
# gives us a somewhat more comprehensive test execution than django's built-in
116
# runner, as well as some special features like a code coverage report.
110
# runner, as well as some special features like a code coverage report.
117
TEST_RUNNER = 'reviewboard.test.runner'
111
TEST_RUNNER = 'reviewboard.test.runner'
118
112
119
# Default diff settings
120
DIFF_CONTEXT_NUM_LINES = 5
121
DIFF_CONTEXT_COLLAPSE_THRESHOLD = 2 * DIFF_CONTEXT_NUM_LINES + 3
122
123
# List of file patterns that will show whitespace-only changes. The
124
# default behavior for diffs is to hide lines showing only leading
125
# whitespace changes.
126
#
127
# For example:
128
#
129
#    DIFF_INCLUDE_SPACE_PATTERNS = ["*.py", "*.txt"]
130
#
131
DIFF_INCLUDE_SPACE_PATTERNS = []
132
133
# When enabled, this will send e-mails for all review requests and comments
134
# out to the e-mail addresses defined for the group.
135
SEND_REVIEW_MAIL = False
136
137
# Enable syntax highlighting in the diff viewer
138
DIFF_SYNTAX_HIGHLIGHTING = False
139
140
# Access method used for the site, used in e-mails.  Override this in
141
# settings_local.py if you choose to use https instead of http.
142
DOMAIN_METHOD = "http"
143
144
# Require a login for accessing any part of the site. If False, review
145
# requests, diffs, lists of review requests, etc. will be accessible without
146
# a login.
147
REQUIRE_SITEWIDE_LOGIN = False
148
149
# Enable search. See the comment in settings_local.py for more information on
150
# what's required to get this working.
151
ENABLE_SEARCH = False
152
SEARCH_INDEX = os.path.join(REVIEWBOARD_ROOT, 'search-index')
153
154
# The number of files to display per page in the diff viewer
155
DIFFVIEWER_PAGINATE_BY = 20
156
157
# The number of extra files required before adding another page
158
DIFFVIEWER_PAGINATE_ORPHANS = 10
159
160
# Dependency checker functionality.  Gives our users nice errors when they start
113
# Dependency checker functionality.  Gives our users nice errors when they start
161
# out, instead of encountering them later on.  Most of the magic for this
114
# out, instead of encountering them later on.  Most of the magic for this
162
# happens in manage.py, not here.
115
# happens in manage.py, not here.
163
install_help = '''
116
install_help = '''
164
Please see http://code.google.com/p/reviewboard/wiki/GettingStarted
117
Please see http://code.google.com/p/reviewboard/wiki/GettingStarted
192
# Base these on the user's SITE_ROOT.
145
# Base these on the user's SITE_ROOT.
193
LOGIN_URL = SITE_ROOT + 'account/login/'
146
LOGIN_URL = SITE_ROOT + 'account/login/'
194
ADMIN_MEDIA_PREFIX = MEDIA_URL + 'admin/'
147
ADMIN_MEDIA_PREFIX = MEDIA_URL + 'admin/'
195
148
196
# Cookie settings
149
# Cookie settings
150
LANGUAGE_COOKIE_NAME = "rblanguage"
197
SESSION_COOKIE_NAME = "rbsessionid"
151
SESSION_COOKIE_NAME = "rbsessionid"
198
SESSION_COOKIE_AGE = 365 * 24 * 24 * 60 # 1 year
152
SESSION_COOKIE_AGE = 365 * 24 * 24 * 60 # 1 year
199
SESSION_COOKIE_PATH = SITE_ROOT
153
SESSION_COOKIE_PATH = SITE_ROOT
/trunk/reviewboard/settings_local.py.tmpl
Revision 1421 New Change
11
11
12
# Cache backend.  Unset this to turn off caching completely.  As with most
12
# Cache backend.  Unset this to turn off caching completely.  As with most
13
# django installations, the best option is probably to use memcached.
13
# django installations, the best option is probably to use memcached.
14
CACHE_BACKEND = 'locmem:///'
14
CACHE_BACKEND = 'locmem:///'
15
15
16
# Whether to send e-mail for review requests.
17
SEND_REVIEW_MAIL = False
18
19
# Local time zone for this installation. All choices can be found here:
16
# Local time zone for this installation. All choices can be found here:
20
# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
17
# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
21
TIME_ZONE = 'US/Pacific'
18
TIME_ZONE = 'US/Pacific'
22
19
23
# Language code for this installation. All choices can be found here:
20
# Language code for this installation. All choices can be found here:
31
28
32
# If you set this to False, Django will make some optimizations so as not
29
# If you set this to False, Django will make some optimizations so as not
33
# to load the internationalization machinery.
30
# to load the internationalization machinery.
34
USE_I18N = True
31
USE_I18N = True
35
32
36
# Enable search.  Search needs PyLucene to be installed.  It also requires
37
# that a regular job be set up to perform the indexing.  To generate the
38
# index, run:
39
#    manage.py index
40
# This command will perform an incremental index.  To do a full reindex, run:
41
#    manage.py index --full
42
#
43
# Incremental indexes should be done fairly often.
44
# A sample cron configuration exists in contrib/conf/search-cron.conf
45
#
46
# If you want the search index to be located somewhere other than the
47
# reviewboard root, set SEARCH_INDEX to the desired path.  The index needs to be
48
# a directory writable by the user creating the index and readable by the user
49
# that Review Board runs as.
50
ENABLE_SEARCH = False
51
52
33
53
# TLS for LDAP.  If you're using LDAP authentication and your LDAP server
34
# TLS for LDAP.  If you're using LDAP authentication and your LDAP server
54
# doesn't support ldaps://, you can enable start-TLS with this.
35
# doesn't support ldaps://, you can enable start-TLS with this.
55
LDAP_TLS = False
36
LDAP_TLS = False
/trunk/reviewboard/urls.py
Revision 1421 New Change
1
import os.path
1
import os.path
2
2
3
from django.conf import settings
3
from django.conf import settings
4
from django.conf.urls.defaults import patterns, include, url
4
from django.conf.urls.defaults import patterns, include, url
5
from django.contrib import admin
5
from django.contrib import admin
6
6
7
from reviewboard.admin.checks import check_updates_required
7
from reviewboard.admin.checks import check_updates_required
8
from reviewboard.admin.siteconfig import load_site_config
8
from reviewboard.reviews.feeds import RssReviewsFeed, AtomReviewsFeed, \
9
from reviewboard.reviews.feeds import RssReviewsFeed, AtomReviewsFeed, \
9
                                      RssSubmitterReviewsFeed, \
10
                                      RssSubmitterReviewsFeed, \
10
                                      AtomSubmitterReviewsFeed, \
11
                                      AtomSubmitterReviewsFeed, \
11
                                      RssGroupReviewsFeed, \
12
                                      RssGroupReviewsFeed, \
12
                                      AtomGroupReviewsFeed
13
                                      AtomGroupReviewsFeed
13
14
14
15
16
# Load all site settings.
17
load_site_config()
18
15
# Load in all the models for the admin UI.
19
# Load in all the models for the admin UI.
16
if not admin.site._registry:
20
if not admin.site._registry:
17
    admin.autodiscover()
21
    admin.autodiscover()
18
22
19
23
56
    }
60
    }
57
61
58
62
59
    # Main includes
63
    # Main includes
60
    urlpatterns += patterns('',
64
    urlpatterns += patterns('',
65
        (r'^account/', include('reviewboard.accounts.urls')),
61
        (r'^api/json/', include('reviewboard.webapi.urls')),
66
        (r'^api/json/', include('reviewboard.webapi.urls')),
62
        (r'^r/', include('reviewboard.reviews.urls')),
67
        (r'^r/', include('reviewboard.reviews.urls')),
63
        (r'^reports/', include('reviewboard.reports.urls')),
68
        (r'^reports/', include('reviewboard.reports.urls')),
64
    )
69
    )
65
70
99
    urlpatterns += patterns('',
104
    urlpatterns += patterns('',
100
        url(r'^$', 'django.views.generic.simple.redirect_to',
105
        url(r'^$', 'django.views.generic.simple.redirect_to',
101
            {'url': 'dashboard/'},
106
            {'url': 'dashboard/'},
102
            name="root"),
107
            name="root"),
103
108
104
        # Authentication and accounts
105
        url(r'^account/login/$', 'djblets.auth.views.login',
106
            {'next_page': settings.SITE_ROOT + 'dashboard/',
107
             'extra_context': {'BUILTIN_AUTH': settings.BUILTIN_AUTH}},
108
            name="login"),
109
        url(r'^account/preferences/$',
110
            'reviewboard.accounts.views.user_preferences',
111
            name="user-preferences"),
112
113
        # This must be last.
109
        # This must be last.
114
        (r'^iphone/', include('reviewboard.iphone.urls')),
110
        (r'^iphone/', include('reviewboard.iphone.urls')),
115
    )
111
    )
116
117
    if settings.BUILTIN_AUTH:
118
        urlpatterns += patterns('',
119
            url(r'^account/register/$', 'djblets.auth.views.register',
120
                {'next_page': settings.SITE_ROOT + 'dashboard/'},
121
                name="register"),
122
        )
123
    else:
124
        urlpatterns += patterns('',
125
            (r'^account/register/$',
126
             'django.views.generic.simple.redirect_to',
127
             {'url': settings.SITE_ROOT + 'account/login/'}))
/trunk/reviewboard/accounts/backends.py
Revision 1421 New Change
88
                ldapo.simple_bind_s(settings.LDAP_ANON_BIND_UID, settings.LDAP_ANON_BIND_PASSWD)
88
                ldapo.simple_bind_s(settings.LDAP_ANON_BIND_UID, settings.LDAP_ANON_BIND_PASSWD)
89
89
90
                passwd = ldapo.search_s(settings.LDAP_UID_MASK % username,
90
                passwd = ldapo.search_s(settings.LDAP_UID_MASK % username,
91
                                        ldap.SCOPE_SUBTREE, "objectclass=*")
91
                                        ldap.SCOPE_SUBTREE, "objectclass=*")
92
92
93
                first_name = passwd[0][1]['givenName'][0]
93
                first_name = passwd[0][1]['givenName']
94
                last_name = passwd[0][1]['sn'][0]
94
                last_name = passwd[0][1]['sn']
95
                email = u'%s@%s' % (username, settings.LDAP_EMAIL_DOMAIN)
95
                email = u'%s@%s' % (username, settings.LDAP_EMAIL_DOMAIN)
96
96
97
                user = User(username=username,
97
                user = User(username=username,
98
                            password='',
98
                            password='',
99
                            first_name=first_name,
99
                            first_name=first_name,
/trunk/reviewboard/accounts/decorators.py
Revision 1421 New Change
1
from urllib import quote
1
from urllib import quote
2
2
3
from django.conf import settings
4
from django.contrib.auth import REDIRECT_FIELD_NAME
3
from django.contrib.auth import REDIRECT_FIELD_NAME
5
from django.core.urlresolvers import reverse
4
from django.core.urlresolvers import reverse
6
from django.http import HttpResponseRedirect
5
from django.http import HttpResponseRedirect
7
6
8
from djblets.auth.util import login_required
7
from djblets.auth.util import login_required
8
from djblets.siteconfig.models import SiteConfiguration
9
from djblets.util.decorators import simple_decorator
9
from djblets.util.decorators import simple_decorator
10
10
11
from reviewboard.accounts.models import Profile
11
from reviewboard.accounts.models import Profile
12
12
13
13
14
@simple_decorator
14
@simple_decorator
15
def check_login_required(view_func):
15
def check_login_required(view_func):
16
    """
16
    """
17
    A decorator that checks whether login is required on this installation
17
    A decorator that checks whether login is required on this installation
18
    and, if so, checks if the user is logged in. If login is required and
18
    and, if so, checks if the user is logged in. If login is required and
19
    the user is not logged in, they're redirected to the login link.
19
    the user is not logged in, they're redirected to the login link.
20
    """
20
    """
21
    def _check(*args, **kwargs):
21
    def _check(*args, **kwargs):
22
        if settings.REQUIRE_SITEWIDE_LOGIN:
22
        siteconfig = SiteConfiguration.objects.get_current()
23
24
        if siteconfig.get("auth_require_sitewide_login"):
23
            return login_required(view_func)(*args, **kwargs)
25
            return login_required(view_func)(*args, **kwargs)
24
        else:
26
        else:
25
            return view_func(*args, **kwargs)
27
            return view_func(*args, **kwargs)
26
28
27
    return _check
29
    return _check
/trunk/reviewboard/accounts/forms.py
Revision 1421 New Change
1
from django import forms
1
from django import forms
2
from django.conf import settings
2
from django.conf import settings
3
from django.forms import widgets
3
from django.forms import widgets
4
from django.utils.translation import ugettext as _
4
from django.utils.translation import ugettext as _
5
5
6
from djblets.siteconfig.models import SiteConfiguration
7
6
from reviewboard.reviews.models import Group
8
from reviewboard.reviews.models import Group
7
9
8
10
9
class PreferencesForm(forms.Form):
11
class PreferencesForm(forms.Form):
10
    redirect_to = forms.CharField(required=False, widget=forms.HiddenInput)
12
    redirect_to = forms.CharField(required=False, widget=forms.HiddenInput)
11
    groups = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,
13
    groups = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,
12
                                       required=False)
14
                                       required=False)
13
    syntax_highlighting = forms.BooleanField(required=False,
15
    syntax_highlighting = forms.BooleanField(required=False,
14
        label=_("Enable syntax highlighting in the diff viewer"))
16
        label=_("Enable syntax highlighting in the diff viewer"))
15
    first_name = forms.CharField(required=False)
17
    first_name = forms.CharField(required=False)
16
    last_name = forms.CharField(required=False)
18
    last_name = forms.CharField(required=False)
17
    email = forms.EmailField(required=settings.BUILTIN_AUTH)
19
    email = forms.EmailField()
18
    password1 = forms.CharField(required=False, widget=widgets.PasswordInput())
20
    password1 = forms.CharField(required=False, widget=widgets.PasswordInput())
19
    password2 = forms.CharField(required=False, widget=widgets.PasswordInput())
21
    password2 = forms.CharField(required=False, widget=widgets.PasswordInput())
20
22
21
    def __init__(self, *args, **kwargs):
23
    def __init__(self, *args, **kwargs):
22
        forms.Form.__init__(self, *args, **kwargs)
24
        forms.Form.__init__(