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': '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['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'] = [
'news-alt', 'news_rss', 'event-alt', 'success_stories-alt', 'download-alt',
'presentations-alt', 'contribute-alt', 'foundation-alt', 'supporters-alt',
'donate-alt', 'donate_thanks', 'donate_cancel', 'service_providers-alt',
'sitemap.xml']
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['SITEMAP_IGNORE_ENDPOINTS'] = ['events', 'events-alt', 'events-ics']
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
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/RobotoCondensed-Regular.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)
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
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)
def index():
return render_template(
'index.html',
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)
@app.route('/robots.txt')
@cache.cached()
def static_from_root():
response = make_response(render_template('robots.txt'))
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)
events = []
for event in cal.walk('vevent'):
end_date = (event.get('dtend') or event.get('dtstart')).dt
if date(end_date) >= today:
events.append(event)
def start_date(event):
return date(event.get('dtstart').dt)
return sorted(events, key=start_date)[:size]
@app.template_filter('date')
def date(datetime):
if hasattr(datetime, 'date'):
return datetime.date()
return datetime
class Case:
def __init__(self, title, description, story=False):
self.title = title
self.description = description
self.story = story
@property
def name(self):
return slugify(self.title.lower())
@property
def url(self):
if self.story:
return url_for('success_story', story=self.name)
@property
def logo(self):
def __eq__(self, other):
if isinstance(other, str):
return self.name == other
elif isinstance(other, Case):
return self.name == other.name
return NotImplemented
Case(title="ALS Swiss",
description="A society for people suffering ALS disease.",
story=True),
Case(title="APAR @ AIIMS",
description="Annual Performance Appraisal for the All Indian "
"Institute of Medical Sciences",
story=True),
title="Advocate Consulting Legal Group",
description="A legal firm servicing the general aviation industry",
story=True),
Case(
title="Banque Française Mutualiste",
description="A French bank for the public service."),
Case(
title="Blue Box Distribution",
description="An international distributor of hair care products.",
story=True),
Case(
title="CAMIR",
description="A provider of spare parts for machinery.",
story=True),
Case(
title="La Cave Thrace",
description="Imports and distributes wine in France."),
Case(
title="Cultural Commons Collection Society",
description="Collects and distributes music royalties."),
Case(
title="Expertise Vision",
description="Produces vision based systems.",
story=True),
Case(
title="Felber",
description="A stamp and signalisation company.",
story=True),
Case(
title="GotSHO LIMS",
description="Software Solution for genomic world.",
story=True),
Case(
title="Grufesa",
description="Exports strawberries in Europe.",
story=True),
Case(
title="Institut Mèdic per la Imatge",
description="Provides all kinds of MRI scans, nuclear medicine "
"and bone densitometry."),
Case(
title="Jordà",
description="Installs and maintains lifts and elevators.",
story=True),
Case(
title="Legna",
description="Graphic design, digital printing "
"and offset solution company",
story=True),
Case(
title="Koolvet",
description="Software for veterinary clinics that deal with small "
"domestic pets and larger farm animals.",
story=True),
Case(
title="MenschensKinder Teltow",
description="Operates municipal day care centers and "
"parent-child groups."),
Case(
description="Leading online parapharmacy.",
story=True),
title="Lackierzentrum Reichenbach",
description="Produces surface coating for automotive, aerospace, "
"construction and mechanical engineering."),
Case(
title="Revelle",
description="Consulting in developing countries and "
"emerging economies.",
story=True),
Case(
title="Sinclair Containers",
description="Sells and rents containers.",
story=True),
Case(
title="Skoda Autohaus Zeidler",
description="A German car dealership and workshop for Skoda."),
Case(
title="Wenger Energie",
description="A provider of vacuum degassing and filter systems.",
story=True),
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
cases = sorted(
sample(CASES, len(CASES)), key=attrgetter('story'), reverse=True)
return render_template('success_stories.html', cases=cases)
@app.route('/business-cases.html', endpoint='success_stories-alt')
def success_stories_alt():
return redirect(url_for('success_stories'))
@app.route('/success-stories/<story>')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
if story == '_':
story = choice(CASES).name
cases = [c for c in CASES if c.story or c.name == story]
try:
next_case = cases[(cases.index(story) + 1) % len(cases)]
except ValueError:
abort(HTTPStatus.NOT_FOUND)
return render_template(
'success_stories/%s.html' % story, next_case=next_case,
canonical=url_for_canonical(story=story))
except TemplateNotFound:
abort(HTTPStatus.NOT_FOUND)
@sitemap.register_generator
def success_story_generator():
for case in CASES:
if case.url:
yield 'success_story', dict(story=case.name)
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def download():
return render_template('download.html')
@app.route('/download.html', endpoint='download-alt')
return redirect(url_for('download'))
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def forum():
return render_template('forum.html')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def presentations():
return render_template('presentations.html')
@app.route('/papers.html', endpoint='presentations-alt')
def presentations_alt():
return redirect(url_for('presentations'))
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
if event == '_':
event = 'layout'
def __init__(self, date, *events, location=None, full=False):
if not isinstance(date, datetime.date):
date = datetime.date(*date)
self.date = date
self.location = location
self.events = []
for event in events:
self.add(*event)
def add(self, summary, start, end, *args, location=None):
if not isinstance(start, datetime.time):
start = datetime.time(*start)
if not isinstance(end, datetime.time):
end = datetime.time(*end)
start = datetime.datetime.combine(self.date, start)
end = datetime.datetime.combine(self.date, end)
if not location:
location = self.location
self.events.append(Event(
summary, start, end, *args, location=location))
@property
def start(self):
if self.events:
return min(e.start for e in self.events)
@property
def end(self):
if self.events:
return max(e.end for e in self.events)
class Event:
def __init__(self, summary, start, end, description='', profiles=(),
location=None):
self.summary = summary
self.start = start
self.end = end
self.location = location
self.description = description
self.profiles = [Profile(*p) for p in profiles]
class Profile:
def __init__(self, name, gravatar, company='', url=''):
self.name = name
self.gravatar = gravatar
self.company = company
self.url = url
return render_template(
'events/%s.html' % event, Day=Day,
canonical=url_for_canonical(event=event))
except TemplateNotFound:
abort(HTTPStatus.NOT_FOUND)
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def contribute():
return render_template('contribute.html')
@app.route('/how-to-contribute.html', endpoint='contribute-alt')
return redirect(url_for('contribute'))
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def develop():
return render_template('develop.html')
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
@app.route('/develop/guidelines/code')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def guidelines_code():
return render_template('guidelines/code.html')
@app.route('/develop/guidelines/documentation')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def guidelines_documentation():
return render_template('guidelines/documentation.html')
@app.route('/develop/guidelines/help-text')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def guidelines_documentation_help():
return render_template('guidelines/help.html')
@app.route('/develop/guidelines/howto')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def guidelines_documentation_howto():
return render_template('guidelines/howto.html')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def foundation():
return render_template('foundation.html')
@app.route('/foundation/', endpoint='foundation-alt')
return redirect(url_for('foundation'))
def fetch_supporters():
headers = {'Content-Type': 'application/json'}
try:
response = requests.get(SUPPORTERS_URL, headers=headers)
except Exception:
app.logger.error('fail to fetch supporters', exc_info=True)
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def supporters():
def url(supporter, start):
for website in supporter['websites']:
if website.startswith(start):
return website
return render_template('supporters.html',
discuss_url=partial(url, start='https://discuss.tryton.org/'),
roundup_url=partial(url, start='https://bugs.tryton.org/'),
heptapod_url=partial(url, start='https://foss.heptapod.net/'))
@app.route('/foundation/supporters.html', endpoint='supporters-alt')
return redirect(url_for('supporters'))
@cache.memoize(timeout=60 * 60 * 24)
def fetch_gravatar(hash, **params):
url = 'https://secure.gravatar.com/avatar/' + hash
response = requests.get(url, params=params)
rv = make_response(response.content)
rv.content_type = response.headers['Content-Type']
rv.cache_control.max_age = 60 * 60 * 24
rv.cache_control.public = True
return rv
@app.route('/avatar/<hash>')
@cache.cached(query_string=True)
if not set(request.args.keys()).issubset(set('sdr')):
abort(HTTPStatus.BAD_REQUEST)
if request.args.get('s'):
try:
if not (1 < int(request.args.get('s')) < 2048):
abort(HTTPStatus.BAD_REQUEST)
except ValueError:
abort(HTTPStatus.BAD_REQUEST)
if request.args.get('d') and request.args.get('d') not in {
'404', 'mp', 'identicon', 'monsterid', 'wavatar', 'retro',
'robohash', 'blank'}:
abort(HTTPStatus.BAD_REQUEST)
if request.args.get('r') and request.args.get('r') not in {
'g', 'pg', 'r', 'x'}:
abort(HTTPStatus.BAD_REQUEST)
@app.template_filter('hostname')
def hostname(url):
return urlparse(url).hostname
@app.template_filter('url_local')
def url_local(url):
return urlparse(url)._replace(scheme='', netloc='').geturl()
@cache.memoize(timeout=60 * 60 * 24)
def fetch_donators():
try:
response = requests.get(DONATORS_URL, headers=headers)
except Exception:
app.logger.error('fail to fetch donators', exc_info=True)
return []
@cache.memoize(timeout=60 * 60 * 24)
def fetch_donations():
headers = {'Content-Type': 'application/json'}
try:
response = requests.get(DONATIONS_URL, headers=headers)
except Exception:
app.logger.error('fail to fetch donations', exc_info=True)
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def donate():
donators=fetch_donators(),
donations=fetch_donations())
@app.route('/foundation/donations.html', endpoint='donate-alt')
return redirect(url_for('donate'))
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def donate_thanks():
return render_template('donate_thanks.html')
@app.route('/donate/cancel')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def donate_cancel():
return render_template('donate_cancel.html')
class Service(enum.Flag):
NONE = 0
CONSULTING = enum.auto()
DEVELOPMENT = enum.auto()
HOSTING = enum.auto()
TRAINING = enum.auto()
def __init__(self, name, positions, services=Service.NONE):
self.name = name
self.positions = positions
self.services = services
def __getattr__(self, name):
if self.services:
return self.services & getattr(Service, name.upper())
def __json__(self):
return {
'name': self.name,
'positions': self.positions,
}
PROVIDERS = [
Provider(name="Adiczion",
positions=[(43.52153, 5.43150)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(50.631123, 5.567552)],
services=Service.CONSULTING | Service.DEVELOPMENT | Service.TRAINING),
positions=[(48.873278, 2.324776)],
services=Service.DEVELOPMENT),
positions=[(37.9596885, -1.2086241)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(38.0131591, 23.7721521)],
services=Service.CONSULTING),
positions=[(-34.59675, -58.43035)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(-11.9753824, -77.0860785)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(18.476389, -69.893333)],
services=Service.CONSULTING),
positions=[(41.5995983, 0.5799085)],
services=Service.CONSULTING | Service.DEVELOPMENT | Service.HOSTING
| Service.TRAINING),
Provider(name="Lampero",
positions=[(44.4758631, 25.8231538)],
services=Service.CONSULTING),
positions=[(-27.978905, 153.389466)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(52.520008, 13.404954)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(41.544063, 2.115122)],
services=Service.CONSULTING | Service.DEVELOPMENT),
positions=[(47.0467674, 8.3048232)],
services=Service.CONSULTING | Service.DEVELOPMENT | Service.HOSTING),
positions=[(45.903956, 6.099937), (43.132028, 5.935532)],
services=Service.CONSULTING | Service.HOSTING),
positions=[(48.13585, 11.577415), (50.775116, 6.083565)],
services=Service.CONSULTING | Service.DEVELOPMENT | Service.TRAINING),
@cache.cached(key_prefix=cache_key_prefix_view, query_string=True)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
providers = PROVIDERS.copy()
shuffle(providers)
services = []
environ = {}
filters = {'consulting', 'development', 'hosting', 'training'}
if not (request.args.keys() <= filters):
abort(HTTPStatus.BAD_REQUEST)
for filter_name in filters:
environ[filter_name] = request.args.get(filter_name, type=int)
if (environ[filter_name] is not None
and environ[filter_name] not in {0, 1}):
abort(HTTPStatus.BAD_REQUEST)
if environ[filter_name]:
services.append(getattr(Service, filter_name.upper()))
if services:
services = functools.reduce(operator.ior, services)
providers = list(
filter(lambda p: (p.services & services) == services, providers))
return render_template(
'service_providers.html', providers=providers, **environ)
@app.route('/services.html', endpoint='service_providers-alt')
def service_providers_alt():
return redirect(url_for('service_providers'))
@app.route('/service-providers/start')
@cache.cached(key_prefix=cache_key_prefix_view)
@add_links(PRECONNECT_HEADERS + JS_LINK_HEADERS + CSS_LINK_HEADERS)
def service_providers_start():
return render_template('service_providers_start.html')