Newer
Older
import re
import unicodedata
from collections import OrderedDict, namedtuple
from logging.handlers import SMTPHandler
from random import choice, sample, shuffle
from urllib.parse import quote, urljoin, urlparse
from colorthief import ColorThief
from flask import (
Flask, abort, make_response, redirect, render_template, request, url_for)
from flask.logging import default_handler
from flask_cdn import CDN
from flask_cdn import url_for as _cdn_url_for
from flask_gravatar import Gravatar
from flask_sitemap import Sitemap
from icalendar import Calendar, Event
from werkzeug.middleware.proxy_fix import ProxyFix
CALENDAR_URL = 'https://discuss.tryton.org/upcoming-events'
CALENDAR_JSON = 'https://discuss.tryton.org/discourse-post-event/events.json'
SUPPORTERS_URL = (
'https://foundation.tryton.org:9000/foundation/foundation/1/supporters')
DONATORS_URL = (
'https://foundation.tryton.org:9000/foundation/foundation/1/donators'
DONATIONS_URL = (
'https://foundation.tryton.org:9000/foundation/foundation/1/donations'
'?account=732&account=734')
CRITICAL_CSS_DIR = os.environ.get('CRITICAL_CSS')
CRITICAL_CSS_COOKIE = 'critical-css'
cache = Cache(config={
'CACHE_TYPE': (
'null' if ast.literal_eval(os.environ.get('DEBUG', 'True'))
else 'simple')})
if os.environ.get('MEMCACHED'):
cache.config['CACHE_TYPE'] = 'memcached'
cache.config['CACHE_MEMCACHED_SERVERS'] = (
os.environ['MEMCACHED'].split(','))
app.wsgi_app = ProxyFix(app.wsgi_app)
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = datetime.timedelta(days=365)
app.config['CACHE_DEFAULT_TIMEOUT'] = 60 * 60
app.config['PREFERRED_URL_SCHEME'] = 'https'
app.config['SERVER_NAME'] = os.environ.get('SERVER_NAME')
app.config['SITEMAP_INCLUDE_RULES_WITHOUT_PARAMS'] = True
app.config['SITEMAP_VIEW_DECORATORS'] = [cache.cached()]
app.config['SITEMAP_IGNORE_ENDPOINTS'] = {
'contribute-alt',
'donate-alt',
'donate_cancel',
'donate_thanks',
'download-alt',
'event-alt',
'events',
'events-alt',
'events-ics',
'favicon',
'flask_sitemap.page',
'flask_sitemap.sitemap',
'foundation-alt',
'news-alt',
'news_rss',
'presentations-alt',
'robots',
'service_providers-alt',
'success_stories-alt',
'supporters-alt',
'warmup',
}
app.config['SITEMAP_URL_SCHEME'] = 'https'
app.config['DOWNLOADS_DOMAIN'] = os.environ.get(
'DOWNLOADS_DOMAIN', 'downloads.tryton.org')
app.config['VIDEOS_DOMAIN'] = os.environ.get(
'VIDEOS_DOMAIN', 'videos.tryton.org')
app.config['CDN_DOMAIN'] = os.environ.get('CDN_DOMAIN')
app.config['CDN_HTTPS'] = ast.literal_eval(os.environ.get('CDN_HTTPS', 'True'))
app.config['GRAVATAR_SIZE'] = 198
app.config['GRAVATAR_DEFAULT'] = 'mp'
app.config['GRAVATAR_USE_SSL'] = True
if app.config['CDN_DOMAIN']:
app.config['GRAVATAR_BASE_URL'] = '%s://%s/' % (
'https' if app.config['CDN_HTTPS'] else 'http',
app.config['CDN_DOMAIN'])
else:
app.config['GRAVATAR_BASE_URL'] = '/'
app.jinja_env.lstrip_blocks = True
app.jinja_env.trim_blocks = True
app.jinja_env.autoescape = (
lambda filename: (
app.select_jinja_autoescape(filename)
or filename.endswith('.html.jinja')))
gravatar = Gravatar(app)
def url_for_self(**args):
return url_for(request.endpoint, **dict(request.args, **args))
@app.context_processor
def inject_self():
return dict(url_for_self=url_for_self)
def json_default(o):
if hasattr(o, '__json__'):
return o.__json__()
raise TypeError(f'Object of type {o.__class__.__name__} '
f'is not JSON serializable')
app.jinja_env.policies['json.dumps_kwargs'] = {
'sort_keys': True,
'default': json_default,
}
_slugify_strip_re = re.compile(r'[^\w\s-]')
_slugify_hyphenate_re = re.compile(r'[-\s]+')
@app.template_filter('slugify')
def slugify(value):
if not isinstance(value, str):
value = str(value)
value = unicodedata.normalize('NFKD', value)
value = str(_slugify_strip_re.sub('', value).strip())
return _slugify_hyphenate_re.sub('-', value)
def url_for_downloads(*args):
return urljoin(
'//' + app.config['DOWNLOADS_DOMAIN'], os.path.join(*map(quote, args)))
@app.context_processor
def inject_url_for_dowloads():
return dict(url_for_downloads=url_for_downloads)
def url_for_videos(*args):
return urljoin(
'//' + app.config['VIDEOS_DOMAIN'], os.path.join(*map(quote, args)))
@app.context_processor
def inject_url_for_videos():
return dict(url_for_videos=url_for_videos)
def cdn_url_for(*args, **kwargs):
if app.config['CDN_DOMAIN']:
return _cdn_url_for(*args, **kwargs)
else:
return url_for(*args, **kwargs)
def cache_key_prefix_view():
scheme = 'https' if request.is_secure else 'http'
if not request.cookies.get(CRITICAL_CSS_COOKIE):
return 'view/%s/%s/%s' % (
scheme, request.path, critical_css(timestamp=True))
else:
return 'view/%s/%s' % (scheme, request.path)
LinkHeader = namedtuple(
'LinkHeader', ['endpoint', 'values', 'params'])
LinkHeader('index', {}, {'rel': 'preconnect'}),
LinkHeader('static', {'filename': 'js/main.js'}, {
'rel': 'preload', 'as': 'script', 'nopush': True}),
]
CSS_LINK_HEADERS = [
LinkHeader(
'static', {'filename': 'css/main.css'}, {
'rel': 'preload', 'as': 'style', 'nopush': True}),
LinkHeader(
'static', {'filename': 'fonts/Roboto.woff2'}, {
'rel': 'preload', 'as': 'font', 'nopush': True,
'crossorigin': True}),
'static', {'filename': 'fonts/material-icons.woff2'}, {
'rel': 'preload', 'as': 'font', 'nopush': True,
'crossorigin': True}),
]
def add_links(links):
def format_param(param):
key, value = param
if value is True:
return key
else:
return '%s=%s' % (key, value)
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
response = make_response(func(*args, **kwargs))
for link in links:
if (link.endpoint == 'index'
or (link.endpoint == 'static'
'filename', '').startswith('fonts/'))):
if (app.config['CDN_DOMAIN']
and not app.config['CDN_DEBUG']):
urls = app.url_map.bind(
app.config['CDN_DOMAIN'], url_scheme='https')
url = urls.build(
link.endpoint, link.values, force_external=True)
url = cdn_url_for(link.endpoint, **link.values)
params = '; '.join(map(format_param, link.params.items()))
value = '<{url}>; {params}'.format(
url=url,
params=params)
response.headers.add('Link', value)
return response
return wrapper
return decorator
@app.after_request
def add_cache_control_header(response):
if 'Cache-Control' not in response.headers:
response.cache_control.max_age = app.config['CACHE_DEFAULT_TIMEOUT']
response.cache_control.public = True
return response
def url_for_canonical(endpoint=None, **values):
if endpoint is None:
endpoint = request.endpoint
if not endpoint:
return ''
scheme = 'https' if not app.debug else None
return url_for(endpoint, _external=True, _scheme=scheme, **values)
@app.context_processor
def inject_canonical():
return dict(url_for_canonical=url_for_canonical)
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
def critical_css(timestamp=False):
if (CRITICAL_CSS_DIR
and request.endpoint
and not request.cookies.get(CRITICAL_CSS_COOKIE)):
file = os.path.join(CRITICAL_CSS_DIR, request.endpoint + '.css')
if os.path.exists(file):
if timestamp:
return int(os.path.getmtime(file))
else:
return open(file, 'r').read()
@app.after_request
def add_critical_css_cookie(response):
if (CRITICAL_CSS_DIR
and response.mimetype == 'text/html'
and not request.cookies.get(CRITICAL_CSS_COOKIE)):
response.set_cookie(CRITICAL_CSS_COOKIE, '1')
return response
@app.context_processor
def inject_critical_css():
return dict(critical_css=critical_css)
@cache.memoize(timeout=365 * 24 * 60 * 60)
def dominant_color(path):
if app.debug:
return '#000'
try:
color = ColorThief(
os.path.join(app.static_folder, path)).get_color(quality=1)
except Exception:
return '#000'
return '#%02x%02x%02x' % color
@app.context_processor
def inject_dominant_color():
return dict(dominant_color=dominant_color)
'style="color:#d9534f; font-size: inherit; vertical-align: middle">'
'favorite'
'</span>')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
news=list(news_items(3)),
next_events=next_events(3))
@app.context_processor
def inject_menu():
menu = OrderedDict()
menu['Tryton'] = [
('Success Stories', url_for('success_stories')),
('Forum', url_for('forum')),
('Get Help', 'https://discuss.tryton.org/c/support'),
('Service Providers', url_for('service_providers')),
('Become a Service Provider', url_for('service_providers_start')),
@app.context_processor
def inject_copyright_dates():
return dict(copyright_dates='2008-%s' % datetime.date.today().year)
@app.context_processor
def inject_heart():
return dict(heart=HEART)
response = make_response(render_template('robots.txt.jinja'))
response.mimetype = 'text/plain'
return response
@app.route('/news/index.html', endpoint='news-alt')
@app.route('/news.rss')
@app.route('/rss.xml')
def news_rss():
def news_items(size=-1):
try:
root = objectify.fromstring(fetch_news_items())
app.logger.error('fail to fetch news', exc_info=True)
for item in root.xpath('/rss/channel/item')[:size]:
yield item
@app.template_filter('news_text')
def news_text(content):
block = html.fromstring(str(content))
for box in block.find_class('lightbox-wrapper'):
box.drop_tree()
@app.route('/events.html', endpoint='events-alt')
@app.route('/events.ics', endpoint='events-ics')
def events_ics():
response = make_response(fetch_events())
response.mimetype = 'text/calendar'
return response
def parse_dt(value):
return datetime.datetime.fromisoformat(value.rstrip('Z'))
data = requests.get(CALENDAR_JSON).json()
calendar = Calendar()
for data in requests.get(CALENDAR_JSON).json()['events']:
event = Event()
if data.get('starts_at'):
event.add('dtstart', parse_dt(data['starts_at']))
if data.get('ends_at'):
event.add('dtend', parse_dt(data['ends_at']))
event['summary'] = data['name']
event['url'] = urljoin(CALENDAR_JSON, data['post']['url'])
calendar.add_component(event)
return calendar.to_ical()
def next_events(size=-1):
today = datetime.date.today()
try:
app.logger.error('fail to fetch events', exc_info=True)
Loading
Loading full blame...