IGN:Development version of Planet calibre

This commit is contained in:
Kovid Goyal 2009-03-27 18:33:05 -07:00
parent c16bf2f26a
commit 5ba21804da
36 changed files with 2327 additions and 1 deletions

View File

@ -41,7 +41,6 @@ def freeze():
'/usr/lib/libxslt.so.1',
'/usr/lib/libxslt.so.1',
'/usr/lib/libgthread-2.0.so.0',
'/usr/lib/libglib-2.0.so.0',
'/usr/lib/gcc/i686-pc-linux-gnu/4.3.3/libstdc++.so.6',
'/usr/lib/libpng12.so.0',
'/usr/lib/libexslt.so.0',

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
__init__.py
"""

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
admin.py
"""
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from calibre.www.apps.feedjack import models
class LinkAdmin(admin.ModelAdmin):
pass
class SiteAdmin(admin.ModelAdmin):
list_display = ('url', 'name')
filter_vertical = ('links',)
class FeedAdmin(admin.ModelAdmin):
list_display = ('name', 'feed_url', 'title', 'last_modified', \
'is_active')
fieldsets = (
(None,
{'fields':('feed_url', 'name', 'shortname', 'is_active')}),
(_('Fields updated automatically by Feedjack'),
{'classes':('collapse',),
'fields':('title', 'tagline', 'link', 'etag', 'last_modified',
'last_checked'),
})
)
search_fields = ['feed_url', 'name', 'title']
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'link', 'author', 'date_modified')
search_fields = ['link', 'title']
date_hierarchy = 'date_modified'
filter_vertical = ('tags',)
class SubscriberAdmin(admin.ModelAdmin):
list_display = ('name', 'site', 'feed')
list_filter = ('site',)
admin.site.register(models.Link, LinkAdmin)
admin.site.register(models.Site, SiteAdmin)
admin.site.register(models.Feed, FeedAdmin)
admin.site.register(models.Post, PostAdmin)
admin.site.register(models.Subscriber, SubscriberAdmin)
#~

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
fjcache.py
"""
import md5
from django.core.cache import cache
from django.conf import settings
T_HOST = 1
T_ITEM = 2
T_META = 3
def str2md5(key):
""" Returns the md5 hash of a string.
"""
ctx = md5.new()
ctx.update(key.encode('utf-8'))
return ctx.hexdigest()
def getkey(stype, site_id=None, key=None):
""" Returns the cache key depending on it's type.
"""
base = '%s.feedjack' % (settings.CACHE_MIDDLEWARE_KEY_PREFIX)
if stype == T_HOST:
return '%s.hostcache' % base
elif stype == T_ITEM:
return '%s.%d.item.%s' % (base, site_id, str2md5(key))
elif stype == T_META:
return '%s.%d.meta' % (base, site_id)
def hostcache_get():
""" Retrieves the hostcache dictionary
"""
return cache.get(getkey(T_HOST))
def hostcache_set(value):
""" Sets the hostcache dictionary
"""
cache.set(getkey(T_HOST), value)
def cache_get(site_id, key):
""" Retrieves cache data from a site.
"""
return cache.get(getkey(T_ITEM, site_id, key))
def cache_set(site, key, data):
""" Sets cache data for a site.
All keys related to a site are stored in a meta key. This key is per-site.
"""
tkey = getkey(T_ITEM, site.id, key)
mkey = getkey(T_META, site.id)
tmp = cache.get(mkey)
longdur = 365*24*60*60
if not tmp:
tmp = [tkey]
cache.set(mkey, [tkey], longdur)
elif tkey not in tmp:
tmp.append(tkey)
cache.set(mkey, tmp, longdur)
cache.set(tkey, data, site.cache_duration)
def cache_delsite(site_id):
""" Removes all cache data from a site.
"""
mkey = getkey(T_META, site_id)
tmp = cache.get(mkey)
if not tmp:
return
for tkey in tmp:
cache.delete(tkey)
cache.delete(mkey)

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
fjcloud.py
"""
import math
from calibre.www.apps.feedjack import fjlib, fjcache
def getsteps(levels, tagmax):
""" Returns a list with the max number of posts per "tagcloud level"
"""
ntw = levels
if ntw < 2:
ntw = 2
steps = [(stp, 1 + (stp * int(math.ceil(tagmax * 1.0 / ntw - 1))))
for stp in range(ntw)]
# just to be sure~
steps[-1] = (steps[-1][0], tagmax+1)
return steps
def build(site, tagdata):
""" Returns the tag cloud for a list of tags.
"""
tagdata.sort()
# we get the most popular tag to calculate the tags' weigth
tagmax = 0
for tagname, tagcount in tagdata:
if tagcount > tagmax:
tagmax = tagcount
steps = getsteps(site.tagcloud_levels, tagmax)
tags = []
for tagname, tagcount in tagdata:
weight = [twt[0] \
for twt in steps if twt[1] >= tagcount and twt[1] > 0][0]+1
tags.append({'tagname':tagname, 'count':tagcount, 'weight':weight})
return tags
def cloudata(site):
""" Returns a dictionary with all the tag clouds related to a site.
"""
tagdata = fjlib.getquery("""
SELECT feedjack_post.feed_id, feedjack_tag.name, COUNT(*)
FROM feedjack_post, feedjack_subscriber, feedjack_tag,
feedjack_post_tags
WHERE feedjack_post.feed_id=feedjack_subscriber.feed_id AND
feedjack_post_tags.tag_id=feedjack_tag.id AND
feedjack_post_tags.post_id=feedjack_post.id AND
feedjack_subscriber.site_id=%d
GROUP BY feedjack_post.feed_id, feedjack_tag.name
ORDER BY feedjack_post.feed_id, feedjack_tag.name""" % site.id)
tagdict = {}
globaldict = {}
cloudict = {}
for feed_id, tagname, tagcount in tagdata:
if feed_id not in tagdict:
tagdict[feed_id] = []
tagdict[feed_id].append((tagname, tagcount))
try:
globaldict[tagname] += tagcount
except KeyError:
globaldict[tagname] = tagcount
tagdict[0] = globaldict.items()
for key, val in tagdict.items():
cloudict[key] = build(site, val)
return cloudict
def getcloud(site, feed_id=None):
""" Returns the tag cloud for a site or a site's subscriber.
"""
cloudict = fjcache.cache_get(site.id, 'tagclouds')
if not cloudict:
cloudict = cloudata(site)
fjcache.cache_set(site, 'tagclouds', cloudict)
# A subscriber's tag cloud has been requested.
if feed_id:
feed_id = int(feed_id)
if feed_id in cloudict:
return cloudict[feed_id]
return []
# The site tagcloud has been requested.
return cloudict[0]

View File

