• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bloomberg / pybossa / 15467779253

05 Jun 2025 01:04PM UTC coverage: 94.131% (-0.04%) from 94.172%
15467779253

push

github

web-flow
priv sync (#1059)

* priv sync

41 of 52 new or added lines in 8 files covered. (78.85%)

1 existing line in 1 file now uncovered.

17771 of 18879 relevant lines covered (94.13%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.31
/pybossa/core.py
1
# -*- coding: utf8 -*-
2
# This file is part of PYBOSSA.
3
#
4
# Copyright (C) 2015 Scifabric LTD.
5
#
6
# PYBOSSA is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU Affero General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# PYBOSSA is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU Affero General Public License for more details.
15
#
16
# You should have received a copy of the GNU Affero General Public License
17
# along with PYBOSSA.  If not, see <http://www.gnu.org/licenses/>.
18
"""Core module for PYBOSSA."""
1✔
19
import os
1✔
20
import logging
1✔
21
import humanize
1✔
22
from flask import Flask, url_for, request, render_template, \
1✔
23
    flash, _app_ctx_stack, abort
24
from flask_login import current_user
1✔
25
from flask_babel import gettext
1✔
26
from flask_assets import Bundle
1✔
27
from flask_json_multidict import get_json_multidict
1✔
28
from flask_talisman import Talisman
1✔
29
from flask_wtf.csrf import CSRFError
1✔
30
from flasgger import Swagger
1✔
31
from pybossa import default_settings
1✔
32
from pybossa.extensions import *
1✔
33
from pybossa.ratelimit import get_view_rate_limit
1✔
34
from raven.contrib.flask import Sentry
1✔
35
from pybossa.util import pretty_date, handle_content_type, get_disqus_sso
1✔
36
from pybossa.util import datetime_filter, grant_access_with_api_key
1✔
37
from pybossa.news import FEED_KEY as NEWS_FEED_KEY
1✔
38
from pybossa.news import get_news
1✔
39
from pybossa.messages import *
1✔
40
import pybossa.app_settings as app_settings
1✔
41
import json
1✔
42
from pybossa.wizard import Wizard
1✔
43
from pybossa.util import copy_directory
1✔
44

45

46
def create_app(run_as_server=True):
1✔
47
    """Create web app."""
48

49
    setup_logging(run_as_server)
1✔
50
    app = Flask(__name__.split('.')[0])
1✔
51
    configure_app(app)
1✔
52
    global talisman
53
    talisman = Talisman(app, content_security_policy={
1✔
54
        'default-src': ['*', '\'unsafe-inline\'', '\'unsafe-eval\'', 'data:',
55
                        'blob:']
56
    }, force_https=app.config.get('FORCE_HTTPS', True))
57
    setup_theme(app)
1✔
58
    setup_assets(app)
1✔
59
    setup_cache_timeouts(app)
1✔
60
    setup_ratelimits(app)
1✔
61
    setup_uploader(app)
1✔
62
    setup_error_email(app)
1✔
63
    setup_login_manager(app)
1✔
64
    setup_babel(app)
1✔
65
    setup_markdown(app)
1✔
66
    setup_db(app)
1✔
67
    setup_repositories(app)
1✔
68
    setup_cache(app)
1✔
69
    setup_strong_password(app)
1✔
70
    mail.init_app(app)
1✔
71
    sentinel.init_app(app)
1✔
72
    setup_exporter(app)
1✔
73
    setup_http_signer(app)
1✔
74
    signer.init_app(app)
1✔
75
    if app.config.get('SENTRY_DSN'):  # pragma: no cover
76
        Sentry(app)
77
    if run_as_server:  # pragma: no cover
78
        setup_scheduled_jobs(app)
79
    setup_blueprints(app)
1✔
80
    setup_hooks(app)
1✔
81
    setup_error_handlers(app)
1✔
82
    setup_ldap(app)
1✔
83
    setup_external_services(app)
1✔
84
    setup_importers(app)
1✔
85
    setup_jinja(app)
1✔
86
    setup_csrf_protection(app)
1✔
87
    setup_debug_toolbar(app)
1✔
88
    setup_jinja2_filters(app)
1✔
89
    setup_newsletter(app)
1✔
90
    setup_sse(app)
1✔
91
    setup_json_serializer(app)
1✔
92
    setup_cors(app)
1✔
93
    setup_profiler(app)
1✔
94
    plugin_manager.init_app(app)
1✔
95
    plugin_manager.install_plugins()
1✔
96
    import pybossa.model.event_listeners
1✔
97
    anonymizer.init_app(app)
1✔
98
    setup_task_presenter_editor(app)
1✔
99
    setup_schedulers(app)
1✔
100
    setup_swagger(app)
1✔
101
    setup_global_configs(app)
1✔
102
    return app
1✔
103

104
# construct rq_dashboard config
105
RQ_DASHBOARD_LEGACY_CONFIG_OPTIONS = {
1✔
106
    "REDIS_URL": "RQ_DASHBOARD_REDIS_URL",
107
    "REDIS_HOST": "RQ_DASHBOARD_REDIS_HOST",
108
    "REDIS_PORT": "RQ_DASHBOARD_REDIS_PORT",
109
    "REDIS_PASSWORD": "RQ_DASHBOARD_REDIS_PASSWORD",
110
    "REDIS_DB": "RQ_DASHBOARD_REDIS_DB",
111
    "REDIS_SENTINELS": "RQ_DASHBOARD_REDIS_SENTINELS",
112
    "REDIS_MASTER_NAME": "RQ_DASHBOARD_REDIS_MASTER_NAME",
113
    "RQ_POLL_INTERVAL": "RQ_DASHBOARD_POLL_INTERVAL",
114
    "WEB_BACKGROUND": "RQ_DASHBOARD_WEB_BACKGROUND",
115
    "DELETE_JOBS": "RQ_DASHBOARD_DELETE_JOBS",
116
}
117

118
def upgrade_rq_config(config):
1✔
119
    """
120
    rq_dashboard requires specific parameter name in config: https://pypi.org/project/rq-dashboard/
121
    old configuration options will be deprecated soon so updates old options with new ones.
122
    """
123
    for old_name, new_name in RQ_DASHBOARD_LEGACY_CONFIG_OPTIONS.items():
1✔
124
        if old_name in config:
1✔
125
            config[new_name] = config[old_name]
1✔
126

127

128
def configure_app(app):
1✔
129
    """Configure web app."""
130
    app.config.from_object(default_settings)
1✔
131
    if app_settings.config_path:
1✔
132
        app.config.from_pyfile(app_settings.config_path)
1✔
133
    upgrade_rq_config(app.config)
1✔
134

135
    # Override DB in case of testing
136
    if app.config.get('SQLALCHEMY_DATABASE_TEST_URI'):
1✔
137
        app.config['SQLALCHEMY_DATABASE_URI'] = \
1✔
138
            app.config['SQLALCHEMY_DATABASE_TEST_URI']
139
    # Enable Slave bind in case is missing using Master node
140
    if not app.config.get('SQLALCHEMY_BINDS'):
1✔
141
        app.config['SQLALCHEMY_BINDS'] = {}
1✔
142
    if app.config['SQLALCHEMY_BINDS'].get('slave') is None:
1✔
143
        print("Slave binds are missing, adding Master as slave too.")
1✔
144
        master = app.config.get('SQLALCHEMY_DATABASE_URI')
1✔
145
        app.config['SQLALCHEMY_BINDS']['slave'] = master
1✔
146
    app.url_map.strict_slashes = app.config.get('STRICT_SLASHES')
1✔
147

148

149
def setup_json_serializer(app):
1✔
150
    app.json_encoder = JSONEncoder
1✔
151

152

153
def setup_cors(app):
1✔
154
    cors.init_app(app, resources=app.config.get('CORS_RESOURCES'))
1✔
155

156
def setup_sse(app):
1✔
157
    if app.config['SSE']:
1✔
158
        msg = "WARNING: async mode is required as Server Sent Events are enabled."
1✔
159
        app.logger.warning(msg)
1✔
160
    else:
161
        msg = "INFO: async mode is disabled."
×
162
        app.logger.info(msg)
×
163

164

165
def setup_theme(app):
1✔
166
    """Configure theme for PYBOSSA app."""
167
    theme = app.config['THEME']
1✔
168
    template_dir = app.config.get('TEMPLATE_DIR', '')
1✔
169
    app.template_folder = os.path.join(template_dir, 'themes', theme, 'templates')
1✔
170
    app.static_folder = os.path.join(template_dir, 'themes', theme, 'static')
1✔
171

172

173
def setup_uploader(app):
1✔
174
    """Setup uploader."""
175
    global uploader
176

177
    upload_method = app.config.get('UPLOAD_METHOD')
1✔
178
    if upload_method == 'local':
1✔
179
        from pybossa.uploader.local import LocalUploader
1✔
180
        uploader = LocalUploader()
1✔
181
    if upload_method == 'cloud':  # pragma: no cover
182
        from pybossa.uploader.cloud_store import CloudStoreUploader
183
        uploader = CloudStoreUploader()
184
        app.url_build_error_handlers.append(uploader.external_url_handler)
185
    if upload_method == 'cloudproxy':  #pragma: no cover
186
        from pybossa.uploader.cloud_proxy import CloudProxyUploader
187
        uploader = CloudProxyUploader()
188
    uploader.init_app(app)
1✔
189

190

191
def setup_exporter(app):
1✔
192
    """Setup exporter."""
193
    global csv_exporter
194
    global task_csv_exporter
195
    global json_exporter
196
    global task_json_exporter
197
    global project_csv_exporter
198
    from pybossa.exporter.csv_export import CsvExporter
1✔
199
    from pybossa.exporter.task_csv_export import TaskCsvExporter
1✔
200
    from pybossa.exporter.json_export import JsonExporter
1✔
201
    from pybossa.exporter.task_json_export import TaskJsonExporter
1✔
202
    from pybossa.exporter.project_csv_export import ProjectCsvExporter
1✔
203
    csv_exporter = CsvExporter()
1✔
204
    task_csv_exporter = TaskCsvExporter()
1✔
205
    json_exporter = JsonExporter()
1✔
206
    task_json_exporter = TaskJsonExporter()
1✔
207
    project_csv_exporter = ProjectCsvExporter()
1✔
208

209

210
def setup_markdown(app):
1✔
211
    """Setup markdown."""
212
    misaka.init_app(app)
1✔
213

214

215
def setup_db(app):
1✔
216
    """Setup database."""
217
    def create_slave_session(db, bind):
1✔
218
        if (app.config.get('SQLALCHEMY_BINDS')['slave'] ==
1✔
219
                app.config.get('SQLALCHEMY_DATABASE_URI')):
220
            return db.session
1✔
221
        engine = db.get_engine(db.app, bind=bind)
×
222
        options = dict(bind=engine, scopefunc=_app_ctx_stack.__ident_func__)
×
223
        slave_session = db.create_scoped_session(options=options)
×
224
        return slave_session
×
225

226
    def create_bulkdel_session(db, bind):
1✔
227
        if not 'bulkdel' in app.config.get('SQLALCHEMY_BINDS'):
1✔
228
            return db.session
1✔
229
        engine = db.get_engine(db.app, bind=bind)
×
230
        options = dict(bind=engine, scopefunc=_app_ctx_stack.__ident_func__)
×
231
        bulkdel_session = db.create_scoped_session(options=options)
×
232
        return bulkdel_session
×
233

234
    db.app = app
1✔
235
    db.init_app(app)
1✔
236
    db.slave_session = create_slave_session(db, bind='slave')
1✔
237
    db.bulkdel_session = create_bulkdel_session(db, bind='bulkdel')
1✔
238

239
    if db.slave_session is not db.session:
1✔
240
        # flask-sqlalchemy does it already for default session db.session
241
        @app.teardown_appcontext
×
242
        def _shutdown_session(response_or_exc):  # pragma: no cover
243
            if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
244
                if response_or_exc is None:
245
                    db.slave_session.commit()
246
            db.slave_session.remove()
247
            return response_or_exc
248

249
    if db.bulkdel_session is not db.session:
1✔
250
        @app.teardown_appcontext
×
251
        def _shutdown_session(response_or_exc):  # pragma: no cover
252
            if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
253
                if response_or_exc is None:
254
                    db.bulkdel_session.commit()
255
            db.bulkdel_session.remove()
256
            return response_or_exc
257

258
def setup_repositories(app):
1✔
259
    """Setup repositories."""
260
    from pybossa.repositories import UserRepository
1✔
261
    from pybossa.repositories import ProjectRepository
1✔
262
    from pybossa.repositories import ProjectStatsRepository
1✔
263
    from pybossa.repositories import AnnouncementRepository
1✔
264
    from pybossa.repositories import BlogRepository
1✔
265
    from pybossa.repositories import TaskRepository
1✔
266
    from pybossa.repositories import AuditlogRepository
1✔
267
    from pybossa.repositories import WebhookRepository
1✔
268
    from pybossa.repositories import ResultRepository
1✔
269
    from pybossa.repositories import HelpingMaterialRepository
1✔
270
    from pybossa.repositories import PerformanceStatsRepository
1✔
271
    global user_repo
272
    global project_repo
273
    global project_stats_repo
274
    global announcement_repo
275
    global blog_repo
276
    global task_repo
277
    global auditlog_repo
278
    global webhook_repo
279
    global result_repo
280
    global helping_repo
281
    global performance_stats_repo
282
    language = app.config.get('FULLTEXTSEARCH_LANGUAGE')
1✔
283
    rdancy_upd_exp = app.config.get('TASK_EXPIRATION', 60)
1✔
284
    user_repo = UserRepository(db)
1✔
285
    project_repo = ProjectRepository(db)
1✔
286
    project_stats_repo = ProjectStatsRepository(db)
1✔
287
    announcement_repo = AnnouncementRepository(db)
1✔
288
    blog_repo = BlogRepository(db)
1✔
289
    task_repo = TaskRepository(db, language, rdancy_upd_exp)
1✔
290
    auditlog_repo = AuditlogRepository(db)
1✔
291
    webhook_repo = WebhookRepository(db)
1✔
292
    result_repo = ResultRepository(db)
1✔
293
    helping_repo = HelpingMaterialRepository(db)
1✔
294
    performance_stats_repo = PerformanceStatsRepository(db)
1✔
295

296

297
def setup_cache(app):
1✔
298
    from pybossa.cache import users
1✔
299
    global cache_users
300
    cache_users = users
1✔
301

302

303
def setup_error_email(app):
1✔
304
    """Setup error email."""
305
    from logging.handlers import SMTPHandler
1✔
306
    ADMINS = app.config.get('ADMINS', '')
1✔
307
    alerts = app.config.get('MAIL_ALERTS', False)
1✔
308
    if not app.debug and alerts and ADMINS:  # pragma: no cover
309
        mail_handler = SMTPHandler('127.0.0.1',
310
                                   'server-error@no-reply.com',
311
                                   ADMINS, 'error')
312
        mail_handler.setLevel(logging.ERROR)
313
        app.logger.addHandler(mail_handler)
314

315

316
def setup_logging(run_as_server=True):
1✔
317
    log_config = app_settings.config.get('LOG_DICT_CONFIG')
1✔
318
    if log_config:
1✔
319
        from logging.config import dictConfig
×
320
        dictConfig(log_config)
×
321

322

323
def setup_login_manager(app):
1✔
324
    """Setup login manager."""
325
    login_manager.login_view = 'account.signin'
1✔
326
    login_manager.login_message = "This feature requires being logged in. If you were previously logged in, your session may have timed out."
1✔
327

328
    @login_manager.user_loader
1✔
329
    def _load_user(username):
1✔
330
        return user_repo.get_by_name(username)
1✔
331
    login_manager.setup_app(app)
1✔
332

333

334
def setup_babel(app):
1✔
335
    """Return babel handler."""
336
    babel.init_app(app)
1✔
337

338
    @babel.localeselector
1✔
339
    def _get_locale():
1✔
340
        locales = [l[0] for l in app.config.get('LOCALES')]
1✔
341
        if current_user and current_user.is_authenticated:
1✔
342
            lang = current_user.locale
1✔
343
        else:
344
            lang = request.cookies.get('language')
1✔
345
        if (lang is None or lang == '' or
1✔
346
            lang.lower() not in locales):
347
            lang = request.accept_languages.best_match(locales)
1✔
348
        if (lang is None or lang == '' or
1✔
349
                lang.lower() not in locales):
350
            lang = app.config.get('DEFAULT_LOCALE') or 'en'
1✔
351
        if request.headers.get('Content-Type') == 'application/json':
1✔
352
            lang = 'en'
1✔
353
        return lang.lower()
1✔
354
    return babel
1✔
355

356

357
def setup_blueprints(app):
1✔
358
    """Configure blueprints."""
359
    from pybossa.api import blueprint as api
1✔
360
    from pybossa.view.account import blueprint as account
1✔
361
    from pybossa.view.projects import blueprint as projects
1✔
362
    from pybossa.view.projects import blueprint_projectid as projectids
1✔
363
    from pybossa.view.admin import blueprint as admin
1✔
364
    from pybossa.view.announcements import blueprint as announcements
1✔
365
    from pybossa.view.leaderboard import blueprint as leaderboard
1✔
366
    from pybossa.view.stats import blueprint as stats
1✔
367
    from pybossa.view.help import blueprint as helper
1✔
368
    from pybossa.view.home import blueprint as home
1✔
369
    from pybossa.view.uploads import blueprint as uploads
1✔
370
    from pybossa.view.amazon import blueprint as amazon
1✔
371
    from pybossa.view.diagnostics import blueprint as diagnostics
1✔
372
    from pybossa.view.fileproxy import blueprint as fileproxy
1✔
373
    from pybossa.view.bloomberg import blueprint as bloomberg
1✔
374

375
    blueprints = [{'handler': home, 'url_prefix': '/'},
1✔
376
                  {'handler': api,  'url_prefix': '/api'},
377
                  {'handler': account, 'url_prefix': '/account'},
378
                  {'handler': bloomberg, 'url_prefix': '/bloomberg'},
379
                  {'handler': projects, 'url_prefix': '/project'},
380
                  {'handler': projectids, 'url_prefix': '/projectid'},
381
                  {'handler': admin, 'url_prefix': '/admin'},
382
                  {'handler': announcements, 'url_prefix': '/announcements'},
383
                  {'handler': leaderboard, 'url_prefix': '/leaderboard'},
384
                  {'handler': helper, 'url_prefix': '/help'},
385
                  {'handler': stats, 'url_prefix': '/stats'},
386
                  {'handler': uploads, 'url_prefix': '/uploads'},
387
                  {'handler': amazon, 'url_prefix': '/amazon'},
388
                  {'handler': diagnostics, 'url_prefix': '/diagnostics'},
389
                  {'handler': fileproxy, 'url_prefix': '/fileproxy'}
390
                  ]
391

392
    for bp in blueprints:
1✔
393
        app.register_blueprint(bp['handler'], url_prefix=bp['url_prefix'])
1✔
394

395
    import rq_dashboard
1✔
396
    rq_dashboard.blueprint.before_request(is_admin)
1✔
397
    csrf.exempt(rq_dashboard.blueprint)
1✔
398
    app.register_blueprint(rq_dashboard.blueprint, url_prefix="/admin/rq",
1✔
399
                           redis_conn=sentinel.master)
400

401

402
def is_admin():
1✔
403
    """Check if user is admin."""
404
    if current_user.is_anonymous:
1✔
405
        return abort(401)
1✔
406
    if current_user.admin is False:
1✔
407
        return abort(403)
1✔
408

409

410
def setup_external_services(app):
1✔
411
    """Setup external services."""
412
    setup_twitter_login(app)
1✔
413
    setup_facebook_login(app)
1✔
414
    setup_google_login(app)
1✔
415
    setup_flickr_importer(app)
1✔
416
    setup_dropbox_importer(app)
1✔
417
    setup_twitter_importer(app)
1✔
418
    setup_youtube_importer(app)
1✔
419
    setup_email_service(app)
1✔
420

421

422
def setup_twitter_login(app):
1✔
423
    try:  # pragma: no cover
424
        if (app.config['TWITTER_CONSUMER_KEY'] and
425
                app.config['TWITTER_CONSUMER_SECRET']):
426
            twitter.init_app(app)
427
            from pybossa.view.twitter import blueprint as twitter_bp
428
            app.register_blueprint(twitter_bp, url_prefix='/twitter')
429
    except Exception as inst:  # pragma: no cover
430
        print(type(inst))
431
        print(inst.args)
432
        print(inst)
433
        print("Twitter signin disabled")
434
        log_message = 'Twitter signin disabled: %s' % str(inst)
435
        app.logger.info(log_message)
436

437

438
def setup_facebook_login(app):
1✔
439
    try:  # pragma: no cover
440
        if (app.config['FACEBOOK_APP_ID']
441
                and app.config['FACEBOOK_APP_SECRET']
442
                and app.config.get('LDAP_HOST') is None):
443
            facebook.init_app(app)
444
            from pybossa.view.facebook import blueprint as facebook_bp
445
            app.register_blueprint(facebook_bp, url_prefix='/facebook')
446
    except Exception as inst:  # pragma: no cover
447
        print(type(inst))
448
        print(inst.args)
449
        print(inst)
450
        print("Facebook signin disabled")
451
        log_message = 'Facebook signin disabled: %s' % str(inst)
452
        app.logger.info(log_message)
453

454

455
def setup_google_login(app):
1✔
456
    try:  # pragma: no cover
457
        if (app.config['GOOGLE_CLIENT_ID']
458
                and app.config['GOOGLE_CLIENT_SECRET']
459
                and app.config.get('LDAP_HOST') is None):
460
            google.init_app(app)
461
            from pybossa.view.google import blueprint as google_bp
462
            app.register_blueprint(google_bp, url_prefix='/google')
463
    except Exception as inst:  # pragma: no cover
464
        print(type(inst))
465
        print(inst.args)
466
        print(inst)
467
        print("Google signin disabled")
468
        log_message = 'Google signin disabled: %s' % str(inst)
469
        app.logger.info(log_message)
470

471

472
def setup_flickr_importer(app):
1✔
473
    try:  # pragma: no cover
474
        if (app.config['FLICKR_API_KEY']
475
                and app.config['FLICKR_SHARED_SECRET']):
476
            flickr.init_app(app)
477
            from pybossa.view.flickr import blueprint as flickr_bp
478
            app.register_blueprint(flickr_bp, url_prefix='/flickr')
479
            importer_params = {'api_key': app.config['FLICKR_API_KEY']}
480
            importer.register_flickr_importer(importer_params)
481
    except Exception as inst:  # pragma: no cover
482
        print(type(inst))
483
        print(inst.args)
484
        print(inst)
485
        print("Flickr importer not available")
486
        log_message = 'Flickr importer not available: %s' % str(inst)
487
        app.logger.info(log_message)
488

489

490
def setup_dropbox_importer(app):
1✔
491
    try:  # pragma: no cover
492
        if app.config['DROPBOX_APP_KEY']:
493
            importer.register_dropbox_importer()
494
    except Exception as inst:  # pragma: no cover
495
        print(type(inst))
496
        print(inst.args)
497
        print(inst)
498
        print("Dropbox importer not available")
499
        log_message = 'Dropbox importer not available: %s' % str(inst)
500
        app.logger.info(log_message)
501

502

503
def setup_twitter_importer(app):
1✔
504
    try:  # pragma: no cover
505
        if (app.config['TWITTER_CONSUMER_KEY'] and
506
                app.config['TWITTER_CONSUMER_SECRET']):
507
            importer_params = {
508
                'consumer_key': app.config['TWITTER_CONSUMER_KEY'],
509
                'consumer_secret': app.config['TWITTER_CONSUMER_SECRET']
510
            }
511
            importer.register_twitter_importer(importer_params)
512
    except Exception as inst:  # pragma: no cover
513
        print(type(inst))
514
        print(inst.args)
515
        print(inst)
516
        print("Twitter importer not available")
517
        log_message = 'Twitter importer not available: %s' % str(inst)
518
        app.logger.info(log_message)
519

520
def setup_youtube_importer(app):
1✔
521
    try:  # pragma: no cover
522
        if app.config['YOUTUBE_API_SERVER_KEY']:
523
            importer_params = {
524
                'youtube_api_server_key': app.config['YOUTUBE_API_SERVER_KEY']
525
            }
526
            importer.register_youtube_importer(importer_params)
527
    except Exception as inst:  # pragma: no cover
528
        print(type(inst))
529
        print(inst.args)
530
        print(inst)
531
        print("Youtube importer not available")
532
        log_message = 'Youtube importer not available: %s' % str(inst)
533
        app.logger.info(log_message)
534

535
def setup_importers(app):
1✔
536
    importers = app.config.get('AVAILABLE_IMPORTERS')
1✔
537
    if importers:
1✔
538
        importer.set_importers(importers)
×
539

540

541
def url_for_other_page(page):
1✔
542
    """Setup url for other pages."""
543
    args = dict(list(request.view_args.items()) + list(request.args.to_dict().items()))
1✔
544
    args['page'] = page
1✔
545
    return url_for(request.endpoint, **args)
1✔
546

547

548
def setup_jinja(app):
1✔
549
    """Setup jinja."""
550
    app.jinja_env.globals['url_for_other_page'] = url_for_other_page
1✔
551

552

553
def setup_error_handlers(app):
1✔
554
    """Setup error handlers."""
555
    @app.errorhandler(400)
1✔
556
    def _bad_request(e):
1✔
557
        msg = str(e)
1✔
558
        app.logger.exception(f'Bad request: {msg}')
1✔
559
        response = dict(template='400.html', code=400,
1✔
560
                        description=msg if msg else BADREQUEST)
561
        return handle_content_type(response)
1✔
562

563
    @app.errorhandler(404)
1✔
564
    def _page_not_found(e):
1✔
565
        msg = str(e)
1✔
566
        response = dict(template='404.html', code=404,
1✔
567
                        description=msg if msg else NOTFOUND)
568
        return handle_content_type(response)
1✔
569

570
    @app.errorhandler(500)
1✔
571
    def _server_error(e):  # pragma: no cover
572
        msg = str(e)
573
        app.logger.exception(f'An error occurred: {msg}')
574
        response = dict(template='500.html', code=500,
575
                        description=msg if msg else INTERNALSERVERERROR)
576
        return handle_content_type(response)
577

578
    @app.errorhandler(403)
1✔
579
    def _forbidden(e):
1✔
580
        msg = str(e)
1✔
581
        response = dict(template='403.html', code=403,
1✔
582
                        description=msg if msg else FORBIDDEN)
583
        return handle_content_type(response)
1✔
584

585
    @app.errorhandler(401)
1✔
586
    def _unauthorized(e):
1✔
587
        msg = str(e)
1✔
588
        response = dict(template='401.html', code=401,
1✔
589
                        description=msg if msg else UNAUTHORIZED)
590
        return handle_content_type(response)
1✔
591

592
    @app.errorhandler(423)
1✔
593
    def _locked(e):
1✔
594
        short_name = request.view_args.get('short_name')
1✔
595
        project = project_repo.get_by_shortname(short_name)
1✔
596
        owner = project.owner.name
1✔
597
        response = dict(template='423.html', code=423,
1✔
598
                        private_instance=app.config.get('PRIVATE_INSTANCE'),
599
                        description=LOCKED,
600
                        owner=owner)
601
        return handle_content_type(response)
1✔
602

603
def setup_hooks(app):
1✔
604
    """Setup hooks."""
605
    @app.after_request
1✔
606
    def _inject_x_rate_headers(response):
1✔
607
        limit = get_view_rate_limit()
1✔
608
        if limit and limit.send_x_headers:
1✔
609
            h = response.headers
1✔
610
            h.add('X-RateLimit-Remaining', str(limit.remaining))
1✔
611
            h.add('X-RateLimit-Limit', str(limit.limit))
1✔
612
            h.add('X-RateLimit-Reset', str(limit.reset))
1✔
613
        return response
1✔
614

615
    @app.before_request
1✔
616
    def _api_authentication():
1✔
617
        """ Attempt API authentication on a per-request basis."""
618
        secure_app_access = app.config.get('SECURE_APP_ACCESS', False)
1✔
619
        if not secure_app_access:
1✔
620
            grant_access_with_api_key(secure_app_access)
1✔
621

622
        # Handle forms
623
        request.body = request.form
1✔
624
        if (request.method == 'POST' and
1✔
625
                request.headers.get('Content-Type') == 'application/json' and
626
                request.data):
627
            try:
1✔
628
                request.body = get_json_multidict(request)
1✔
629
            except TypeError:
1✔
630
                abort(400)
1✔
631

632

633
    @app.context_processor
1✔
634
    def _global_template_context():
1✔
635
        notify_admin = False
1✔
636
        if (current_user and current_user.is_authenticated
1✔
637
            and current_user.admin):
638
            key = NEWS_FEED_KEY + str(current_user.id)
1✔
639
            if sentinel.slave.get(key):
1✔
640
                notify_admin = True
×
641
            news = get_news()
1✔
642
        else:
643
            news = None
1✔
644

645
        # Cookies warning
646
        cookie_name = app.config['BRAND'] + "_accept_cookies"
1✔
647
        show_cookies_warning = False
1✔
648
        if request and (not request.cookies.get(cookie_name)):
1✔
649
            show_cookies_warning = True
1✔
650

651
        # Announcement sections
652
        announcement_levels = app.config.get('ANNOUNCEMENT_LEVELS')
1✔
653
        if announcement_levels:
1✔
654
            announcements = cache_users.get_announcements_cached(current_user, announcement_levels)
1✔
655
            for announcement in announcements:
1✔
656
                flash(announcement, 'announcement')
1✔
657

658
        if app.config.get('CONTACT_EMAIL'):  # pragma: no cover
659
            contact_email = app.config.get('CONTACT_EMAIL')
660
        else:
661
            contact_email = 'info@pybossa.com'
1✔
662

663
        if app.config.get('CONTACT_TWITTER'):  # pragma: no cover
664
            contact_twitter = app.config.get('CONTACT_TWITTER')
665
        else:
666
            contact_twitter = 'PYBOSSA'
1✔
667

668
        # Available plugins
669
        plugins = plugin_manager.plugins
1✔
670

671
        # LDAP enabled
672
        ldap_enabled = app.config.get('LDAP_HOST', False)
1✔
673

674
        def get_wizard_steps(project=None):
1✔
675
            show_wizard = True
1✔
676
            if project is not None:
1✔
677
                if isinstance(project, dict):
1✔
678
                    owners_id = project['owners_ids']
1✔
679
                else:
680
                    owners_id = project.owners_ids
×
681

682
                show_wizard = (current_user.subadmin and current_user.id in owners_id) or current_user.admin
1✔
683

684
            if not show_wizard:
1✔
685
                return json.dumps(dict(list=[]))
1✔
686

687
            wizard_steps = app.config.get('WIZARD_STEPS') or {}
1✔
688
            request_details = {'url': request.url, 'path': request.path}
1✔
689
            project_wizard = Wizard(project, wizard_steps, request_details)
1✔
690
            return json.dumps(dict(list=project_wizard.get_wizard_list()))
1✔
691

692
        return dict(
1✔
693
            brand=app.config['BRAND'],
694
            title=app.config['TITLE'],
695
            logo=app.config['LOGO'],
696
            copyright=app.config['COPYRIGHT'],
697
            description=app.config['DESCRIPTION'],
698
            terms_of_use=app.config['TERMSOFUSE'],
699
            data_use=app.config['DATAUSE'],
700
            enforce_privacy=app.config['ENFORCE_PRIVACY'],
701
            # version=pybossa.__version__,
702
            current_user=current_user,
703
            show_cookies_warning=show_cookies_warning,
704
            contact_email=contact_email,
705
            contact_twitter=contact_twitter,
706
            upload_method=app.config['UPLOAD_METHOD'],
707
            news=news,
708
            notify_admin=notify_admin,
709
            plugins=plugins,
710
            ldap_enabled=ldap_enabled,
711
            get_wizard_steps=get_wizard_steps,
712
            server_type=app.config['SERVER_TYPE'])
713

714
    @app.errorhandler(CSRFError)
1✔
715
    def csrf_error_handler(err):
1✔
716
        response = dict(template='400.html', code=400,
1✔
717
                        description=err.description)
718
        return handle_content_type(response)
1✔
719

720

721
def setup_jinja2_filters(app):
1✔
722
    """Setup jinja2 filters."""
723
    @app.template_filter('pretty_date')
1✔
724
    def _pretty_date_filter(s):
1✔
725
        return pretty_date(s)
1✔
726

727
    @app.template_filter('humanize_intword')
1✔
728
    def _humanize_intword(obj):
1✔
729
        return humanize.intword(obj)
1✔
730

731
    @app.template_filter('disqus_sso')
1✔
732
    def _disqus_sso(obj): # pragma: no cover
733
        return get_disqus_sso(obj)
734

735
    @app.template_filter('datetime')
1✔
736
    def _datetime_filter(s,f='%m-%d-%y %H:%M'):
1✔
737
        return datetime_filter(s,f)
1✔
738

739

740
def setup_csrf_protection(app):
1✔
741
    """Setup csrf protection."""
742
    csrf.init_app(app)
1✔
743

744

745
def setup_debug_toolbar(app):  # pragma: no cover
746
    """Setup debug toolbar."""
747
    if app.config['ENABLE_DEBUG_TOOLBAR']:
748
        debug_toolbar.init_app(app)
749

750

751
def setup_ratelimits(app):
1✔
752
    """Setup ratelimits."""
753
    global ratelimits
754
    ratelimits['LIMIT'] = app.config['LIMIT']
1✔
755
    ratelimits['PER'] = app.config['PER']
1✔
756
    ratelimits['BULK_RATE_LIMIT'] = app.config.get('BULK_RATE_LIMIT') or app.config['LIMIT']
1✔
757

758

759
def setup_cache_timeouts(app):
1✔
760
    """Setup cache timeouts."""
761
    global timeouts
762
    # Apps
763
    timeouts['AVATAR_TIMEOUT'] = app.config['AVATAR_TIMEOUT']
1✔
764
    timeouts['APP_TIMEOUT'] = app.config['APP_TIMEOUT']
1✔
765
    timeouts['REGISTERED_USERS_TIMEOUT'] = \
1✔
766
        app.config['REGISTERED_USERS_TIMEOUT']
767
    timeouts['ANON_USERS_TIMEOUT'] = app.config['ANON_USERS_TIMEOUT']
1✔
768
    timeouts['STATS_FRONTPAGE_TIMEOUT'] = app.config['STATS_FRONTPAGE_TIMEOUT']
1✔
769
    timeouts['STATS_APP_TIMEOUT'] = app.config['STATS_APP_TIMEOUT']
1✔
770
    timeouts['STATS_DRAFT_TIMEOUT'] = app.config['STATS_DRAFT_TIMEOUT']
1✔
771
    timeouts['N_APPS_PER_CATEGORY_TIMEOUT'] = \
1✔
772
        app.config['N_APPS_PER_CATEGORY_TIMEOUT']
773
    timeouts['BROWSE_TASKS_TIMEOUT'] = app.config['BROWSE_TASKS_TIMEOUT']
1✔
774
    # Categories
775
    timeouts['CATEGORY_TIMEOUT'] = app.config['CATEGORY_TIMEOUT']
1✔
776
    # Users
777
    timeouts['USER_TIMEOUT'] = app.config['USER_TIMEOUT']
1✔
778
    timeouts['USER_TOP_TIMEOUT'] = app.config['USER_TOP_TIMEOUT']
1✔
779
    timeouts['USER_TOTAL_TIMEOUT'] = app.config['USER_TOTAL_TIMEOUT']
1✔
780

781

782
def setup_scheduled_jobs(app):  # pragma: no cover
783
    """Setup scheduled jobs."""
784
    from datetime import datetime
785
    from pybossa.jobs import enqueue_periodic_jobs, schedule_job, \
786
        get_quarterly_date
787
    from rq_scheduler import Scheduler
788
    redis_conn = sentinel.master
789
    scheduler = Scheduler(queue_name='scheduled_jobs', connection=redis_conn)
790
    MINUTE = 60
791
    HOUR = 60 * 60
792
    MONTH = 30 * (24 * HOUR)
793

794
    first_quaterly_execution = get_quarterly_date(datetime.utcnow())
795
    JOBS = [dict(name=enqueue_periodic_jobs, args=['email'], kwargs={},
796
                 interval=(1 * MINUTE), timeout=(10 * MINUTE)),
797
            dict(name=enqueue_periodic_jobs, args=['maintenance'], kwargs={},
798
                 interval=(1 * MINUTE), timeout=(10 * MINUTE)),
799
            dict(name=enqueue_periodic_jobs, args=['super'], kwargs={},
800
                 interval=(10 * MINUTE), timeout=(10 * MINUTE)),
801
            dict(name=enqueue_periodic_jobs, args=['high'], kwargs={},
802
                 interval=(1 * HOUR), timeout=(10 * MINUTE)),
803
            dict(name=enqueue_periodic_jobs, args=['medium'], kwargs={},
804
                 interval=(12 * HOUR), timeout=(10 * MINUTE)),
805
            dict(name=enqueue_periodic_jobs, args=['low'], kwargs={},
806
                 interval=(24 * HOUR), timeout=(10 * MINUTE)),
807
            dict(name=enqueue_periodic_jobs, args=['monthly'], kwargs={},
808
                 interval=(1 * MONTH), timeout=(30 * MINUTE)),
809
            dict(name=enqueue_periodic_jobs, args=['quaterly'], kwargs={},
810
                 interval=(3 * MONTH), timeout=(30 * MINUTE),
811
                 scheduled_time=first_quaterly_execution)]
812

813
    for job in JOBS:
814
        schedule_job(job, scheduler)
815

816

817
def setup_newsletter(app):
1✔
818
    """Setup mailchimp newsletter."""
819
    if app.config.get('MAILCHIMP_API_KEY'):
1✔
820
        newsletter.init_app(app)
×
821

822

823
def setup_assets(app):
1✔
824
    """Setup assets."""
825
    from flask_assets import Environment, Bundle
1✔
826

827
    # when configured, copy themes template to a writable target directory
828
    source = os.environ.get("TEMPLATE_SOURCE")
1✔
829
    target = os.environ.get("TEMPLATE_TARGET")
1✔
830
    if source and target:
1✔
NEW
831
        copy_directory(source, target)
×
832

833
    assets = Environment(app)
1✔
834
    all_assets = [{
1✔
835
            'name': 'js_base',
836
            'args': [
837
                'js/vendor/jquery-2.2.3.js',
838
                'js/vendor/bootstrap.js',
839
                'js/vendor/modernizr.min.js',
840
                'js/flashmessages.js',
841
                'js/vendor/cookieconsent.min.js'
842
            ],
843
            'kwargs': {
844
                'filters': 'jsmin',
845
                'output': 'js/gen/default.min.js'
846
            }
847
        }, {
848
            'name': 'js_multi',
849
            'args': ['js/vendor/multi.js'],
850
            'kwargs': {
851
                'filters': 'jsmin',
852
                'output': 'js/gen/multi.min.js'
853
            }
854
        }, {
855
            'name': 'js_enrich',
856
            'args': ['js/vendor/jsgrid.min.js'],
857
            'kwargs': {
858
                'filters': 'jsmin',
859
                'output': 'js/gen/enrichment.min.js'
860
            }
861
        }, {
862
            'name': 'js_project_new',
863
            'args': [
864
                'js/forms.js',
865
                'js/vendor/select2.min.js',
866
                'js/project_forms.js'
867
            ],
868
            'kwargs': {
869
                'filters': 'jsmin',
870
                'output': 'js/gen/projects_new.min.js'
871
            }
872
        }, {
873
            'name': 'js_full',
874
            'args': [
875
                'js/vendor/jquery-2.2.3.js',
876
                'js/vendor/bootstrap.js',
877
                'js/vendor/modernizr.min.js',
878
                'js/flashmessages.js',
879
                'js/vendor/cookieconsent.min.js',
880
                'js/pybossa/pybossa.js',
881
                'js/pybossa-player/dist/pybossa-player.min.js'
882
            ],
883
            'kwargs': {
884
                'filters': 'jsmin',
885
                'output': 'js/gen/full.min.js'
886
            }
887
        }, {
888
            'name': 'js_taskpresentereditor',
889
            'args': [
890
              'vendor/codemirror/codemirror.js',
891
              'vendor/codemirror/mode/xml/xml.js',
892
              'vendor/codemirror/mode/javascript/javascript.js',
893
              'vendor/codemirror/mode/css/css.js',
894
              'vendor/codemirror/mode/htmlmixed/htmlmixed.js',
895
              'vendor/codemirror/addons/search/search.js',
896
              'vendor/codemirror/addons/search/searchcursor.js',
897
              'vendor/codemirror/addons/search/jump-to-line.js',
898
              'vendor/codemirror/addons/edit/closebrackets.js',
899
              'vendor/codemirror/addons/edit/closetag.js',
900
              'vendor/codemirror/addons/edit/continuelist.js',
901
              'vendor/codemirror/addons/edit/matchbrackets.js',
902
              'vendor/codemirror/addons/edit/matchtags.js',
903
              'vendor/codemirror/addons/edit/trailingspace.js',
904
              'vendor/codemirror/addons/fold/xml-fold.js',
905
              'vendor/codemirror/addons/display/fullscreen.js',
906
              'vendor/codemirror/addons/dialog/dialog.js'
907
            ],
908
            'kwargs': {
909
                'filters': 'jsmin',
910
                'output': 'js/gen/pybossa.taskpresentereditor.min.js'
911
            }
912
        },
913
            {
914
            'name': 'js_annotationguidelineeditor',
915
            'args': [
916
              'vendor/summernote/summernote.min.js'
917
            ],
918
            'kwargs': {
919
                'output': 'js/gen/pybossa.js_annotationguidelineeditor.min.js'
920
            }
921
        }, {
922
            'name': 'js_taskbrowse',
923
            'args': [
924
                "js/vendor/jquery-ui-1.12.1.min.js",
925
                "js/vendor/select2.min.js",
926
                "vendor/slider/js/bootstrap-slider.js",
927
                "js/tasks_browse.js"
928
            ],
929
            'kwargs': {
930
                'filters': 'jsmin',
931
                'output': 'js/gen/taskbrowse.min.js'
932
            }
933
        }, {
934
            'name': 'js_projects_update',
935
            'args': [
936
                'js/vendor/cropper.min.js',
937
                'js/image_crop.js',
938
                'js/vendor/select2.min.js',
939
                'js/vendor/jquery-ui-1.12.1.min.js',
940
                'js/project_forms.js'
941
            ],
942
            'kwargs': {
943
                'filters': 'jsmin',
944
                'output': 'js/gen/projects_update.min.js'
945
            }
946
        },
947

948
        {
949
            'name': 'css_pybossa',
950
            'args': [
951
                'sass/_pybossa.scss'
952
            ],
953
            'kwargs': {
954
                'filters': 'libsass',
955
                'output': 'css/gen/pybossa.min.css',
956
                'depends': 'sass/**/*.scss'
957
            }
958
        }, {
959
            'name': 'css_multi',
960
            'args': [
961
                'css/multi.css'
962
            ],
963
            'kwargs': {
964
                'output': 'css/gen/multi.min.css',
965
            }
966
        }, {
967
            'name': 'css_enrich',
968
            'args': [
969
                "css/jsgrid.min.css",
970
                "css/jsgrid-theme.min.css"
971
            ],
972
            'kwargs': {
973
                'output': 'css/gen/enrichment.min.css'
974
            }
975
        },
976
        {
977
            'name': 'css_blpstrap3_style',
978
            'args': [
979
                "css/bootstrap.blpstrap3.css"
980
                ],
981
            'kwargs': {
982
                'output': 'css/gen/bootstrap.blpstrap3.css'
983
            }
984
        },
985
        {
986
            'name': 'css_taskbrowse',
987
            'args': [
988
                'vendor/slider/css/slider.css',
989
                'css/task_browse.css',
990
                'css/select2.min.css'
991
            ],
992
            'kwargs': {
993
                'output': 'css/gen/taskbrowse.min.css'
994
            }
995
        }
996
    ]
997

998
    for conf in all_assets:
1✔
999
        args = conf.get('args') or []
1✔
1000
        kwargs = conf.get('kwargs') or []
1✔
1001
        bundle = Bundle(*args, **kwargs)
1✔
1002
        assets.register(conf['name'], bundle)
1✔
1003
        bundle.build(force=True)
1✔
1004

1005

1006
def setup_strong_password(app):
1✔
1007
    global enable_strong_password
1008
    enable_strong_password = app.config.get('ENABLE_STRONG_PASSWORD')
1✔
1009

1010

1011
def setup_ldap(app):
1✔
1012
    if app.config.get('LDAP_HOST'):
1✔
1013
        ldap.init_app(app)
×
1014

1015

1016
def setup_profiler(app):
1✔
1017
    if app.config.get('FLASK_PROFILER'):
1✔
1018
        flask_profiler.init_app(app)
1✔
1019

1020

1021
def setup_task_presenter_editor(app):
1✔
1022
    if app.config.get('DISABLE_TASK_PRESENTER_EDITOR_PAYLOAD'):
1✔
1023
        from pybossa.api.project import ProjectAPI
1✔
1024
        ProjectAPI.restricted_keys.add('info::task_presenter')
1✔
1025

1026

1027
def setup_schedulers(app):
1✔
1028
    opts = app.config.get('AVAILABLE_SCHEDULERS')
1✔
1029
    if opts:
1✔
1030
        from pybossa.forms.forms import TaskSchedulerForm
1✔
1031
        TaskSchedulerForm.update_sched_options(opts)
1✔
1032

1033

1034
def setup_http_signer(app):
1✔
1035
    global http_signer
1036
    from pybossa.http_signer import HttpSigner
1✔
1037
    secret = app.config.get('SIGNATURE_SECRET')
1✔
1038
    http_signer = HttpSigner(secret, 'X-Pybossa-Signature')
1✔
1039

1040

1041
def setup_swagger(app):
1✔
1042
    swagger_path = app.config.get('SWAGGER_HEADER_PATH')
1✔
1043
    if swagger_path is None:
1✔
1044
        return
1✔
1045

1046
    try:
1✔
1047
        with open(swagger_path, 'r') as file:
1✔
1048
            html_as_string = file.read()
1✔
1049
            app.config.get('SWAGGER_TEMPLATE')['head_text'] = html_as_string
1✔
1050
    except (FileNotFoundError, TypeError):
1✔
1051
        msg = "WARNING: Swagger custom header file not found."
1✔
1052
        app.logger.warning(msg)
1✔
1053
    Swagger.DEFAULT_CONFIG.update(app.config.get('SWAGGER_TEMPLATE'))
1✔
1054
    Swagger(app, template=app.config.get('SWAGGER_TEMPLATE'))
1✔
1055

1056
def setup_global_configs(app):
1✔
1057
    """Setup global configs that requires extraction / parsing from settings"""
1058
    global private_required_fields
1059

1060
    # settings that require additional extraction / parsing
1061
    # can go here so that it can be done just once upon app load
1062
    # instead of on each api / endpoint call thereby reducing
1063
    # repeatative logic resulting into quick response time
1064
    private_required_fields = list(app.config.get("TASK_REQUIRED_FIELDS", {}).keys())
1✔
1065
    if "data_access_level" not in private_required_fields:
1✔
1066
        private_required_fields.append("data_access_level")
1✔
1067

1068
def setup_email_service(app):
1✔
1069
    """Setup email service and endpoint to access its email attachment"""
1070
    from pybossa.view.attachment import blueprint as attachment_bp
1✔
1071

1072
    app.register_blueprint(attachment_bp, url_prefix='/attachment')
1✔
1073

1074
    proxy_service_config = app.config.get("PROXY_SERVICE_CONFIG", {})
1✔
1075
    email_config = proxy_service_config.get("email_service", {})
1✔
1076
    uri = email_config.get("uri")
1✔
1077
    if email_config and uri:
1✔
1078
        email_service.init_app(app)
×
1079

1080
    app.logger.info("email_service enabled %r", email_service.enabled)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc