from django.contrib.auth.models import User
from django.core.files import File
from django.template.loader import render_to_string
+from django.utils.datastructures import SortedDict
from django.utils.safestring import mark_safe
from django.utils.translation import get_language
from django.core.urlresolvers import reverse
tags = managers.TagDescriptor(Tag)
html_built = django.dispatch.Signal()
+ published = django.dispatch.Signal()
class AlreadyExists(Exception):
pass
book.reset_tag_counter()
book.reset_theme_counter()
+ cls.published.send(sender=book)
return book
def reset_tag_counter(self):
return objects
+ @classmethod
+ def book_list(cls, filter=None):
+ """Generates a hierarchical listing of all books.
+
+ Books are optionally filtered with a test function.
+
+ """
+
+ books_by_parent = {}
+ books = cls.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug')
+ if filter:
+ books = books.filter(filter).distinct()
+ book_ids = set((book.pk for book in books))
+ for book in books:
+ parent = book.parent_id
+ if parent not in book_ids:
+ parent = None
+ books_by_parent.setdefault(parent, []).append(book)
+ else:
+ for book in books:
+ books_by_parent.setdefault(book.parent_id, []).append(book)
+
+ orphans = []
+ books_by_author = SortedDict()
+ for tag in Tag.objects.filter(category='author'):
+ books_by_author[tag] = []
+
+ for book in books_by_parent.get(None,()):
+ authors = list(book.tags.filter(category='author'))
+ if authors:
+ for author in authors:
+ books_by_author[author].append(book)
+ else:
+ orphans.append(book)
+
+ return books_by_author, orphans, books_by_parent
+
def _has_factory(ftype):
has = lambda self: bool(getattr(self, "%s_file" % ftype))
else:
return ''
+@register.simple_tag
+def book_tree_texml(book_list, books_by_parent, depth=0):
+ return "".join("""
+ <cmd name='hspace'><parm>%(depth)dem</parm></cmd>%(title)s
+ <spec cat='align' />%(audiobook)s
+ <ctrl ch='\\' />
+ %(children)s
+ """ % {
+ "depth": depth,
+ "title": book.title,
+ "audiobook": "audiobook" if book.has_media('mp3') else "",
+ "children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
+ } for book in book_list)
+
@register.simple_tag
def all_editors(extra_info):
form = forms.SearchForm()
- books_by_parent = {}
- books = models.Book.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug')
- if filter:
- books = books.filter(filter).distinct()
- book_ids = set((book.pk for book in books))
- for book in books:
- parent = book.parent_id
- if parent not in book_ids:
- parent = None
- books_by_parent.setdefault(parent, []).append(book)
- else:
- for book in books:
- books_by_parent.setdefault(book.parent_id, []).append(book)
-
- orphans = []
- books_by_author = SortedDict()
+ books_by_author, orphans, books_by_parent = models.Book.book_list(filter)
books_nav = SortedDict()
- for tag in models.Tag.objects.filter(category='author'):
- books_by_author[tag] = []
-
- for book in books_by_parent.get(None,()):
- authors = list(book.tags.filter(category='author'))
- if authors:
- for author in authors:
- books_by_author[author].append(book)
- else:
- orphans.append(book)
-
for tag in books_by_author:
if books_by_author[tag]:
books_nav.setdefault(tag.sort_key[0], []).append(tag)
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+
+
+# import views here, so that signals are attached correctly
+from reporting.views import catalogue_pdf
--- /dev/null
+{% load catalogue_tags %}
+<TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
+ <TeXML escape="0">
+ \documentclass[a4paper, oneside, 11pt]{book}
+
+\usepackage[MeX]{polski}
+
+\usepackage[xetex]{graphicx}
+\usepackage{xunicode}
+\usepackage{xltxtra}
+
+\usepackage{scalefnt}
+\usepackage[colorlinks=true,linkcolor=black,setpagesize=false,urlcolor=black,xetex]{hyperref}
+
+\setmainfont [
+%ExternalLocation,
+UprightFont = JunicodeWL-Regular,
+ItalicFont = JunicodeWL-Italic,
+BoldFont = JunicodeWL-Regular,
+BoldItalicFont = JunicodeWL-Italic,
+SmallCapsFont = JunicodeWL-Regular,
+SmallCapsFeatures = {Letters={SmallCaps,UppercaseSmallCaps}},
+Numbers=OldStyle,
+Scale=1.04,
+LetterSpace=-1.0
+] {JunicodeWL}
+
+\pagestyle{plain}
+\usepackage{fancyhdr}
+
+\makeatletter
+
+\usepackage{color}
+\definecolor{theme}{gray}{.3}
+
+\setlength{\hoffset}{-1cm}
+\setlength{\oddsidemargin}{0pt}
+\setlength{\marginparsep}{0pt}
+\setlength{\marginparwidth}{0pt}
+
+\setlength{\voffset}{0pt}
+\setlength{\topmargin}{0pt}
+\setlength{\headheight}{0pt}
+\setlength{\headsep}{0pt}
+\setlength{\leftmargin}{0em}
+\setlength{\rightmargin}{0em}
+\setlength{\textheight}{24cm}
+\setlength{\textwidth}{18cm}
+
+
+\pagestyle{fancy}
+\fancyhf{}
+\renewcommand{\headrulewidth}{0pt}
+\renewcommand{\footrulewidth}{0pt}
+\lfoot{\footnotesize Katalog biblioteki internetowej WolneLektury.pl, \today}
+\cfoot{}
+\rfoot{\footnotesize \thepage}
+
+\clubpenalty=100000
+\widowpenalty=100000
+
+
+% see http://osdir.com/ml/tex.xetex/2005-10/msg00003.html
+\newsavebox{\ximagebox}\newlength{\ximageheight}
+\newsavebox{\xglyphbox}\newlength{\xglyphheight}
+\newcommand{\xbox}[1]
+{\savebox{\ximagebox}{#1}\settoheight{\ximageheight}{\usebox {\ximagebox}}%
+\savebox{\xglyphbox}{\char32}\settoheight{\xglyphheight}{\usebox {\xglyphbox}}%
+\raisebox{\ximageheight}[0pt][0pt]{%\raisebox{-\xglyphheight}[0pt] [0pt]{%
+\makebox[0pt][l]{\usebox{\xglyphbox}}}%}%
+\usebox{\ximagebox}%
+\raisebox{0pt}[0pt][0pt]{\makebox[0pt][r]{\usebox{\xglyphbox}}}}
+
+
+\newcommand{\name}[1]{%
+\vspace{.5em}\Large{#1}%
+}
+
+
+ \begin{document}
+
+ \noindent \begin{minipage}[t]{.35\textwidth}\vspace{0pt}
+ \href{http://www.wolnelektury.pl}{\xbox{\includegraphics[width=\textwidth]{wl-logo.png}}}
+ \vspace{1em}
+ \end{minipage}
+
+ \begin{minipage}[t]{.65\textwidth}\vspace{0pt}
+
+ \begin{flushright}
+
+ \section*{Katalog biblioteki internetowej \href{http://www.wolnelektury.pl/}{WolneLektury.pl}.}
+ stan na \today
+
+ \end{flushright}
+
+ \end{minipage}
+
+
+ \begin{tabular}{p{12cm} p{2cm}}
+
+ <TeXML escape="1">
+ {% book_tree_texml orphans books_by_parent %}
+ {% for author, group in books_by_author.items %}
+ {% if group %}
+ <cmd name="name"><parm>{{ author }}</parm></cmd>
+ <ctrl ch='\' />
+
+ {% book_tree_texml group books_by_parent %}
+ {% endif %}
+ {% endfor %}
+ </TeXML>
+
+ \end{tabular}
+ \end{document}
+ </TeXML>
+</TeXML>
\ No newline at end of file
--- /dev/null
+{% extends "base.html" %}
+{% load i18n %}
+{% load reporting_stats catalogue_tags %}
+
+{% block title %}Statystyka w WolneLektury.pl{% endblock %}
+
+{% block bodyid %}reports-stats{% endblock %}
+
+
+{% block body %}
+ <h1>Statystyka</h1>
+ {% search_form %}
+
+ <p><a href="{% url reporting_catalogue_pdf %}">Katalog biblioteki w formacie PDF.</a></p>
+
+ <table class="stats">
+ <tr><th>Utwory</th></tr>
+ <tr><td>Wszystkie utwory:</td><td>{% count_books_all %}</td></tr>
+ <tr><td>Utwory z własną treścią:</td><td>{% count_books_nonempty %}</td></tr>
+ <tr><td>Utwory bez własnej treści:</td><td>{% count_books_empty %}</td></tr>
+ <tr><td>Niezależne książki:</td><td>{% count_books_root %}</td></tr>
+
+ <tr><th>Media</th><th>Liczba</th><th>Rozmiar</th><th>Do wymiany</th></tr>
+ {% for mt in media_types %}
+ <tr><td>{{ mt.type }}:</td>
+ <td>{{ mt.count }}</td>
+ <td>{{ mt.size|filesizeformat }}</td>
+ <td>{{ mt.deprecated }}
+ {% for m in mt.deprecated_files %}
+ <br/><a href="{{ m.book.get_absolute_url }}">{{ m }}</a>
+ {% endfor %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+
+{% endblock %}
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import feedparser
+from functools import wraps
+import datetime
+
+from django import template
+
+from catalogue.models import Book, BookMedia
+
+
+register = template.Library()
+
+class StatsNode(template.Node):
+ def __init__(self, value, varname=None):
+ self.value = value
+ self.varname = varname
+
+ def render(self, context):
+ if self.varname:
+ context[self.varname] = self.value
+ return ''
+ else:
+ return self.value
+
+
+def register_counter(f):
+ """Turns a simple counting function into a registered counter tag.
+
+ You can run a counter tag as a simple {% tag_name %} tag, or
+ as {% tag_name var_name %} to store the result in a variable.
+
+ """
+ @wraps(f)
+ def wrapped(parser, token):
+ try:
+ tag_name, args = token.contents.split(None, 1)
+ except ValueError:
+ args = None
+ return StatsNode(f(), args)
+
+ return register.tag(wrapped)
+
+
+@register_counter
+def count_books_all():
+ return Book.objects.all().count()
+
+@register_counter
+def count_books_nonempty():
+ return Book.objects.exclude(html_file='').count()
+
+@register_counter
+def count_books_empty():
+ return Book.objects.filter(html_file='').count()
+
+@register_counter
+def count_books_root():
+ return Book.objects.filter(parent=None).count()
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('reporting.views',
+ url(r'^$', 'stats_page', name='reporting_stats'),
+ url(r'^katalog.pdf$', 'catalogue_pdf', name='reporting_catalogue_pdf'),
+)
+
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import os
+import os.path
+import shutil
+import subprocess
+from tempfile import mkdtemp
+from StringIO import StringIO
+from django.conf import settings
+import logging
+from django.http import HttpResponse
+from django.template.loader import render_to_string
+
+logger = logging.getLogger(__name__)
+
+
+def render_to_pdf(output_path, template, context=None, add_files=None):
+ """Renders a TeXML document into a PDF file.
+
+ :param str output_path: is where the PDF file should go
+ :param str template: is a TeXML template path
+ :param context: is context for rendering the template
+ :param dict add_files: a dictionary of additional files XeTeX will need
+ """
+
+ import Texml.processor
+ rendered = render_to_string(template, context)
+ texml = StringIO(rendered.encode('utf-8'))
+ tempdir = mkdtemp(prefix = "render_to_pdf-")
+ tex_path = os.path.join(tempdir, "doc.tex")
+ with open(tex_path, 'w') as tex_file:
+ Texml.processor.process(texml, tex_file, encoding="utf-8")
+
+ if add_files:
+ for add_name, src_file in add_files.items():
+ add_path = os.path.join(tempdir, add_name)
+ if hasattr(src_file, "read"):
+ with open(add_path, 'w') as add_file:
+ add_file.write(add_file.read())
+ else:
+ shutil.copy(src_file, add_path)
+
+ cwd = os.getcwd()
+ os.chdir(tempdir)
+ try:
+ subprocess.check_call(['xelatex', '-interaction=batchmode', tex_path],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ try:
+ os.makedirs(os.path.dirname(output_path))
+ except:
+ pass
+ shutil.move(os.path.join(tempdir, "doc.pdf"), output_path)
+ finally:
+ os.chdir(cwd)
+ shutil.rmtree(tempdir)
+
+
+def read_chunks(f, size=8192):
+ chunk = f.read(size)
+ while chunk:
+ yield chunk
+ chunk = f.read(size)
+
+
+def generated_file_view(file_name, mime_type, send_name=None, signals=None):
+ file_path = os.path.join(settings.MEDIA_ROOT, file_name)
+ file_url = os.path.join(settings.MEDIA_URL, file_name)
+ if send_name is None:
+ send_name = os.path.basename(file_name)
+
+ def signal_handler(*args, **kwargs):
+ os.unlink(file_path)
+
+ if signals:
+ for signal in signals:
+ signal.connect(signal_handler, weak=False)
+
+ def decorator(func):
+ def view(request, *args, **kwargs):
+ if not os.path.exists(file_path) or True:
+ func(file_path, *args, **kwargs)
+
+ if hasattr(send_name, "__call__"):
+ name = send_name()
+ else:
+ name = send_name
+
+ response = HttpResponse(mimetype=mime_type)
+ response['Content-Disposition'] = 'attachment; filename=%s' % name
+ with open(file_path) as f:
+ for chunk in read_chunks(f):
+ response.write(chunk)
+ return response
+ return view
+ return decorator
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import os.path
+from datetime import date
+from django.conf import settings
+from django.db.models import Count
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from catalogue.models import Book, BookMedia
+from reporting.utils import render_to_pdf, generated_file_view
+
+
+def stats_page(request):
+ media = BookMedia.objects.count()
+ media_types = BookMedia.objects.values('type').\
+ annotate(count=Count('type')).\
+ order_by('type')
+ for mt in media_types:
+ mt['size'] = sum(b.file.size for b in BookMedia.objects.filter(type=mt['type']))
+ if mt['type'] in ('mp3', 'ogg'):
+ deprecated = BookMedia.objects.filter(
+ type=mt['type'], source_sha1=None)
+ mt['deprecated'] = deprecated.count()
+ mt['deprecated_files'] = deprecated.order_by('book', 'name')
+ else:
+ mt['deprecated'] = '-'
+
+ return render_to_response('reporting/main.html',
+ locals(), context_instance=RequestContext(request))
+
+
+@generated_file_view('reports/katalog.pdf', 'application/pdf',
+ send_name=lambda: 'wolnelektury_%s.pdf' % date.today(),
+ signals=[Book.published])
+def catalogue_pdf(path):
+ books_by_author, orphans, books_by_parent = Book.book_list()
+ print books_by_parent
+ render_to_pdf(path, 'reporting/catalogue.texml', locals(), {
+ "wl-logo.png": os.path.join(settings.STATIC_ROOT, "img/logo-big.png"),
+ })
+++ /dev/null
-{% extends "base.html" %}
-{% load i18n %}
-{% load stats catalogue_tags %}
-
-{% block title %}Statystyka w WolneLektury.pl{% endblock %}
-
-{% block bodyid %}tagged-object-list{% endblock %}
-
-{% block body %}
- <h1>Statystyka</h1>
- {% search_form %}
-
- <table>
- <tr><th>Utwory</th></tr>
- <tr><td>Wszystkie utwory:</td><td>{% count_books_all %}</td></tr>
- <tr><td>Utwory z własną treścią:</td><td>{% count_books_nonempty %}</td></tr>
- <tr><td>Utwory bez własnej treści:</td><td>{% count_books_empty %}</td></tr>
- <tr><td>Niezależne książki:</td><td>{% count_books_root %}</td></tr>
-
- <tr><th>Media</th><th>Liczba</th><th>Rozmiar</th><th>Do wymiany</th></tr>
- {% for mt in media_types %}
- <tr><td>{{ mt.type }}:</td>
- <td>{{ mt.count }}</td>
- <td>{{ mt.size|filesizeformat }}</td>
- <td>{{ mt.deprecated }}</td>
- </tr>
- {% endfor %}
- </table>
-
-{% endblock %}
+++ /dev/null
-# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import feedparser
-from functools import wraps
-import datetime
-
-from django import template
-
-from catalogue.models import Book, BookMedia
-
-
-register = template.Library()
-
-class StatsNode(template.Node):
- def __init__(self, value, varname=None):
- self.value = value
- self.varname = varname
-
- def render(self, context):
- if self.varname:
- context[self.varname] = self.value
- return ''
- else:
- return self.value
-
-
-def register_counter(f):
- """Turns a simple counting function into a registered counter tag.
-
- You can run a counter tag as a simple {% tag_name %} tag, or
- as {% tag_name var_name %} to store the result in a variable.
-
- """
- @wraps(f)
- def wrapped(parser, token):
- try:
- tag_name, args = token.contents.split(None, 1)
- except ValueError:
- args = None
- return StatsNode(f(), args)
-
- return register.tag(wrapped)
-
-
-@register_counter
-def count_books_all():
- return Book.objects.all().count()
-
-@register_counter
-def count_books_nonempty():
- return Book.objects.exclude(html_file='').count()
-
-@register_counter
-def count_books_empty():
- return Book.objects.filter(html_file='').count()
-
-@register_counter
-def count_books_root():
- return Book.objects.filter(parent=None).count()
+++ /dev/null
-# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.conf.urls.defaults import *
-
-
-urlpatterns = patterns('stats.views',
- url(r'^$', 'stats_page', name='stats'),
-)
-
-
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
from django.contrib.sites.models import Site
from piwik.django.models import PiwikSite
from django.conf import settings
+++ /dev/null
-# -*- coding: utf-8 -*-
-# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-
-from django.db.models import Count
-from django.shortcuts import render_to_response
-from django.template import RequestContext
-
-from catalogue.models import Book, BookMedia
-
-
-def stats_page(request):
- media = BookMedia.objects.count()
- media_types = BookMedia.objects.values('type').\
- annotate(count=Count('type')).\
- order_by('type')
- for mt in media_types:
- mt['size'] = sum(b.file.size for b in BookMedia.objects.filter(type=mt['type']))
- mt['deprecated'] = BookMedia.objects.filter(
- type=mt['type'], source_sha1=None).count() if mt['type'] in ('mp3', 'ogg') else '-'
-
- return render_to_response('stats/main.html',
- locals(), context_instance=RequestContext(request))
'newtagging',
'opds',
'pdcounter',
+ 'reporting',
'sponsors',
'stats',
'suggest',
#footnotes .pagination {
margin-top: 1em;
}
+
+
+/* report */
+.stats td {
+ vertical-align: top;
+}
\ No newline at end of file
url(r'^sugestia/', include('suggest.urls')),
url(r'^lesmianator/', include('lesmianator.urls')),
url(r'^przypisy/', include('dictionary.urls')),
- url(r'^statystyka/', include('stats.urls')),
+ url(r'^raporty/', include('reporting.urls')),
# Static pages
url(r'^mozesz-nam-pomoc/$', 'infopages.views.infopage', {'slug': 'help_us'}, name='help_us'),