New website; with mailinglist archives.

This commit is contained in:
Armin Ronacher 2010-04-21 17:44:16 +02:00
parent 7e8019565f
commit ca2d32a700
17 changed files with 614 additions and 29 deletions

1
.gitignore vendored
View file

@ -3,4 +3,5 @@
*.pyo *.pyo
env env
dist dist
website/_mailinglist/*
*.egg-info *.egg-info

View file

110
website/flask_website.py Normal file
View file

@ -0,0 +1,110 @@
from __future__ import with_statement
from hashlib import md5
from werkzeug import parse_date
from flask import Flask, render_template, json, url_for, abort, Markup
from jinja2.utils import urlize
app = Flask(__name__)
THREADS_PER_PAGE = 15
class Mail(object):
def __init__(self, d):
self.msgid = d['msgid']
self.author_name, self.author_addr = d['author']
self.date = parse_date(d['date'])
self.subject = d['subject']
self.children = [Mail(x) for x in d['children']]
self.text = d['text']
@property
def rendered_text(self):
result = []
in_sig = False
for line in self.text.splitlines():
if line == u'-- ':
in_sig = True
if in_sig:
line = Markup(u'<span class=sig>%s</span>' % line)
elif line.startswith('>'):
line = Markup(u'<span class=quote>%s</span>' % line)
result.append(urlize(line))
return Markup(u'\n'.join(result))
@property
def id(self):
return md5(self.msgid.encode('utf-8')).hexdigest()
class Thread(object):
def __init__(self, d):
self.slug = d['slug'].rsplit('/', 1)[-1]
self.title = d['title']
self.reply_count = d['reply_count']
self.author_name, self.author_email = d['author']
self.date = parse_date(d['date'])
if 'root' in d:
self.root = Mail(d['root'])
@staticmethod
def get(year, month, day, slug):
try:
with app.open_resource('_mailinglist/threads/%s-%02d-%02d/%s' %
(year, month, day, slug)) as f:
return Thread(json.load(f))
except IOError:
pass
@staticmethod
def get_list():
with app.open_resource('_mailinglist/threads/threadlist') as f:
return [Thread(x) for x in json.load(f)]
@property
def url(self):
return url_for('mailinglist_show_thread', year=self.date.year,
month=self.date.month, day=self.date.day,
slug=self.slug)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/mailinglist/')
def mailinglist_index():
return render_template('mailinglist/index.html')
@app.route('/mailinglist/archive/', defaults={'page': 1})
@app.route('/mailinglist/archive/page/<int:page>')
def mailinglist_archive(page):
all_threads = Thread.get_list()
offset = (page - 1) * THREADS_PER_PAGE
threads = all_threads[offset:offset + THREADS_PER_PAGE]
if page != 1 and not threads:
abort(404)
return render_template('mailinglist/archive.html',
page_count=len(threads) // THREADS_PER_PAGE + 1,
page=page, threads=threads)
@app.route('/mailinglist/archives/<int:year>/<int:month>/<int:day>/<slug>')
def mailinglist_show_thread(year, month, day, slug):
thread = Thread.get(year, month, day, slug)
if thread is None:
abort(404)
return render_template('mailinglist/show_thread.html', thread=thread)
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
if __name__ == '__main__':
app.run(debug=True)

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before After
Before After

View file

@ -0,0 +1,33 @@
$(function() {
var first_mail = $('div.mail:first')[0].id;
function display(id) {
var pos = {
x: window.pageXOffset || document.body.scrollLeft,
y: window.pageYOffset || document.body.scrollTop
};
$('ul.mailtree div.link').removeClass('selected');
$('#link-' + id).addClass('selected').focus();
$('div.mail').hide();
$('#' + id).show();
if (!(document.location.hash == '' && id == first_mail))
document.location.href = '#' + id;
window.scrollTo(pos.x, pos.y);
}
$('div.mail')
.addClass('dynamic-mail')
.appendTo($('<div></div>').insertBefore('div.mail:first'))
.hide();
$('div.link').each(function() {
var id = $('a', $(this).parent()).attr('href').substr(1);
$(this).click(function() {
display(id);
return false;
});
}).css({cursor: 'pointer'});
var href = document.location.href.split(/#/, 2)[1];
display(href != null ? href : first_mail);
});

BIN
website/static/mailinglist.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

41
website/static/style.css Normal file
View file

@ -0,0 +1,41 @@
body { font-family: 'Georgia', serif; font-size: 17px; color: #000; }
a { color: #004B6B; }
a:hover { color: #6D4100; }
.box { width: 540px; margin: 40px auto; }
h1, h2, h3 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; }
h2 { font-size: 28px; margin: 15px 0 5px 0; }
h3 { font-size: 22px; margin: 15px 0 5px 0; }
code,
pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono',
monospace; 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; }
ul li:before { content: "\00BB \0020"; color: #888; position: absolute; margin-left: -19px; }
blockquote { margin: 0; font-style: italic; color: #444; }
.footer { font-size: 13px; color: #888; text-align: right; margin-top: 25px; }
.backnav { text-align: center; color: #444; font-style: italic; }
/* mailinglist */
.pagination { text-align: center; font-size: 15px; margin: 20px 0 0 0; }
.disabled { color: #888; }
.archive .meta { font-size: 0.9em; display: block; margin: 0 0 0.5em 1em; }
.mailtree { border-top: 1px solid black; padding: 5px 10px 0 10px;
border-bottom: 1px solid black; font-size: 14px; }
.mailtree ul { margin: 5px 0 5px 15px; }
.mailtree li:before { display: none; }
.mailtree .selected:before { content: "\00BB \0020"; color: #888;
position: absolute; margin-left: -10px; }
.mail { margin: 15px 0; }
.children .mail { margin: 15px 0 15px 20px; }
.dynamic-mail { margin-left: 0!important; }
.mail dl { margin: 0; padding-bottom: 10px;
border-bottom: 1px solid black; }
.mail dl dt { color: #888; width: 70px; float: left; height: 20px; }
.mail dl dd { height: 20px; width: 500px; }
.mail dl dd.from { text-decoration: underline; }
.mail pre { background: transparent; font-size: 13px;
line-height: 1.15; }
.mail .quote { color: #004B6B; }
.mail .sig { color: #888; }

302
website/sync-librelist.py Normal file
View file

@ -0,0 +1,302 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
sync librelist
~~~~~~~~~~~~~~
Pulls in the latest version of the mails from the Flask librelist
mailinglist and sorts them by thread into the processed folder as
json dumps with the most relevant information.
This will also trigger the rsync.
:copyright: Copyright 2010 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
from __future__ import with_statement
import os
import re
import unicodedata
from glob import glob
from subprocess import Popen
from flask import json
from werkzeug import Headers, parse_date
INCOMING_MAIL_FOLDER = '_mailinglist/incoming'
THREAD_FOLDER = '_mailinglist/threads'
LIST_NAME = 'zine'
RSYNC_PATH = 'librelist.com::json/%s'
SUBJECT_PREFIX = '[zine]'
_punctuation_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.:]+')
_mail_split_re = re.compile(r'"?(.*?)"?(?:\s+<([^>]+)>)?$')
_string_inc_re = re.compile(r'(\d+)$')
_msgid_re = re.compile(r'<([^>]+)>')
def unquote_msgid(msgid):
msgid = (msgid or '').strip().strip('<>')
if msgid:
if '@' in msgid:
a, b = msgid.split('@', 1)
return a.strip('"') + '@' + b
return msgid.decode('iso-8859-15', 'replace')
def split_email(s):
p1, p2 = _mail_split_re.match(s.strip()).groups()
if p2:
words = p1.split()
for idx, word in enumerate(words):
if word.isupper():
words[idx] = word.capitalize()
return u' '.join(words), p2
elif '@' in p1:
return None, p1
return p1, None
def increment_string(string):
match = _string_inc_re.search(string)
if match is None:
return string + u'2'
return string[:match.start()] + unicode(int(match.group(1)) + 1)
def strip_subject_prefix(string):
"""Unstrips a title"""
if string.startswith(SUBJECT_PREFIX):
return string[len(SUBJECT_PREFIX):].lstrip()
if string[:3].lower() in (u'aw:', u're:'):
return u'Re: ' + strip_subject_prefix(string[3:].lstrip())
if string[:3].lower() in (u'fw:', u'wg:'):
return u'Fw: ' + strip_subject_prefix(string[3:].lstrip())
return string
def rsync():
"""Invokes rsync"""
Popen(['rsync', '-qazv', RSYNC_PATH % LIST_NAME,
INCOMING_MAIL_FOLDER]).wait()
class Tree(object):
def __init__(self, threads):
self.threads = threads
self.processed_mail = set()
self._new_mail = []
self._known_ids = {}
def _walk_mails(mails):
for mail in mails:
self.processed_mail.add(mail['fsid'])
self._known_ids[mail['msgid']] = mail
_walk_mails(mail['children'])
_walk_mails(x['root'] for x in threads)
def slug_used(self, slug):
for thread in self.threads:
if thread['slug'] == slug:
return True
return False
def generate_slug(self, mail):
date = parse_date(mail['date'])
if date is None:
date = 'missing-date'
else:
date = date.strftime('%Y-%m-%d')
rv = u'%s/%s' % (date,
'-'.join(x for x in _punctuation_re.split(
unicodedata.normalize('NFKC', unicode(mail['subject']))
.encode('ascii', 'ignore')) if x).lower())
while self.slug_used(rv):
rv = increment_string(rv)
return rv
def walk(self):
return self._known_ids.itervalues()
def add_new_mail(self, f, fsid):
mail = parse_mail(f, fsid)
self._new_mail.append(mail)
self._known_ids[mail['msgid']] = mail
def add_thread_for(self, mail):
self.threads.append({
'title': mail['subject'],
'slug': self.generate_slug(mail),
'date': mail['date'],
'author': mail['author'],
'root': mail,
'reply_count': 0
})
def has_mail(self, msgid):
return msgid in self._known_ids
def get_mail(self, msgid):
return self._known_ids.get(msgid)
def find_parent(self, mail):
# first check the reply to, some clients actually set that to
# something useful :)
if mail['in-reply-to']:
referenced_mail = self.get_mail(mail['in-reply-to'])
if referenced_mail is not None and referenced_mail is not mail:
return referenced_mail
# next check the references, pick the most recent one.
last = last_date = None
for msgid in mail['references']:
referenced_mail = self.get_mail(msgid)
if referenced_mail is None:
continue
other_date = parse_date(referenced_mail['date'])
if last is None or last_date < other_date:
last_date = other_date
last = referenced_mail
if last is not None and last is not mail:
return last
# oh boy, nothing matched, find the oldest matching subject
# then. That could take a while, we really check all mails...
def _strip_subject(subject):
if subject[:3].lower() in (u'aw:', u're:'):
subject = subject[3:]
return subject.strip().lower()
subject = _strip_subject(mail['subject'])
last = mail
last_date = parse_date(mail['date'])
for other_mail in self.walk():
if _strip_subject(other_mail['subject']) == subject:
other_date = parse_date(other_mail['date'])
if last is None or other_date < last_date:
last = other_mail
last_date = other_date
if last is not mail:
return last
def integrate_new_mail(self):
while self._new_mail:
mail = self._new_mail.pop()
print "A", mail['msgid']
parent = self.find_parent(mail)
if parent is not None:
parent['children'].append(mail)
else:
self.add_thread_for(mail)
self.processed_mail.add(mail['fsid'])
def _count_mails(children):
rv = len(children)
for child in children:
rv += _count_mails(child['children'])
return rv
for thread in self.threads:
thread['reply_count'] = _count_mails(thread['root']['children'])
def save(self):
for thread in self.threads:
filename = os.path.join(THREAD_FOLDER, thread['slug'])
try:
os.makedirs(os.path.dirname(filename))
except OSError:
pass
with open(filename, 'w') as f:
json.dump(thread, f, indent=2)
with open(os.path.join(THREAD_FOLDER, 'threadlist'), 'w') as f:
threads = sorted(self.threads, reverse=True,
key=lambda x: parse_date(x['date']))
for idx, thread in enumerate(threads):
thread = dict(thread)
del thread['root']
threads[idx] = thread
json.dump(threads, f, indent=2)
def get_processed_tree():
"""Returns the tree of already processed mails (from
the THREAD_FOLDER).
"""
threads = []
for thread in glob(THREAD_FOLDER + '/*/*/*/*'):
if os.path.isfile(thread):
with open(thread) as f:
threads.append(json.load(f))
return Tree(threads)
def parse_mail(f, fsid):
"""Parses an email and returns the information we care about"""
msg = json.load(f)
headers = Headers(msg['headers'])
irt = None
match = _msgid_re.search(headers.get('in-reply-to', ''))
if match is not None:
irt = unquote_msgid(match.group(1))
references = [unquote_msgid(msgid) for msgid
in headers.get('references', '').split() if msgid]
body = msg['body']
if body is None:
for part in msg['parts']:
if part['encoding']['type'] == 'text/plain':
body = part['body']
break
else:
body = 'could not decode message'
return {
'fsid': fsid,
'msgid': unquote_msgid(headers.get('message-id') or 'fakdeid-' + fsid),
'in-reply-to': irt,
'references': references,
'author': split_email(headers['from']),
'date': headers['Date'],
'subject': strip_subject_prefix(headers['subject']),
'text': body,
'children': []
}
def process_mails(tree):
to_process = []
# find the unprocessed mails
for folder in glob('%s/%s/*/*/*/json' % (INCOMING_MAIL_FOLDER, LIST_NAME)):
for fsid in os.listdir(folder):
if fsid not in tree.processed_mail:
filename = os.path.join(folder, fsid)
if os.path.isfile(filename):
to_process.append((filename, fsid))
# now parse all mails and append them to the tree as new mails
for filename, fsid in to_process:
with open(filename) as f:
tree.add_new_mail(f, fsid)
tree.integrate_new_mail()
# and write the information to the file system
tree.save()
def main():
tree = get_processed_tree()
rsync()
process_mails(tree)
if __name__ == '__main__':
main()

View file

@ -8,7 +8,7 @@ body, html {
} }
body { body {
background: url(/ship.png) no-repeat center right; background: url(/static/ship.png) no-repeat center right;
} }
body:after { body:after {
@ -18,7 +18,7 @@ body:after {
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 30px; width: 30px;
background: url(/mask.png) repeat-y left; background: url(/static/mask.png) repeat-y left;
} }
a { color: #004B6B; } a { color: #004B6B; }

View file

@ -1,27 +1,12 @@
<!doctype html> {% extends "layout.html" %}
<title>Flask (A Python Microframework)</title> {% block head %}
<meta charset=utf-8> {{ super() }}
<style type=text/css> <style type=text/css>
body { font-family: 'Georgia', serif; font-size: 17px; color: #000; } h1 { margin: 0 0 30px 0; background: url(/static/logo.png) no-repeat center; height: 165px; }
a { color: #004B6B; }
a:hover { color: #6D4100; }
.box { width: 540px; margin: 40px auto; }
h1, h2 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; }
h1 { margin: 0 0 30px 0; background: url(logo.png) no-repeat center; height: 165px; }
h2 { font-size: 28px; margin: 15px 0 5px 0; }
code,
pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono',
monospace; 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; }
ul li:before { content: "\00BB \0020"; color: #888; position: absolute; margin-left: -19px; }
h1 span, p.tagline { display: none; } h1 span, p.tagline { display: none; }
blockquote { margin: 0; font-style: italic; color: #444; } </style>
.footer { font-size: 13px; color: #888; text-align: right; margin-top: 25px; } {% endblock %}
</style> {% block body %}
<div class=box>
<h1><span>Flask</span></h1> <h1><span>Flask</span></h1>
<p class=tagline>because sometimes a pocket knife is not enough <p class=tagline>because sometimes a pocket knife is not enough
<blockquote> <blockquote>
@ -45,6 +30,7 @@ def hello():
<h2>Interested?</h2> <h2>Interested?</h2>
<ul> <ul>
<li><a href=docs/>Read the documentation</a> <li><a href=docs/>Read the documentation</a>
<li><a href=mailinglist/>Join the mailinglist</a>
<li><a href=http://github.com/mitsuhiko/flask>Fork it on github</a> <li><a href=http://github.com/mitsuhiko/flask>Fork it on github</a>
<li><a href=http://pypi.python.org/pypi/Flask>Download it from PyPI</a> <li><a href=http://pypi.python.org/pypi/Flask>Download it from PyPI</a>
</ul> </ul>
@ -74,8 +60,6 @@ def hello():
create a new ticket or fork. If you just want to chat with fellow create a new ticket or fork. If you just want to chat with fellow
developers, go to <code>#pocoo</code> on irc.freenode.net. developers, go to <code>#pocoo</code> on irc.freenode.net.
<p class=footer>&copy; Copyright 2010 by <a href=http://lucumr.pocoo.org/>Armin Ronacher</a>
</div>
<a href="http://github.com/mitsuhiko/flask"><img style="position: fixed; top: 0; right: 0; border: 0;" <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> src="http://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub"></a>
{% endblock %}

View file

@ -0,0 +1,12 @@
<!doctype html>
{% block head %}
<title>{% block title %}Welcome{% endblock %} | Flask (A Python Microframework)</title>
<meta charset=utf-8>
<link rel=stylesheet type=text/css href="/static/style.css">
<script type=text/javascript
src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
{% endblock %}
<div class=box>
{% block body %}{% endblock %}
<p class=footer>&copy; Copyright 2010 by <a href=http://lucumr.pocoo.org/>Armin Ronacher</a>
</div>

View file

@ -0,0 +1,27 @@
{% extends "mailinglist/layout.html" %}
{% block title %}Mailinglist Archive{% endblock %}
{% block mailbody %}
<h2>Mailinglist Archive</h2>
<ul class=archive>
{% for thread in threads %}
<li><a href="{{ thread.url }}">{{ thread.title }}</a>
<span class=meta>
by {{ thread.author_name or thread.author_email }}
on {{ thread.date.strftime('%Y-%m-%d @ %H:%M') }}
({{ thread.reply_count }} repl{{ thread.reply_count == 1 and 'y' or 'ies' }})</span>
{% endfor %}
</ul>
<div class=pagination>
{% if page > 1 %}
<a href="{{ url_for('mailinglist_archive', page=page - 1) }}">&laquo; Previous</a>
{% else %}
<span class=disabled>&laquo; Previous</span>
{% endif %}
| <strong>{{ page }}</strong> |
{% if page < page_count %}
<a href="{{ url_for('mailinglist_archive', page=page + 1) }}">Next &raquo;</a>
{% else %}
<span class=disabled>Next &raquo;</span>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends "mailinglist/layout.html" %}
{% block title %}Mailinglist{% endblock %}
{% block mailbody %}
<p>
There is a mailinglist for Flask hosted on <a
href=http://librelist.com/>librelist</a> you can use for both user requests
and development discussions.
<p>
To subscribe, send a mail to <em>flask@librelist.com</em> and reply
to the confirmation mail. Make sure to check your Spam folder, just in
case. To unsubscribe again, send a mail to
<em>flask-unsubscribe@librelist.com</em> and reply to the
confirmation mail.
<p>
The <a href="{{ url_for('mailinglist_archive') }}">mailinglist archive</a>
is synched every hour. Go there to read up old discussions grouped by
thread.
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends "layout.html" %}
{% block head %}
{{ super() }}
<style type=text/css>
h1 { margin: 0 0 30px 0; background: url(/static/mailinglist.png) no-repeat center; height: 165px; }
h1 span { display: none; }
</style>
{% endblock %}
{% block body %}
<h1><span>Flask Mailinglist</span></h1>
<div class=backnav>
<a href="{{ url_for('index') }}">back to website</a>
{% if request.endpoint != 'mailinglist_index' %}//
<a href="{{ url_for('mailinglist_index') }}">list information</a>
{% endif %}
{% if request.endpoint != 'mailinglist_archive' %}//
<a href="{{ url_for('mailinglist_archive') }}">go to archive</a>
{% endif %}
</div>
{% block mailbody %}{% endblock %}
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends "mailinglist/layout.html" %}
{% block title %}{{ thread.title }}{% endblock %}
{% block head %}
{{ super() }}
<script type=text/javascript src=/static/mailinglist.js></script>
{% endblock %}
{% block mailbody %}
<h2>{{ thread.title }}</h2>
<ul class=mailtree>
{% for mail in [thread.root] recursive %}
<li><div class=link id="link-{{ mail.id }}"><a href="#{{
mail.id }}">{{ mail.subject }}</a> by {{
mail.author_name or mail.author_email }}</div>
{% if mail.children %}<ul>{{ loop(mail.children) }}</ul>{% endif %}
{% endfor %}
</ul>
{% for mail in [thread.root] recursive %}
<div class=mail id="{{ mail.id }}">
<h3>{{ mail.subject }}</h3>
<dl>
<dt>From:
<dd class=from>{{ mail.author_name or mail.author_email }}
<dt>Date:
<dd>{{ mail.date.strftime('%Y-%m-%d @ %H:%M') }}
</dl>
<pre>{{ mail.rendered_text }}</pre>
{% if mail.children %}
<div class=children>{{ loop(mail.children) }}</div>
{% endif %}
</div>
{% endfor %}
{% endblock %}