diff --git a/flask_website/__init__.py b/flask_website/__init__.py index 8f84970e..5246a6ee 100644 --- a/flask_website/__init__.py +++ b/flask_website/__init__.py @@ -34,3 +34,7 @@ app.register_module(snippets) app.register_module(extensions) from flask_website.database import User, db_session +from flask_website import utils + +app.jinja_env.filters['datetimeformat'] = utils.format_datetime +app.jinja_env.filters['timedeltaformat'] = utils.format_timedelta diff --git a/flask_website/static/style.css b/flask_website/static/style.css index 7c346404..6a492d8d 100644 --- a/flask_website/static/style.css +++ b/flask_website/static/style.css @@ -1,38 +1,44 @@ -body { font-family: 'Georgia', serif; font-size: 17px; color: #000; } -a { color: #004B6B; } -a:hover { color: #6D4100; } -.box { width: 540px; margin: 40px auto; } -h2, h3 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; } -h1 { margin: 0 0 30px 0; background: url(/static/flask.png) no-repeat center; - height: 165px; } -h1 span { display: none; } -h2 { font-size: 28px; margin: 15px 0 5px 0; } -h3 { font-size: 22px; margin: 15px 0 5px 0; } +body { font-family: 'Georgia', serif; font-size: 17px; color: #000; } +a { color: #004B6B; } +a:hover { color: #6D4100; } +.box { width: 540px; margin: 40px auto; } +h2, h3 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; } +h1 { margin: 0 0 30px 0; background: url(/static/flask.png) no-repeat center; + height: 165px; } +h1 span { display: none; } +h2 { font-size: 28px; margin: 15px 0 5px 0; } +h3 { font-size: 22px; margin: 15px 0 5px 0; } textarea, code, -pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', - monospace!important; font-size: 15px; background: #eee; } -pre { padding: 7px 30px; margin: 15px -30px; line-height: 1.3; } -.ig { color: #888; } -p { line-height: 1.4; } -ul { margin: 15px 0 15px 0; padding: 0; list-style: none; line-height: 1.4; } +pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', + monospace!important; font-size: 15px; background: #eee; } +pre { padding: 7px 30px; margin: 15px -30px; line-height: 1.3; } +.ig { color: #888; } +p { line-height: 1.4; } +ul { margin: 15px 0 15px 0; padding: 0; list-style: none; line-height: 1.4; } ul li:before { content: "\00BB \0020"; color: #888; position: absolute; margin-left: -19px; } -ol { line-height: 1.4; margin: 15px 0 15px 30px; padding: 0; } -blockquote { margin: 0; font-style: italic; color: #444; } -.footer { font-size: 13px; color: #888; text-align: right; margin-top: 25px; } -.nav { text-align: center; } -.nav a { font-style: italic; } -.backnav { float: right; color: #444; font-style: italic; - margin: 5px 0 0 0; font-size: 0.9em; } -.message { background: #DEEBF3; color: #004B6B; padding: 5px 30px; - margin: 10px -30px; } -.actions { margin-top: 0; } -table { border: 1px solid black; border-collapse: collapse; - margin: 15px 0; } -td, th { border: 1px solid black; padding: 4px 10px; - text-align: left; } -th { background: #eee; font-weight: normal; } +ol { line-height: 1.4; margin: 15px 0 15px 30px; padding: 0; } +blockquote { margin: 0; font-style: italic; color: #444; } +.footer { font-size: 13px; color: #888; text-align: right; margin-top: 25px; } +.nav { text-align: center; } +.nav a { font-style: italic; } +.backnav { float: right; color: #444; font-style: italic; + margin: 5px 0 0 0; font-size: 0.9em; } +.message { background: #DEEBF3; color: #004B6B; padding: 5px 30px; + margin: 10px -30px; } +.actions { margin-top: 0; } +.twitter:before { background: url(twitter.png) no-repeat; content: " "; + float: right; width: 64px; height: 64px; + margin: -25px 0 0 0; padding: 0 0 10px 10px; } +.twitter li { margin: 15px 0; line-height: 1.2; font-size: 15px; } +.twitter li:before { content: "♯"; padding-left: 4px; } +.twitter .meta, .twitter .meta a { color: #888; font-size: 13px; } +table { border: 1px solid black; border-collapse: collapse; + margin: 15px 0; } +td, th { border: 1px solid black; padding: 4px 10px; + text-align: left; } +th { background: #eee; font-weight: normal; } -td input { border: none; padding: 0; } +td input { border: none; padding: 0; } /* forms */ input, textarea, select { border: 1px solid black; padding: 2px; background: white; diff --git a/flask_website/static/twitter.png b/flask_website/static/twitter.png new file mode 100755 index 00000000..bdffd361 Binary files /dev/null and b/flask_website/static/twitter.png differ diff --git a/flask_website/templates/general/index.html b/flask_website/templates/general/index.html index d9a9a27f..ebeb1796 100644 --- a/flask_website/templates/general/index.html +++ b/flask_website/templates/general/index.html @@ -5,6 +5,7 @@ h1 { margin: 0 0 30px 0; background: url(/static/logo.png) no-repeat center; height: 165px; } h1 span, p.tagline { display: none; } + {% endblock %} {% block body_title %} {{ super() }} @@ -67,6 +68,22 @@ def hello(): create a new ticket or fork. If you just want to chat with fellow developers, go to #pocoo on irc.freenode.net. + {% if tweets %} +

Recent Tweets

+
+

+ What people say about Flask on Twitter: +

+
+ {% endif %} + Fork me on GitHub {% endblock %} diff --git a/flask_website/templates/mailinglist/show_thread.html b/flask_website/templates/mailinglist/show_thread.html index 8e4b3117..67274ac1 100644 --- a/flask_website/templates/mailinglist/show_thread.html +++ b/flask_website/templates/mailinglist/show_thread.html @@ -25,7 +25,7 @@
From:
{{ mail.author_name or mail.author_email }}
Date: -
{{ mail.date.strftime('%Y-%m-%d @ %H:%M') }} +
{{ mail.date|datetimeformat }}
{{ mail.rendered_text() }}
{% if mail.children %} diff --git a/flask_website/twitter.py b/flask_website/twitter.py new file mode 100644 index 00000000..5ebb9885 --- /dev/null +++ b/flask_website/twitter.py @@ -0,0 +1,91 @@ +from __future__ import with_statement +import urllib2 +import time +import threading +from flask import json, Markup +from werkzeug import url_encode, parse_date + + +class SearchResult(object): + + def __init__(self, result): + self.text = Markup(result['text']).unescape() + self.user = result['from_user'] + self.via = Markup(Markup(result['source']).unescape()) + self.pub_date = parse_date(result['created_at']) + self.profile_image = result['profile_image_url'] + self.type = result['metadata']['result_type'] + self.retweets = result['metadata'].get('recent_retweets') or 0 + + +class SearchQuery(object): + fetch_timeout = 10 + + def __init__(self, required=None, optional=None, limit=5, timeout=60, lang=None): + self.required = set(x.lower() for x in (required or ())) + self.optional = set(x.lower() for x in (optional or ())) + self.limit = limit + self.lang = lang + self.timeout = timeout + self._last_fetch = 0 + self._last_scheduled_fetch = 0 + self._cached = None + + @property + def query(self): + def _quote_if(x): + if len(x.split()) != 1: + return u'"%s"' % x + return x + q = u' '.join(map(_quote_if, self.required)) + q += u' ' + u' OR '.join(map(_quote_if, self.optional)) + return q + + @property + def feed_url(self): + return self.get_url(kind='atom') + + def get_url(self, kind='json'): + return 'http://search.twitter.com/search.%s?%s' % (kind, url_encode({ + 'q': self.query, + 'result_type': 'mixed', + 'lang': self.lang + })) + + def fetch(self): + def _accept(text): + text = text.lower() + for word in self.required: + if word not in text: + return False + for word in self.optional: + if word in text: + return True + return False + rv = json.load(urllib2.urlopen(self.get_url())) + return [SearchResult(x) for x in rv['results'] if + _accept(x['text'])] + + @property + def up_to_date(self): + return time.time() < self._last_fetch + self.timeout + + def _try_refresh(self): + if self.up_to_date: + return + if time.time() > self._last_scheduled_fetch + self.fetch_timeout: + self._last_scheduled_fetch = time.time() + threading.Thread(target=self._fetch_and_store).start() + + def _fetch_and_store(self): + self._cached = self.fetch() + self._last_fetch = time.time() + + def __len__(self): + self._try_refresh() + return len(self._cached or ()) + + def __iter__(self): + self._try_refresh() + for item in (self._cached or ())[:self.limit]: + yield item diff --git a/flask_website/utils.py b/flask_website/utils.py index eacdb77f..f63a2a41 100644 --- a/flask_website/utils.py +++ b/flask_website/utils.py @@ -1,5 +1,6 @@ import re import creoleparser +from datetime import datetime, timedelta from genshi import builder from functools import wraps from creoleparser.elements import PreBlock @@ -17,6 +18,17 @@ pygments_formatter = HtmlFormatter(style=FlaskyStyle) _ws_split_re = re.compile(r'(\s+)') +TIMEDELTA_UNITS = ( + ('year', 3600 * 24 * 365), + ('month', 3600 * 24 * 30), + ('week', 3600 * 24 * 7), + ('day', 3600 * 24), + ('hour', 3600), + ('minute', 60), + ('second', 1) +) + + class CodeBlock(PreBlock): def __init__(self): @@ -94,3 +106,29 @@ def requires_admin(f): abort(401) return f(*args, **kwargs) return requires_login(decorated_function) + + +def format_datetime(dt): + return dt.strftime('%Y-%m-%d @ %H:%M') + + +def format_timedelta(delta, granularity='second', threshold=.85): + if isinstance(delta, datetime): + delta = datetime.utcnow() - delta + if isinstance(delta, timedelta): + seconds = int((delta.days * 86400) + delta.seconds) + else: + seconds = delta + + for unit, secs_per_unit in TIMEDELTA_UNITS: + value = abs(seconds) / secs_per_unit + if value >= threshold or unit == granularity: + if unit == granularity and value > 0: + value = max(1, value) + value = str(int(round(value))) + value += u' ' + unit + if value != 1: + value += u's' + return value + + return u'' diff --git a/flask_website/views/general.py b/flask_website/views/general.py index 7854d8d2..188a5efa 100644 --- a/flask_website/views/general.py +++ b/flask_website/views/general.py @@ -1,14 +1,20 @@ from flask import Module, render_template, session, redirect, url_for, \ request, flash, g, Response -from flask_website import oid +from flask_website import oid, twitter from flask_website.database import db_session, User general = Module(__name__) +tweets = twitter.SearchQuery(required=['flask'], + optional=['code', 'web', 'python', 'py', + 'pocoo', 'micro', 'mitsuhiko', + 'framework', 'django', 'jinja', + 'werkzeug', 'pylons'], + lang='en') @general.route('/') def index(): - return render_template('general/index.html') + return render_template('general/index.html', tweets=tweets) @general.route('/logout/')