@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
fjlib.py
"""
from django.conf import settings
from django.db import connection
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
from django.utils.encoding import smart_unicode
from calibre.www.apps.feedjack import models, fjcache
class PageNotAnInteger(ValueError):
pass
# this is taken from django, it was removed in r8191
class ObjectPaginator(Paginator):
"""
Legacy ObjectPaginator class, for backwards compatibility.
Note that each method on this class that takes page_number expects a
zero-based page number, whereas the new API (Paginator/Page) uses one-based
page numbers.
"""
def __init__(self, query_set, num_per_page, orphans=0):
Paginator.__init__(self, query_set, num_per_page, orphans)
import warnings
warnings.warn("The ObjectPaginator is deprecated. Use django.core.paginator.Paginator instead.", DeprecationWarning)
# Keep these attributes around for backwards compatibility.
self.query_set = query_set
self.num_per_page = num_per_page
self._hits = self._pages = None
def validate_page_number(self, page_number):
try:
page_number = int(page_number) + 1
except ValueError:
raise PageNotAnInteger
return self.validate_number(page_number)
def get_page(self, page_number):
try:
page_number = int(page_number) + 1
except ValueError:
raise PageNotAnInteger
return self.page(page_number).object_list
def has_next_page(self, page_number):
return page_number < self.pages - 1
def has_previous_page(self, page_number):
return page_number > 0
def first_on_page(self, page_number):
"""
Returns the 1-based index of the first object on the given page,
relative to total objects found (hits).
"""
page_number = self.validate_page_number(page_number)
return (self.num_per_page * (page_number - 1)) + 1
def last_on_page(self, page_number):
"""
Returns the 1-based index of the last object on the given page,
relative to total objects found (hits).
"""
page_number = self.validate_page_number(page_number)
if page_number == self.num_pages:
return self.count
return page_number * self.num_per_page
# The old API called it "hits" instead of "count".
hits = Paginator.count
# The old API called it "pages" instead of "num_pages".
pages = Paginator.num_pages
def sitefeeds(siteobj):
""" Returns the active feeds of a site.
"""
return siteobj.subscriber_set.filter(is_active=True).select_related()
#return [subscriber['feed'] \
# for subscriber \
# in siteobj.subscriber_set.filter(is_active=True).values('feed')]
def getquery(query):
""" Performs a query and get the results.
"""
try:
conn = connection.cursor()
conn.execute(query)
data = conn.fetchall()
conn.close()
except:
data = []
return data
def get_extra_content(site, sfeeds_ids, ctx):
""" Returns extra data useful to the templates.
"""
# get the subscribers' feeds
if sfeeds_ids:
basefeeds = models.Feed.objects.filter(id__in=sfeeds_ids)
try:
ctx['feeds'] = basefeeds.order_by('name').select_related()
except:
ctx['feeds'] = []
# get the last_checked time
try:
ctx['last_modified'] = basefeeds.filter(\
last_checked__isnull=False).order_by(\
'-last_checked').select_related()[0].last_checked.ctime()
except:
ctx['last_modified'] = '??'
else:
ctx['feeds'] = []
ctx['last_modified'] = '??'
ctx['site'] = site
ctx['media_url'] = settings.MEDIA_URL
def get_posts_tags(object_list, sfeeds_obj, user_id, tag_name):
""" Adds a qtags property in every post object in a page.
Use "qtags" instead of "tags" in templates to avoid innecesary DB hits.
"""
tagd = {}
user_obj = None
tag_obj = None
tags = models.Tag.objects.extra(\
select={'post_id':'%s.%s' % (\
connection.ops.quote_name('feedjack_post_tags'), \
connection.ops.quote_name('post_id'))}, \
tables=['feedjack_post_tags'], \
where=[\
'%s.%s=%s.%s' % (\
connection.ops.quote_name('feedjack_tag'), \
connection.ops.quote_name('id'), \
connection.ops.quote_name('feedjack_post_tags'), \
connection.ops.quote_name('tag_id')), \
'%s.%s IN (%s)' % (\
connection.ops.quote_name('feedjack_post_tags'), \
connection.ops.quote_name('post_id'), \
', '.join([str(post.id) for post in object_list]))])
for tag in tags:
if tag.post_id not in tagd:
tagd[tag.post_id] = []
tagd[tag.post_id].append(tag)
if tag_name and tag.name == tag_name:
tag_obj = tag
subd = {}
for sub in sfeeds_obj:
subd[sub.feed.id] = sub
for post in object_list:
if post.id in tagd:
post.qtags = tagd[post.id]
else:
post.qtags = []
post.subscriber = subd[post.feed.id]
if user_id and int(user_id) == post.feed.id:
user_obj = post.subscriber
return user_obj, tag_obj
def getcurrentsite(http_post, path_info, query_string):
""" Returns the site id and the page cache key based on the request.
"""
url = u'http://%s/%s' % (smart_unicode(http_post.rstrip('/')), \
smart_unicode(path_info.lstrip('/')))
pagecachekey = '%s?%s' % (smart_unicode(path_info), \
smart_unicode(query_string))
hostdict = fjcache.hostcache_get()
if not hostdict:
hostdict = {}
if url not in hostdict:
default, ret = None, None
for site in models.Site.objects.all():
if url.startswith(site.url):
ret = site
break
if not default or site.default_site:
default = site
if not ret:
if default:
ret = default
else:
# Somebody is requesting something, but the user didn't create
# a site yet. Creating a default one...
ret = models.Site(name='Default Feedjack Site/Planet', \
url='www.feedjack.org', \
title='Feedjack Site Title', \
description='Feedjack Site Description. ' \
'Please change this in the admin interface.')
ret.save()
hostdict[url] = ret.id
fjcache.hostcache_set(hostdict)
return hostdict[url], pagecachekey
def get_paginator(site, sfeeds_ids, page=0, tag=None, user=None):
""" Returns a paginator object and a requested page from it.
"""
if tag:
try:
localposts = models.Tag.objects.get(name=tag).post_set.filter(\
feed__in=sfeeds_ids)
except:
raise Http404
else:
localposts = models.Post.objects.filter(feed__in=sfeeds_ids)
if user:
try:
localposts = localposts.filter(feed=user)
except:
raise Http404
if site.order_posts_by == 2:
localposts = localposts.order_by('-date_created', '-date_modified')
else:
localposts = localposts.order_by('-date_modified')
paginator = ObjectPaginator(localposts.select_related(), \
site.posts_per_page)
try:
object_list = paginator.get_page(page)
except InvalidPage:
if page == 0:
object_list = []
else:
raise Http404
return (paginator, object_list)
def page_context(request, site, tag=None, user_id=None, sfeeds=None):
""" Returns the context dictionary for a page view.
"""
sfeeds_obj, sfeeds_ids = sfeeds
try:
page = int(request.GET.get('page', 0))
except ValueError:
page = 0
paginator, object_list = get_paginator(site, sfeeds_ids, \
page=page, tag=tag, user=user_id)
if object_list:
# This will hit the DB once per page instead of once for every post in
# a page. To take advantage of this the template designer must call
# the qtags property in every item, instead of the default tags
# property.
user_obj, tag_obj = get_posts_tags(object_list, sfeeds_obj, \
user_id, tag)
else:
user_obj, tag_obj = None, None
ctx = {
'object_list': object_list,
'is_paginated': paginator.pages > 1,
'results_per_page': site.posts_per_page,
'has_next': paginator.has_next_page(page),
'has_previous': paginator.has_previous_page(page),
'page': page + 1,
'next': page + 1,
'previous': page - 1,
'pages': paginator.pages,
'hits' : paginator.hits,
}
get_extra_content(site, sfeeds_ids, ctx)
from calibre.www.apps.feedjack import fjcloud
ctx['tagcloud'] = fjcloud.getcloud(site, user_id)
ctx['user_id'] = user_id
ctx['user'] = user_obj
ctx['tag'] = tag_obj
ctx['subscribers'] = sfeeds_obj
return ctx

View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
# pylint: disable-msg=W0232, R0903, W0131
"""
feedjack
Gustavo Picón
models.py
"""
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
from calibre.www.apps.feedjack import fjcache
SITE_ORDERBY_CHOICES = (
(1, _('Date published.')),
(2, _('Date the post was first obtained.'))
)
class Link(models.Model):
name = models.CharField(_('name'), max_length=100, unique=True)
link = models.URLField(_('link'), verify_exists=True)
class Meta:
verbose_name = _('link')
verbose_name_plural = _('links')
class Admin:
pass
def __unicode__(self):
return u'%s (%s)' % (self.name, self.link)
class Site(models.Model):
name = models.CharField(_('name'), max_length=100)
url = models.CharField(_('url'),
max_length=100,
unique=True,
help_text=u'%s: %s, %s' % (smart_unicode(_('Example')),
u'http://www.planetexample.com',
u'http://www.planetexample.com:8000/foo'))
title = models.CharField(_('title'), max_length=200)
description = models.TextField(_('description'))
welcome = models.TextField(_('welcome'), null=True, blank=True)
greets = models.TextField(_('greets'), null=True, blank=True)
default_site = models.BooleanField(_('default site'), default=False)
posts_per_page = models.IntegerField(_('posts per page'), default=20)
order_posts_by = models.IntegerField(_('order posts by'), default=1,
choices=SITE_ORDERBY_CHOICES)
tagcloud_levels = models.IntegerField(_('tagcloud level'), default=5)
show_tagcloud = models.BooleanField(_('show tagcloud'), default=True)
use_internal_cache = models.BooleanField(_('use internal cache'), default=True)
cache_duration = models.IntegerField(_('cache duration'), default=60*60*24,
help_text=_('Duration in seconds of the cached pages and data.') )
links = models.ManyToManyField(Link, verbose_name=_('links'),
null=True, blank=True)
template = models.CharField(_('template'), max_length=100, null=True,
blank=True,
help_text=_('This template must be a directory in your feedjack '
'templates directory. Leave blank to use the default template.') )
class Meta:
verbose_name = _('site')
verbose_name_plural = _('sites')
ordering = ('name',)
def __unicode__(self):
return self.name
def save(self):
if not self.template:
self.template = 'default'
# there must be only ONE default site
defs = Site.objects.filter(default_site=True)
if not defs:
self.default_site = True
elif self.default_site:
for tdef in defs:
if tdef.id != self.id:
tdef.default_site = False
tdef.save()
self.url = self.url.rstrip('/')
fjcache.hostcache_set({})
super(Site, self).save()
class Feed(models.Model):
feed_url = models.URLField(_('feed url'), unique=True)
name = models.CharField(_('name'), max_length=100)
shortname = models.CharField(_('shortname'), max_length=50)
is_active = models.BooleanField(_('is active'), default=True,
help_text=_('If disabled, this feed will not be further updated.') )
title = models.CharField(_('title'), max_length=200, blank=True)
tagline = models.TextField(_('tagline'), blank=True)
link = models.URLField(_('link'), blank=True)
# http://feedparser.org/docs/http-etag.html
etag = models.CharField(_('etag'), max_length=50, blank=True)
last_modified = models.DateTimeField(_('last modified'), null=True, blank=True)
last_checked = models.DateTimeField(_('last checked'), null=True, blank=True)
class Meta:
verbose_name = _('feed')
verbose_name_plural = _('feeds')
ordering = ('name', 'feed_url',)
def __unicode__(self):
return u'%s (%s)' % (self.name, self.feed_url)
def save(self):
super(Feed, self).save()
class Tag(models.Model):
name = models.CharField(_('name'), max_length=50, unique=True)
class Meta:
verbose_name = _('tag')
verbose_name_plural = _('tags')
ordering = ('name',)
def __unicode__(self):
return self.name
def save(self):
super(Tag, self).save()
class Post(models.Model):
feed = models.ForeignKey(Feed, verbose_name=_('feed'), null=False, blank=False)
title = models.CharField(_('title'), max_length=255)
link = models.URLField(_('link'), )
content = models.TextField(_('content'), blank=True)
date_modified = models.DateTimeField(_('date modified'), null=True, blank=True)
guid = models.CharField(_('guid'), max_length=200, db_index=True)
author = models.CharField(_('author'), max_length=50, blank=True)
author_email = models.EmailField(_('author email'), blank=True)
comments = models.URLField(_('comments'), blank=True)
tags = models.ManyToManyField(Tag, verbose_name=_('tags'))
date_created = models.DateField(_('date created'), auto_now_add=True)
class Meta:
verbose_name = _('post')
verbose_name_plural = _('posts')
ordering = ('-date_modified',)
unique_together = (('feed', 'guid'),)
def __unicode__(self):
return self.title
def save(self):
super(Post, self).save()
def get_absolute_url(self):
return self.link
class Subscriber(models.Model):
site = models.ForeignKey(Site, verbose_name=_('site') )
feed = models.ForeignKey(Feed, verbose_name=_('feed') )
name = models.CharField(_('name'), max_length=100, null=True, blank=True,
help_text=_('Keep blank to use the Feed\'s original name.') )
shortname = models.CharField(_('shortname'), max_length=50, null=True,
blank=True,
help_text=_('Keep blank to use the Feed\'s original shortname.') )
is_active = models.BooleanField(_('is active'), default=True,
help_text=_('If disabled, this subscriber will not appear in the site or '
'in the site\'s feed.') )
class Meta:
verbose_name = _('subscriber')
verbose_name_plural = _('subscribers')
ordering = ('site', 'name', 'feed')
unique_together = (('site', 'feed'),)
def __unicode__(self):
return u'%s in %s' % (self.feed, self.site)
def get_cloud(self):
from calibre.www.apps.feedjack import fjcloud
return fjcloud.getcloud(self.site, self.feed.id)
def save(self):
if not self.name:
self.name = self.feed.name
if not self.shortname:
self.shortname = self.feed.shortname
super(Subscriber, self).save()

View File

@ -0,0 +1,506 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
update_feeds.py
"""
import os
import time
import optparse
import datetime
import socket
import traceback
import sys
import feedparser
try:
import threadpool
except ImportError:
threadpool = None
VERSION = '0.9.16'
URL = 'http://www.feedjack.org/'
USER_AGENT = 'Feedjack %s - %s' % (VERSION, URL)
SLOWFEED_WARNING = 10
ENTRY_NEW, ENTRY_UPDATED, ENTRY_SAME, ENTRY_ERR = range(4)
FEED_OK, FEED_SAME, FEED_ERRPARSE, FEED_ERRHTTP, FEED_ERREXC = range(5)
def encode(tstr):
""" Encodes a unicode string in utf-8
"""
if not tstr:
return ''
# this is _not_ pretty, but it works
try:
return tstr.encode('utf-8', "xmlcharrefreplace")
except UnicodeDecodeError:
# it's already UTF8.. sigh
return tstr.decode('utf-8').encode('utf-8')
def prints(tstr):
""" lovely unicode
"""
sys.stdout.write('%s\n' % (tstr.encode(sys.getdefaultencoding(),
'replace')))
sys.stdout.flush()
def mtime(ttime):
""" datetime auxiliar function.
"""
return datetime.datetime.fromtimestamp(time.mktime(ttime))
class ProcessEntry:
def __init__(self, feed, options, entry, postdict, fpf):
self.feed = feed
self.options = options
self.entry = entry
self.postdict = postdict
self.fpf = fpf
def get_tags(self):
""" Returns a list of tag objects from an entry.
"""
from calibre.www.apps.feedjack import models
fcat = []
if self.entry.has_key('tags'):
for tcat in self.entry.tags:
if tcat.label != None:
term = tcat.label
else:
term = tcat.term
qcat = term.strip()
if ',' in qcat or '/' in qcat:
qcat = qcat.replace(',', '/').split('/')
else:
qcat = [qcat]
for zcat in qcat:
tagname = zcat.lower()
while ' ' in tagname:
tagname = tagname.replace(' ', ' ')
tagname = tagname.strip()
if not tagname or tagname == ' ':
continue
if not models.Tag.objects.filter(name=tagname):
cobj = models.Tag(name=tagname)
cobj.save()
fcat.append(models.Tag.objects.get(name=tagname))
return fcat
def get_entry_data(self):
""" Retrieves data from a post and returns it in a tuple.
"""
try:
link = self.entry.link
except AttributeError:
link = self.feed.link
try:
title = self.entry.title
except AttributeError:
title = link
guid = self.entry.get('id', title)
if self.entry.has_key('author_detail'):
author = self.entry.author_detail.get('name', '')
author_email = self.entry.author_detail.get('email', '')
else:
author, author_email = '', ''
if not author:
author = self.entry.get('author', self.entry.get('creator', ''))
if not author_email:
# this should be optional~
author_email = 'nospam@nospam.com'
try:
content = self.entry.content[0].value
except:
content = self.entry.get('summary',
self.entry.get('description', ''))
if self.entry.has_key('modified_parsed'):
date_modified = mtime(self.entry.modified_parsed)
else:
date_modified = None
fcat = self.get_tags()
comments = self.entry.get('comments', '')
return (link, title, guid, author, author_email, content,
date_modified, fcat, comments)
def process(self):
""" Process a post in a feed and saves it in the DB if necessary.
"""
from calibre.www.apps.feedjack import models
(link, title, guid, author, author_email, content, date_modified,
fcat, comments) = self.get_entry_data()
if False and self.options.verbose:
prints(u'[%d] Entry\n' \
u' title: %s\n' \
u' link: %s\n' \
u' guid: %s\n' \
u' author: %s\n' \
u' author_email: %s\n' \
u' tags: %s' % (
self.feed.id,
title, link, guid, author, author_email,
u' '.join(tcat.name for tcat in fcat)))
if guid in self.postdict:
tobj = self.postdict[guid]
if tobj.content != content or (date_modified and
tobj.date_modified != date_modified):
retval = ENTRY_UPDATED
if self.options.verbose:
prints('[%d] Updating existing post: %s' % (
self.feed.id, link))
if not date_modified:
# damn non-standard feeds
date_modified = tobj.date_modified
tobj.title = title
tobj.link = link
tobj.content = content
tobj.guid = guid
tobj.date_modified = date_modified
tobj.author = author
tobj.author_email = author_email
tobj.comments = comments
tobj.tags.clear()
[tobj.tags.add(tcat) for tcat in fcat]
tobj.save()
else:
retval = ENTRY_SAME
if self.options.verbose:
prints('[%d] Post has not changed: %s' % (self.feed.id,
link))
else:
retval = ENTRY_NEW
if self.options.verbose:
prints('[%d] Saving new post: %s' % (self.feed.id, link))
if not date_modified and self.fpf:
# if the feed has no date_modified info, we use the feed
# mtime or the current time
if self.fpf.feed.has_key('modified_parsed'):
date_modified = mtime(self.fpf.feed.modified_parsed)
elif self.fpf.has_key('modified'):
date_modified = mtime(self.fpf.modified)
if not date_modified:
date_modified = datetime.datetime.now()
tobj = models.Post(feed=self.feed, title=title, link=link,
content=content, guid=guid, date_modified=date_modified,
author=author, author_email=author_email,
comments=comments)
tobj.save()
[tobj.tags.add(tcat) for tcat in fcat]
return retval
class ProcessFeed:
def __init__(self, feed, options):
self.feed = feed
self.options = options
self.fpf = None
def process_entry(self, entry, postdict):
""" wrapper for ProcessEntry
"""
entry = ProcessEntry(self.feed, self.options, entry, postdict,
self.fpf)
ret_entry = entry.process()
del entry
return ret_entry
def process(self):
""" Downloads and parses a feed.
"""
from calibre.www.apps.feedjack import models
ret_values = {
ENTRY_NEW:0,
ENTRY_UPDATED:0,
ENTRY_SAME:0,
ENTRY_ERR:0}
prints(u'[%d] Processing feed %s' % (self.feed.id,
self.feed.feed_url))
# we check the etag and the modified time to save bandwith and
# avoid bans
try:
self.fpf = feedparser.parse(self.feed.feed_url,
agent=USER_AGENT,
etag=self.feed.etag)
except:
prints('! ERROR: feed cannot be parsed')
return FEED_ERRPARSE, ret_values
if hasattr(self.fpf, 'status'):
if self.options.verbose:
prints(u'[%d] HTTP status %d: %s' % (self.feed.id,
self.fpf.status,
self.feed.feed_url))
if self.fpf.status == 304:
# this means the feed has not changed
if self.options.verbose:
prints('[%d] Feed has not changed since ' \
'last check: %s' % (self.feed.id,
self.feed.feed_url))
return FEED_SAME, ret_values
if self.fpf.status >= 400:
# http error, ignore
prints('[%d] !HTTP_ERROR! %d: %s' % (self.feed.id,
self.fpf.status,
self.feed.feed_url))
return FEED_ERRHTTP, ret_values
if hasattr(self.fpf, 'bozo') and self.fpf.bozo:
prints('[%d] !BOZO! Feed is not well formed: %s' % (
self.feed.id, self.feed.feed_url))
# the feed has changed (or it is the first time we parse it)
# saving the etag and last_modified fields
self.feed.etag = self.fpf.get('etag', '')
# some times this is None (it never should) *sigh*
if self.feed.etag is None:
self.feed.etag = ''
try:
self.feed.last_modified = mtime(self.fpf.modified)
except:
pass
self.feed.title = self.fpf.feed.get('title', '')[0:254]
self.feed.tagline = self.fpf.feed.get('tagline', '')
self.feed.link = self.fpf.feed.get('link', '')
self.feed.last_checked = datetime.datetime.now()
if False and self.options.verbose:
prints(u'[%d] Feed info for: %s\n' \
u' title %s\n' \
u' tagline %s\n' \
u' link %s\n' \
u' last_checked %s' % (
self.feed.id, self.feed.feed_url, self.feed.title,
self.feed.tagline, self.feed.link, self.feed.last_checked))
guids = []
for entry in self.fpf.entries:
if entry.get('id', ''):
guids.append(entry.get('id', ''))
elif entry.title:
guids.append(entry.title)
elif entry.link:
guids.append(entry.link)
self.feed.save()
if guids:
postdict = dict([(post.guid, post)
for post in models.Post.objects.filter(
feed=self.feed.id).filter(guid__in=guids)])
else:
postdict = {}
for entry in self.fpf.entries:
try:
ret_entry = self.process_entry(entry, postdict)
except:
(etype, eobj, etb) = sys.exc_info()
print '[%d] ! -------------------------' % (self.feed.id,)
print traceback.format_exception(etype, eobj, etb)
traceback.print_exception(etype, eobj, etb)
print '[%d] ! -------------------------' % (self.feed.id,)
ret_entry = ENTRY_ERR
ret_values[ret_entry] += 1
self.feed.save()
return FEED_OK, ret_values
class Dispatcher:
def __init__(self, options, num_threads):
self.options = options
self.entry_stats = {
ENTRY_NEW:0,
ENTRY_UPDATED:0,
ENTRY_SAME:0,
ENTRY_ERR:0}
self.feed_stats = {
FEED_OK:0,
FEED_SAME:0,
FEED_ERRPARSE:0,
FEED_ERRHTTP:0,
FEED_ERREXC:0}
self.entry_trans = {
ENTRY_NEW:'new',
ENTRY_UPDATED:'updated',
ENTRY_SAME:'same',
ENTRY_ERR:'error'}
self.feed_trans = {
FEED_OK:'ok',
FEED_SAME:'unchanged',
FEED_ERRPARSE:'cant_parse',
FEED_ERRHTTP:'http_error',
FEED_ERREXC:'exception'}
self.entry_keys = sorted(self.entry_trans.keys())
self.feed_keys = sorted(self.feed_trans.keys())
if threadpool:
self.tpool = threadpool.ThreadPool(num_threads)
else:
self.tpool = None
self.time_start = datetime.datetime.now()
def add_job(self, feed):
""" adds a feed processing job to the pool
"""
if self.tpool:
req = threadpool.WorkRequest(self.process_feed_wrapper,
(feed,))
self.tpool.putRequest(req)
else:
# no threadpool module, just run the job
self.process_feed_wrapper(feed)
def process_feed_wrapper(self, feed):
""" wrapper for ProcessFeed
"""
start_time = datetime.datetime.now()
try:
pfeed = ProcessFeed(feed, self.options)
ret_feed, ret_entries = pfeed.process()
del pfeed
except:
(etype, eobj, etb) = sys.exc_info()
print '[%d] ! -------------------------' % (feed.id,)
print traceback.format_exception(etype, eobj, etb)
traceback.print_exception(etype, eobj, etb)
print '[%d] ! -------------------------' % (feed.id,)
ret_feed = FEED_ERREXC
ret_entries = {}
delta = datetime.datetime.now() - start_time
if delta.seconds > SLOWFEED_WARNING:
comment = u' (SLOW FEED!)'
else:
comment = u''
prints(u'[%d] Processed %s in %s [%s] [%s]%s' % (
feed.id, feed.feed_url, unicode(delta),
self.feed_trans[ret_feed],
u' '.join(u'%s=%d' % (self.entry_trans[key],
ret_entries[key]) for key in self.entry_keys),
comment))
self.feed_stats[ret_feed] += 1
for key, val in ret_entries.items():
self.entry_stats[key] += val
return ret_feed, ret_entries
def poll(self):
""" polls the active threads
"""
if not self.tpool:
# no thread pool, nothing to poll
return
while True:
try:
time.sleep(0.2)
self.tpool.poll()
except KeyboardInterrupt:
prints('! Cancelled by user')
break
except threadpool.NoResultsPending:
prints(u'* DONE in %s\n* Feeds: %s\n* Entries: %s' % (
unicode(datetime.datetime.now() - self.time_start),
u' '.join(u'%s=%d' % (self.feed_trans[key],
self.feed_stats[key])
for key in self.feed_keys),
u' '.join(u'%s=%d' % (self.entry_trans[key],
self.entry_stats[key])
for key in self.entry_keys)
))
break
def main():
""" Main function. Nothing to see here. Move along.
"""
parser = optparse.OptionParser(usage='%prog [options]',
version=USER_AGENT)
parser.add_option('--settings',
help='Python path to settings module. If this isn\'t provided, ' \
'the DJANGO_SETTINGS_MODULE enviroment variable will be used.')
parser.add_option('-f', '--feed', action='append', type='int',
help='A feed id to be updated. This option can be given multiple ' \
'times to update several feeds at the same time ' \
'(-f 1 -f 4 -f 7).')
parser.add_option('-s', '--site', type='int',
help='A site id to update.')
parser.add_option('-v', '--verbose', action='store_true',
dest='verbose', default=False, help='Verbose output.')
parser.add_option('-t', '--timeout', type='int', default=10,
help='Wait timeout in seconds when connecting to feeds.')
parser.add_option('-w', '--workerthreads', type='int', default=10,
help='Worker threads that will fetch feeds in parallel.')
options = parser.parse_args()[0]
if options.settings:
os.environ["DJANGO_SETTINGS_MODULE"] = options.settings
from calibre.www.apps.feedjack import models, fjcache
# settting socket timeout (default= 10 seconds)
socket.setdefaulttimeout(options.timeout)
# our job dispatcher
disp = Dispatcher(options, options.workerthreads)
prints('* BEGIN: %s' % (unicode(datetime.datetime.now()),))
if options.feed:
feeds = models.Feed.objects.filter(id__in=options.feed)
known_ids = []
for feed in feeds:
known_ids.append(feed.id)
disp.add_job(feed)
for feed in options.feed:
if feed not in known_ids:
prints('! Unknown feed id: %d' % (feed,))
elif options.site:
try:
site = models.Site.objects.get(pk=int(options.site))
except models.Site.DoesNotExist:
site = None
prints('! Unknown site id: %d' % (options.site,))
if site:
feeds = [sub.feed for sub in site.subscriber_set.all()]
for feed in feeds:
disp.add_job(feed)
else:
for feed in models.Feed.objects.filter(is_active=True):
disp.add_job(feed)
disp.poll()
# removing the cached data in all sites, this will only work with the
# memcached, db and file backends
[fjcache.cache_delsite(site.id) for site in models.Site.objects.all()]
if threadpool:
tcom = u'%d threads' % (options.workerthreads,)
else:
tcom = u'no threadpool module available, no parallel fetching'
prints('* END: %s (%s)' % (unicode(datetime.datetime.now()), tcom))
if __name__ == '__main__':
main()

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
urls.py
"""
from django.conf.urls.defaults import patterns
from django.views.generic.simple import redirect_to
from calibre.www.apps.feedjack import views
urlpatterns = patterns('',
(r'^rss20.xml$', redirect_to,
{'url':'/feed/rss/'}),
(r'^feed/$', redirect_to,
{'url':'/feed/atom/'}),
(r'^feed/rss/$', views.rssfeed),
(r'^feed/atom/$', views.atomfeed),
(r'^feed/user/(?P<user>\d+)/tag/(?P<tag>.*)/$', redirect_to,
{'url':'/feed/atom/user/%(user)s/tag/%(tag)s/'}),
(r'^feed/user/(?P<user>\d+)/$', redirect_to,
{'url':'/feed/atom/user/%(user)s/'}),
(r'^feed/tag/(?P<tag>.*)/$', redirect_to,
{'url':'/feed/atom/tag/%(tag)s/'}),
(r'^feed/atom/user/(?P<user>\d+)/tag/(?P<tag>.*)/$', views.atomfeed),
(r'^feed/atom/user/(?P<user>\d+)/$', views.atomfeed),
(r'^feed/atom/tag/(?P<tag>.*)/$', views.atomfeed),
(r'^feed/rss/user/(?P<user>\d+)/tag/(?P<tag>.*)/$', views.rssfeed),
(r'^feed/rss/user/(?P<user>\d+)/$', views.rssfeed),
(r'^feed/rss/tag/(?P<tag>.*)/$', views.rssfeed),
(r'^user/(?P<user>\d+)/tag/(?P<tag>.*)/$', views.mainview),
(r'^user/(?P<user>\d+)/$', views.mainview),
(r'^tag/(?P<tag>.*)/$', views.mainview),
(r'^opml/$', views.opml),
(r'^foaf/$', views.foaf),
(r'^$', views.mainview),
)
#~

View File

@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""
feedjack
Gustavo Picón
views.py
"""
from django.utils import feedgenerator
from django.shortcuts import render_to_response
from django.http import HttpResponse
from django.utils.cache import patch_vary_headers
from django.template import Context, loader
from calibre.www.apps.feedjack import models, fjlib, fjcache
def initview(request):
""" Retrieves the basic data needed by all feeds (host, feeds, etc)
Returns a tuple of:
1. A valid cached response or None
2. The current site object
3. The cache key
4. The subscribers for the site (objects)
5. The feeds for the site (ids)
"""
site_id, cachekey = fjlib.getcurrentsite(request.META['HTTP_HOST'], \
request.META.get('REQUEST_URI', request.META.get('PATH_INFO', '/')), \
request.META['QUERY_STRING'])
response = fjcache.cache_get(site_id, cachekey)
if response:
return response, None, cachekey, [], []
site = models.Site.objects.get(pk=site_id)
sfeeds_obj = fjlib.sitefeeds(site)
sfeeds_ids = [subscriber.feed.id for subscriber in sfeeds_obj]
return None, site, cachekey, sfeeds_obj, sfeeds_ids
def blogroll(request, btype):
""" View that handles the generation of blogrolls.
"""
response, site, cachekey, sfeeds_obj, sfeeds_ids = initview(request)
if response:
return response
# for some reason this isn't working:
#
#response = render_to_response('feedjack/%s.xml' % btype, \
# fjlib.get_extra_content(site, sfeeds_ids))
#response.mimetype = 'text/xml; charset=utf-8'
#
# so we must use this:
template = loader.get_template('feedjack/%s.xml' % btype)
ctx = {}
fjlib.get_extra_content(site, sfeeds_ids, ctx)
ctx = Context(ctx)
response = HttpResponse(template.render(ctx) , \
mimetype='text/xml; charset=utf-8')
patch_vary_headers(response, ['Host'])
fjcache.cache_set(site, cachekey, response)
return response
def foaf(request):
""" View that handles the generation of the FOAF blogroll.
"""
return blogroll(request, 'foaf')
def opml(request):
""" View that handles the generation of the OPML blogroll.
"""
return blogroll(request, 'opml')
def buildfeed(request, feedclass, tag=None, user=None):
""" View that handles the feeds.
"""
response, site, cachekey, sfeeds_obj, sfeeds_ids = initview(request)
if response:
return response
object_list = fjlib.get_paginator(site, sfeeds_ids, page=0, tag=tag, \
user=user)[1]
feed = feedclass(\
title=site.title,
link=site.url,
description=site.description,
feed_url='%s/%s' % (site.url, '/feed/rss/'))
for post in object_list:
feed.add_item( \
title = '%s: %s' % (post.feed.name, post.title), \
link = post.link, \
description = post.content, \
author_email = post.author_email, \
author_name = post.author, \
pubdate = post.date_modified, \
unique_id = post.link, \
categories = [tag.name for tag in post.tags.all()])
response = HttpResponse(mimetype=feed.mime_type)
# per host caching
patch_vary_headers(response, ['Host'])
feed.write(response, 'utf-8')
if site.use_internal_cache:
fjcache.cache_set(site, cachekey, response)
return response
def rssfeed(request, tag=None, user=None):
""" Generates the RSS2 feed.
"""
return buildfeed(request, feedgenerator.Rss201rev2Feed, tag, user)
def atomfeed(request, tag=None, user=None):
""" Generates the Atom 1.0 feed.
"""
return buildfeed(request, feedgenerator.Atom1Feed, tag, user)
def mainview(request, tag=None, user=None):
""" View that handles all page requests.
"""
response, site, cachekey, sfeeds_obj, sfeeds_ids = initview(request)
if response:
return response
ctx = fjlib.page_context(request, site, tag, user, (sfeeds_obj, \
sfeeds_ids))
response = render_to_response('feedjack/%s/post_list.html' % \
(site.template), ctx)
# per host caching, in case the cache middleware is enabled
patch_vary_headers(response, ['Host'])
if site.use_internal_cache:
fjcache.cache_set(site, cachekey, response)
return response
#~

View File

@ -0,0 +1,29 @@
Test
=====
* Install django
* Run ``python manage.py syncdb`` to create database in /tmp/planet.db
* Run ``python manage.py runserver``
* Goto `http://localhost:8000/admin` and create Feeds, Sites and Subscribers
* Planet is at `http://localhost:8000`
Update feeds by running::
DJANGO_SETTINGS_MODULE=calibre.www.planet.settings feedjack_update.py
Deploy
=======
* Add settings for deployment environment to settings.py
* In particular setup caching
* Run python manage.py syncdb
* Add super user when asked
* Setup Apache
* Goto /admin and add feeds

View File

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)

View File

@ -0,0 +1,42 @@
# Django settings for planet project.
from calibre.www.settings import DEBUG, TEMPLATE_DEBUG, ADMINS, MANAGERS, \
TEMPLATE_LOADERS, TEMPLATE_DIRS, MIDDLEWARE_CLASSES, MEDIA_ROOT, \
MEDIA_URL, ADMIN_MEDIA_PREFIX
DATABASE_USER = '' # Not used with sqlite3.
DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
if DEBUG:
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = '/tmp/planet.db' # Or path to database file if using sqlite3.
else:
DATABASE_ENGINE = 'mysql'
DATABASE_NAME = 'calibre_planet'
DATABASE_USER = 'calibre_django'
DATABASE_PASSWORD = open('/var/www/calibre-ebook.com/dbpass').read().strip()
SITE_ID = 1
# Make this unique, and don't share it with anybody.
if DEBUG:
SECRET_KEY = '06mv&t$cobjkijgg#0ndwm5#&90_(tm=oqi1bv-x^vii$*33n5'
else:
SECRET_KEY = open('/var/www/planet.calibre-ebook.com/django_secret_key').read().strip()
ROOT_URLCONF = 'planet.urls'
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
'calibre.www.apps.feedjack',
)

View File

@ -0,0 +1,25 @@
from django.conf.urls.defaults import patterns, include
from django.conf import settings
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
#(r'^admin/', include('django.contrib.admin.urls')),
(r'^admin/(.*)', admin.site.root),
(r'', include('calibre.www.apps.feedjack.urls')),
)
if settings.DEBUG:
urlpatterns += patterns('',
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
{'document_root': settings.MEDIA_ROOT}),
)

View File

@ -0,0 +1,79 @@
# Django settings
# Import base settings from here into the site specific settings files
import socket, os
DEBUG = socket.gethostname() != 'divok'
TEMPLATE_DEBUG = DEBUG
ADMINS = (
('Kovid Goyal', 'kovid@kovidgoyal.net'),
)
MANAGERS = ADMINS
# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
if DEBUG:
MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'static/')
else:
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
if DEBUG:
MEDIA_URL = 'http://127.0.0.1:8000/site_media/'
else:
MEDIA_URL = 'http://planet.calibre-ebook.com/site_media/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/Los_Angeles'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = False
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.load_template_source',
'django.template.loaders.app_directories.load_template_source',
# 'django.template.loaders.eggs.load_template_source',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
ROOT_URLCONF = 'planet.urls'
if DEBUG:
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.join(os.path.dirname(__file__), 'templates'),
)
else:
TEMPLATE_DIRS = (
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,39 @@
body {
font-family: sansserif;
background-color: #eeeeee;
}
img {
border:0 none;
}
#_header {
width: 100%;
border-bottom: 1px solid black;
margin-bottom: 20px;
height: 100px;
overflow: hidden;
}
#_header > h1 {
margin: 0px; padding: 0px;
font-size: 60px;
font-family: sansserif;
font-weight: normal;
}
#_logo {
vertical-align: middle;
}
#_footer {
font-size: small;
text-align: right;
width: 100%;
border-top: 1px solid black;
font-style: italic;
padding-top: 10px;
padding-bottom: 5px;
}
#_footer img { vertical-align: middle; }

View File

@ -0,0 +1,115 @@
/* codebox header */
.wp_codebox_msgheader {
width: 100%;
border: 1px solid #DEDEB8;
border-bottom: 0;
font-weight: bold;
background: #F3F8D7 url(../images/arrow-square.gif) no-repeat right 5px;
color: #000000;
}
.wp_codebox_msgheader.active {
background-position: right -51px;
}
.wp_codebox_msgheader .right {
float: right;
text-align: right;
padding: 5px;
margin-right: 20px;
}
.wp_codebox_msgheader .right a {
font: 12px Arial, Tahoma !important;
font: 11px Arial, Tahoma;
}
.wp_codebox_msgheader .left,.wp_codebox_msgheader .left2 {
float: left;
/* background-color:#FFFFFF;
border:1px solid #DCDCDC;
padding:8px 0px 2px 8px;*/
font-family: tahoma, arial, verdana;
/* display: block;
width:50%;
margin: 0 auto;*/
padding: 5px 5px 5px 20px;
margin-left: 5px;
}
.wp_codebox_msgheader .left {
background: url(../images/view_code.png) no-repeat left;
}
.wp_codebox_msgheader .left2 {
background: url(../images/down.gif) no-repeat left;
}
.wp_codebox_msgheader .left a { /* margin:0px 5px 0px 10px;*/
font-weight: bold;
}
.wp_codebox_msgheader .left2 a { /* margin:0px 5px 0px 5px;*/
font-weight: bold;
}
.wp_codebox_msgheader .codebox_clear {
clear: both;
}
/* codebox */
.wp_codebox {
color: #100;
background-color: #f9f9f9;
border: 1px solid silver;
margin: 0 0 1.5em 0;
overflow: auto;
}
/* IE FIX */
.wp_codebox {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: expression(this.scrollWidth > this.offsetWidth ? 15 : 0)
;
width: 100%;
}
.wp_codebox table {
border-collapse: collapse;
border: none;
margin: 0px;
}
.wp_codebox div,.wp_codebox td {
vertical-align: top;
padding: 2px 4px;
}
.wp_codebox td.line_numbers {
text-align: right;
background-color: #def;
color: #666;
overflow: visible;
border-right: 1px solid #B0BEC7;
table-layout: auto;
width: 15px;
}
/* potential overrides for other styles */
.wp_codebox pre {
border: none;
background: none;
margin: 0;
padding: 0;
width: auto;
float: none;
clear: none;
overflow: visible;
font-size: 12px;
line-height: 1.333;
}
.line_numbers pre {
padding-left: 10px;
}

View File

@ -0,0 +1,314 @@
/*
* Feedjack LostWoods theme
**************************
* Simple and green (where's the brown? -brown doesn't count)
*
* Copyright Diego Escalante Urrelo <diegoe@gnome.org>
*
*/
body {
font-size: 0.8em;
font-family: verdana;
margin: 0;
}
div {
/*border: 1px solid blue;
padding: 5px;
margin: 5px;*/
}
/*
* Structure
*/
#logo {
padding-top: 3px;
padding-left: 1em;
}
#tags {
overflow: auto;
padding: 5px;
text-align: right;
}
#paginate {
margin-bottom: 15px;
margin-left: 0px;
margin-top: 5px;
padding-left: 0;
text-align: left;
vertical-align: middle;
float: left;
width: 60%;
color: black;
}
#buttons {
text-align: right;
float: right;
vertical-align: middle;
color: #aaa;
width: 40%;
line-height: 1.7em;
padding:0;
padding-bottom: 10px;
}
#usertags {
clear: both;
margin: 5px;
padding: 5px;
width: 75%;
text-align: center;
}
#post_list {
clear : both;
width: 75%;
}
#sidebar {
width: 20%;
position: absolute;
right:0;
top: 170px;
border-left: 10px outset #6E9C60;
padding-left: 10px;
padding-right: 10px;
}
div.date {
font-size: x-large;
text-align: center;
font-style: italic;
padding: 5px;
color: black;
border: 2px solid #6E9C60;
border-right: 0;
border-left: 0;
margin-bottom: 10px;
}
/*
* Post structure
*/
div.post {
overflow: auto;
margin-bottom: 50px;
padding-bottom: 30px;
border-bottom: 12px solid #6E9C60;
border-right: 1px inset #6E9C60;
padding-right: 10px;
}
div.avatar {
float: right;
width: 15%;
text-align: center;
}
div.post-title {
text-align: left;
font-size: 180%;
font-family: trebuchet ms;
font-weight: bold;
padding: 5px;
width: 75%;
}
div.post-content {
width: 72%;
overflow: auto;
text-align: justify;
font-size: 90%;
line-height: 1.8em;
padding-left: 4em;
padding-right: 4em;
border-right: 1px dotted #ccc;
}
div.post-content li {
margin: 0;
padding: 0;
line-height: 130%;
}
div.post-content table{
border: 0;
margin-left: 50px;
}
div.post-content td {
border: 2px solid #ccc;
}
div.post-meta {
color: #666;
margin-top: 20px;
border-top: 1px solid #ccc;
width: 100%;
text-align: right;
}
div.tags {
margin-top: 10px;
}
/*
* Elements
*/
blockquote {
color: #777;
margin: 15px 30px 0px 10px;
padding: 20px;
border: 1px solid #ddd;
border-left: 7px solid #ddd;
}
a:link {
color: #4C6B46;
}
a:hover {
color: #33408A;
}
h1 a:link {
text-decoration: none;
color: inherit;
}
#buttons img {
vertical-align: middle;
}
#head h1 {
font-style: italic;
font-size: xx-large;
border-bottom: 3px solid #6E9C60;
margin-bottom: 5px;
margin-top: 10px;
}
#head a:link, a:visited, #head a:active {
color: inherit;
}
.love_feedjack {
font-size: 145%;
font-weight: bold;
font-style: italic;
}
.cloud_1 {
font-size: 50%;
}
.cloud_2 {
font-size: 100%;
}
.cloud_3 {
font-size: 120%;
font-weight: bold;
}
.cloud_4 {
font-size: 140%;
font-weight: bold;
}
.cloud_5 {
font-size: 160%;
font-weight: bold;
}
#paginate ul {
margin:0;
padding:0;
}
#paginate li {
display: inline;
margin: 2px;
padding: 10px;
background-color: #fbfbfb;
border: 2px solid #ddd;
line-height: 1.7em;
margin-left: 3px;
margin-right: 3px;
text-align: center;
vertical-align: middle;
}
#paginate li.tagname {
font-weight: bold;
font-size: 140%;
}
#paginate a:link {
color: #4C6B46;
}
#paginate a { text-decoration: none; }
img {
border: 0;
}
#sidebar ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#sidebar li {
display: block;
clear: both;
line-height: 25px;
}
#sidebar a.nombre {
display: inline;
color: #33408A;
vertical-align: middle;
margin-left: 2px;
margin-right: 2px;
}
#sidebar img.face {
vertical-align: middle;
margin-right: 5px;
}
#sidebar h4 {
font-style: italic;
border-bottom: 1px solid #ccc;
}
#sidebar h4 + p {
text-align: justify;
}
#tags ul, #usertags ul {
background-color: #fbfbfb;
padding: 5px;
border: 2px solid #ddd;
margin:0;
text-align: center;
}
#tags li, #usertags li {
display: inline;
margin: 0;
line-height: 1.7em;
margin-left: 3px;
margin-right: 3px;
}
#tags a:link, #usertags a:link {
color: #4C6B46;
}
span.name {
color: #333;
}
span.nick {
color: #555;
}
span.url a {
color: #bbb;
}
span.url a:hover {
color: #777;
}
div.tags ul {
background-color: #fbfbfb;
padding: 5px;
border: 2px solid #ddd;
margin:0;
text-align: center;
}
div.tags li {
display: inline;
margin: 0;
line-height: 1.7em;
margin-left: 3px;
margin-right: 3px;
}
#planet_donate
{
text-align: center;
border-top: 1px solid gray;
border-bottom: 1px solid gray;
padding-top: 5px; padding-bottom: 5px;
}
#planet_ads {
text-align: center;
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<!--
Base template that defines navigation/sidebars etc.
-->
<head>
<title>{% block title %}calibre - E-book management{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{{ media_url }}/styles/base.css" />
{% block extra_header %} {% endblock %}
</head>
<body>
<div id="_header">
<h1>
<img id="_logo" alt="calibre" src="{{ media_url }}/img/logo.png" />
{% block header_text %}e-book management{% endblock %}
</h1>
</div>
<div id="_content">
{% block content %}Hello world{% endblock %}
</div>
<div id="_footer">
Created by Kovid Goyal.
Powered by <a href="http://www.djangoproject.com">
<img alt="Django" src="{{ media_url }}/img/button-django.png"/>
</a>&nbsp;&nbsp;
</div>
</body>
</html>

View File

@ -0,0 +1,189 @@
{% extends "base.html" %}
{% block title%}Calibre Planet{% endblock %}
{% block header_text %}Planet{% endblock %}
{% block extra_header %}
<link rel="stylesheet" type="text/css" href="{{ media_url }}/styles/planet.css" />
<link rel="stylesheet" type="text/css" href="{{ media_url }}/styles/codebox.css" />
{% endblock %}
{% block content %}
<div id="paginate">
<ul>
{% if has_previous %}
<li><a href="?page={{ previous }}">&lt;&lt;</a></li>
{% endif %}
<li>
Page {{ page }} of {{ pages }} ({{ hits }} posts)
</li>
{% if has_next %}
<li><a href="?page={{ next }}">&gt;&gt;</a></li>
{% endif %}
{% if user %}
<li class="username"><a href="{{ user.feed.link }}">{{ user.name }}</a>talks about »</li>
{% endif %}
{% if tag %}
<li class="tagname">{{ tag.name }}</li>
{% endif %}
</ul>
</div> <!-- end paginate -->
<div id="buttons">
<a href="{{ site.url }}/feed/rss/" title="RSS 2.0 feed"><img src="{{ media_url }}/img/button-rss.png"/></a> &bull;
<a href="{{ site.url }}/feed/atom/" title="Atom 1.0 feed"><img src="{{ media_url }}/img/button-atom.png"/></a> &bull;
<a href="{{ site.url }}/opml/" title="OPML"><img src="{{ media_url }}/img/button-opml.png"/></a> &bull;
<a href="{{ site.url }}/foaf/" title="FOAF"><img src="{{ media_url }}/img/button-foaf.png"/></a>
</div> <!-- end buttons -->
<div id="post_list">
{% for item in object_list %}
{% ifchanged %}
<div class="date">{{ item.date_modified|date:"F j, Y" }}</div>
{% endifchanged %}
<div class="post">
{% ifchanged %}
<!-- {{ item.date_modified|date:"F j, Y" }} -->
<div class="avatar">
<a href="{{ item.feed.link }}">
<img
src="{{ media_url }}/img/faces/{{ item.subscriber.shortname}}.png" alt="" />
<br/>
<span class="url">{{ item.feed.title }}</span>
</a>
</div>
{% endifchanged %}
{% if item.title %}
<div class="post-title">» {{ item.title }}</div>
{% else %}
<div class="post-title">» {{ item.subscriber.name }}</div>
{% endif %}
<div class="post-content">
<p>{{ item.content|safe }}</p>
<div class="post-meta">
<a href="{{ item.link }}">
{% if item.author %}by {{ item.author }} at{% endif %}
{{ item.date_modified|date:"g:i A" }}</a>
{% for tag in item.qtags %}
{% if forloop.first %}under{% endif %}
<a href="{{ site.url }}/tag/{{ tag.name }}">{{ tag.name }}</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
{% if item.comments %}
<a href="{{ item.comments }}">(Comments)</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<div id="sidebar">
<h4>{{ site.name }}</h4>
<p style="text-align:left">
Planet Calibre is a window into the world, work and lives of Calibre developers and contributors.
</p>
<p style="text-align:left">
If you have a question or would like your blog added to the feed. Please email
<a href="mailto:kovid@kovidgoyal.net">Kovid Goyal</a>.
</p>
<div id="planet_donate">
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_s-xclick" />
<input type="hidden" name="hosted_button_id" value="3028915" />
<input type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="Donate to support calibre development" />
<img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1" />
<div>Donate to support the development of calibre.</div>
</form>
</div>
<div id="tags">
<ul id="cloud">
{% for tag in tagcloud %}
<li><a
{% if user_id %}
href="{{ site.url }}/user/{{ user_id }}/tag/{{ tag.tagname|urlencode }}/"
{% else %}
href="{{ site.url }}/tag/{{ tag.tagname|urlencode }}/"
{% endif %}
title="{{ tag.count }} posts"
class="cloud_{{ tag.weight }}">{{ tag.tagname }}</a></li>
{% endfor %}
</ul>
</div>
<div id='planet_ads' style="z-index:0">
<script type="text/javascript"><!--
google_ad_client = "pub-2595272032872519";
/* Calibre Planet */
google_ad_slot = "6940385240";
google_ad_width = 120;
google_ad_height = 600;
//-->
</script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>
<h4>Last update</h4>
<b>{{ last_modified }}</b>
<h4>People</h4>
<ul class="suscriptores">
{% for feed in subscribers %}
<li>
<a href="{{ feed.feed.feed_url }}"
{% if feed.feed.last_modified %}
title="feed (last modified: {{ feed.feed.last_modified }})"
{% else %}
title="feed"
{% endif %}
>
<img src="{{ media_url }}/img/feed.png" alt="feed"></a>
<a class="nombre" href="{{ site.url }}/user/{{ feed.feed.id }}"
title="{{ feed.feed.title }}">{{ feed.name }}</a></li>
{% endfor %}
</ul>
</div>
<div id="paginate">
<ul>
{% if has_previous %}
<li><a href="?page={{ previous }}">&lt;&lt;</a></li>
{% endif %}
<li>
Page {{ page }} of {{ pages }} (
{{ hits }} posts
)
</li>
{% if has_next %}
<li><a href="?page={{ next }}">&gt;&gt;</a></li>
{% endif %}
{% if user %}
<li class="username"><a href="{{ user.feed.link }}">{{ user.name }}</a></li>
{% endif %}
{% if tag %}
<li class="tagname">{{ tag.name }}</li>
{% endif %}
</ul>
</div>
<div style="height:50px">&nbsp;</div>
{% endblock %}