From ca2d32a700dce3be2022737c1c28e26d9c47b42a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Apr 2010 17:44:16 +0200 Subject: [PATCH] New website; with mailinglist archives. --- .gitignore | 1 + website/_mailinglist/.ignore | 0 website/flask_website.py | 110 +++++++ website/{ => static}/logo.png | Bin website/static/mailinglist.js | 33 ++ website/static/mailinglist.png | Bin 0 -> 34718 bytes website/{ => static}/mask.png | Bin website/{ => static}/ship.png | Bin website/static/style.css | 41 +++ website/sync-librelist.py | 302 ++++++++++++++++++ website/{ => templates}/404.html | 4 +- website/{ => templates}/index.html | 38 +-- website/templates/layout.html | 12 + website/templates/mailinglist/archive.html | 27 ++ website/templates/mailinglist/index.html | 20 ++ website/templates/mailinglist/layout.html | 21 ++ .../templates/mailinglist/show_thread.html | 34 ++ 17 files changed, 614 insertions(+), 29 deletions(-) create mode 100644 website/_mailinglist/.ignore create mode 100644 website/flask_website.py rename website/{ => static}/logo.png (100%) create mode 100644 website/static/mailinglist.js create mode 100755 website/static/mailinglist.png rename website/{ => static}/mask.png (100%) rename website/{ => static}/ship.png (100%) create mode 100644 website/static/style.css create mode 100644 website/sync-librelist.py rename website/{ => templates}/404.html (87%) rename website/{ => templates}/index.html (65%) create mode 100644 website/templates/layout.html create mode 100644 website/templates/mailinglist/archive.html create mode 100644 website/templates/mailinglist/index.html create mode 100644 website/templates/mailinglist/layout.html create mode 100644 website/templates/mailinglist/show_thread.html diff --git a/.gitignore b/.gitignore index 5250e072..f250e7a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.pyo env dist +website/_mailinglist/* *.egg-info diff --git a/website/_mailinglist/.ignore b/website/_mailinglist/.ignore new file mode 100644 index 00000000..e69de29b diff --git a/website/flask_website.py b/website/flask_website.py new file mode 100644 index 00000000..5db93155 --- /dev/null +++ b/website/flask_website.py @@ -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'%s' % line) + elif line.startswith('>'): + line = Markup(u'%s' % 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/') +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////') +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) diff --git a/website/logo.png b/website/static/logo.png similarity index 100% rename from website/logo.png rename to website/static/logo.png diff --git a/website/static/mailinglist.js b/website/static/mailinglist.js new file mode 100644 index 00000000..abd3b30f --- /dev/null +++ b/website/static/mailinglist.js @@ -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($('
').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); +}); diff --git a/website/static/mailinglist.png b/website/static/mailinglist.png new file mode 100755 index 0000000000000000000000000000000000000000..96aeb5832d01e73a325076cec2d5eb2abfb3c5cd GIT binary patch literal 34718 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�Tq|Su0m;2C%9thyjA1Mqk$FV$$ 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 literal 0 HcmV?d00001 diff --git a/website/mask.png b/website/static/mask.png similarity index 100% rename from website/mask.png rename to website/static/mask.png diff --git a/website/ship.png b/website/static/ship.png similarity index 100% rename from website/ship.png rename to website/static/ship.png diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 00000000..d3ca4793 --- /dev/null +++ b/website/static/style.css @@ -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; } diff --git a/website/sync-librelist.py b/website/sync-librelist.py new file mode 100644 index 00000000..04dbe33d --- /dev/null +++ b/website/sync-librelist.py @@ -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() diff --git a/website/404.html b/website/templates/404.html similarity index 87% rename from website/404.html rename to website/templates/404.html index c4de489d..82b6e5be 100644 --- a/website/404.html +++ b/website/templates/404.html @@ -8,7 +8,7 @@ body, html { } body { - background: url(/ship.png) no-repeat center right; + background: url(/static/ship.png) no-repeat center right; } body:after { @@ -18,7 +18,7 @@ body:after { top: 0; bottom: 0; width: 30px; - background: url(/mask.png) repeat-y left; + background: url(/static/mask.png) repeat-y left; } a { color: #004B6B; } diff --git a/website/index.html b/website/templates/index.html similarity index 65% rename from website/index.html rename to website/templates/index.html index 5db6b850..322931d1 100644 --- a/website/index.html +++ b/website/templates/index.html @@ -1,27 +1,12 @@ - -Flask (A Python Microframework) - - -
+{% extends "layout.html" %} +{% block head %} + {{ super() }} + +{% endblock %} +{% block body %}

Flask

because sometimes a pocket knife is not enough

@@ -45,6 +30,7 @@ def hello():

Interested?

@@ -74,8 +60,6 @@ def hello(): create a new ticket or fork. If you just want to chat with fellow developers, go to #pocoo on irc.freenode.net. -
- Fork me on GitHub +{% endblock %} diff --git a/website/templates/layout.html b/website/templates/layout.html new file mode 100644 index 00000000..a48fa502 --- /dev/null +++ b/website/templates/layout.html @@ -0,0 +1,12 @@ + +{% block head %} +{% block title %}Welcome{% endblock %} | Flask (A Python Microframework) + + + +{% endblock %} +
+ {% block body %}{% endblock %} +
diff --git a/website/templates/mailinglist/archive.html b/website/templates/mailinglist/archive.html new file mode 100644 index 00000000..53c1d73d --- /dev/null +++ b/website/templates/mailinglist/archive.html @@ -0,0 +1,27 @@ +{% extends "mailinglist/layout.html" %} +{% block title %}Mailinglist Archive{% endblock %} +{% block mailbody %} +

Mailinglist Archive

+
    + {% for thread in threads %} +
  • {{ thread.title }} + + 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' }}) + {% endfor %} +
+ +{% endblock %} diff --git a/website/templates/mailinglist/index.html b/website/templates/mailinglist/index.html new file mode 100644 index 00000000..2cb236f4 --- /dev/null +++ b/website/templates/mailinglist/index.html @@ -0,0 +1,20 @@ +{% extends "mailinglist/layout.html" %} +{% block title %}Mailinglist{% endblock %} +{% block mailbody %} +

+ There is a mailinglist for Flask hosted on librelist you can use for both user requests + and development discussions. + +

+ To subscribe, send a mail to flask@librelist.com and reply + to the confirmation mail. Make sure to check your Spam folder, just in + case. To unsubscribe again, send a mail to + flask-unsubscribe@librelist.com and reply to the + confirmation mail. + +

+ The mailinglist archive + is synched every hour. Go there to read up old discussions grouped by + thread. +{% endblock %} diff --git a/website/templates/mailinglist/layout.html b/website/templates/mailinglist/layout.html new file mode 100644 index 00000000..8dfe4620 --- /dev/null +++ b/website/templates/mailinglist/layout.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% block head %} + {{ super() }} + +{% endblock %} +{% block body %} +

Flask Mailinglist

+
+ back to website + {% if request.endpoint != 'mailinglist_index' %}// + list information + {% endif %} + {% if request.endpoint != 'mailinglist_archive' %}// + go to archive + {% endif %} +
+ {% block mailbody %}{% endblock %} +{% endblock %} diff --git a/website/templates/mailinglist/show_thread.html b/website/templates/mailinglist/show_thread.html new file mode 100644 index 00000000..7be8adae --- /dev/null +++ b/website/templates/mailinglist/show_thread.html @@ -0,0 +1,34 @@ +{% extends "mailinglist/layout.html" %} +{% block title %}{{ thread.title }}{% endblock %} +{% block head %} + {{ super() }} + +{% endblock %} +{% block mailbody %} +

{{ thread.title }}

+ +
    + {% for mail in [thread.root] recursive %} +
  • + {% if mail.children %}
      {{ loop(mail.children) }}
    {% endif %} + {% endfor %} +
+ + {% for mail in [thread.root] recursive %} +
+

{{ mail.subject }}

+
+
From: +
{{ mail.author_name or mail.author_email }} +
Date: +
{{ mail.date.strftime('%Y-%m-%d @ %H:%M') }} +
+
{{ mail.rendered_text }}
+ {% if mail.children %} +
{{ loop(mail.children) }}
+ {% endif %} +
+ {% endfor %} +{% endblock %}