summaryrefslogtreecommitdiffstats
path: root/askbot/startup_procedures.py
blob: 691f0a0305eca615cfc483b71d7c65e9afba5464 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
"""tests to be performed
in the beginning of models/__init__.py

the purpose of this module is to validate deployment of askbot

question: why not run these from askbot/__init__.py?

the main function is run_startup_tests
"""
import sys
import os
import askbot
from django.db import transaction
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from askbot.utils.loading import load_module
from askbot.utils.functions import enumerate_string_list
from urlparse import urlparse

PREAMBLE = """\n
************************
*                      *
*   Askbot self-test   *
*                      *
************************\n
"""

FOOTER = """\n
If necessary, type ^C (Ctrl-C) to stop the program.
"""

class AskbotConfigError(ImproperlyConfigured):
    """Prints an error with a preamble and possibly a footer"""
    def __init__(self, error_message):
        msg = PREAMBLE + error_message
        if sys.__stdin__.isatty():
            #print footer only when askbot is run from the shell
            msg += FOOTER
            super(AskbotConfigError, self).__init__(msg)

def askbot_warning(line):
    """prints a warning with the nice header, but does not quit"""
    print >> sys.stderr, PREAMBLE + '\n' + line

def print_errors(error_messages, header = None, footer = None):
    """if there is one or more error messages,
    raise ``class:AskbotConfigError`` with the human readable
    contents of the message
    * ``header`` - text to show above messages
    * ``footer`` - text to show below messages
    """
    if len(error_messages) == 0: return
    if len(error_messages) > 1:
        error_messages = enumerate_string_list(error_messages)

    message = ''
    if header: message += header + '\n'
    message += 'Please attend to the following:\n\n'
    message += '\n\n'.join(error_messages)
    if footer: message += '\n\n' + footer
    raise AskbotConfigError(message)

def format_as_text_tuple_entries(items):
    """prints out as entries or tuple containing strings
    ready for copy-pasting into say django settings file"""
    return "    '%s'," % "',\n    '".join(items)

#todo:
#
# *validate emails in settings.py
def test_askbot_url():
    """Tests the ASKBOT_URL setting for the
    well-formedness and raises the ImproperlyConfigured
    exception, if the setting is not good.
    """
    url = django_settings.ASKBOT_URL
    if url != '':

        if isinstance(url, str) or isinstance(url, unicode):
            pass
        else:
            msg = 'setting ASKBOT_URL must be of string or unicode type'
            raise AskbotConfigError(msg)

        if url == '/':
            msg = 'value "/" for ASKBOT_URL is invalid. '+ \
                'Please, either make ASKBOT_URL an empty string ' + \
                'or a non-empty path, ending with "/" but not ' + \
                'starting with "/", for example: "forum/"'
            raise AskbotConfigError(msg)
        else:
            try:
                assert(url.endswith('/'))
            except AssertionError:
                msg = 'if ASKBOT_URL setting is not empty, ' + \
                        'it must end with /'
                raise AskbotConfigError(msg)
            try:
                assert(not url.startswith('/'))
            except AssertionError:
                msg = 'if ASKBOT_URL setting is not empty, ' + \
                        'it must not start with /'

def test_middleware():
    """Checks that all required middleware classes are
    installed in the django settings.py file. If that is not the
    case - raises an AskbotConfigError exception.
    """
    required_middleware = (
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware',
        'askbot.middleware.forum_mode.ForumModeMiddleware',
        'askbot.middleware.pagesize.QuestionsPageSizeMiddleware',
        'askbot.middleware.cancel.CancelActionMiddleware',
        'django.middleware.transaction.TransactionMiddleware',
        'askbot.middleware.view_log.ViewLogMiddleware',
    )
    if 'debug_toolbar' in django_settings.INSTALLED_APPS:
        required_middleware += (
            'debug_toolbar.middleware.DebugToolbarMiddleware',
        )

    installed_middleware_set = set(django_settings.MIDDLEWARE_CLASSES)
    missing_middleware_set = set(required_middleware) - installed_middleware_set

    if missing_middleware_set:
        error_message = """\n\nPlease add the following middleware (listed after this message)
to the MIDDLEWARE_CLASSES variable in your site settings.py file.
The order the middleware records may be important, please take a look at the example in
https://github.com/ASKBOT/askbot-devel/blob/master/askbot/setup_templates/settings.py:\n\n"""
        middleware_text = format_as_text_tuple_entries(missing_middleware_set)
        raise AskbotConfigError(error_message + middleware_text)


    #middleware that was used in the past an now removed
    canceled_middleware = (
        'askbot.deps.recaptcha_django.middleware.ReCaptchaMiddleware',
    )
    #'debug_toolbar.middleware.DebugToolbarMiddleware',

    remove_middleware_set = set(canceled_middleware) \
                                & installed_middleware_set
    if remove_middleware_set:
        error_message = """\n\nPlease remove the following middleware entries from
the list of MIDDLEWARE_CLASSES in your settings.py - these are not used any more:\n\n"""
        middleware_text = format_as_text_tuple_entries(remove_middleware_set)
        raise AskbotConfigError(error_message + middleware_text)

