New website; with mailinglist archives.
1
.gitignore
vendored
|
|
@ -3,4 +3,5 @@
|
||||||
*.pyo
|
*.pyo
|
||||||
env
|
env
|
||||||
dist
|
dist
|
||||||
|
website/_mailinglist/*
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
|
|
||||||
0
website/_mailinglist/.ignore
Normal file
110
website/flask_website.py
Normal 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)
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
33
website/static/mailinglist.js
Normal 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
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
41
website/static/style.css
Normal 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
|
|
@ -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()
|
||||||
|
|
@ -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; }
|
||||||
|
|
@ -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>© 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 %}
|
||||||
12
website/templates/layout.html
Normal 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>© Copyright 2010 by <a href=http://lucumr.pocoo.org/>Armin Ronacher</a>
|
||||||
|
</div>
|
||||||
27
website/templates/mailinglist/archive.html
Normal 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) }}">« Previous</a>
|
||||||
|
{% else %}
|
||||||
|
<span class=disabled>« Previous</span>
|
||||||
|
{% endif %}
|
||||||
|
| <strong>{{ page }}</strong> |
|
||||||
|
{% if page < page_count %}
|
||||||
|
<a href="{{ url_for('mailinglist_archive', page=page + 1) }}">Next »</a>
|
||||||
|
{% else %}
|
||||||
|
<span class=disabled>Next »</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
20
website/templates/mailinglist/index.html
Normal 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 %}
|
||||||
21
website/templates/mailinglist/layout.html
Normal 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 %}
|
||||||
34
website/templates/mailinglist/show_thread.html
Normal 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 %}
|
||||||