IGN:Development version of Planet calibre
@ -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',
|
||||
|
10
src/calibre/www/__init__.py
Normal 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'
|
||||
|
||||
|
||||
|
10
src/calibre/www/apps/__init__.py
Normal 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'
|
||||
|
||||
|
||||
|
8
src/calibre/www/apps/feedjack/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
feedjack
|
||||
Gustavo Picón
|
||||
__init__.py
|
||||
"""
|
||||
|
60
src/calibre/www/apps/feedjack/admin.py
Normal 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)
|
||||
|
||||
#~
|
83
src/calibre/www/apps/feedjack/fjcache.py
Normal 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)
|
||||
|
||||
|
93
src/calibre/www/apps/feedjack/fjcloud.py
Normal 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]
|
||||
|
282
src/calibre/www/apps/feedjack/fjlib.py
Normal 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
|
||||
|
||||
|
202
src/calibre/www/apps/feedjack/models.py
Normal 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()
|
||||
|
||||
|
506
src/calibre/www/apps/feedjack/update.py
Executable 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()
|
||||
|
47
src/calibre/www/apps/feedjack/urls.py
Normal 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),
|
||||
)
|
||||
|
||||
#~
|
151
src/calibre/www/apps/feedjack/views.py
Normal 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
|
||||
|
||||
#~
|
||||
|
29
src/calibre/www/planet/README.rst
Normal 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
|
||||
|
||||
|
0
src/calibre/www/planet/__init__.py
Normal file
11
src/calibre/www/planet/manage.py
Normal 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)
|
42
src/calibre/www/planet/settings.py
Normal 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',
|
||||
)
|
||||
|
||||
|
25
src/calibre/www/planet/urls.py
Normal 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}),
|
||||
)
|
||||
|
||||
|
||||
|
79
src/calibre/www/settings.py
Normal 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 = (
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
BIN
src/calibre/www/static/img/button-atom.png
Normal file
After Width: | Height: | Size: 279 B |
BIN
src/calibre/www/static/img/button-css.png
Normal file
After Width: | Height: | Size: 299 B |
BIN
src/calibre/www/static/img/button-django.png
Normal file
After Width: | Height: | Size: 299 B |
BIN
src/calibre/www/static/img/button-foaf.png
Normal file
After Width: | Height: | Size: 292 B |
BIN
src/calibre/www/static/img/button-hacker.png
Normal file
After Width: | Height: | Size: 399 B |
BIN
src/calibre/www/static/img/button-opml.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
src/calibre/www/static/img/button-rss.png
Normal file
After Width: | Height: | Size: 280 B |
BIN
src/calibre/www/static/img/button-xhtml.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
src/calibre/www/static/img/faces/calibre.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/calibre/www/static/img/faces/john.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/calibre/www/static/img/faces/nobody.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/calibre/www/static/img/feed.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/calibre/www/static/img/logo.png
Normal file
After Width: | Height: | Size: 17 KiB |
39
src/calibre/www/static/styles/base.css
Normal 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; }
|
115
src/calibre/www/static/styles/codebox.css
Normal 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;
|
||||
}
|
314
src/calibre/www/static/styles/planet.css
Normal 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;
|
||||
}
|
||||
|
32
src/calibre/www/templates/base.html
Normal 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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
189
src/calibre/www/templates/feedjack/default/post_list.html
Normal 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 }}"><<</a></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
Page {{ page }} of {{ pages }} ({{ hits }} posts)
|
||||
</li>
|
||||
{% if has_next %}
|
||||
<li><a href="?page={{ next }}">>></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> •
|
||||
<a href="{{ site.url }}/feed/atom/" title="Atom 1.0 feed"><img src="{{ media_url }}/img/button-atom.png"/></a> •
|
||||
<a href="{{ site.url }}/opml/" title="OPML"><img src="{{ media_url }}/img/button-opml.png"/></a> •
|
||||
<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 }}"><<</a></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
Page {{ page }} of {{ pages }} (
|
||||
{{ hits }} posts
|
||||
)
|
||||
</li>
|
||||
{% if has_next %}
|
||||
<li><a href="?page={{ next }}">>></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"> </div>
|
||||
|
||||
|
||||
{% endblock %}
|