Passed
Push — master ( 956510...210001 )
by Jochen
04:10
created

byceps/application.py (1 issue)

1
"""
2
byceps.application
3
~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2006-2020 Jochen Kupperschmidt
6
:License: Modified BSD, see LICENSE for details.
7
"""
8
9
from importlib import import_module
10
from pathlib import Path
11
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Union
12
13
from flask import current_app, Flask, g, redirect
14
import jinja2
15
16
from . import config, config_defaults
17
from .database import db
18
from . import email
19
from .redis import redis
20
from .util.framework.blueprint import register_blueprint
21
from .util.l10n import set_locale
22
from .util import templatefilters
23
from .util.templating import SiteTemplateOverridesLoader
24
25
26
BlueprintReg = Tuple[str, Optional[str]]
27
28
29
def create_app(
30
    config_filename: Union[Path, str],
31
    config_overrides: Optional[Dict[str, Any]] = None,
32
) -> Flask:
33
    """Create the actual Flask application."""
34
    app = Flask(__name__)
35
36
    app.config.from_object(config_defaults)
37
    app.config.from_pyfile(str(config_filename))
38
    if config_overrides is not None:
39
        app.config.from_mapping(config_overrides)
40
41
    # Throw an exception when an undefined name is referenced in a template.
42
    app.jinja_env.undefined = jinja2.StrictUndefined
43
44
    # Set the locale.
45
    set_locale(app.config['LOCALE'])  # Fail if not configured.
46
47
    # Initialize database.
48
    db.init_app(app)
49
50
    # Initialize Redis connection.
51
    redis.init_app(app)
52
53
    email.init_app(app)
54
55
    config.init_app(app)
56
57
    _register_blueprints(app)
58
59
    templatefilters.register(app)
60
61
    _add_static_file_url_rules(app)
62
63
    return app
64
65
66
def _register_blueprints(app: Flask) -> None:
67
    """Register blueprints depending on the configuration."""
68
    for name, url_prefix in _get_blueprints(app):
69
        register_blueprint(app, name, url_prefix)
70
71
72
def _get_blueprints(app: Flask) -> Iterator[BlueprintReg]:
73
    """Yield blueprints to register on the application."""
74
    yield from _get_blueprints_common()
75
76
    current_mode = config.get_site_mode(app)
77
    if current_mode.is_public():
78
        yield from _get_blueprints_site()
79
    elif current_mode.is_admin():
80
        yield from _get_blueprints_admin()
81
82
    yield from _get_blueprints_api()
83
84
    yield from _get_blueprints_health()
85
86
    if app.config['METRICS_ENABLED']:
87
        yield from _get_blueprints_metrics()
88
89
    if app.debug:
90
        yield from _get_blueprints_debug()
91
92
93
def _get_blueprints_common() -> Iterator[BlueprintReg]:
94
    yield from [
95
        ('authentication',              '/authentication'           ),
96
        ('authorization',               None                        ),
97
        ('core',                        '/core'                     ),
98
        ('user',                        None                        ),
99
        ('user.avatar',                 '/users'                    ),
100
        ('user.creation',               '/users'                    ),
101
        ('user.current',                '/users'                    ),
102
        ('user.email_address',          '/users/email_address'      ),
103
    ]
104
105
106
def _get_blueprints_site() -> Iterator[BlueprintReg]:
107
    yield from [
108
        ('attendance',                  '/attendance'               ),
109
        ('board',                       '/board'                    ),
110
        ('consent',                     '/consent'                  ),
111
        ('news',                        '/news'                     ),
112
        ('newsletter',                  '/newsletter'               ),
113
        ('orga_team',                   '/orgas'                    ),
114
        ('party',                       None                        ),
115
        ('seating',                     '/seating'                  ),
116
        ('shop.order',                  '/shop'                     ),
117
        ('shop.orders',                 '/shop/orders'              ),
118
        ('snippet',                     None                        ),
119
        ('terms',                       '/terms'                    ),
120
        ('ticketing',                   '/tickets'                  ),
121
        ('user.profile',                '/users'                    ),
122
        ('user_badge',                  '/user_badges'              ),
123
        ('user_group',                  '/user_groups'              ),
124
        ('user_message',                '/user_messages'            ),
125
    ]
126
127
128
def _get_blueprints_admin() -> Iterator[BlueprintReg]:
129
    yield from [
130
        ('admin.attendance',            '/admin/attendance'         ),
131
        ('admin.authorization',         '/admin/authorization'      ),
132
        ('admin.board',                 '/admin/board'              ),
133
        ('admin.brand',                 '/admin/brands'             ),
134
        ('admin.consent',               '/admin/consent'            ),
135
        ('admin.core',                  None                        ),
136
        ('admin.dashboard',             '/admin/dashboard'          ),
137
        ('admin.email',                 '/admin/email'              ),
138
        ('admin.news',                  '/admin/news'               ),
139
        ('admin.newsletter',            '/admin/newsletter'         ),
140
        ('admin.jobs',                  '/admin/jobs'               ),
141
        ('admin.orga',                  '/admin/orgas'              ),
142
        ('admin.orga_presence',         '/admin/presence'           ),
143
        ('admin.orga_team',             '/admin/orga_teams'         ),
144
        ('admin.party',                 '/admin/parties'            ),
145
        ('admin.seating',               '/admin/seating'            ),
146
        ('admin.shop',                  None                        ),
147
        ('admin.shop.article',          '/admin/shop/articles'      ),
148
        ('admin.shop.email',            '/admin/shop/email'         ),
149
        ('admin.shop.order',            '/admin/shop/orders'        ),
150
        ('admin.shop.shipping',         '/admin/shop/shipping'      ),
151
        ('admin.shop.shop',             '/admin/shop/shop'          ),
152
        ('admin.site',                  '/admin/sites'              ),
153
        ('admin.snippet',               '/admin/snippets'           ),
154
        ('admin.terms',                 '/admin/terms'              ),
155
        ('admin.ticketing',             '/admin/ticketing'          ),
156
        ('admin.ticketing.checkin',     '/admin/ticketing/checkin'  ),
157
        ('admin.tourney',               '/admin/tourney'            ),
158
        ('admin.user',                  '/admin/users'              ),
159
        ('admin.user_badge',            '/admin/user_badges'        ),
160
    ]
161
162
163
def _get_blueprints_api() -> Iterator[BlueprintReg]:
164
    yield from [
165
        ('api.attendance',              '/api/attendances'          ),
166
        ('api.snippet',                 '/api/snippets'             ),
167
        ('api.tourney.avatar',          '/api/tourney/avatars'      ),
168
        ('api.tourney.match.comments',  '/api/tourney'              ),
169
        ('api.user',                    '/api/users'                ),
170
        ('api.user_badge',              '/api/user_badges'          ),
171
    ]
172
173
174
def _get_blueprints_health() -> Iterator[BlueprintReg]:
175
    yield from [
176
        ('healthcheck',                 '/health'                   ),
177
    ]
178
179
180
def _get_blueprints_metrics() -> Iterator[BlueprintReg]:
181
    yield from [
182
        ('metrics',                     '/metrics'                  ),
183
    ]
184
185
186
def _get_blueprints_debug() -> Iterator[BlueprintReg]:
187
    yield from [
188
        ('style_guide',                 '/style_guide'              ),
189
    ]
190
191
192
def _add_static_file_url_rules(app: Flask) -> None:
193
    """Add URL rules to for static files."""
194
    for rule_prefix, endpoint in [
195
        ('/party', 'party_file'),
196
        ('/site', 'site_file'),
197
    ]:
198
        rule = rule_prefix + '/<path:filename>'
199
        app.add_url_rule(
200
            rule, endpoint=endpoint, methods=['GET'], build_only=True
201
        )
202
203
204
def init_app(app: Flask) -> None:
205
    """Initialize the application after is has been created."""
206
    with app.app_context():
207
        _set_url_root_path(app)
208
209
        site_mode = config.get_site_mode()
210
        if site_mode.is_public():
211
            # Incorporate site-specific template overrides.
212
            app.jinja_loader = SiteTemplateOverridesLoader()
213
214
            # Set up site-aware template context processor.
215
            app._site_context_processors = {}
216
            app.context_processor(_get_site_template_context)
217
        elif site_mode.is_admin() and app.config['RQ_DASHBOARD_ENABLED']:
218
            import rq_dashboard
219
220
            app.register_blueprint(
221
                rq_dashboard.blueprint, url_prefix='/admin/rq'
222
            )
223
224
225
def _set_url_root_path(app: Flask) -> None:
226
    """Set an optional URL path to redirect to from the root URL path (`/`).
227
228
    Important: Don't specify the target with a leading slash unless you
229
    really mean the root of the host.
230
    """
231
    target_url = app.config['ROOT_REDIRECT_TARGET']
232
    if target_url is None:
233
        return
234
235
    status_code = app.config['ROOT_REDIRECT_STATUS_CODE']
236
237
    def _redirect():
238
        return redirect(target_url, status_code)
239
240
    app.add_url_rule('/', endpoint='root', view_func=_redirect)
241
242
243
def _get_site_template_context() -> Dict[str, Any]:
244
    """Return the site-specific additions to the template context."""
245
    site_context_processor = _find_site_template_context_processor_cached(
246
        g.site_id
247
    )
248
    return site_context_processor()
249
250
251
def _find_site_template_context_processor_cached(
252
    site_id: str
253
) -> Optional[Callable[[], Dict[str, Any]]]:
254
    """Return the template context processor for the site.
255
256
    A processor will be cached after it has been obtained for the first
257
    time.
258
    """
259
    # `None` is a valid value for a site that does not specify a
260
    # template context processor.
261
262
    if site_id in current_app._site_context_processors:
263
        return current_app._site_context_processors.get(site_id)
264
    else:
265
        context_processor = _find_site_template_context_processor(site_id)
266
        current_app._site_context_processors[site_id] = context_processor
267
        return context_processor
268
269
270
def _find_site_template_context_processor(
1 ignored issue
show
This code seems to be duplicated in your project.
Loading history...
271
    site_id: str
272
) -> Optional[Callable[[], Dict[str, Any]]]:
273
    """Import a template context processor from the site's package.
274
275
    If a site package contains a module named `extension` and that
276
    contains a top-level callable named `template_context_processor`,
277
    then that callable is imported and returned.
278
    """
279
    module_name = f'sites.{site_id}.extension'
280
    try:
281
        module = import_module(module_name)
282
    except ModuleNotFoundError:
283
        # No extension module found in site package.
284
        return None
285
286
    context_processor = getattr(module, 'template_context_processor', None)
287
    if context_processor is None:
288
        # Context processor not found in module.
289
        return None
290
291
    if not callable(context_processor):
292
        # Context processor object is not callable.
293
        return None
294
295
    return context_processor
296