def try_import(module_name, pypi_package_name):
    """tries importing a module and advises to install
    A corresponding Python package in the case import fails"""
    try:
        load_module(module_name)
    except ImportError, error:
        message = 'Error: ' + unicode(error) 
        message += '\n\nPlease run: >pip install %s' % pypi_package_name
        message += '\n\nTo install all the dependencies at once, type:'
        message += '\npip install -r askbot_requirements.txt\n'
        message += '\nType ^C to quit.'
        raise AskbotConfigError(message)

def test_modules():
    """tests presence of required modules"""
    from askbot import REQUIREMENTS
    for module_name, pip_path in REQUIREMENTS.items():
        try_import(module_name, pip_path)

def test_postgres():
    """Checks for the postgres buggy driver, version 2.4.2"""
    if hasattr(django_settings, 'DATABASE_ENGINE'):
        if django_settings.DATABASE_ENGINE in ('postgresql_psycopg2',):
            try:
                import psycopg2
                version = psycopg2.__version__.split(' ')[0].split('.')
                if version == ['2', '4', '2']:
                    raise AskbotConfigError(
                        'Please install psycopg2 version 2.4.1,\n version 2.4.2 has a bug'
                    )
                elif version > ['2', '4', '2']:
                    pass #don't know what to do
                else:
                    pass #everythin is ok
            except ImportError:
                #Using mysql not a problem
                pass
        else:
            pass #using other thing than postgres
    else:
        pass #TODO: test new django dictionary databases

def test_encoding():
    """prints warning if encoding error is not UTF-8"""
    if hasattr(sys.stdout, 'encoding'):
        if sys.stdout.encoding != 'UTF-8':
            askbot_warning(
                'Your output encoding is not UTF-8, there may be '
                'issues with the software when anything is printed '
                'to the terminal or log files'
            )

def test_template_loader():
    """Sends a warning if you have an old style template
    loader that used to send a warning"""
    old_template_loader = 'askbot.skins.loaders.load_template_source'
    if old_template_loader in django_settings.TEMPLATE_LOADERS:
        raise AskbotConfigError(
                "\nPlease change: \n"
                "'askbot.skins.loaders.load_template_source', to\n"
                "'askbot.skins.loaders.filesystem_load_template_source',\n"
                "in the TEMPLATE_LOADERS of your settings.py file"
        )

def test_celery():
    """Tests celery settings
    todo: we are testing two things here
    that correct name is used for the setting
    and that a valid value is chosen
    """
    broker_backend = getattr(django_settings, 'BROKER_BACKEND', None)
    broker_transport = getattr(django_settings, 'BROKER_TRANSPORT', None)

    if broker_backend is None:
        if broker_transport is None:
            raise AskbotConfigError(
                "\nPlease add\n"
                'BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport"\n'
                "or other valid value to your settings.py file"
            )
        else:
            #todo: check that broker transport setting is valid
            return

    if broker_backend != broker_transport:
        raise AskbotConfigError(
            "\nPlease rename setting BROKER_BACKEND to BROKER_TRANSPORT\n"
            "in your settings.py file\n"
            "If you have both in your settings.py - then\n"
            "delete the BROKER_BACKEND setting and leave the BROKER_TRANSPORT"
        )

    if hasattr(django_settings, 'BROKER_BACKEND') and not hasattr(django_settings, 'BROKER_TRANSPORT'):
        raise AskbotConfigError(
            "\nPlease rename setting BROKER_BACKEND to BROKER_TRANSPORT\n"
            "in your settings.py file"
        )

def test_media_url():
    """makes sure that setting `MEDIA_URL`
    has leading slash"""
    media_url = django_settings.MEDIA_URL
    #todo: add proper url validation to MEDIA_URL setting
    if not (media_url.startswith('/') or media_url.startswith('http')):
        raise AskbotConfigError(
            "\nMEDIA_URL parameter must be a unique url on the site\n"
            "and must start with a slash - e.g. /media/ or http(s)://"
        )

class SettingsTester(object):
    """class to test contents of the settings.py file"""

    def __init__(self, requirements = None):
        """loads the settings module and inits some variables
        parameter `requirements` is a dictionary with keys
        as setting names and values - another dictionary, which
        has keys (optional, if noted and required otherwise)::

        * required_value (optional)
        * error_message
        """
        self.settings = load_module(os.environ['DJANGO_SETTINGS_MODULE'])
        self.messages = list()
        self.requirements = requirements


    def test_setting(self, name,
            value = None, message = None,
            test_for_absence = False,
            replace_hint = None
        ):
        """if setting does is not present or if the value != required_value,
        adds an error message
        """
        if test_for_absence:
            if hasattr(self.settings, name):
                if replace_hint:
                    value = getattr(self.settings, name)
                    message += replace_hint % value
                self.messages.append(message)
        else:
            if not hasattr(self.settings, name):
                self.messages.append(message)
            elif value and getattr(self.settings, name) != value:
                self.messages.append(message)

    def run(self):
        for setting_name in self.requirements:
            self.test_setting(
                setting_name,
                **self.requirements[setting_name]
            )
        if len(self.messages) != 0:
            raise AskbotConfigError(
                '\n\nTime to do some maintenance of your settings.py:\n\n* ' + 
                '\n\n* '.join(self.messages)
            )
            
def test_staticfiles():
    """tests configuration of the staticfiles app"""
    errors = list()
    import django
    django_version = django.VERSION
    if django_version[0] == 1 and django_version[1] < 3:
        staticfiles_app_name = 'staticfiles'
        try_import('staticfiles', 'django-staticfiles')
        import staticfiles
        if staticfiles.__version__[0] != 1:
            raise AskbotConfigError(
                'Please use the newest available version of '
                'django-staticfiles app, type\n'
                'pip install --upgrade django-staticfiles'
            )
    else:
        staticfiles_app_name = 'django.contrib.staticfiles'

    if staticfiles_app_name not in django_settings.INSTALLED_APPS:
        errors.append(
            'Add to the INSTALLED_APPS section of your settings.py:\n'
            "    '%s'," % staticfiles_app_name
        )
    static_url = django_settings.STATIC_URL
    if static_url is None or str(static_url).strip() == '':
        errors.append(
            'Add STATIC_URL setting to your settings.py file. '
            'The setting must be a url at which static files ' 
            'are accessible.'
        )
    url = urlparse(static_url).path
    if not (url.startswith('/') and url.endswith('/')):
        #a simple check for the url
        errors.append(
            'Path in the STATIC_URL must start and end with the /.'
        )
    if django_settings.ADMIN_MEDIA_PREFIX != static_url + 'admin/':
        errors.append(
            'Set ADMIN_MEDIA_PREFIX as: \n'
            "    ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/'"
        )

    askbot_root = os.path.dirname(askbot.__file__)
    skin_dir = os.path.abspath(os.path.join(askbot_root, 'skins'))
    if skin_dir not in map(os.path.abspath, django_settings.STATICFILES_DIRS):
        errors.append(
            'Add to STATICFILES_DIRS list of your settings.py file:\n'
            "    '%s'," % skin_dir
        )
    extra_skins_dir = getattr(django_settings, 'ASKBOT_EXTRA_SKINS_DIR', None)
    if extra_skins_dir is not None:
        if not os.path.isdir(extra_skins_dir):
            errors.append(
                'Directory specified with settning ASKBOT_EXTRA_SKINS_DIR '
                'must exist and contain your custom skins for askbot.'
            )
        if extra_skins_dir not in django_settings.STATICFILES_DIRS:
            errors.append(
                'Add ASKBOT_EXTRA_SKINS_DIR to STATICFILES_DIRS entry in '
                'your settings.py file.\n'
                'NOTE: it might be necessary to move the line with '
                'ASKBOT_EXTRA_SKINS_DIR just above STATICFILES_DIRS.'
            )

    if errors:
        errors.append(
            'Run command (after fixing the above errors)\n'
            '    python manage.py collectstatic\n'
        )
    print_errors(errors)
    if django_settings.DEBUG and django_settings.STATICFILES_STORAGE != \
        'django.contrib.staticfiles.storage.StaticFilesStorage':
        if not os.path.isdir(django_settings.STATIC_ROOT):
            askbot_warning(
                'Please run command\n\n'
                '    python manage.py collectstatic'

            )

def run_startup_tests():
    """function that runs
    all startup tests, mainly checking settings config so far
    """

    #todo: refactor this when another test arrives
    test_template_loader()
    test_encoding()
    test_modules()
    test_askbot_url()
    #test_postgres()
    test_middleware()
    test_celery()
    test_staticfiles()
    settings_tester = SettingsTester({
        'CACHE_MIDDLEWARE_ANONYMOUS_ONLY': {
            'value': True,
            'message': "add line CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True"
        },
        'USE_I18N': {
            'value': True,
            'message': 'Please set USE_I18N = True and\n'
                'set the LANGUAGE_CODE parameter correctly'
        },
        'LOGIN_REDIRECT_URL': {
            'message': 'add setting LOGIN_REDIRECT_URL - an url\n'
                'where you want to send users after they log in\n'
                'a reasonable default is\n'
                'LOGIN_REDIRECT_URL = ASKBOT_URL'
        },
        'ASKBOT_FILE_UPLOAD_DIR': {
            'test_for_absence': True,
            'message': 'Please replace setting ASKBOT_FILE_UPLOAD_DIR ',
            'replace_hint': "with MEDIA_ROOT = '%s'"
        },
        'ASKBOT_UPLOADED_FILES_URL': {
            'test_for_absence': True,
            'message': 'Please replace setting ASKBOT_UPLOADED_FILES_URL ',
            'replace_hint': "with MEDIA_URL = '/%s'"
        }
    })
    settings_tester.run()
    test_media_url()

@transaction.commit_manually
def run():
    """runs all the startup procedures"""
    try:
        run_startup_tests()
    except AskbotConfigError, error:
        transaction.rollback()
        print error
        sys.exit(1)
    try:
        from askbot.models import badges
        badges.init_badges()
        transaction.commit()
    except Exception, error:
        print error
        transaction.rollback()