From 39eb088afcd2d66ea9f05c8ef1d62ff5c3ce05a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 16 May 2010 01:06:37 +0200 Subject: [PATCH] Added Twitter to index page --- flask_website/__init__.py | 4 + flask_website/static/style.css | 70 ++++++++------ flask_website/static/twitter.png | Bin 0 -> 2682 bytes flask_website/templates/general/index.html | 17 ++++ .../templates/mailinglist/show_thread.html | 2 +- flask_website/twitter.py | 91 ++++++++++++++++++ flask_website/utils.py | 38 ++++++++ flask_website/views/general.py | 10 +- 8 files changed, 197 insertions(+), 35 deletions(-) create mode 100755 flask_website/static/twitter.png create mode 100644 flask_website/twitter.py 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 0000000000000000000000000000000000000000..bdffd361b49fd6a2eb45e60dacc83a4178369468 GIT binary patch literal 2682 zcmV-=3WfEFP)S zNz|a=LR_M$fT+PS76en6K(s?=ge$RhB~Fda9{-RZ{X)cMNb5?uEHM01t510B0xdswr(8j4zIZ zWAGk{&+*a}S!H?fmEk*AlO8Yyc-zcgiaA0}l9}zqr`RM(-6HaC0N%z&6*Jpu8MfmL z)C_3XAS9cWI^hvsyJC%gT6?v?D@0spfPq2J(g-|Vfw!9SfHA-`X0|=bsKg~Rn-4q* z%#&0Vfpsy^9ylxlPdDISkozofF7R{&p#_tf&A>afNK)54*$&6&v?4Us8Tf?2*oY*Z z4t!x|>mmqEgv{)ANgn|VC9MM%0gqbtuyz!%(4w6++5+1kivjR0;*|&j6D2cyO46%% z=SBnLB)tdx7x&uIP}&0V64V)wZ?Z0e6}YvJ!;N%- zq?7ZJEUAs82PNGhsYepp7J*s;pF(y6>;vWl{|wIpI_4?-GT;i}XTZGyGW*t12*tBb zff8UB&uc<}Q%aDRB$3YZP-0Nw{a2HqeXCl5rykI}cr zwzJXeiVbZ z1|LkP6w6fOpvMj2c@WSa_z>uw!IDFPbwF**k!TA{DZmT*10Mj#M&$cFQ1^cxAgK%( z40I1d!DE2ifQe?-X#X{{1^D6R${3>nzP9X*$%~o2h9lB5B(;dZ+6ABD`{`QX?>N$X zREnjp0Ul|>npfi?Wz-{V74QzwErIpmVPIy2`pbdNDbLgccVwveDsWa7A>HsP@**Ce z9~x2VyMbE@?R=2cEa)*c&$^3K-<9AY;igoVkklV&X=X2n=hl+0lk}*h)p$rcGw^N3 zlOB1Iq>*@TVRF7axyT%+ybw4wk45?WWC*8^#*+wTDVCa!qpR2ANa}VxHZa_Up21=A zUl9S-#WOZ5NSSI$6+BXX3mBiH&Qqk}n++cx1tz3aJQbK5I=>W;;m4yPgLuG zo_Q)b8dymJd4(KpRO1-oA4r49fJ1;Kjd;Sx-gw5RO9r7=0*?Uq2LtTI)nnkZ8Y%@6i zHV@zbJEhh8A#TLiaAVyJ99HZRZ^9c+z<0ARfIBnR$>2V#fYVdn4aN8R6?h1IE`BI_ zIgMAqnZQ9Yl^qH6E0Q5D5Bg67&Ld@g$W-}KU?VAM<&-Vd21dah-Oi)3EDVWh!6Ktm%cnFwemOU}D8PBtg%@Jqc z1GJWONtW-C$XvTIz)(qplaQIEoh|sWzPjN>HcO4ela@2cauWnUTdxAj6C9x*(Wuo(`d{00xDZ%)_TA10AFk;65%O#DM)F!SxNr&PP zdu#j}?qW%!C8egp-^cR~J%BZmZpjlK+Z}j^#J(w-LvaB^fOGJ~%6j}XINvP}@~T29$5;Q4@oRD$%*?YxNiJGV%)16-+=IvC@8QVOS2(ut5DvV|1}f6{-T@9z zc~^rYQqSR0Yej6mz9|Ew8RI@Y-|ztLqLtx&+hCxT_yMd0zp1tlxG;;XNPuj5De4r+ zL?Hw9AdV?i0+#??_q(Lm8H6J^yYXE9E?||Jou9ZuMIPyLeDP^0Kfy7(0l;tZsZf`N z_h1}}>H(aLFKUgp!a_^7>~O&(X%c>iw|OuG_`&HK;CHxt*P7YtO#EiHQ_|;nYW{<8 zaVl3&7{X?K8+z9&`zs&rEq!~cHnY~#IMT=#CCHOX04GcH4O#tEA-T8PP zui;YjPJET0iJvS>aDZi#q+vMF+Pg@49+)^FZM84Oy<;mLtqn-|HXc}6%=QoX4KDNB zTAM<-0(eu>d`Tnle}lA?bX2k0|HcEMMhZx^I9`7-zDvx;Q|?FL`~N6B!Sd5$w12=o z0Kez^IPL)p@Z971c)(N#+=;_(#i4J(8c>TLcD4gg0Bytj&f)z~d@IfS4-t{tKhOpU ooh>Ir<{1Cq-)hqGl}VQW0jp`%w&Hz$d;kCd07*qoM6N<$f+K+AhX4Qo literal 0 HcmV?d00001 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

+ + {% 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/')