')
+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 00000000..96aeb583
Binary files /dev/null and b/website/static/mailinglist.png differ
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.
-
-
+{% 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
+
+ {% 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 %}