IGN:Development version of Planet calibre
@ -41,7 +41,6 @@ def freeze():
|
|||||||
'/usr/lib/libxslt.so.1',
|
'/usr/lib/libxslt.so.1',
|
||||||
'/usr/lib/libxslt.so.1',
|
'/usr/lib/libxslt.so.1',
|
||||||
'/usr/lib/libgthread-2.0.so.0',
|
'/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/gcc/i686-pc-linux-gnu/4.3.3/libstdc++.so.6',
|
||||||
'/usr/lib/libpng12.so.0',
|
'/usr/lib/libpng12.so.0',
|
||||||
'/usr/lib/libexslt.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 %}
|