New website; with mailinglist archives.
1
.gitignore
vendored
|
|
@ -3,4 +3,5 @@
|
|||
*.pyo
|
||||
env
|
||||
dist
|
||||
website/_mailinglist/*
|
||||
*.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 {
|
||||
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; }
|
||||
|
|
@ -1,27 +1,12 @@
|
|||
<!doctype html>
|
||||
<title>Flask (A Python Microframework)</title>
|
||||
<meta charset=utf-8>
|
||||
<style type=text/css>
|
||||
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 { 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; }
|
||||
blockquote { margin: 0; font-style: italic; color: #444; }
|
||||
.footer { font-size: 13px; color: #888; text-align: right; margin-top: 25px; }
|
||||
</style>
|
||||
<div class=box>
|
||||
{% extends "layout.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style type=text/css>
|
||||
h1 { margin: 0 0 30px 0; background: url(/static/logo.png) no-repeat center; height: 165px; }
|
||||
h1 span, p.tagline { display: none; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<h1><span>Flask</span></h1>
|
||||
<p class=tagline>because sometimes a pocket knife is not enough
|
||||
<blockquote>
|
||||
|
|
@ -45,6 +30,7 @@ def hello():
|
|||
<h2>Interested?</h2>
|
||||
<ul>
|
||||
<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://pypi.python.org/pypi/Flask>Download it from PyPI</a>
|
||||
</ul>
|
||||
|
|
@ -74,8 +60,6 @@ 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.
|
||||
|
||||
<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;"
|
||||
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 %}
|
||||