commit
577131fc3b
|
@ -0,0 +1,3 @@
|
|||
.venv/
|
||||
__pycache__/
|
||||
config-*.py
|
15
README.md
15
README.md
|
@ -1,3 +1,14 @@
|
|||
# Running
|
||||
# AfpyLogs
|
||||
|
||||
Web view of IRC logs from #afpy channel on Freenode.
|
||||
|
||||
|
||||
## Installing
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
gunicorn --workers 2 --bind 0.0.0.0:8000 app
|
||||
|
||||
Run with: `gunicorn -w 2 --paste deploy.ini`.
|
||||
|
|
142
afpylogs/app.py
142
afpylogs/app.py
|
@ -1,142 +0,0 @@
|
|||
import datetime
|
||||
import os
|
||||
from operator import itemgetter
|
||||
from collections import defaultdict
|
||||
import codecs
|
||||
|
||||
from webob import Request, Response
|
||||
from webob.exc import HTTPFound
|
||||
from paste.fileapp import FileApp
|
||||
from paste.deploy.config import ConfigMiddleware
|
||||
from paste.deploy.config import CONFIG
|
||||
|
||||
from afpylogs.parser import parse
|
||||
from afpylogs.utils import get_stylesheets, get_header
|
||||
|
||||
dirname = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def path_from_here(*args):
|
||||
return os.path.join(dirname, *args)
|
||||
|
||||
MONTH_TPL = u"""
|
||||
<dt class="portletHeader {active}"><a href="#">{date:%Y %m}</a></dt>
|
||||
<dd class="portletItem">{rendered_days}</dd>
|
||||
"""
|
||||
DAY_TPL = u'<a class="{active}" href="/archives/{date:%Y/%m/%d}/">{date:%d}</a> '
|
||||
|
||||
def render_archives(archives, active):
|
||||
"""Given an archive dict, return the corresponding HTML."""
|
||||
rendered = ['<dl class="portlet">']
|
||||
for year, months in sorted(archives.items(), key=itemgetter(0),
|
||||
reverse=True):
|
||||
for month, days in sorted(months.items(), key=itemgetter(0),
|
||||
reverse=True):
|
||||
is_active_month = (active.year, active.month) == (year, month)
|
||||
rendered.append(MONTH_TPL.format(
|
||||
active='active' if is_active_month else '',
|
||||
date=datetime.date(year, month, 1), # first day of the current month
|
||||
rendered_days=render_days(year, month, days, active)
|
||||
))
|
||||
|
||||
rendered.append('</dl>')
|
||||
return u'\n'.join(rendered)
|
||||
|
||||
def render_days(year, month, days, active):
|
||||
"""Return the HTML corresponding to all the available days of a given month."""
|
||||
rendered = []
|
||||
for day in sorted(days):
|
||||
current_date = datetime.date(year, month, day)
|
||||
is_active_day = active == current_date
|
||||
rendered.append(DAY_TPL.format(
|
||||
date=current_date,
|
||||
active='active' if is_active_day else '',
|
||||
))
|
||||
|
||||
return u'\n'.join(rendered)
|
||||
|
||||
def get_archives(path, active):
|
||||
"""Return HTML with links corresponding to all available days.
|
||||
The list is reversed chronologically, with most recent days appearing at
|
||||
the top."""
|
||||
archives = defaultdict(lambda: defaultdict(list))
|
||||
for filename in os.listdir(path):
|
||||
if not filename.endswith('.txt'):
|
||||
continue
|
||||
_, year, month, day = filename.replace('.txt','').split('-')
|
||||
archives[int(year)][int(month)].append(int(day))
|
||||
|
||||
return render_archives(archives, active)
|
||||
|
||||
def serve_static(filepath):
|
||||
return FileApp(filepath)
|
||||
|
||||
def r404(environ, start_response, text=u"File not found"):
|
||||
resp = Response(status=404, text=text)
|
||||
return resp(environ, start_response)
|
||||
|
||||
def redirect(environ, start_response, location):
|
||||
resp = HTTPFound(location=location)
|
||||
return resp(environ, start_response)
|
||||
|
||||
def application(environ, start_response):
|
||||
logfile_encoding = CONFIG.get('charset', 'utf-8') # XXX: actually depends on the date (see below)
|
||||
response_encoding = 'utf-8'
|
||||
template_encoding = 'utf-8'
|
||||
STATIC = ['jquery.js', 'transcript.js']
|
||||
template = CONFIG.get('template', path_from_here('template.html'))
|
||||
channel = CONFIG.get('channel', '')
|
||||
path = CONFIG.get('path', '')
|
||||
|
||||
path_info = Request(environ).path_info[1:] # remove leading /
|
||||
|
||||
if path_info in STATIC:
|
||||
app = serve_static(path_from_here(path_info))
|
||||
return app(environ, start_response)
|
||||
|
||||
if not path_info:
|
||||
date = datetime.date.today()
|
||||
location = 'archives/{:%Y/%m/%d/}'.format(date)
|
||||
return redirect(environ, start_response, location)
|
||||
else:
|
||||
try:
|
||||
date = datetime.datetime.strptime(path_info, 'archives/%Y/%m/%d/').date()
|
||||
except ValueError:
|
||||
return r404(environ, start_response)
|
||||
|
||||
# XXX: logfiles changed encoding at some point
|
||||
if date <= datetime.date(2011, 6, 5):
|
||||
logfile_encoding = 'latin1'
|
||||
|
||||
filename = 'log-{:%Y-%m-%d}.txt'.format(date)
|
||||
filepath = os.path.join(path, filename)
|
||||
|
||||
if not os.path.isfile(filepath):
|
||||
return r404(environ, start_response)
|
||||
|
||||
output = parse('#%s' % channel, filepath, logfile_encoding)
|
||||
archives = get_archives(path, active=date)
|
||||
with codecs.open(template, encoding=template_encoding) as fd:
|
||||
tpl = fd.read()
|
||||
|
||||
body = tpl % {
|
||||
'channel': channel,
|
||||
'body': output,
|
||||
'archives': archives,
|
||||
'stylesheets': get_stylesheets(),
|
||||
'header': get_header(),
|
||||
'date': date.strftime('%Y/%m/%d'),
|
||||
}
|
||||
|
||||
resp = Response(
|
||||
content_type="text/html",
|
||||
charset=response_encoding,
|
||||
)
|
||||
resp.text = body
|
||||
|
||||
return resp(environ, start_response)
|
||||
|
||||
def factory(global_config, **local_config):
|
||||
"""Aplication factory to expand configs"""
|
||||
conf = global_config.copy()
|
||||
conf.update(**local_config)
|
||||
return ConfigMiddleware(application, conf)
|
File diff suppressed because one or more lines are too long
|
@ -1,96 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import re
|
||||
import codecs
|
||||
from collections import namedtuple
|
||||
from datetime import time
|
||||
from html import escape
|
||||
|
||||
IRCLine = namedtuple('IRCLine', ['timestamp', 'nick', 'message'])
|
||||
|
||||
# Auto-linkification
|
||||
_re_link = re.compile(r'.*(https?://\S+).*') # detect URLs
|
||||
_tpl_link = '<a href="{link}" rel="nofollow">{link}</a>' # Template used to render them
|
||||
|
||||
# IRSSI log format
|
||||
_re_line = re.compile(r'''^
|
||||
(?P<hour>\d{2}):(?P<minute>\d{2})\s+
|
||||
((<(?P<nick>[^>]+)>)|(\[\#\]))\s+
|
||||
(?P<message>.*)
|
||||
$
|
||||
''', re.VERBOSE)
|
||||
|
||||
|
||||
_tpl_action_message = '<span class="action">[#] {message}</span>'
|
||||
_tpl_normal_message = (
|
||||
'<span class="barket"><</span>'
|
||||
'<span class="nick">{nick}</span>'
|
||||
'<span class="barket">></span>'
|
||||
' {message}'
|
||||
)
|
||||
_tpl_line = (
|
||||
'<div>'
|
||||
'<a id="{line_number}" href="#{line_number}">{timestamp:%H}:{timestamp:%M}</a>'
|
||||
' {message}'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
def get_lines(path, encoding='utf-8'):
|
||||
"""Yield IRCLine tuples from the given path to an irssi log file.
|
||||
Lines beginning with '---' will be omitted.
|
||||
All lines are stripped from whitespace before being returned."""
|
||||
with codecs.open(path, encoding=encoding) as f:
|
||||
for line in f:
|
||||
if line.startswith('---'):
|
||||
continue
|
||||
try:
|
||||
yield parse_line(line.strip())
|
||||
except ValueError:
|
||||
continue # TODO: log failure?
|
||||
|
||||
def linkify(message):
|
||||
"""Make <a> tags for URLs in the given message. Return the HTML."""
|
||||
for link in set(_re_link.findall(message)):
|
||||
message = message.replace(link, _tpl_link.format(link=link))
|
||||
return message
|
||||
|
||||
def render_line(**context):
|
||||
"""Render a line to HTML.
|
||||
Depending on whether the message is a normal or an "action" one, use
|
||||
a different template."""
|
||||
message_tpl = _tpl_normal_message if context['nick'] else _tpl_action_message
|
||||
context['message'] = message_tpl.format(**context)
|
||||
return _tpl_line.format(**context)
|
||||
|
||||
def parse_line(line):
|
||||
"""Parse a line from the log and return an IRCLine tuple."""
|
||||
match = _re_line.search(line)
|
||||
if not match:
|
||||
raise ValueError('Unhandled line %r' % line)
|
||||
data = match.groupdict()
|
||||
data['timestamp'] = time(hour=int(data.pop('hour')), minute=int(data.pop('minute')))
|
||||
return IRCLine(**data)
|
||||
|
||||
def parse(channel, path, encoding='utf-8'):
|
||||
"""Turn an irssi log line into HTML, URLs are converted to <a>.
|
||||
|
||||
Normal lines look like this:
|
||||
09:42 < bmispelon> this is the message
|
||||
|
||||
Some lines are "action" ones (usually, the result of a "/me ... command")
|
||||
and look like this:
|
||||
09:42 [#] this is the message
|
||||
"""
|
||||
stdout = []
|
||||
for i, line_tuple in enumerate(get_lines(path, encoding), start=1):
|
||||
message = linkify(escape(line_tuple.message))
|
||||
line_tuple = line_tuple._replace(message=message)
|
||||
stdout.append(render_line(
|
||||
line_number=i,
|
||||
**line_tuple._asdict()
|
||||
))
|
||||
return '\n'.join(stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
doctest.testmod()
|
|
@ -1,39 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Logs du chan #afpy pour le %(date)s</title>
|
||||
<style type="text/css">
|
||||
.barket {color: #ccc;}
|
||||
.nick {color: #8A6642;}
|
||||
.action {color: #a4c;}
|
||||
#logs_navigation dt.active a {
|
||||
color: rgb(117, 173, 10);
|
||||
}
|
||||
#logs_navigation dd a.active {
|
||||
font-weight: bold;
|
||||
color: rgb(117, 173, 10);
|
||||
}
|
||||
|
||||
</style>
|
||||
%(stylesheets)s
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="visual-portal-wrapper">
|
||||
%(header)s
|
||||
<div id="portal-columns" class="row">
|
||||
<div id="portal-column-content" class="cell width-3:4 position-1:4">
|
||||
<div id="letters"></div>
|
||||
<div id="contents">
|
||||
%(body)s
|
||||
</div>
|
||||
</div>
|
||||
<div id="portal-column-one" class="cell width-1:4 position-0">
|
||||
<div id="logs_navigation">%(archives)s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/jquery.js" type="text/javascript"></script>
|
||||
<script src="/transcript.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,14 +0,0 @@
|
|||
jQuery(document).ready(
|
||||
function(e) {
|
||||
$('#logs_navigation dt').each(function () {
|
||||
var dt=$(this);
|
||||
var dd = dt.next('dd');
|
||||
if (!dt.hasClass('active')) {
|
||||
dd.hide();
|
||||
}
|
||||
dt.click(function (e) {
|
||||
dd.slideToggle();
|
||||
e.preventDefault();
|
||||
})
|
||||
});
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
"""
|
||||
We do it this way so that we can re-use the afpy stylesheets and navigation menu
|
||||
that plone generates.
|
||||
We use a global variable as the document as a poor man's cache.
|
||||
"""
|
||||
from pyquery import PyQuery
|
||||
|
||||
|
||||
document = PyQuery(url='https://www.afpy.org')
|
||||
# Remove some stuff we don't want
|
||||
document('#portal-searchbox, #portal-personaltools-wrapper').remove()
|
||||
|
||||
def as_absolute(html):
|
||||
for attr in 'href', 'src':
|
||||
html = html.replace(f'{attr}="/', f'{attr}="https://afpy.org/')
|
||||
return html
|
||||
|
||||
def get_stylesheets():
|
||||
"""
|
||||
All the stylesheets used in the <head>, as a string.
|
||||
"""
|
||||
global document
|
||||
STYLESHEET_SELECTOR = 'head style, head link[@rel=stylesheet]'
|
||||
return as_absolute('\n'.join(str(elt) for elt in document(STYLESHEET_SELECTOR).items()))
|
||||
|
||||
def get_header():
|
||||
"""
|
||||
The navigation menu as a string.
|
||||
"""
|
||||
global document
|
||||
HEADER_SELECTOR = 'nav.menu'
|
||||
return as_absolute(str(document(HEADER_SELECTOR)))
|
|
@ -0,0 +1,89 @@
|
|||
# encoding: utf-8
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from bleach import Cleaner
|
||||
from bleach.linkifier import LinkifyFilter
|
||||
from flask import Flask, g, redirect, render_template, url_for
|
||||
|
||||
|
||||
application = Flask(__name__, template_folder=".")
|
||||
application.config.from_object("config")
|
||||
try:
|
||||
application.config.from_object(f"config-{application.config['ENV']}")
|
||||
except Exception as e:
|
||||
print(
|
||||
"Starting without specific configuration"
|
||||
f"file config-{application.config['ENV']}.py"
|
||||
)
|
||||
application.jinja_env.trim_blocks = application.config["JINJA_ENV"]["TRIM_BLOCKS"]
|
||||
application.jinja_env.lstrip_blocks = application.config["JINJA_ENV"]["LSTRIP_BLOCKS"]
|
||||
|
||||
LOG_PATTERN = re.compile(application.config["LOG_PATTERN"])
|
||||
BOLD_PATTERN = re.compile(application.config["BOLD_PATTERN"])
|
||||
|
||||
|
||||
def get_archives():
|
||||
archives = []
|
||||
dates = {"years": [], "months": {}, "days": {}}
|
||||
for filename in sorted(os.listdir(application.config["LOG_PATH"])):
|
||||
date = filename[:-4].split("-")[1:]
|
||||
archives.append(date)
|
||||
if date[0] not in dates["years"]:
|
||||
dates["years"].append(date[0])
|
||||
dates["months"][date[0]] = []
|
||||
if date[1] not in dates["months"][date[0]]:
|
||||
dates["months"][date[0]].append(date[1])
|
||||
dates["days"]["%s%s" % tuple(date[:2])] = []
|
||||
if date[2] not in dates["days"]["%s%s" % tuple(date[:2])]:
|
||||
dates["days"]["%s%s" % tuple(date[:2])].append(date[2])
|
||||
return archives, dates
|
||||
|
||||
|
||||
@application.route("/")
|
||||
@application.route("/archives/<year>")
|
||||
@application.route("/archives/<year>/<month>")
|
||||
@application.route("/archives/<year>/<month>/<day>")
|
||||
def archives(year=None, month=None, day=None):
|
||||
# Récupération des fichiers disponibles
|
||||
archives, g.dates = get_archives()
|
||||
# Récupération de la date souhaitée
|
||||
if (
|
||||
year is None
|
||||
or month is None
|
||||
or day is None
|
||||
or [year, month, day] not in archives
|
||||
):
|
||||
# Si date mal ou non fournie ou inexistante, on prend la dernière
|
||||
year = archives[-1][0]
|
||||
month = archives[-1][1]
|
||||
day = archives[-1][2]
|
||||
# Et on redirige proprement
|
||||
return redirect(url_for("archives", year=year, month=month, day=day))
|
||||
# Ok, on charge et on affiche le contenu du fichier
|
||||
filename = "log-%s-%s-%s.txt" % (year, month, day)
|
||||
filepath = os.path.join(application.config["LOG_PATH"], filename)
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
g.lines = []
|
||||
g.year, g.month, g.day = year, month, day
|
||||
cleaner = Cleaner(tags=["b"], filters=[LinkifyFilter])
|
||||
for line in lines:
|
||||
result = LOG_PATTERN.match(line)
|
||||
if result is not None:
|
||||
message = result.group("message")
|
||||
for text in BOLD_PATTERN.findall(message):
|
||||
message = message.replace(
|
||||
text, application.config["BOLD_HTML"].format(text=text)
|
||||
)
|
||||
message = cleaner.clean(message)
|
||||
g.lines.append(
|
||||
{
|
||||
"time": result.group("time"),
|
||||
"nick": result.group("nick"),
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template("template.html")
|
179
bootstrap.py
179
bootstrap.py
|
@ -1,179 +0,0 @@
|
|||
##############################################################################
|
||||
#
|
||||
# Copyright (c) 2006 Zope Foundation and Contributors.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# This software is subject to the provisions of the Zope Public License,
|
||||
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||||
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||||
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE.
|
||||
#
|
||||
##############################################################################
|
||||
"""Bootstrap a buildout-based project
|
||||
|
||||
Simply run this script in a directory containing a buildout.cfg.
|
||||
The script accepts buildout command-line options, so you can
|
||||
use the -c option to specify an alternate configuration file.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from optparse import OptionParser
|
||||
|
||||
tmpeggs = tempfile.mkdtemp()
|
||||
|
||||
usage = '''\
|
||||
[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
|
||||
|
||||
Bootstraps a buildout-based project.
|
||||
|
||||
Simply run this script in a directory containing a buildout.cfg, using the
|
||||
Python that you want bin/buildout to use.
|
||||
|
||||
Note that by using --find-links to point to local resources, you can keep
|
||||
this script from going over the network.
|
||||
'''
|
||||
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option("-v", "--version", help="use a specific zc.buildout version")
|
||||
|
||||
parser.add_option("-t", "--accept-buildout-test-releases",
|
||||
dest='accept_buildout_test_releases',
|
||||
action="store_true", default=False,
|
||||
help=("Normally, if you do not specify a --version, the "
|
||||
"bootstrap script and buildout gets the newest "
|
||||
"*final* versions of zc.buildout and its recipes and "
|
||||
"extensions for you. If you use this flag, "
|
||||
"bootstrap and buildout will get the newest releases "
|
||||
"even if they are alphas or betas."))
|
||||
parser.add_option("-c", "--config-file",
|
||||
help=("Specify the path to the buildout configuration "
|
||||
"file to be used."))
|
||||
parser.add_option("-f", "--find-links",
|
||||
help=("Specify a URL to search for buildout releases"))
|
||||
parser.add_option("--allow-site-packages",
|
||||
action="store_true", default=False,
|
||||
help=("Let bootstrap.py use existing site packages"))
|
||||
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
######################################################################
|
||||
# load/install setuptools
|
||||
|
||||
try:
|
||||
if options.allow_site_packages:
|
||||
import setuptools
|
||||
import pkg_resources
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
|
||||
ez = {}
|
||||
exec(urlopen('https://bitbucket.org/pypa/setuptools/downloads/ez_setup.py'
|
||||
).read(), ez)
|
||||
if not options.allow_site_packages:
|
||||
# ez_setup imports site, which adds site packages
|
||||
# this will remove them from the path to ensure that incompatible versions
|
||||
# of setuptools are not in the path
|
||||
import site
|
||||
# inside a virtualenv, there is no 'getsitepackages'.
|
||||
# We can't remove these reliably
|
||||
if hasattr(site, 'getsitepackages'):
|
||||
for sitepackage_path in site.getsitepackages():
|
||||
sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
|
||||
|
||||
setup_args = dict(to_dir=tmpeggs, download_delay=0)
|
||||
ez['use_setuptools'](**setup_args)
|
||||
import setuptools
|
||||
import pkg_resources
|
||||
|
||||
# This does not (always?) update the default working set. We will
|
||||
# do it.
|
||||
for path in sys.path:
|
||||
if path not in pkg_resources.working_set.entries:
|
||||
pkg_resources.working_set.add_entry(path)
|
||||
|
||||
######################################################################
|
||||
# Install buildout
|
||||
|
||||
ws = pkg_resources.working_set
|
||||
|
||||
cmd = [sys.executable, '-c',
|
||||
'from setuptools.command.easy_install import main; main()',
|
||||
'-mZqNxd', tmpeggs]
|
||||
|
||||
find_links = os.environ.get(
|
||||
'bootstrap-testing-find-links',
|
||||
options.find_links or
|
||||
('http://downloads.buildout.org/'
|
||||
if options.accept_buildout_test_releases else None)
|
||||
)
|
||||
if find_links:
|
||||
cmd.extend(['-f', find_links])
|
||||
|
||||
setuptools_path = ws.find(
|
||||
pkg_resources.Requirement.parse('setuptools')).location
|
||||
|
||||
requirement = 'zc.buildout'
|
||||
version = options.version
|
||||
if version is None and not options.accept_buildout_test_releases:
|
||||
# Figure out the most recent final version of zc.buildout.
|
||||
import setuptools.package_index
|
||||
_final_parts = '*final-', '*final'
|
||||
|
||||
def _final_version(parsed_version):
|
||||
for part in parsed_version:
|
||||
if (part[:1] == '*') and (part not in _final_parts):
|
||||
return False
|
||||
return True
|
||||
index = setuptools.package_index.PackageIndex(
|
||||
search_path=[setuptools_path])
|
||||
if find_links:
|
||||
index.add_find_links((find_links,))
|
||||
req = pkg_resources.Requirement.parse(requirement)
|
||||
if index.obtain(req) is not None:
|
||||
best = []
|
||||
bestv = None
|
||||
for dist in index[req.project_name]:
|
||||
distv = dist.parsed_version
|
||||
if _final_version(distv):
|
||||
if bestv is None or distv > bestv:
|
||||
best = [dist]
|
||||
bestv = distv
|
||||
elif distv == bestv:
|
||||
best.append(dist)
|
||||
if best:
|
||||
best.sort()
|
||||
version = best[-1].version
|
||||
if version:
|
||||
requirement = '=='.join((requirement, version))
|
||||
cmd.append(requirement)
|
||||
|
||||
import subprocess
|
||||
if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
|
||||
raise Exception(
|
||||
"Failed to execute command:\n%s",
|
||||
repr(cmd)[1:-1])
|
||||
|
||||
######################################################################
|
||||
# Import and run buildout
|
||||
|
||||
ws.add_entry(tmpeggs)
|
||||
ws.require(requirement)
|
||||
import zc.buildout.buildout
|
||||
|
||||
if not [a for a in args if '=' not in a]:
|
||||
args.append('bootstrap')
|
||||
|
||||
# if -c was provided, we push it back into args for buildout' main function
|
||||
if options.config_file is not None:
|
||||
args[0:0] = ['-c', options.config_file]
|
||||
|
||||
zc.buildout.buildout.main(args)
|
||||
shutil.rmtree(tmpeggs)
|
|
@ -0,0 +1,17 @@
|
|||
# encoding: utf-8
|
||||
|
||||
SECRET_KEY = "Choose a secret key"
|
||||
|
||||
JINJA_ENV = {
|
||||
"TRIM_BLOCKS": True,
|
||||
"LSTRIP_BLOCKS": True,
|
||||
}
|
||||
|
||||
LOG_PATH = "/var/www/logs.afpy.org"
|
||||
|
||||
# IRSSI log pattern
|
||||
DATE_FORMAT = "(\d+-\d+-\d+ )?(?P<time>\d\d:\d\d)"
|
||||
LOG_PATTERN = r"^%s\s+[<*]\s*(?P<nick>[^> ]+)[> ]\s+(?P<message>.*)$" % DATE_FORMAT
|
||||
|
||||
BOLD_PATTERN = r"\*[^*\s]+\*"
|
||||
BOLD_HTML = "<b>{text}</b>"
|
10
deploy.ini
10
deploy.ini
|
@ -1,10 +0,0 @@
|
|||
[app:main]
|
||||
use = egg:AfpyLogs
|
||||
path = /var/www/logs.afpy.org
|
||||
channel = #afpy
|
||||
|
||||
[server:main]
|
||||
use = egg:Paste#http
|
||||
host = 0.0.0.0
|
||||
port = 6085
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
flask
|
||||
gunicorn
|
|
@ -1,7 +0,0 @@
|
|||
[egg_info]
|
||||
tag_build = dev
|
||||
tag_svn_revision = true
|
||||
|
||||
[aliases]
|
||||
iw_upload = register sdist bdist_egg upload -r http://products.ingeniweb.com/catalog
|
||||
|
23
setup.py
23
setup.py
|
@ -1,23 +0,0 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
version = "0.1"
|
||||
|
||||
setup(
|
||||
name="AfpyLogs",
|
||||
version=version,
|
||||
keywords="",
|
||||
author="Gael Pasgrimaud",
|
||||
author_email="gael@gawel.org",
|
||||
url="",
|
||||
license="GPL",
|
||||
packages=find_packages(exclude=["ez_setup"]),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
# uncomment this to be able to run tests with setup.py
|
||||
# test_suite = "gp.transcript.tests.test_transcriptdocs.test_suite",
|
||||
install_requires=["paste", "pyquery", "webob", "pastedeploy", "gunicorn"],
|
||||
entry_points="""
|
||||
[paste.app_factory]
|
||||
main = afpylogs.app:factory
|
||||
""",
|
||||
)
|
|
@ -0,0 +1,76 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Logs du chan #afpy pour le </title>
|
||||
<style type="text/css">
|
||||
body {background-color: #000; color: #fff; font-family: "Verdana"}
|
||||
.time {color: #ffd738; font-size: .9em; font-weight: bold}
|
||||
.bracket {color: #ccc; font-size: .8em}
|
||||
.nick {color: #dAa642}
|
||||
.message {color: #eaeaea}
|
||||
.action {color: #a4c}
|
||||
.click {cursor: pointer}
|
||||
.off {display: none}
|
||||
.on {display: block}
|
||||
#content a {color: #ffd738; font-size: .9em; font-weight: bold; text-decoration: none}
|
||||
#content a:hover {text-decoration: underline}
|
||||
.calendar {color: #ffd738}
|
||||
.day {font-weight: bold}
|
||||
#calendar {padding-bottom: .6em; margin-bottom: .6em; border-bottom: 1px dashed #ffd738}
|
||||
#calendar a {color: #ffd738; text-decoration: none}
|
||||
#calendar a:hover {text-decoration: underline}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
function hide(id) {
|
||||
var els = document.getElementsByClassName("on");
|
||||
[].forEach.call(els, function (el){
|
||||
console.log(el.id);
|
||||
console.log(id);
|
||||
console.log(id.substr(0, el.id.length));
|
||||
console.log(id.substr(0, el.id.length) != el.id);
|
||||
if (el.id != id.substr(0, el.id.length)) {
|
||||
el.className = "off";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function display(id) {
|
||||
hide(id.toString());
|
||||
var el = document.getElementById(id);
|
||||
if (el!=null) {
|
||||
el.className = "on";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="calendar">
|
||||
<nav>
|
||||
{% for year in g.dates.years %}
|
||||
<span onclick="display('{{ year }}')" class="calendar click {%if year==g.year%}day{%endif%}">{{ year }}</span>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% for year in g.dates.years %}
|
||||
<nav id="{{year}}" class="{%if year==g.year%}on{%else%}off{%endif%}">
|
||||
{% for month in g.dates.months[year] %}
|
||||
<span onclick="display('{{ year }}{{ month }}')" class="calendar click {%if year==g.year and month==g.month%}day{%endif%}">{{ month }}</span>
|
||||
{% endfor %}
|
||||
{% for month in g.dates.months[year] %}
|
||||
<nav id="{{year}}{{month}}" class="{%if year==g.year and month==g.month%}on{%else%}off{%endif%}">
|
||||
{% for day in g.dates.days[year+month] %}
|
||||
<a href="{{url_for('archives', year=year, month=month, day=day)}}" class="calendar {%if year==g.year and month==g.month and day==g.day%}day{%endif%}">{{ day }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="content">
|
||||
{% for line in g.lines %}
|
||||
<span class="time">{{ line.time }}</span> <span class="bracket"><</span><span class="nick">{{ line.nick }}</span><span class="bracket">></span> <span class="message">{{ line.message|safe }}</span><br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue