Added Twitter to index page
This commit is contained in:
parent
57fd968fcc
commit
39eb088afc
8 changed files with 197 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
BIN
flask_website/static/twitter.png
Executable file
BIN
flask_website/static/twitter.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -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; }
|
||||
</style>
|
||||
<link href="{{ tweets.feed_url }}" rel="alternate" title="Flask on Twitter" type="application/atom+xml">
|
||||
{% 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 <code>#pocoo</code> on irc.freenode.net.
|
||||
|
||||
{% if tweets %}
|
||||
<h2>Recent Tweets</h2>
|
||||
<div class=twitter>
|
||||
<p>
|
||||
What people say about Flask on <a href=http://twitter.com/>Twitter</a>:
|
||||
<ul>
|
||||
{%- for tweet in tweets %}
|
||||
<li>
|
||||
<a href="http://twitter.com/{{ tweet.user }}">{{ tweet.user }}</a>:
|
||||
{{ tweet.text|urlize }}
|
||||
<span class=meta>— {{ tweet.pub_date|timedeltaformat }} ago via {{ tweet.via }}</span>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="http://github.com/mitsuhiko/flask"><img style="position: fixed; top: 0; right: 0; border: 0;"
|
||||
src="http://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub"></a>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<dt>From:
|
||||
<dd class=from>{{ mail.author_name or mail.author_email }}
|
||||
<dt>Date:
|
||||
<dd>{{ mail.date.strftime('%Y-%m-%d @ %H:%M') }}
|
||||
<dd>{{ mail.date|datetimeformat }}
|
||||
</dl>
|
||||
<pre>{{ mail.rendered_text() }}</pre>
|
||||
{% if mail.children %}
|
||||
|
|
|
|||
91
flask_website/twitter.py
Normal file
91
flask_website/twitter.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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''
|
||||
|
|
|
|||
|
|
@ -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/')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue