Remove the calibre planet website source code from the tree. Will now be maintained separately.
@ -1,10 +0,0 @@
|
||||
#!/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'
|
||||
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
#!/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'
|
||||
|
||||
|
||||
|
@ -1,85 +0,0 @@
|
||||
changes:
|
||||
date: 2008-09-17
|
||||
change: Enabled the ability to override the default template names.
|
||||
|
||||
date: 2008-08-26
|
||||
change: Upgraded post_detail.html to now use new Django refactored comments. Sidenote: basic.remarks have been removed.
|
||||
|
||||
date: 2008-07-14
|
||||
change: Removed get_query_set from Blog manager to fix a problem where saving a post marked as Draft would not save.
|
||||
change: Added a get_previous_post and get_next_post method for front end template. These will not return Draft posts.
|
||||
|
||||
date: 2008-06-17
|
||||
change: BlogPostFeed is now BlogPostsFeed and there is a new BlogPostsByCategory.
|
||||
|
||||
date: 2008-05-18
|
||||
change: Converted everything to 4 space tabs and made a few other changes to comply with Python Style Guide.
|
||||
|
||||
date: 2008-04-23
|
||||
change: Added an inline admin interface helper for choosing inlines to go into posts.
|
||||
change: The inline app is now a dependancy of the blog.
|
||||
|
||||
date: 2008-04-22
|
||||
change: Removed the 'render_inlines' filter from the Blog template tags. The tag is now in an app called inlines which can be used with any django app.
|
||||
|
||||
date: 2008-02-27
|
||||
change: Added 'allow_comments' field to the Post model.
|
||||
change: Removed 'Closed' choice from status field of Post model
|
||||
|
||||
date: 2008-02-18
|
||||
fix: Fixed feed pointing to hardcoded url.
|
||||
|
||||
date: 2008-02-15
|
||||
change: Internationalized models
|
||||
|
||||
date: 2008-02-04
|
||||
change: Added 'get_links' template filter.
|
||||
change: Templates: added a {% block content_title %}
|
||||
|
||||
date: 2008-02-02
|
||||
change: Added a sitemap
|
||||
|
||||
date: 2008-01-30
|
||||
change: Renamed 'do_inlines' filter to 'render_inlines'
|
||||
|
||||
date: 2008-01-29
|
||||
change: BeautifulSoup is no longer a dependancy unless you want to use the do_inlines filter.
|
||||
|
||||
date: 2008-01-27
|
||||
fix: removed 'tagging.register(Post)' from model. It was causing too many unnecessary SQL JOINS.
|
||||
change: Changed the inlines tag to a filter. (Example: {{ object.text|do_inlines }})
|
||||
|
||||
date: 2008-01-22
|
||||
change: Registered the Post model with the tagging app
|
||||
|
||||
date: 2008-01-19
|
||||
change: Renamed the 'list' class to 'link_list'
|
||||
|
||||
date: 2008-01-09
|
||||
change: Changed urls.py so you can have /posts/page/2/ or /posts/?page=2
|
||||
|
||||
date: 2008-01-07
|
||||
change: Removed PublicPostManager in favor of ManagerWithPublished.
|
||||
change: Made wrappers for generic views.
|
||||
|
||||
date: 2008-01-06
|
||||
fix: In blog.py changed 'beautifulsoup' to 'BeautifulSoup'
|
||||
|
||||
date: 2007-12-31
|
||||
change: Changed some syntax in managers.py to hopefully fix a bug.
|
||||
change: Removed an inline template that didn't belong.
|
||||
|
||||
date: 2007-12-21
|
||||
change: Added markup tag that formats inlines.
|
||||
|
||||
date: 2007-12-12
|
||||
change: Cleaned up unit tests.
|
||||
|
||||
date: 2007-12-11
|
||||
change: Add documentation to templatetags and views.
|
||||
change: Smartened up the previous/next blog part of the post_detail.html template.
|
||||
|
||||
date: 2007-12-09
|
||||
change: Added feed templates and wrapped up feeds.py.
|
||||
change: Changed Post.live manager to Post.public
|
||||
change: Added a search view along with templates
|
@ -1,18 +0,0 @@
|
||||
===========================================
|
||||
Django Basic Blog
|
||||
http://code.google.com/p/django-basic-apps/
|
||||
===========================================
|
||||
|
||||
A simple blog application for Django projects.
|
||||
|
||||
To install this app, simply create a folder somewhere in
|
||||
your PYTHONPATH named 'basic' and place the 'blog'
|
||||
app inside. Then add 'basic.blog' to your projects
|
||||
INSTALLED_APPS list in your settings.py file.
|
||||
|
||||
=== Dependancies ===
|
||||
* Basic Inlines
|
||||
* [http://www.djangoproject.com/documentation/add_ons/#comments Django Comments]
|
||||
* [http://code.google.com/p/django-tagging Django Tagging]
|
||||
* [http://www.djangoproject.com/documentation/add_ons/#markup Markup]
|
||||
* [http://www.crummy.com/software/BeautifulSoup/ BeautifulSoup] - only if you want to use the [http://code.google.com/p/django-basic-blog/wiki/BlogInlinesProposal render_inlines] filter, otherwise it's not necessary.
|
@ -1,17 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from calibre.www.apps.blog.models import *
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
|
||||
|
||||
class PostAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'publish', 'status')
|
||||
list_filter = ('publish', 'categories', 'status')
|
||||
search_fields = ('title', 'body')
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
|
||||
admin.site.register(Post, PostAdmin)
|
@ -1,42 +0,0 @@
|
||||
from django.contrib.syndication.feeds import FeedDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.sites.models import Site
|
||||
from django.contrib.syndication.feeds import Feed
|
||||
from django.core.urlresolvers import reverse
|
||||
from calibre.www.apps.blog.models import Post, Category
|
||||
|
||||
|
||||
class BlogPostsFeed(Feed):
|
||||
_site = Site.objects.get_current()
|
||||
title = '%s feed' % _site.name
|
||||
description = '%s posts feed.' % _site.name
|
||||
|
||||
def link(self):
|
||||
return reverse('blog_index')
|
||||
|
||||
def items(self):
|
||||
return Post.objects.published()[:10]
|
||||
|
||||
def item_pubdate(self, obj):
|
||||
return obj.publish
|
||||
|
||||
|
||||
class BlogPostsByCategory(Feed):
|
||||
_site = Site.objects.get_current()
|
||||
title = '%s posts category feed' % _site.name
|
||||
|
||||
def get_object(self, bits):
|
||||
if len(bits) != 1:
|
||||
raise ObjectDoesNotExist
|
||||
return Category.objects.get(slug__exact=bits[0])
|
||||
|
||||
def link(self, obj):
|
||||
if not obj:
|
||||
raise FeedDoesNotExist
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def description(self, obj):
|
||||
return "Posts recently categorized as %s" % obj.title
|
||||
|
||||
def items(self, obj):
|
||||
return obj.post_set.published()[:10]
|
@ -1,9 +0,0 @@
|
||||
from django.db.models import Manager
|
||||
import datetime
|
||||
|
||||
|
||||
class PublicManager(Manager):
|
||||
"""Returns published posts that are not in the future."""
|
||||
|
||||
def published(self):
|
||||
return self.get_query_set().filter(status__gte=2, publish__lte=datetime.datetime.now())
|
@ -1,80 +0,0 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models import permalink
|
||||
from django.contrib.auth.models import User
|
||||
from tagging.fields import TagField
|
||||
from calibre.www.apps.blog.managers import PublicManager
|
||||
|
||||
import tagging
|
||||
|
||||
class Category(models.Model):
|
||||
"""Category model."""
|
||||
title = models.CharField(_('title'), max_length=100)
|
||||
slug = models.SlugField(_('slug'), unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('category')
|
||||
verbose_name_plural = _('categories')
|
||||
db_table = 'blog_categories'
|
||||
ordering = ('title',)
|
||||
|
||||
class Admin:
|
||||
pass
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.title
|
||||
|
||||
@permalink
|
||||
def get_absolute_url(self):
|
||||
return ('blog_category_detail', None, {'slug': self.slug})
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
"""Post model."""
|
||||
STATUS_CHOICES = (
|
||||
(1, _('Draft')),
|
||||
(2, _('Public')),
|
||||
)
|
||||
title = models.CharField(_('title'), max_length=200)
|
||||
slug = models.SlugField(_('slug'), unique_for_date='publish')
|
||||
author = models.ForeignKey(User, blank=True, null=True)
|
||||
body = models.TextField(_('body'))
|
||||
tease = models.TextField(_('tease'), blank=True)
|
||||
status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=2)
|
||||
allow_comments = models.BooleanField(_('allow comments'), default=True)
|
||||
publish = models.DateTimeField(_('publish'))
|
||||
created = models.DateTimeField(_('created'), auto_now_add=True)
|
||||
modified = models.DateTimeField(_('modified'), auto_now=True)
|
||||
categories = models.ManyToManyField(Category, blank=True)
|
||||
tags = TagField()
|
||||
objects = PublicManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('post')
|
||||
verbose_name_plural = _('posts')
|
||||
db_table = 'blog_posts'
|
||||
ordering = ('-publish',)
|
||||
get_latest_by = 'publish'
|
||||
|
||||
class Admin:
|
||||
list_display = ('title', 'publish', 'status')
|
||||
list_filter = ('publish', 'categories', 'status')
|
||||
search_fields = ('title', 'body')
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.title
|
||||
|
||||
@permalink
|
||||
def get_absolute_url(self):
|
||||
return ('blog_detail', None, {
|
||||
'year': self.publish.year,
|
||||
'month': self.publish.strftime('%b').lower(),
|
||||
'day': self.publish.day,
|
||||
'slug': self.slug
|
||||
})
|
||||
|
||||
def get_previous_post(self):
|
||||
return self.get_previous_by_publish(status__gte=2)
|
||||
|
||||
def get_next_post(self):
|
||||
return self.get_next_by_publish(status__gte=2)
|
@ -1,13 +0,0 @@
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from calibre.www.apps.blog.models import Post
|
||||
|
||||
|
||||
class BlogSitemap(Sitemap):
|
||||
changefreq = "never"
|
||||
priority = 0.5
|
||||
|
||||
def items(self):
|
||||
return Post.objects.published()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.publish
|
@ -1,56 +0,0 @@
|
||||
{% extends "admin/change_form.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{% load adminmedia inlines %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript">
|
||||
function InlineInit() {
|
||||
var body_div = document.getElementById('id_body').parentNode;
|
||||
var content = ''
|
||||
content += '{% get_inline_types as inline_list %}'
|
||||
content += '<label>Body inlines:</label>'
|
||||
|
||||
content += '<strong>Inline type:</strong> '
|
||||
content += '<select id="id_inline_content_type" onchange="document.getElementById(\'lookup_id_inline\').href = \'../../../\'+this.value+\'/\';" style="margin-right:20px;">'
|
||||
content += ' <option>----------</option>'
|
||||
content += ' {% for inline in inline_list %}'
|
||||
content += ' <option value="{{ inline.content_type.app_label }}/{{ inline.content_type.model }}">{{ inline.content_type.app_label|capfirst }}: {{ inline.content_type.model|capfirst }}</option>'
|
||||
content += ' {% endfor %}'
|
||||
content += '</select> '
|
||||
|
||||
content += '<strong>Object:</strong> '
|
||||
content += '<input type="text" class="vIntegerField" id="id_inline" size="10" /> '
|
||||
content += '<a id="lookup_id_inline" href="#" class="related-lookup" onclick="if(document.getElementById(\'id_inline_content_type\').value != \'----------\') { return showRelatedObjectLookupPopup(this); }" style="margin-right:20px;"><img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Loopup" /></a> '
|
||||
|
||||
content += '<strong>Class:</strong> '
|
||||
content += '<select id="id_inline_class">'
|
||||
content += ' <option value="small_left">Small left</option>'
|
||||
content += ' <option value="small_right">Small right</option>'
|
||||
content += ' <option value="medium_left">Medium left</option>'
|
||||
content += ' <option value="medium_right">Medium right</option>'
|
||||
content += ' <option value="large_left">Large left</option>'
|
||||
content += ' <option value="large_right">Large right</option>'
|
||||
content += ' <option value="full">Full</option>'
|
||||
content += '</select>'
|
||||
|
||||
content += '<input type="button" value="Add" style="margin-left:10px;" onclick="return insertInline(document.getElementById(\'id_inline_content_type\').value, document.getElementById(\'id_inline\').value, document.getElementById(\'id_inline_class\').value)" />'
|
||||
content += '<p class="help">Insert inlines into your body by choosing an inline type, then an object, then a class.</p>'
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.setAttribute('style', 'margin-top:10px;');
|
||||
div.innerHTML = content;
|
||||
|
||||
body_div.insertBefore(div);
|
||||
}
|
||||
|
||||
function insertInline(type, id, classname) {
|
||||
if (type != '----------' && id != '') {
|
||||
inline = '<inline type="'+type.replace('/', '.')+'" id="'+id+'" class="'+classname+'" />';
|
||||
body = document.getElementById('id_body');
|
||||
body.value = body.value + inline + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(window, 'load', InlineInit);
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
</head>
|
||||
<body id="{% block body_id %}{% endblock %}">
|
||||
<div id="body">
|
||||
{% block body %}
|
||||
<div>
|
||||
{% block content_title %}{% endblock %}
|
||||
</div>
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,4 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block body_class %}blog{% endblock %}
|
@ -1,25 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Posts for {{ category.title }}{% endblock %}
|
||||
{% block body_class %}{{ block.super }} category_detail{% endblock %}
|
||||
{% block body_id %}category_{{ category.id }}{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Posts for {{ category.title }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% load markup %}
|
||||
<div class="post_list">
|
||||
{% for post in object_list %}
|
||||
<div>
|
||||
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
|
||||
<p class="date">{{ post.publish|date:"Y F d" }}</p>
|
||||
<p class="tease">{{ post.tease }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,20 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Post categories{% endblock %}
|
||||
{% block body_class %}{{ block.super }} category_list{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Post categories</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% load markup %}
|
||||
<ul class="link_list">
|
||||
{% for category in object_list %}
|
||||
<li><a href="{{ category.get_absolute_url }}">{{ category }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
@ -1,23 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Post archive for {{ day|date:"d F Y" }}{% endblock %}
|
||||
{% block body_class %}{{ block.super }} post_archive_day{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Post archive for {{ day|date:"d F Y" }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="post_list">
|
||||
{% for post in object_list %}
|
||||
<div>
|
||||
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
|
||||
<p class="date">{{ post.publish|date:"Y F d" }}</p>
|
||||
<p class="tease">{{ post.tease }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,23 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Post archive for {{ month|date:"F Y" }}{% endblock %}
|
||||
{% block body_class %}{{ block.super }} post_archive_month{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Post archive for {{ month|date:"F Y" }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="post_list">
|
||||
{% for post in object_list %}
|
||||
<div>
|
||||
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
|
||||
<p class="date">{{ post.publish|date:"Y F d" }}</p>
|
||||
<p class="tease">{{ post.tease }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Post archive for {{ year }}{% endblock %}
|
||||
{% block body_class %}{{ block.super }} post_archive_year{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Post archive for {{ year }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% load markup %}
|
||||
|
||||
<ul class="link_list">
|
||||
{% for month in date_list %}
|
||||
<li><a href="{% url blog_index %}{{ year }}/{{ month|date:"b" }}/">{{ month|date:"F" }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
@ -1,67 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}{{ object.title }}{% endblock %}
|
||||
{% block body_class %}{{ block.super }} post_detail{% endblock %}
|
||||
{% block body_id %}post_{{ object.id }}{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>{{ object.title }}</h2>
|
||||
<p class="other_posts">
|
||||
{% if object.get_previous_by_publish %}
|
||||
<a class="previous" href="{{ object.get_previous_post.get_absolute_url }}">« {{ object.get_previous_post }}</a>
|
||||
{% endif %}
|
||||
{% if object.get_next_by_publish %}
|
||||
| <a class="next" href="{{ object.get_next_post.get_absolute_url }}">{{ object.get_next_post }} »</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% load blog markup comments tagging_tags %}
|
||||
|
||||
<p class="date">{{ object.publish|date:"j F Y" }}</p>
|
||||
|
||||
<div class="body">
|
||||
{{ object.body|markdown:"safe" }}
|
||||
</div>
|
||||
|
||||
{% tags_for_object object as tag_list %}
|
||||
{% if tag_list %}
|
||||
<p class="inline_tag_list"><strong>Related tags:</strong>
|
||||
{% for tag in tag_list %}
|
||||
{{ tag }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% get_comment_list for object as comment_list %}
|
||||
{% if comment_list %}
|
||||
<div id="comments">
|
||||
<a name="comments"></a>
|
||||
<h3 class="comments_title">Comments</h3>
|
||||
{% for comment in comment_list %}
|
||||
{% if comment.is_public %}
|
||||
<div class="comment">
|
||||
<h5 class="name">
|
||||
<a name="c{{ comment.id }}" href="{{ comment.get_absolute_url }}" title="Permalink to {{ comment.person_name }}'s comment" class="count">{{ forloop.counter }}</a>
|
||||
{% if comment.user_url %}<a href="{{ comment.user_url }}">{{ comment.user_name }}</a>{% else %}{{ comment.user_name }}{% endif %} says...
|
||||
</h5>
|
||||
{{ comment.comment|urlizetrunc:"60"|markdown:"safe" }}
|
||||
<p class="date">Posted at {{ comment.submit_date|date:"P" }} on {{ comment.submit_date|date:"F j, Y" }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if object.allow_comments %}
|
||||
{% render_comment_form for object %}
|
||||
{% else %}
|
||||
<div id="comment_form">
|
||||
<h3>Comments are closed.</h3>
|
||||
<p>Comments have been close for this post.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -1,35 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Post archive{% endblock %}
|
||||
{% block body_class %}{{ block.super }} post_list{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Post archive</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="post_list">
|
||||
{% for post in object_list %}
|
||||
<div>
|
||||
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
|
||||
<p class="date">{{ post.publish|date:"Y F d" }}</p>
|
||||
<p class="tease">{{ post.tease }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<p class="pagination">
|
||||
{% if has_next %}
|
||||
<a class="older" href="?page={{ next }}">Older</a>
|
||||
{% endif %}
|
||||
{% if has_next and has_previous %} | {% endif %}
|
||||
{% if has_previous %}
|
||||
<a class="newer" href="?page={{ previous }}">Newer</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -1,37 +0,0 @@
|
||||
{% extends "blog/base_blog.html" %}
|
||||
|
||||
|
||||
{% block title %}Post search{% endblock %}
|
||||
{% block body_class %}{{ block.super }} post_search{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2>Search</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="get" id="post_search_form">
|
||||
<p>
|
||||
<input type="text" name="q" value="{{ search_term }}" id="search">
|
||||
<input type="submit" class="button" value="Search">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{% if message %}
|
||||
<p class="message">{{ message }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if object_list %}
|
||||
<div class="post_list">
|
||||
{% for post in object_list %}
|
||||
<div>
|
||||
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
|
||||
<p class="date">{{ post.publish|date:"Y F d" }}</p>
|
||||
<p class="tease">{{ post.tease }}</p>
|
||||
<p class="comments">{% if comment_count %}{{ comment_count }} comment{{ comment_count|pluralize }}{% endif %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -1 +0,0 @@
|
||||
{{ obj.tease }}
|
@ -1 +0,0 @@
|
||||
{{ obj.title }}
|
@ -1,7 +0,0 @@
|
||||
{% if object %}
|
||||
{{ object }}
|
||||
{% else %}
|
||||
{% for object in object_list %}
|
||||
{{ object }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
@ -1,103 +0,0 @@
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
import re
|
||||
|
||||
Post = models.get_model('blog', 'post')
|
||||
Category = models.get_model('blog', 'category')
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class LatestPosts(template.Node):
|
||||
def __init__(self, limit, var_name):
|
||||
self.limit = limit
|
||||
self.var_name = var_name
|
||||
|
||||
def render(self, context):
|
||||
posts = Post.objects.published()[:int(self.limit)]
|
||||
if posts and (int(self.limit) == 1):
|
||||
context[self.var_name] = posts[0]
|
||||
else:
|
||||
context[self.var_name] = posts
|
||||
return ''
|
||||
|
||||
@register.tag
|
||||
def get_latest_posts(parser, token):
|
||||
"""
|
||||
Gets any number of latest posts and stores them in a varable.
|
||||
|
||||
Syntax::
|
||||
|
||||
{% get_latest_posts [limit] as [var_name] %}
|
||||
|
||||
Example usage::
|
||||
|
||||
{% get_latest_posts 10 as latest_post_list %}
|
||||
"""
|
||||
try:
|
||||
tag_name, arg = token.contents.split(None, 1)
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
|
||||
m = re.search(r'(.*?) as (\w+)', arg)
|
||||
if not m:
|
||||
raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
|
||||
format_string, var_name = m.groups()
|
||||
return LatestPosts(format_string, var_name)
|
||||
|
||||
|
||||
class BlogCategories(template.Node):
|
||||
def __init__(self, var_name):
|
||||
self.var_name = var_name
|
||||
|
||||
def render(self, context):
|
||||
categories = Category.objects.all()
|
||||
context[self.var_name] = categories
|
||||
return ''
|
||||
|
||||
@register.tag
|
||||
def get_blog_categories(parser, token):
|
||||
"""
|
||||
Gets all blog categories.
|
||||
|
||||
Syntax::
|
||||
|
||||
{% get_blog_categories as [var_name] %}
|
||||
|
||||
Example usage::
|
||||
|
||||
{% get_blog_categories as category_list %}
|
||||
"""
|
||||
try:
|
||||
tag_name, arg = token.contents.split(None, 1)
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
|
||||
m = re.search(r'as (\w+)', arg)
|
||||
if not m:
|
||||
raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
|
||||
var_name = m.groups()[0]
|
||||
return BlogCategories(var_name)
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_links(value):
|
||||
"""
|
||||
Extracts links from a ``Post`` body and returns a list.
|
||||
|
||||
Template Syntax::
|
||||
|
||||
{{ post.body|markdown:"safe"|get_links }}
|
||||
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
except ImportError:
|
||||
from beautifulsoup import BeautifulSoup
|
||||
soup = BeautifulSoup(value)
|
||||
return soup.findAll('a')
|
||||
except ImportError:
|
||||
if settings.DEBUG:
|
||||
raise template.TemplateSyntaxError, "Error in 'get_links' filter: BeautifulSoup isn't installed."
|
||||
return value
|
@ -1,66 +0,0 @@
|
||||
"""
|
||||
>>> from django.test import Client
|
||||
>>> from calibre.www.apps.blog.models import Post, Category
|
||||
>>> import datetime
|
||||
>>> from django.core.urlresolvers import reverse
|
||||
>>> client = Client()
|
||||
|
||||
>>> category = Category(title='Django', slug='django')
|
||||
>>> category.save()
|
||||
>>> category2 = Category(title='Rails', slug='rails')
|
||||
>>> category2.save()
|
||||
|
||||
>>> post = Post(title='DJ Ango', slug='dj-ango', body='Yo DJ! Turn that music up!', status=2, publish=datetime.datetime(2008,5,5,16,20))
|
||||
>>> post.save()
|
||||
|
||||
>>> post2 = Post(title='Where my grails at?', slug='where', body='I Can haz Holy plez?', status=2, publish=datetime.datetime(2008,4,2,11,11))
|
||||
>>> post2.save()
|
||||
|
||||
>>> post.categories.add(category)
|
||||
>>> post2.categories.add(category2)
|
||||
|
||||
>>> response = client.get(reverse('blog_index'))
|
||||
>>> response.context[-1]['object_list']
|
||||
[<Post: DJ Ango>, <Post: Where my grails at?>]
|
||||
>>> response.status_code
|
||||
200
|
||||
|
||||
>>> response = client.get(reverse('blog_category_list'))
|
||||
>>> response.context[-1]['object_list']
|
||||
[<Category: Django>, <Category: Rails>]
|
||||
>>> response.status_code
|
||||
200
|
||||
|
||||
>>> response = client.get(category.get_absolute_url())
|
||||
>>> response.context[-1]['object_list']
|
||||
[<Post: DJ Ango>]
|
||||
>>> response.status_code
|
||||
200
|
||||
|
||||
>>> response = client.get(post.get_absolute_url())
|
||||
>>> response.context[-1]['object']
|
||||
<Post: DJ Ango>
|
||||
>>> response.status_code
|
||||
200
|
||||
|
||||
>>> response = client.get(reverse('blog_search'), {'q': 'DJ'})
|
||||
>>> response.context[-1]['object_list']
|
||||
[<Post: DJ Ango>]
|
||||
>>> response.status_code
|
||||
200
|
||||
>>> response = client.get(reverse('blog_search'), {'q': 'Holy'})
|
||||
>>> response.context[-1]['object_list']
|
||||
[<Post: Where my grails at?>]
|
||||
>>> response.status_code
|
||||
200
|
||||
>>> response = client.get(reverse('blog_search'), {'q': ''})
|
||||
>>> response.context[-1]['message']
|
||||
'Search term was too vague. Please try again.'
|
||||
|
||||
>>> response = client.get(reverse('blog_detail', args=[2008, 'apr', 2, 'where']))
|
||||
>>> response.context[-1]['object']
|
||||
<Post: Where my grails at?>
|
||||
>>> response.status_code
|
||||
200
|
||||
"""
|
||||
|
@ -1,41 +0,0 @@
|
||||
from django.conf.urls.defaults import *
|
||||
from calibre.www.apps.blog import views as blog_views
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^(?P<year>\d{4})/(?P<month>\w{3})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
|
||||
view=blog_views.post_detail,
|
||||
name='blog_detail'),
|
||||
|
||||
url(r'^(?P<year>\d{4})/(?P<month>\w{3})/(?P<day>\d{1,2})/$',
|
||||
view=blog_views.post_archive_day,
|
||||
name='blog_archive_day'),
|
||||
|
||||
url(r'^(?P<year>\d{4})/(?P<month>\w{3})/$',
|
||||
view=blog_views.post_archive_month,
|
||||
name='blog_archive_month'),
|
||||
|
||||
url(r'^(?P<year>\d{4})/$',
|
||||
view=blog_views.post_archive_year,
|
||||
name='blog_archive_year'),
|
||||
|
||||
url(r'^categories/(?P<slug>[-\w]+)/$',
|
||||
view=blog_views.category_detail,
|
||||
name='blog_category_detail'),
|
||||
|
||||
url (r'^categories/$',
|
||||
view=blog_views.category_list,
|
||||
name='blog_category_list'),
|
||||
|
||||
url (r'^search/$',
|
||||
view=blog_views.search,
|
||||
name='blog_search'),
|
||||
|
||||
url(r'^page/(?P<page>\w)/$',
|
||||
view=blog_views.post_list,
|
||||
name='blog_index_paginated'),
|
||||
|
||||
url(r'^$',
|
||||
view=blog_views.post_list,
|
||||
name='blog_index'),
|
||||
)
|
@ -1,160 +0,0 @@
|
||||
from django.shortcuts import render_to_response, get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.http import Http404
|
||||
from django.views.generic import date_based, list_detail
|
||||
from django.db.models import Q
|
||||
from calibre.www.apps.blog.models import *
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
|
||||
def post_list(request, page=0, **kwargs):
|
||||
return list_detail.object_list(
|
||||
request,
|
||||
queryset = Post.objects.published(),
|
||||
paginate_by = 20,
|
||||
page = page,
|
||||
**kwargs
|
||||
)
|
||||
post_list.__doc__ = list_detail.object_list.__doc__
|
||||
|
||||
|
||||
def post_archive_year(request, year, **kwargs):
|
||||
return date_based.archive_year(
|
||||
request,
|
||||
year = year,
|
||||
date_field = 'publish',
|
||||
queryset = Post.objects.published(),
|
||||
make_object_list = True,
|
||||
**kwargs
|
||||
)
|
||||
post_archive_year.__doc__ = date_based.archive_year.__doc__
|
||||
|
||||
|
||||
def post_archive_month(request, year, month, **kwargs):
|
||||
return date_based.archive_month(
|
||||
request,
|
||||
year = year,
|
||||
month = month,
|
||||
date_field = 'publish',
|
||||
queryset = Post.objects.published(),
|
||||
**kwargs
|
||||
)
|
||||
post_archive_month.__doc__ = date_based.archive_month.__doc__
|
||||
|
||||
|
||||
def post_archive_day(request, year, month, day, **kwargs):
|
||||
return date_based.archive_day(
|
||||
request,
|
||||
year = year,
|
||||
month = month,
|
||||
day = day,
|
||||
date_field = 'publish',
|
||||
queryset = Post.objects.published(),
|
||||
**kwargs
|
||||
)
|
||||
post_archive_day.__doc__ = date_based.archive_day.__doc__
|
||||
|
||||
|
||||
def post_detail(request, slug, year, month, day, **kwargs):
|
||||
return date_based.object_detail(
|
||||
request,
|
||||
year = year,
|
||||
month = month,
|
||||
day = day,
|
||||
date_field = 'publish',
|
||||
slug = slug,
|
||||
queryset = Post.objects.published(),
|
||||
**kwargs
|
||||
)
|
||||
post_detail.__doc__ = date_based.object_detail.__doc__
|
||||
|
||||
|
||||
def category_list(request, template_name = 'blog/category_list.html', **kwargs):
|
||||
"""
|
||||
Category list
|
||||
|
||||
Template: ``blog/category_list.html``
|
||||
Context:
|
||||
object_list
|
||||
List of categories.
|
||||
"""
|
||||
return list_detail.object_list(
|
||||
request,
|
||||
queryset = Category.objects.all(),
|
||||
template_name = template_name,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def category_detail(request, slug, template_name = 'blog/category_detail.html', **kwargs):
|
||||
"""
|
||||
Category detail
|
||||
|
||||
Template: ``blog/category_detail.html``
|
||||
Context:
|
||||
object_list
|
||||
List of posts specific to the given category.
|
||||
category
|
||||
Given category.
|
||||
"""
|
||||
category = get_object_or_404(Category, slug__iexact=slug)
|
||||
|
||||
return list_detail.object_list(
|
||||
request,
|
||||
queryset = category.post_set.published(),
|
||||
extra_context = {'category': category},
|
||||
template_name = template_name,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
# Stop Words courtesy of http://www.dcs.gla.ac.uk/idom/ir_resources/linguistic_utils/stop_words
|
||||
STOP_WORDS = r"""\b(a|about|above|across|after|afterwards|again|against|all|almost|alone|along|already|also|
|
||||
although|always|am|among|amongst|amoungst|amount|an|and|another|any|anyhow|anyone|anything|anyway|anywhere|are|
|
||||
around|as|at|back|be|became|because|become|becomes|becoming|been|before|beforehand|behind|being|below|beside|
|
||||
besides|between|beyond|bill|both|bottom|but|by|call|can|cannot|cant|co|computer|con|could|couldnt|cry|de|describe|
|
||||
detail|do|done|down|due|during|each|eg|eight|either|eleven|else|elsewhere|empty|enough|etc|even|ever|every|everyone|
|
||||
everything|everywhere|except|few|fifteen|fify|fill|find|fire|first|five|for|former|formerly|forty|found|four|from|
|
||||
front|full|further|get|give|go|had|has|hasnt|have|he|hence|her|here|hereafter|hereby|herein|hereupon|hers|herself|
|
||||
him|himself|his|how|however|hundred|i|ie|if|in|inc|indeed|interest|into|is|it|its|itself|keep|last|latter|latterly|
|
||||
least|less|ltd|made|many|may|me|meanwhile|might|mill|mine|more|moreover|most|mostly|move|much|must|my|myself|name|
|
||||
namely|neither|never|nevertheless|next|nine|no|nobody|none|noone|nor|not|nothing|now|nowhere|of|off|often|on|once|
|
||||
one|only|onto|or|other|others|otherwise|our|ours|ourselves|out|over|own|part|per|perhaps|please|put|rather|re|same|
|
||||
see|seem|seemed|seeming|seems|serious|several|she|should|show|side|since|sincere|six|sixty|so|some|somehow|someone|
|
||||
something|sometime|sometimes|somewhere|still|such|system|take|ten|than|that|the|their|them|themselves|then|thence|
|
||||
there|thereafter|thereby|therefore|therein|thereupon|these|they|thick|thin|third|this|those|though|three|through|
|
||||
throughout|thru|thus|to|together|too|top|toward|towards|twelve|twenty|two|un|under|until|up|upon|us|very|via|was|
|
||||
we|well|were|what|whatever|when|whence|whenever|where|whereafter|whereas|whereby|wherein|whereupon|wherever|whether|
|
||||
which|while|whither|who|whoever|whole|whom|whose|why|will|with|within|without|would|yet|you|your|yours|yourself|
|
||||
yourselves)\b"""
|
||||
|
||||
|
||||
def search(request, template_name='blog/post_search.html'):
|
||||
"""
|
||||
Search for blog posts.
|
||||
|
||||
This template will allow you to setup a simple search form that will try to return results based on
|
||||
given search strings. The queries will be put through a stop words filter to remove words like
|
||||
'the', 'a', or 'have' to help imporve the result set.
|
||||
|
||||
Template: ``blog/post_search.html``
|
||||
Context:
|
||||
object_list
|
||||
List of blog posts that match given search term(s).
|
||||
search_term
|
||||
Given search term.
|
||||
"""
|
||||
context = {}
|
||||
if request.GET:
|
||||
stop_word_list = re.compile(STOP_WORDS, re.IGNORECASE)
|
||||
search_term = '%s' % request.GET['q']
|
||||
cleaned_search_term = stop_word_list.sub('', search_term)
|
||||
cleaned_search_term = cleaned_search_term.strip()
|
||||
if len(cleaned_search_term) != 0:
|
||||
post_list = Post.objects.published().filter(Q(body__icontains=cleaned_search_term) | Q(tags__icontains=cleaned_search_term) | Q(categories__title__icontains=cleaned_search_term))
|
||||
context = {'object_list': post_list, 'search_term':search_term}
|
||||
else:
|
||||
message = 'Search term was too vague. Please try again.'
|
||||
context = {'message':message}
|
||||
return render_to_response(template_name, context, context_instance=RequestContext(request))
|
@ -1,8 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
feedjack
|
||||
Gustavo Picón
|
||||
__init__.py
|
||||
"""
|
||||
|
@ -1,60 +0,0 @@
|
||||
# -*- 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)
|
||||
|
||||
#~
|
@ -1,83 +0,0 @@
|
||||
# -*- 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)
|
||||
|
||||
|
@ -1,93 +0,0 @@
|
||||
# -*- 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]
|
||||
|
@ -1,280 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
feedjack
|
||||
Gustavo Picón
|
||||
fjlib.py
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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 unnecessary 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
|
||||
|
||||
|
@ -1,202 +0,0 @@
|
||||
# -*- 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()
|
||||
|
||||
|
@ -1,506 +0,0 @@
|
||||
#!/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()
|
||||
|
@ -1,47 +0,0 @@
|
||||
# -*- 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),
|
||||
)
|
||||
|
||||
#~
|
@ -1,152 +0,0 @@
|
||||
# -*- 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 RequestContext, 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.get('HTTP_HOST',
|
||||
'planet.calibre-ebook.com'), \
|
||||
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 = RequestContext(request, 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, context_instance=RequestContext(request))
|
||||
|
||||
# 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
|
||||
|
||||
#~
|
||||
|
@ -1,10 +0,0 @@
|
||||
changes:
|
||||
date: 2008-05-18
|
||||
change: Converted everything to 4 space tabs and made a few other changes to comply with Python Style Guide.
|
||||
|
||||
date: 2008-04-23
|
||||
change: Added a mode called InlineType and a template tag that returns this models objects.
|
||||
|
||||
date: 2008-04-22
|
||||
change: Added an 'extract_inlines' filter so you can loop over a list of inlines in a body of text.
|
||||
change: Creating new inlines app.
|
@ -1,27 +0,0 @@
|
||||
==============================================
|
||||
Django Basic Inlines
|
||||
http://code.google.com/p/django-basic-apps/
|
||||
==============================================
|
||||
|
||||
A simple book library application for Django projects.
|
||||
|
||||
To install this app, simply create a folder somewhere in
|
||||
your PYTHONPATH named 'basic' and place the 'inlines'
|
||||
app inside. Then add 'basic.inlines' to your projects
|
||||
INSTALLED_APPS list in your settings.py file.
|
||||
|
||||
Inlines is a template filter that can be used in
|
||||
conjunction with inline markup to insert content objects
|
||||
into other pieces of content. An example would be inserting
|
||||
a photo into a blog post body.
|
||||
|
||||
An example of the markup is:
|
||||
<inline type="media.photo" id="1" />
|
||||
|
||||
The type attribute is app_name.model_name and the id is
|
||||
the object id. Pretty simple.
|
||||
|
||||
In your template you would say:
|
||||
{% load inlines %}
|
||||
|
||||
{{ post.body|render_inlines }}
|
@ -1,5 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from calibre.www.apps.inlines.models import *
|
||||
|
||||
|
||||
admin.site.register(InlineType)
|
@ -1,17 +0,0 @@
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
|
||||
class InlineType(models.Model):
|
||||
""" InlineType model """
|
||||
title = models.CharField(max_length=200)
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
|
||||
class Meta:
|
||||
db_table = 'inline_types'
|
||||
|
||||
class Admin:
|
||||
pass
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
@ -1,92 +0,0 @@
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.utils.encoding import smart_unicode
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
def inlines(value, return_list=False):
|
||||
try:
|
||||
from BeautifulSoup import BeautifulStoneSoup
|
||||
except ImportError:
|
||||
from beautifulsoup import BeautifulStoneSoup
|
||||
|
||||
content = BeautifulStoneSoup(value, selfClosingTags=['inline','img','br','input','meta','link','hr'])
|
||||
inline_list = []
|
||||
|
||||
if return_list:
|
||||
for inline in content.findAll('inline'):
|
||||
rendered_inline = render_inline(inline)
|
||||
inline_list.append(rendered_inline['context'])
|
||||
return inline_list
|
||||
else:
|
||||
for inline in content.findAll('inline'):
|
||||
rendered_inline = render_inline(inline)
|
||||
inline.replaceWith(render_to_string(rendered_inline['template'], rendered_inline['context']))
|
||||
return mark_safe(content)
|
||||
|
||||
|
||||
def render_inline(inline):
|
||||
"""
|
||||
Replace inline markup with template markup that matches the
|
||||
appropriate app and model.
|
||||
|
||||
"""
|
||||
|
||||
# Look for inline type, 'app.model'
|
||||
try:
|
||||
app_label, model_name = inline['type'].split('.')
|
||||
except:
|
||||
if settings.DEBUG:
|
||||
raise TemplateSyntaxError, "Couldn't find the attribute 'type' in the <inline> tag."
|
||||
else:
|
||||
return ''
|
||||
|
||||
# Look for content type
|
||||
try:
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model_name)
|
||||
model = content_type.model_class()
|
||||
except ContentType.DoesNotExist:
|
||||
if settings.DEBUG:
|
||||
raise TemplateSyntaxError, "Inline ContentType not found."
|
||||
else:
|
||||
return ''
|
||||
|
||||
# Check for an inline class attribute
|
||||
try:
|
||||
inline_class = smart_unicode(inline['class'])
|
||||
except:
|
||||
inline_class = ''
|
||||
|
||||
try:
|
||||
try:
|
||||
id_list = [int(i) for i in inline['ids'].split(',')]
|
||||
obj_list = model.objects.in_bulk(id_list)
|
||||
obj_list = list(obj_list[int(i)] for i in id_list)
|
||||
context = { 'object_list': obj_list, 'class': inline_class }
|
||||
except ValueError:
|
||||
if settings.DEBUG:
|
||||
raise ValueError, "The <inline> ids attribute is missing or invalid."
|
||||
else:
|
||||
return ''
|
||||
except KeyError:
|
||||
try:
|
||||
obj = model.objects.get(pk=inline['id'])
|
||||
context = { 'content_type':"%s.%s" % (app_label, model_name), 'object': obj, 'class': inline_class, 'settings': settings }
|
||||
except model.DoesNotExist:
|
||||
if settings.DEBUG:
|
||||
raise model.DoesNotExist, "Object matching '%s' does not exist"
|
||||
else:
|
||||
return ''
|
||||
except:
|
||||
if settings.DEBUG:
|
||||
raise TemplateSyntaxError, "The <inline> id attribute is missing or invalid."
|
||||
else:
|
||||
return ''
|
||||
|
||||
template = ["inlines/%s_%s.html" % (app_label, model_name), "inlines/default.html"]
|
||||
rendered_inline = {'template':template, 'context':context}
|
||||
|
||||
return rendered_inline
|
@ -1,81 +0,0 @@
|
||||
from django import template
|
||||
from calibre.www.apps.inlines.parser import inlines
|
||||
from calibre.www.apps.inlines.models import InlineType
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def render_inlines(value):
|
||||
"""
|
||||
Renders inlines in a ``Post`` by passing them through inline templates.
|
||||
|
||||
Template Syntax::
|
||||
|
||||
{{ post.body|render_inlines|markdown:"safe" }}
|
||||
|
||||
Inline Syntax (singular)::
|
||||
|
||||
<inline type="<app_name>.<model_name>" id="<id>" class="med_left" />
|
||||
|
||||
Inline Syntax (plural)::
|
||||
|
||||
<inline type="<app_name>.<model_name>" ids="<id>, <id>, <id>" />
|
||||
|
||||
An inline template will be used to render the inline. Templates will be
|
||||
locaed in the following maner:
|
||||
|
||||
``inlines/<app_name>_<model_name>.html``
|
||||
|
||||
The template will be passed the following context:
|
||||
|
||||
``object``
|
||||
An object for the corresponding passed id.
|
||||
|
||||
or
|
||||
|
||||
``object_list``
|
||||
A list of objects for the corresponding ids.
|
||||
|
||||
It would be wise to anticipate both object_list and object unless
|
||||
you know for sure one or the other will only be present.
|
||||
"""
|
||||
return inlines(value)
|
||||
|
||||
@register.filter
|
||||
def extract_inlines(value):
|
||||
return inlines(value, True)
|
||||
|
||||
|
||||
class InlineTypes(template.Node):
|
||||
def __init__(self, var_name):
|
||||
self.var_name = var_name
|
||||
|
||||
def render(self, context):
|
||||
types = InlineType.objects.all()
|
||||
context[self.var_name] = types
|
||||
return ''
|
||||
|
||||
@register.tag(name='get_inline_types')
|
||||
def do_get_inline_types(parser, token):
|
||||
"""
|
||||
Gets all inline types.
|
||||
|
||||
Syntax::
|
||||
|
||||
{% get_inline_types as [var_name] %}
|
||||
|
||||
Example usage::
|
||||
|
||||
{% get_inline_types as inline_list %}
|
||||
"""
|
||||
try:
|
||||
tag_name, arg = token.contents.split(None, 1)
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
|
||||
m = re.search(r'as (\w+)', arg)
|
||||
if not m:
|
||||
raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
|
||||
var_name = m.groups()[0]
|
||||
return InlineTypes(var_name)
|
@ -1,11 +0,0 @@
|
||||
#!/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)
|
@ -1,49 +0,0 @@
|
||||
# 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, TEMPLATE_CONTEXT_PROCESSORS
|
||||
|
||||
FORCE_LOWERCASE_TAGS = False
|
||||
MAX_TAG_LENGTH = 50
|
||||
|
||||
if not DEBUG:
|
||||
MEDIA_URL = 'http://kovid.calibre-ebook.com/site_media/'
|
||||
ADMIN_MEDIA_PREFIX = 'http://kovid.calibre-ebook.com/admin_media/'
|
||||
MEDIA_ROOT = '/usr/local/calibre/src/calibre/www/static/'
|
||||
|
||||
|
||||
if DEBUG:
|
||||
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
DATABASE_NAME = '/tmp/kovid.db' # Or path to database file if using sqlite3.
|
||||
else:
|
||||
DATABASE_ENGINE = 'mysql'
|
||||
DATABASE_NAME = 'calibre_kovid'
|
||||
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/kovid.calibre-ebook.com/django_secret_key').read().strip()
|
||||
|
||||
|
||||
ROOT_URLCONF = 'calibre.www.kovid.urls'
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.comments',
|
||||
'django.contrib.markup',
|
||||
'calibre.www.apps.inlines',
|
||||
'tagging',
|
||||
'calibre.www.apps.blog',
|
||||
)
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
from django.conf.urls.defaults import patterns, include, handler404, handler500
|
||||
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/(.*)', admin.site.root),
|
||||
|
||||
(r'^comments/', include('django.contrib.comments.urls')),
|
||||
(r'', include('calibre.www.apps.blog.urls')),
|
||||
|
||||
|
||||
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += patterns('',
|
||||
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
|
||||
{'document_root': settings.MEDIA_ROOT}),
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
Test
|
||||
=====
|
||||
|
||||
Calibre planet can be run either in development mode or deployment mode. For testing,
|
||||
it should be run in development mode as follows:
|
||||
|
||||
* Install django
|
||||
* ``cd test && ./test``
|
||||
* Planet is at `http://localhost:8000`
|
||||
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
#!/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)
|
@ -1,42 +0,0 @@
|
||||
# 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, TEMPLATE_CONTEXT_PROCESSORS
|
||||
|
||||
if not DEBUG:
|
||||
MEDIA_URL = 'http://planet.calibre-ebook.com/site_media/'
|
||||
ADMIN_MEDIA_PREFIX = 'http://planet.calibre-ebook.com/admin_media/'
|
||||
MEDIA_ROOT = '/usr/local/calibre/src/calibre/www/static/'
|
||||
|
||||
|
||||
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 = 'calibre.www.planet.urls'
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.admin',
|
||||
'calibre.www.apps.feedjack',
|
||||
)
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
cp planet.db /tmp
|
||||
cd ..
|
||||
python manage.py runserver
|
@ -1,24 +0,0 @@
|
||||
from django.conf.urls.defaults import patterns, include, handler404, handler500
|
||||
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/(.*)', 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}),
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
ssh divok "cd /usr/local/calibre && bzr up"
|
||||
ssh divok /etc/init.d/apache2 graceful
|
@ -1,86 +0,0 @@
|
||||
# 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 = True
|
||||
|
||||
# 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 = (
|
||||
'/usr/local/calibre/src/calibre/www/templates',
|
||||
)
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
"django.core.context_processors.auth",
|
||||
"django.core.context_processors.debug",
|
||||
"django.core.context_processors.i18n",
|
||||
"django.core.context_processors.media"
|
||||
)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 279 B |
Before Width: | Height: | Size: 299 B |
Before Width: | Height: | Size: 299 B |
Before Width: | Height: | Size: 292 B |
Before Width: | Height: | Size: 399 B |
Before Width: | Height: | Size: 317 B |
Before Width: | Height: | Size: 280 B |
Before Width: | Height: | Size: 321 B |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 17 KiB |
@ -1,40 +0,0 @@
|
||||
body {
|
||||
font-family: sansserif;
|
||||
background-color: #f1fff1;
|
||||
}
|
||||
|
||||
img {
|
||||
border:0 none;
|
||||
}
|
||||
|
||||
#_header {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid black;
|
||||
margin-bottom: 20px;
|
||||
height: 100px;
|
||||
background-color: #A2B964;
|
||||
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; }
|
@ -1,115 +0,0 @@
|
||||
/* 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;
|
||||
}
|
@ -1,313 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
line-height: 130%;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page not found{% endblock %}
|
||||
|
||||
{% block header_text %}Page not found{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>Sorry, but the requested page could not be found.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer_text %} {% endblock %}
|
@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Page unavailable</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Page unavailable</h1>
|
||||
|
||||
<p>Sorry, but the requested page is unavailable due to a
|
||||
server hiccup.</p>
|
||||
|
||||
<p>Our engineers have been notified, so check back later.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,35 +0,0 @@
|
||||
<?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" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
{% 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">
|
||||
{% block footer_text %}
|
||||
Created by Kovid Goyal.
|
||||
Powered by <a href="http://www.djangoproject.com">
|
||||
<img alt="Django" src="{{ MEDIA_URL }}/img/button-django.png"/>
|
||||
</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,190 +0,0 @@
|
||||
{% 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="" />
|
||||
<div class="url">
|
||||
{{ item.feed.name|safe }}
|
||||
</div>
|
||||
</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 planet, 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 %}
|
@ -1,34 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<rdf:RDF
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
|
||||
xmlns:foaf="http://xmlns.com/foaf/0.1/"
|
||||
xmlns:rss="http://purl.org/rss/1.0/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
>
|
||||
<!-- based on http://www-128.ibm.com/developerworks/xml/library/x-pblog/ -->
|
||||
<foaf:Group>
|
||||
|
||||
<foaf:name>{{ site.title }}</foaf:name>
|
||||
<foaf:homepage>{{ site.url }}</foaf:homepage>
|
||||
<rdfs:seeAlso rdf:resource="{{ site.url }}/foaf/" />
|
||||
|
||||
{% for feed in feeds %}
|
||||
<foaf:member>
|
||||
<foaf:Person>
|
||||
<foaf:name>{{ feed.name }}</foaf:name>
|
||||
<foaf:weblog>
|
||||
<foaf:Document rdf:about="{{ feed.link }}">
|
||||
<dc:title>{{ feed.title }}</dc:title>
|
||||
<rdfs:seeAlso>
|
||||
<rss:channel rdf:about="{{ feed.feed_url }}" />
|
||||
</rdfs:seeAlso>
|
||||
</foaf:Document>
|
||||
</foaf:weblog>
|
||||
</foaf:Person>
|
||||
</foaf:member>
|
||||
{% endfor %}
|
||||
|
||||
</foaf:Group>
|
||||
</rdf:RDF>
|
||||
|
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf8"?>
|
||||
<opml version="1.1">
|
||||
<head>
|
||||
<title>{{ site.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{% for feed in feeds %}
|
||||
<outline type="rss" text="{{ feed.name }}" description="{{ feed.title }}" htmlUrl="{{ feed.link }}" xmlUrl="{{ feed.feed_url }}"/>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</opml>
|
@ -1,30 +0,0 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from tagging.managers import ModelTaggedItemManager, TagDescriptor
|
||||
|
||||
VERSION = (0, 3, 'pre')
|
||||
|
||||
class AlreadyRegistered(Exception):
|
||||
"""
|
||||
An attempt was made to register a model more than once.
|
||||
"""
|
||||
pass
|
||||
|
||||
registry = []
|
||||
|
||||
def register(model, tag_descriptor_attr='tags',
|
||||
tagged_item_manager_attr='tagged'):
|
||||
"""
|
||||
Sets the given model class up for working with tags.
|
||||
"""
|
||||
if model in registry:
|
||||
raise AlreadyRegistered(
|
||||
_('The model %s has already been registered.') % model.__name__)
|
||||
registry.append(model)
|
||||
|
||||
# Add tag descriptor
|
||||
setattr(model, tag_descriptor_attr, TagDescriptor())
|
||||
|
||||
# Add custom manager
|
||||
ModelTaggedItemManager().contribute_to_class(model,
|
||||
tagged_item_manager_attr)
|
@ -1,5 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from tagging.models import Tag, TaggedItem
|
||||
|
||||
admin.site.register(TaggedItem)
|
||||
admin.site.register(Tag)
|
@ -1,107 +0,0 @@
|
||||
"""
|
||||
A custom Model Field for tagging.
|
||||
"""
|
||||
from django.db.models import signals
|
||||
from django.db.models.fields import CharField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from tagging import settings
|
||||
from tagging.models import Tag
|
||||
from tagging.utils import edit_string_for_tags
|
||||
|
||||
class TagField(CharField):
|
||||
"""
|
||||
A "special" character field that actually works as a relationship to tags
|
||||
"under the hood". This exposes a space-separated string of tags, but does
|
||||
the splitting/reordering/etc. under the hood.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 255)
|
||||
kwargs['blank'] = kwargs.get('blank', True)
|
||||
super(TagField, self).__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
super(TagField, self).contribute_to_class(cls, name)
|
||||
|
||||
# Make this object the descriptor for field access.
|
||||
setattr(cls, self.name, self)
|
||||
|
||||
# Save tags back to the database post-save
|
||||
signals.post_save.connect(self._save, cls, True)
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
"""
|
||||
Tag getter. Returns an instance's tags if accessed on an instance, and
|
||||
all of a model's tags if called on a class. That is, this model::
|
||||
|
||||
class Link(models.Model):
|
||||
...
|
||||
tags = TagField()
|
||||
|
||||
Lets you do both of these::
|
||||
|
||||
>>> l = Link.objects.get(...)
|
||||
>>> l.tags
|
||||
'tag1 tag2 tag3'
|
||||
|
||||
>>> Link.tags
|
||||
'tag1 tag2 tag3 tag4'
|
||||
|
||||
"""
|
||||
# Handle access on the model (i.e. Link.tags)
|
||||
if instance is None:
|
||||
return edit_string_for_tags(Tag.objects.usage_for_model(owner))
|
||||
|
||||
tags = self._get_instance_tag_cache(instance)
|
||||
if tags is None:
|
||||
if instance.pk is None:
|
||||
self._set_instance_tag_cache(instance, '')
|
||||
else:
|
||||
self._set_instance_tag_cache(
|
||||
instance, edit_string_for_tags(Tag.objects.get_for_object(instance)))
|
||||
return self._get_instance_tag_cache(instance)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
Set an object's tags.
|
||||
"""
|
||||
if instance is None:
|
||||
raise AttributeError(_('%s can only be set on instances.') % self.name)
|
||||
if settings.FORCE_LOWERCASE_TAGS and value is not None:
|
||||
value = value.lower()
|
||||
self._set_instance_tag_cache(instance, value)
|
||||
|
||||
def _save(self, **kwargs): #signal, sender, instance):
|
||||
"""
|
||||
Save tags back to the database
|
||||
"""
|
||||
tags = self._get_instance_tag_cache(kwargs['instance'])
|
||||
if tags is not None:
|
||||
Tag.objects.update_tags(kwargs['instance'], tags)
|
||||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Clear all of an object's tags.
|
||||
"""
|
||||
self._set_instance_tag_cache(instance, '')
|
||||
|
||||
def _get_instance_tag_cache(self, instance):
|
||||
"""
|
||||
Helper: get an instance's tag cache.
|
||||
"""
|
||||
return getattr(instance, '_%s_cache' % self.attname, None)
|
||||
|
||||
def _set_instance_tag_cache(self, instance, tags):
|
||||
"""
|
||||
Helper: set an instance's tag cache.
|
||||
"""
|
||||
setattr(instance, '_%s_cache' % self.attname, tags)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'CharField'
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
from tagging import forms
|
||||
defaults = {'form_class': forms.TagField}
|
||||
defaults.update(kwargs)
|
||||
return super(TagField, self).formfield(**defaults)
|
@ -1,40 +0,0 @@
|
||||
"""
|
||||
Tagging components for Django's form library.
|
||||
"""
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from tagging import settings
|
||||
from tagging.models import Tag
|
||||
from tagging.utils import parse_tag_input
|
||||
|
||||
class AdminTagForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Tag
|
||||
|
||||
def clean_name(self):
|
||||
value = self.cleaned_data['name']
|
||||
tag_names = parse_tag_input(value)
|
||||
if len(tag_names) > 1:
|
||||
raise ValidationError(_('Multiple tags were given.'))
|
||||
elif len(tag_names[0]) > settings.MAX_TAG_LENGTH:
|
||||
raise forms.ValidationError(
|
||||
_('A tag may be no more than %s characters long.') %
|
||||
settings.MAX_TAG_LENGTH)
|
||||
return value
|
||||
|
||||
class TagField(forms.CharField):
|
||||
"""
|
||||
A ``CharField`` which validates that its input is a valid list of
|
||||
tag names.
|
||||
"""
|
||||
def clean(self, value):
|
||||
value = super(TagField, self).clean(value)
|
||||
if value == u'':
|
||||
return value
|
||||
for tag_name in parse_tag_input(value):
|
||||
if len(tag_name) > settings.MAX_TAG_LENGTH:
|
||||
raise forms.ValidationError(
|
||||
_('Each tag may be no more than %s characters long.') %
|
||||
settings.MAX_TAG_LENGTH)
|
||||
return value
|
@ -1,40 +0,0 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
def fetch_content_objects(tagged_items, select_related_for=None):
|
||||
"""
|
||||
Retrieves ``ContentType`` and content objects for the given list of
|
||||
``TaggedItems``, grouping the retrieval of content objects by model
|
||||
type to reduce the number of queries executed.
|
||||
|
||||
This results in ``number_of_content_types + 1`` queries rather than
|
||||
the ``number_of_tagged_items * 2`` queries you'd get by iterating
|
||||
over the list and accessing each item's ``object`` attribute.
|
||||
|
||||
A ``select_related_for`` argument can be used to specify a list of
|
||||
of model names (corresponding to the ``model`` field of a
|
||||
``ContentType``) for which ``select_related`` should be used when
|
||||
retrieving model instances.
|
||||
"""
|
||||
if select_related_for is None: select_related_for = []
|
||||
|
||||
# Group content object pks by their content type pks
|
||||
objects = {}
|
||||
for item in tagged_items:
|
||||
objects.setdefault(item.content_type_id, []).append(item.object_id)
|
||||
|
||||
# Retrieve content types and content objects in bulk
|
||||
content_types = ContentType._default_manager.in_bulk(objects.keys())
|
||||
for content_type_pk, object_pks in objects.iteritems():
|
||||
model = content_types[content_type_pk].model_class()
|
||||
if content_types[content_type_pk].model in select_related_for:
|
||||
objects[content_type_pk] = model._default_manager.select_related().in_bulk(object_pks)
|
||||
else:
|
||||
objects[content_type_pk] = model._default_manager.in_bulk(object_pks)
|
||||
|
||||
# Set content types and content objects in the appropriate cache
|
||||
# attributes, so accessing the 'content_type' and 'object'
|
||||
# attributes on each tagged item won't result in further database
|
||||
# hits.
|
||||
for item in tagged_items:
|
||||
item._object_cache = objects[item.content_type_id][item.object_id]
|
||||
item._content_type_cache = content_types[item.content_type_id]
|
@ -1,68 +0,0 @@
|
||||
"""
|
||||
Custom managers for Django models registered with the tagging
|
||||
application.
|
||||
"""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
|
||||
from tagging.models import Tag, TaggedItem
|
||||
|
||||
class ModelTagManager(models.Manager):
|
||||
"""
|
||||
A manager for retrieving tags for a particular model.
|
||||
"""
|
||||
def get_query_set(self):
|
||||
ctype = ContentType.objects.get_for_model(self.model)
|
||||
return Tag.objects.filter(
|
||||
items__content_type__pk=ctype.pk).distinct()
|
||||
|
||||
def cloud(self, *args, **kwargs):
|
||||
return Tag.objects.cloud_for_model(self.model, *args, **kwargs)
|
||||
|
||||
def related(self, tags, *args, **kwargs):
|
||||
return Tag.objects.related_for_model(tags, self.model, *args, **kwargs)
|
||||
|
||||
def usage(self, *args, **kwargs):
|
||||
return Tag.objects.usage_for_model(self.model, *args, **kwargs)
|
||||
|
||||
class ModelTaggedItemManager(models.Manager):
|
||||
"""
|
||||
A manager for retrieving model instances based on their tags.
|
||||
"""
|
||||
def related_to(self, obj, queryset=None, num=None):
|
||||
if queryset is None:
|
||||
return TaggedItem.objects.get_related(obj, self.model, num=num)
|
||||
else:
|
||||
return TaggedItem.objects.get_related(obj, queryset, num=num)
|
||||
|
||||
def with_all(self, tags, queryset=None):
|
||||
if queryset is None:
|
||||
return TaggedItem.objects.get_by_model(self.model, tags)
|
||||
else:
|
||||
return TaggedItem.objects.get_by_model(queryset, tags)
|
||||
|
||||
def with_any(self, tags, queryset=None):
|
||||
if queryset is None:
|
||||
return TaggedItem.objects.get_union_by_model(self.model, tags)
|
||||
else:
|
||||
return TaggedItem.objects.get_union_by_model(queryset, tags)
|
||||
|
||||
class TagDescriptor(object):
|
||||
"""
|
||||
A descriptor which provides access to a ``ModelTagManager`` for
|
||||
model classes and simple retrieval, updating and deletion of tags
|
||||
for model instances.
|
||||
"""
|
||||
def __get__(self, instance, owner):
|
||||
if not instance:
|
||||
tag_manager = ModelTagManager()
|
||||
tag_manager.model = owner
|
||||
return tag_manager
|
||||
else:
|
||||
return Tag.objects.get_for_object(instance)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
Tag.objects.update_tags(instance, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
Tag.objects.update_tags(instance, None)
|
@ -1,480 +0,0 @@
|
||||
"""
|
||||
Models and managers for generic tagging.
|
||||
"""
|
||||
# Python 2.3 compatibility
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import connection, models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from tagging import settings
|
||||
from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input
|
||||
from tagging.utils import LOGARITHMIC
|
||||
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
############
|
||||
# Managers #
|
||||
############
|
||||
|
||||
class TagManager(models.Manager):
|
||||
def update_tags(self, obj, tag_names):
|
||||
"""
|
||||
Update tags associated with an object.
|
||||
"""
|
||||
ctype = ContentType.objects.get_for_model(obj)
|
||||
current_tags = list(self.filter(items__content_type__pk=ctype.pk,
|
||||
items__object_id=obj.pk))
|
||||
updated_tag_names = parse_tag_input(tag_names)
|
||||
if settings.FORCE_LOWERCASE_TAGS:
|
||||
updated_tag_names = [t.lower() for t in updated_tag_names]
|
||||
|
||||
# Remove tags which no longer apply
|
||||
tags_for_removal = [tag for tag in current_tags \
|
||||
if tag.name not in updated_tag_names]
|
||||
if len(tags_for_removal):
|
||||
TaggedItem._default_manager.filter(content_type__pk=ctype.pk,
|
||||
object_id=obj.pk,
|
||||
tag__in=tags_for_removal).delete()
|
||||
# Add new tags
|
||||
current_tag_names = [tag.name for tag in current_tags]
|
||||
for tag_name in updated_tag_names:
|
||||
if tag_name not in current_tag_names:
|
||||
tag, created = self.get_or_create(name=tag_name)
|
||||
TaggedItem._default_manager.create(tag=tag, object=obj)
|
||||
|
||||
def add_tag(self, obj, tag_name):
|
||||
"""
|
||||
Associates the given object with a tag.
|
||||
"""
|
||||
tag_names = parse_tag_input(tag_name)
|
||||
if not len(tag_names):
|
||||
raise AttributeError(_('No tags were given: "%s".') % tag_name)
|
||||
if len(tag_names) > 1:
|
||||
raise AttributeError(_('Multiple tags were given: "%s".') % tag_name)
|
||||
tag_name = tag_names[0]
|
||||
if settings.FORCE_LOWERCASE_TAGS:
|
||||
tag_name = tag_name.lower()
|
||||
tag, created = self.get_or_create(name=tag_name)
|
||||
ctype = ContentType.objects.get_for_model(obj)
|
||||
TaggedItem._default_manager.get_or_create(
|
||||
tag=tag, content_type=ctype, object_id=obj.pk)
|
||||
|
||||
def get_for_object(self, obj):
|
||||
"""
|
||||
Create a queryset matching all tags associated with the given
|
||||
object.
|
||||
"""
|
||||
ctype = ContentType.objects.get_for_model(obj)
|
||||
return self.filter(items__content_type__pk=ctype.pk,
|
||||
items__object_id=obj.pk)
|
||||
|
||||
def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None):
|
||||
"""
|
||||
Perform the custom SQL query for ``usage_for_model`` and
|
||||
``usage_for_queryset``.
|
||||
"""
|
||||
if min_count is not None: counts = True
|
||||
|
||||
model_table = qn(model._meta.db_table)
|
||||
model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
|
||||
query = """
|
||||
SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s
|
||||
FROM
|
||||
%(tag)s
|
||||
INNER JOIN %(tagged_item)s
|
||||
ON %(tag)s.id = %(tagged_item)s.tag_id
|
||||
INNER JOIN %(model)s
|
||||
ON %(tagged_item)s.object_id = %(model_pk)s
|
||||
%%s
|
||||
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||
%%s
|
||||
GROUP BY %(tag)s.id, %(tag)s.name
|
||||
%%s
|
||||
ORDER BY %(tag)s.name ASC""" % {
|
||||
'tag': qn(self.model._meta.db_table),
|
||||
'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
|
||||
'tagged_item': qn(TaggedItem._meta.db_table),
|
||||
'model': model_table,
|
||||
'model_pk': model_pk,
|
||||
'content_type_id': ContentType.objects.get_for_model(model).pk,
|
||||
}
|
||||
|
||||
min_count_sql = ''
|
||||
if min_count is not None:
|
||||
min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
|
||||
params.append(min_count)
|
||||
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
|
||||
tags = []
|
||||
for row in cursor.fetchall():
|
||||
t = self.model(*row[:2])
|
||||
if counts:
|
||||
t.count = row[2]
|
||||
tags.append(t)
|
||||
return tags
|
||||
|
||||
def usage_for_model(self, model, counts=False, min_count=None, filters=None):
|
||||
"""
|
||||
Obtain a list of tags associated with instances of the given
|
||||
Model class.
|
||||
|
||||
If ``counts`` is True, a ``count`` attribute will be added to
|
||||
each tag, indicating how many times it has been used against
|
||||
the Model class in question.
|
||||
|
||||
If ``min_count`` is given, only tags which have a ``count``
|
||||
greater than or equal to ``min_count`` will be returned.
|
||||
Passing a value for ``min_count`` implies ``counts=True``.
|
||||
|
||||
To limit the tags (and counts, if specified) returned to those
|
||||
used by a subset of the Model's instances, pass a dictionary
|
||||
of field lookups to be applied to the given Model as the
|
||||
``filters`` argument.
|
||||
"""
|
||||
if filters is None: filters = {}
|
||||
|
||||
queryset = model._default_manager.filter()
|
||||
for f in filters.items():
|
||||
queryset.query.add_filter(f)
|
||||
usage = self.usage_for_queryset(queryset, counts, min_count)
|
||||
|
||||
return usage
|
||||
|
||||
def usage_for_queryset(self, queryset, counts=False, min_count=None):
|
||||
"""
|
||||
Obtain a list of tags associated with instances of a model
|
||||
contained in the given queryset.
|
||||
|
||||
If ``counts`` is True, a ``count`` attribute will be added to
|
||||
each tag, indicating how many times it has been used against
|
||||
the Model class in question.
|
||||
|
||||
If ``min_count`` is given, only tags which have a ``count``
|
||||
greater than or equal to ``min_count`` will be returned.
|
||||
Passing a value for ``min_count`` implies ``counts=True``.
|
||||
"""
|
||||
|
||||
extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:])
|
||||
where, params = queryset.query.where.as_sql()
|
||||
if where:
|
||||
extra_criteria = 'AND %s' % where
|
||||
else:
|
||||
extra_criteria = ''
|
||||
return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params)
|
||||
|
||||
def related_for_model(self, tags, model, counts=False, min_count=None):
|
||||
"""
|
||||
Obtain a list of tags related to a given list of tags - that
|
||||
is, other tags used by items which have all the given tags.
|
||||
|
||||
If ``counts`` is True, a ``count`` attribute will be added to
|
||||
each tag, indicating the number of items which have it in
|
||||
addition to the given list of tags.
|
||||
|
||||
If ``min_count`` is given, only tags which have a ``count``
|
||||
greater than or equal to ``min_count`` will be returned.
|
||||
Passing a value for ``min_count`` implies ``counts=True``.
|
||||
"""
|
||||
if min_count is not None: counts = True
|
||||
tags = get_tag_list(tags)
|
||||
tag_count = len(tags)
|
||||
tagged_item_table = qn(TaggedItem._meta.db_table)
|
||||
query = """
|
||||
SELECT %(tag)s.id, %(tag)s.name%(count_sql)s
|
||||
FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
|
||||
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||
AND %(tagged_item)s.object_id IN
|
||||
(
|
||||
SELECT %(tagged_item)s.object_id
|
||||
FROM %(tagged_item)s, %(tag)s
|
||||
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||
AND %(tag)s.id = %(tagged_item)s.tag_id
|
||||
AND %(tag)s.id IN (%(tag_id_placeholders)s)
|
||||
GROUP BY %(tagged_item)s.object_id
|
||||
HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
|
||||
)
|
||||
AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
|
||||
GROUP BY %(tag)s.id, %(tag)s.name
|
||||
%(min_count_sql)s
|
||||
ORDER BY %(tag)s.name ASC""" % {
|
||||
'tag': qn(self.model._meta.db_table),
|
||||
'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
|
||||
'tagged_item': tagged_item_table,
|
||||
'content_type_id': ContentType.objects.get_for_model(model).pk,
|
||||
'tag_id_placeholders': ','.join(['%s'] * tag_count),
|
||||
'tag_count': tag_count,
|
||||
'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
|
||||
}
|
||||
|
||||
params = [tag.pk for tag in tags] * 2
|
||||
if min_count is not None:
|
||||
params.append(min_count)
|
||||
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query, params)
|
||||
related = []
|
||||
for row in cursor.fetchall():
|
||||
tag = self.model(*row[:2])
|
||||
if counts is True:
|
||||
tag.count = row[2]
|
||||
related.append(tag)
|
||||
return related
|
||||
|
||||
def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC,
|
||||
filters=None, min_count=None):
|
||||
"""
|
||||
Obtain a list of tags associated with instances of the given
|
||||
Model, giving each tag a ``count`` attribute indicating how
|
||||
many times it has been used and a ``font_size`` attribute for
|
||||
use in displaying a tag cloud.
|
||||
|
||||
``steps`` defines the range of font sizes - ``font_size`` will
|
||||
be an integer between 1 and ``steps`` (inclusive).
|
||||
|
||||
``distribution`` defines the type of font size distribution
|
||||
algorithm which will be used - logarithmic or linear. It must
|
||||
be either ``tagging.utils.LOGARITHMIC`` or
|
||||
``tagging.utils.LINEAR``.
|
||||
|
||||
To limit the tags displayed in the cloud to those associated
|
||||
with a subset of the Model's instances, pass a dictionary of
|
||||
field lookups to be applied to the given Model as the
|
||||
``filters`` argument.
|
||||
|
||||
To limit the tags displayed in the cloud to those with a
|
||||
``count`` greater than or equal to ``min_count``, pass a value
|
||||
for the ``min_count`` argument.
|
||||
"""
|
||||
tags = list(self.usage_for_model(model, counts=True, filters=filters,
|
||||
min_count=min_count))
|
||||
return calculate_cloud(tags, steps, distribution)
|
||||
|
||||
class TaggedItemManager(models.Manager):
|
||||
"""
|
||||
FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING``
|
||||
SQL clauses required by many of this manager's methods into
|
||||
Django's ORM.
|
||||
|
||||
For now, we manually execute a query to retrieve the PKs of
|
||||
objects we're interested in, then use the ORM's ``__in``
|
||||
lookup to return a ``QuerySet``.
|
||||
|
||||
Now that the queryset-refactor branch is in the trunk, this can be
|
||||
tidied up significantly.
|
||||
"""
|
||||
def get_by_model(self, queryset_or_model, tags):
|
||||
"""
|
||||
Create a ``QuerySet`` containing instances of the specified
|
||||
model associated with a given tag or list of tags.
|
||||
"""
|
||||
tags = get_tag_list(tags)
|
||||
tag_count = len(tags)
|
||||
if tag_count == 0:
|
||||
# No existing tags were given
|
||||
queryset, model = get_queryset_and_model(queryset_or_model)
|
||||
return model._default_manager.none()
|
||||
elif tag_count == 1:
|
||||
# Optimisation for single tag - fall through to the simpler
|
||||
# query below.
|
||||
tag = tags[0]
|
||||
else:
|
||||
return self.get_intersection_by_model(queryset_or_model, tags)
|
||||
|
||||
queryset, model = get_queryset_and_model(queryset_or_model)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
opts = self.model._meta
|
||||
tagged_item_table = qn(opts.db_table)
|
||||
return queryset.extra(
|
||||
tables=[opts.db_table],
|
||||
where=[
|
||||
'%s.content_type_id = %%s' % tagged_item_table,
|
||||
'%s.tag_id = %%s' % tagged_item_table,
|
||||
'%s.%s = %s.object_id' % (qn(model._meta.db_table),
|
||||
qn(model._meta.pk.column),
|
||||
tagged_item_table)
|
||||
],
|
||||
params=[content_type.pk, tag.pk],
|
||||
)
|
||||
|
||||
def get_intersection_by_model(self, queryset_or_model, tags):
|
||||
"""
|
||||
Create a ``QuerySet`` containing instances of the specified
|
||||
model associated with *all* of the given list of tags.
|
||||
"""
|
||||
tags = get_tag_list(tags)
|
||||
tag_count = len(tags)
|
||||
queryset, model = get_queryset_and_model(queryset_or_model)
|
||||
|
||||
if not tag_count:
|
||||
return model._default_manager.none()
|
||||
|
||||
model_table = qn(model._meta.db_table)
|
||||
# This query selects the ids of all objects which have all the
|
||||
# given tags.
|
||||
query = """
|
||||
SELECT %(model_pk)s
|
||||
FROM %(model)s, %(tagged_item)s
|
||||
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||
AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
|
||||
AND %(model_pk)s = %(tagged_item)s.object_id
|
||||
GROUP BY %(model_pk)s
|
||||
HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
|
||||
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
|
||||
'model': model_table,
|
||||
'tagged_item': qn(self.model._meta.db_table),
|
||||
'content_type_id': ContentType.objects.get_for_model(model).pk,
|
||||
'tag_id_placeholders': ','.join(['%s'] * tag_count),
|
||||
'tag_count': tag_count,
|
||||
}
|
||||
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query, [tag.pk for tag in tags])
|
||||
object_ids = [row[0] for row in cursor.fetchall()]
|
||||
if len(object_ids) > 0:
|
||||
return queryset.filter(pk__in=object_ids)
|
||||
else:
|
||||
return model._default_manager.none()
|
||||
|
||||
def get_union_by_model(self, queryset_or_model, tags):
|
||||
"""
|
||||
Create a ``QuerySet`` containing instances of the specified
|
||||
model associated with *any* of the given list of tags.
|
||||
"""
|
||||
tags = get_tag_list(tags)
|
||||
tag_count = len(tags)
|
||||
queryset, model = get_queryset_and_model(queryset_or_model)
|
||||
|
||||
if not tag_count:
|
||||
return model._default_manager.none()
|
||||
|
||||
model_table = qn(model._meta.db_table)
|
||||
# This query selects the ids of all objects which have any of
|
||||
# the given tags.
|
||||
query = """
|
||||
SELECT %(model_pk)s
|
||||
FROM %(model)s, %(tagged_item)s
|
||||
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||
AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
|
||||
AND %(model_pk)s = %(tagged_item)s.object_id
|
||||
GROUP BY %(model_pk)s""" % {
|
||||
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
|
||||
'model': model_table,
|
||||
'tagged_item': qn(self.model._meta.db_table),
|
||||
'content_type_id': ContentType.objects.get_for_model(model).pk,
|
||||
'tag_id_placeholders': ','.join(['%s'] * tag_count),
|
||||
}
|
||||
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query, [tag.pk for tag in tags])
|
||||
object_ids = [row[0] for row in cursor.fetchall()]
|
||||
if len(object_ids) > 0:
|
||||
return queryset.filter(pk__in=object_ids)
|
||||
else:
|
||||
return model._default_manager.none()
|
||||
|
||||
def get_related(self, obj, queryset_or_model, num=None):
|
||||
"""
|
||||
Retrieve a list of instances of the specified model which share
|
||||
tags with the model instance ``obj``, ordered by the number of
|
||||
shared tags in descending order.
|
||||
|
||||
If ``num`` is given, a maximum of ``num`` instances will be
|
||||
returned.
|
||||
"""
|
||||
queryset, model = get_queryset_and_model(queryset_or_model)
|
||||
model_table = qn(model._meta.db_table)
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
related_content_type = ContentType.objects.get_for_model(model)
|
||||
query = """
|
||||
SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
|
||||
FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
|
||||
WHERE %(tagged_item)s.object_id = %%s
|
||||
AND %(tagged_item)s.content_type_id = %(content_type_id)s
|
||||
AND %(tag)s.id = %(tagged_item)s.tag_id
|
||||
AND related_tagged_item.content_type_id = %(related_content_type_id)s
|
||||
AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
|
||||
AND %(model_pk)s = related_tagged_item.object_id"""
|
||||
if content_type.pk == related_content_type.pk:
|
||||
# Exclude the given instance itself if determining related
|
||||
# instances for the same model.
|
||||
query += """
|
||||
AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
|
||||
query += """
|
||||
GROUP BY %(model_pk)s
|
||||
ORDER BY %(count)s DESC
|
||||
%(limit_offset)s"""
|
||||
query = query % {
|
||||
'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
|
||||
'count': qn('count'),
|
||||
'model': model_table,
|
||||
'tagged_item': qn(self.model._meta.db_table),
|
||||
'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
|
||||
'content_type_id': content_type.pk,
|
||||
'related_content_type_id': related_content_type.pk,
|
||||
# Hardcoding this for now just to get tests working again - this
|
||||
# should now be handled by the query object.
|
||||
'limit_offset': num is not None and 'LIMIT %s' or '',
|
||||
}
|
||||
|
||||
cursor = connection.cursor()
|
||||
params = [obj.pk]
|
||||
if num is not None:
|
||||
params.append(num)
|
||||
cursor.execute(query, params)
|
||||
object_ids = [row[0] for row in cursor.fetchall()]
|
||||
if len(object_ids) > 0:
|
||||
# Use in_bulk here instead of an id__in lookup, because id__in would
|
||||
# clobber the ordering.
|
||||
object_dict = queryset.in_bulk(object_ids)
|
||||
return [object_dict[object_id] for object_id in object_ids \
|
||||
if object_id in object_dict]
|
||||
else:
|
||||
return []
|
||||
|
||||
##########
|
||||
# Models #
|
||||
##########
|
||||
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
A tag.
|
||||
"""
|
||||
name = models.CharField(_('name'), max_length=50, unique=True, db_index=True)
|
||||
|
||||
objects = TagManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('tag')
|
||||
verbose_name_plural = _('tags')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class TaggedItem(models.Model):
|
||||
"""
|
||||
Holds the relationship between a tag and the item being tagged.
|
||||
"""
|
||||
tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
|
||||
content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
|
||||
object_id = models.PositiveIntegerField(_('object id'), db_index=True)
|
||||
object = generic.GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
objects = TaggedItemManager()
|
||||
|
||||
class Meta:
|
||||
# Enforce unique tag association per object
|
||||
unique_together = (('tag', 'content_type', 'object_id'),)
|
||||
verbose_name = _('tagged item')
|
||||
verbose_name_plural = _('tagged items')
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s [%s]' % (self.object, self.tag)
|
@ -1,13 +0,0 @@
|
||||
"""
|
||||
Convenience module for access of custom tagging application settings,
|
||||
which enforces default settings when the main settings module does not
|
||||
contain the appropriate settings.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
# The maximum length of a tag's name.
|
||||
MAX_TAG_LENGTH = getattr(settings, 'MAX_TAG_LENGTH', 50)
|
||||
|
||||
# Whether to force all tags to lowercase before they are saved to the
|
||||
# database.
|
||||
FORCE_LOWERCASE_TAGS = getattr(settings, 'FORCE_LOWERCASE_TAGS', False)
|
@ -1,231 +0,0 @@
|
||||
from django.db.models import get_model
|
||||
from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from tagging.models import Tag, TaggedItem
|
||||
from tagging.utils import LINEAR, LOGARITHMIC
|
||||
|
||||
register = Library()
|
||||
|
||||
class TagsForModelNode(Node):
|
||||
def __init__(self, model, context_var, counts):
|
||||
self.model = model
|
||||
self.context_var = context_var
|
||||
self.counts = counts
|
||||
|
||||
def render(self, context):
|
||||
model = get_model(*self.model.split('.'))
|
||||
if model is None:
|
||||
raise TemplateSyntaxError(_('tags_for_model tag was given an invalid model: %s') % self.model)
|
||||
context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts)
|
||||
return ''
|
||||
|
||||
class TagCloudForModelNode(Node):
|
||||
def __init__(self, model, context_var, **kwargs):
|
||||
self.model = model
|
||||
self.context_var = context_var
|
||||
self.kwargs = kwargs
|
||||
|
||||
def render(self, context):
|
||||
model = get_model(*self.model.split('.'))
|
||||
if model is None:
|
||||
raise TemplateSyntaxError(_('tag_cloud_for_model tag was given an invalid model: %s') % self.model)
|
||||
context[self.context_var] = \
|
||||
Tag.objects.cloud_for_model(model, **self.kwargs)
|
||||
return ''
|
||||
|
||||
class TagsForObjectNode(Node):
|
||||
def __init__(self, obj, context_var):
|
||||
self.obj = Variable(obj)
|
||||
self.context_var = context_var
|
||||
|
||||
def render(self, context):
|
||||
context[self.context_var] = \
|
||||
Tag.objects.get_for_object(self.obj.resolve(context))
|
||||
return ''
|
||||
|
||||
class TaggedObjectsNode(Node):
|
||||
def __init__(self, tag, model, context_var):
|
||||
self.tag = Variable(tag)
|
||||
self.context_var = context_var
|
||||
self.model = model
|
||||
|
||||
def render(self, context):
|
||||
model = get_model(*self.model.split('.'))
|
||||
if model is None:
|
||||
raise TemplateSyntaxError(_('tagged_objects tag was given an invalid model: %s') % self.model)
|
||||
context[self.context_var] = \
|
||||
TaggedItem.objects.get_by_model(model, self.tag.resolve(context))
|
||||
return ''
|
||||
|
||||
def do_tags_for_model(parser, token):
|
||||
"""
|
||||
Retrieves a list of ``Tag`` objects associated with a given model
|
||||
and stores them in a context variable.
|
||||
|
||||
Usage::
|
||||
|
||||
{% tags_for_model [model] as [varname] %}
|
||||
|
||||
The model is specified in ``[appname].[modelname]`` format.
|
||||
|
||||
Extended usage::
|
||||
|
||||
{% tags_for_model [model] as [varname] with counts %}
|
||||
|
||||
If specified - by providing extra ``with counts`` arguments - adds
|
||||
a ``count`` attribute to each tag containing the number of
|
||||
instances of the given model which have been tagged with it.
|
||||
|
||||
Examples::
|
||||
|
||||
{% tags_for_model products.Widget as widget_tags %}
|
||||
{% tags_for_model products.Widget as widget_tags with counts %}
|
||||
|
||||
"""
|
||||
bits = token.contents.split()
|
||||
len_bits = len(bits)
|
||||
if len_bits not in (4, 6):
|
||||
raise TemplateSyntaxError(_('%s tag requires either three or five arguments') % bits[0])
|
||||
if bits[2] != 'as':
|
||||
raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
|
||||
if len_bits == 6:
|
||||
if bits[4] != 'with':
|
||||
raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
|
||||
if bits[5] != 'counts':
|
||||
raise TemplateSyntaxError(_("if given, fifth argument to %s tag must be 'counts'") % bits[0])
|
||||
if len_bits == 4:
|
||||
return TagsForModelNode(bits[1], bits[3], counts=False)
|
||||
else:
|
||||
return TagsForModelNode(bits[1], bits[3], counts=True)
|
||||
|
||||
def do_tag_cloud_for_model(parser, token):
|
||||
"""
|
||||
Retrieves a list of ``Tag`` objects for a given model, with tag
|
||||
cloud attributes set, and stores them in a context variable.
|
||||
|
||||
Usage::
|
||||
|
||||
{% tag_cloud_for_model [model] as [varname] %}
|
||||
|
||||
The model is specified in ``[appname].[modelname]`` format.
|
||||
|
||||
Extended usage::
|
||||
|
||||
{% tag_cloud_for_model [model] as [varname] with [options] %}
|
||||
|
||||
Extra options can be provided after an optional ``with`` argument,
|
||||
with each option being specified in ``[name]=[value]`` format. Valid
|
||||
extra options are:
|
||||
|
||||
``steps``
|
||||
Integer. Defines the range of font sizes.
|
||||
|
||||
``min_count``
|
||||
Integer. Defines the minimum number of times a tag must have
|
||||
been used to appear in the cloud.
|
||||
|
||||
``distribution``
|
||||
One of ``linear`` or ``log``. Defines the font-size
|
||||
distribution algorithm to use when generating the tag cloud.
|
||||
|
||||
Examples::
|
||||
|
||||
{% tag_cloud_for_model products.Widget as widget_tags %}
|
||||
{% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %}
|
||||
|
||||
"""
|
||||
bits = token.contents.split()
|
||||
len_bits = len(bits)
|
||||
if len_bits != 4 and len_bits not in range(6, 9):
|
||||
raise TemplateSyntaxError(_('%s tag requires either three or between five and seven arguments') % bits[0])
|
||||
if bits[2] != 'as':
|
||||
raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
|
||||
kwargs = {}
|
||||
if len_bits > 5:
|
||||
if bits[4] != 'with':
|
||||
raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0])
|
||||
for i in range(5, len_bits):
|
||||
try:
|
||||
name, value = bits[i].split('=')
|
||||
if name == 'steps' or name == 'min_count':
|
||||
try:
|
||||
kwargs[str(name)] = int(value)
|
||||
except ValueError:
|
||||
raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid integer: '%(value)s'") % {
|
||||
'tag': bits[0],
|
||||
'option': name,
|
||||
'value': value,
|
||||
})
|
||||
elif name == 'distribution':
|
||||
if value in ['linear', 'log']:
|
||||
kwargs[str(name)] = {'linear': LINEAR, 'log': LOGARITHMIC}[value]
|
||||
else:
|
||||
raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid choice: '%(value)s'") % {
|
||||
'tag': bits[0],
|
||||
'option': name,
|
||||
'value': value,
|
||||
})
|
||||
else:
|
||||
raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % {
|
||||
'tag': bits[0],
|
||||
'option': name,
|
||||
})
|
||||
except ValueError:
|
||||
raise TemplateSyntaxError(_("%(tag)s tag was given a badly formatted option: '%(option)s'") % {
|
||||
'tag': bits[0],
|
||||
'option': bits[i],
|
||||
})
|
||||
return TagCloudForModelNode(bits[1], bits[3], **kwargs)
|
||||
|
||||
def do_tags_for_object(parser, token):
|
||||
"""
|
||||
Retrieves a list of ``Tag`` objects associated with an object and
|
||||
stores them in a context variable.
|
||||
|
||||
Usage::
|
||||
|
||||
{% tags_for_object [object] as [varname] %}
|
||||
|
||||
Example::
|
||||
|
||||
{% tags_for_object foo_object as tag_list %}
|
||||
"""
|
||||
bits = token.contents.split()
|
||||
if len(bits) != 4:
|
||||
raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0])
|
||||
if bits[2] != 'as':
|
||||
raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0])
|
||||
return TagsForObjectNode(bits[1], bits[3])
|
||||
|
||||
def do_tagged_objects(parser, token):
|
||||
"""
|
||||
Retrieves a list of instances of a given model which are tagged with
|
||||
a given ``Tag`` and stores them in a context variable.
|
||||
|
||||
Usage::
|
||||
|
||||
{% tagged_objects [tag] in [model] as [varname] %}
|
||||
|
||||
The model is specified in ``[appname].[modelname]`` format.
|
||||
|
||||
The tag must be an instance of a ``Tag``, not the name of a tag.
|
||||
|
||||
Example::
|
||||
|
||||
{% tagged_objects comedy_tag in tv.Show as comedies %}
|
||||
|
||||
"""
|
||||
bits = token.contents.split()
|
||||
if len(bits) != 6:
|
||||
raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0])
|
||||
if bits[2] != 'in':
|
||||
raise TemplateSyntaxError(_("second argument to %s tag must be 'in'") % bits[0])
|
||||
if bits[4] != 'as':
|
||||
raise TemplateSyntaxError(_("fourth argument to %s tag must be 'as'") % bits[0])
|
||||
return TaggedObjectsNode(bits[1], bits[3], bits[5])
|
||||
|
||||
register.tag('tags_for_model', do_tags_for_model)
|
||||
register.tag('tag_cloud_for_model', do_tag_cloud_for_model)
|
||||
register.tag('tags_for_object', do_tags_for_object)
|
||||
register.tag('tagged_objects', do_tagged_objects)
|
@ -1,263 +0,0 @@
|
||||
"""
|
||||
Tagging utilities - from user tag input parsing to tag cloud
|
||||
calculation.
|
||||
"""
|
||||
import math
|
||||
import types
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.encoding import force_unicode
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
# Python 2.3 compatibility
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
def parse_tag_input(input):
|
||||
"""
|
||||
Parses tag input, with multiple word input being activated and
|
||||
delineated by commas and double quotes. Quotes take precedence, so
|
||||
they may contain commas.
|
||||
|
||||
Returns a sorted list of unique tag names.
|
||||
"""
|
||||
if not input:
|
||||
return []
|
||||
|
||||
input = force_unicode(input)
|
||||
|
||||
# Special case - if there are no commas or double quotes in the
|
||||
# input, we don't *do* a recall... I mean, we know we only need to
|
||||
# split on spaces.
|
||||
if u',' not in input and u'"' not in input:
|
||||
words = list(set(split_strip(input, u' ')))
|
||||
words.sort()
|
||||
return words
|
||||
|
||||
words = []
|
||||
buffer = []
|
||||
# Defer splitting of non-quoted sections until we know if there are
|
||||
# any unquoted commas.
|
||||
to_be_split = []
|
||||
saw_loose_comma = False
|
||||
open_quote = False
|
||||
i = iter(input)
|
||||
try:
|
||||
while 1:
|
||||
c = i.next()
|
||||
if c == u'"':
|
||||
if buffer:
|
||||
to_be_split.append(u''.join(buffer))
|
||||
buffer = []
|
||||
# Find the matching quote
|
||||
open_quote = True
|
||||
c = i.next()
|
||||
while c != u'"':
|
||||
buffer.append(c)
|
||||
c = i.next()
|
||||
if buffer:
|
||||
word = u''.join(buffer).strip()
|
||||
if word:
|
||||
words.append(word)
|
||||
buffer = []
|
||||
open_quote = False
|
||||
else:
|
||||
if not saw_loose_comma and c == u',':
|
||||
saw_loose_comma = True
|
||||
buffer.append(c)
|
||||
except StopIteration:
|
||||
# If we were parsing an open quote which was never closed treat
|
||||
# the buffer as unquoted.
|
||||
if buffer:
|
||||
if open_quote and u',' in buffer:
|
||||
saw_loose_comma = True
|
||||
to_be_split.append(u''.join(buffer))
|
||||
if to_be_split:
|
||||
if saw_loose_comma:
|
||||
delimiter = u','
|
||||
else:
|
||||
delimiter = u' '
|
||||
for chunk in to_be_split:
|
||||
words.extend(split_strip(chunk, delimiter))
|
||||
words = list(set(words))
|
||||
words.sort()
|
||||
return words
|
||||
|
||||
def split_strip(input, delimiter=u','):
|
||||
"""
|
||||
Splits ``input`` on ``delimiter``, stripping each resulting string
|
||||
and returning a list of non-empty strings.
|
||||
"""
|
||||
if not input:
|
||||
return []
|
||||
|
||||
words = [w.strip() for w in input.split(delimiter)]
|
||||
return [w for w in words if w]
|
||||
|
||||
def edit_string_for_tags(tags):
|
||||
"""
|
||||
Given list of ``Tag`` instances, creates a string representation of
|
||||
the list suitable for editing by the user, such that submitting the
|
||||
given string representation back without changing it will give the
|
||||
same list of tags.
|
||||
|
||||
Tag names which contain commas will be double quoted.
|
||||
|
||||
If any tag name which isn't being quoted contains whitespace, the
|
||||
resulting string of tag names will be comma-delimited, otherwise
|
||||
it will be space-delimited.
|
||||
"""
|
||||
names = []
|
||||
use_commas = False
|
||||
for tag in tags:
|
||||
name = tag.name
|
||||
if u',' in name:
|
||||
names.append('"%s"' % name)
|
||||
continue
|
||||
elif u' ' in name:
|
||||
if not use_commas:
|
||||
use_commas = True
|
||||
names.append(name)
|
||||
if use_commas:
|
||||
glue = u', '
|
||||
else:
|
||||
glue = u' '
|
||||
return glue.join(names)
|
||||
|
||||
def get_queryset_and_model(queryset_or_model):
|
||||
"""
|
||||
Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
|
||||
(queryset, model).
|
||||
|
||||
If a ``Model`` is given, the ``QuerySet`` returned will be created
|
||||
using its default manager.
|
||||
"""
|
||||
try:
|
||||
return queryset_or_model, queryset_or_model.model
|
||||
except AttributeError:
|
||||
return queryset_or_model._default_manager.all(), queryset_or_model
|
||||
|
||||
def get_tag_list(tags):
|
||||
"""
|
||||
Utility function for accepting tag input in a flexible manner.
|
||||
|
||||
If a ``Tag`` object is given, it will be returned in a list as
|
||||
its single occupant.
|
||||
|
||||
If given, the tag names in the following will be used to create a
|
||||
``Tag`` ``QuerySet``:
|
||||
|
||||
* A string, which may contain multiple tag names.
|
||||
* A list or tuple of strings corresponding to tag names.
|
||||
* A list or tuple of integers corresponding to tag ids.
|
||||
|
||||
If given, the following will be returned as-is:
|
||||
|
||||
* A list or tuple of ``Tag`` objects.
|
||||
* A ``Tag`` ``QuerySet``.
|
||||
|
||||
"""
|
||||
from tagging.models import Tag
|
||||
if isinstance(tags, Tag):
|
||||
return [tags]
|
||||
elif isinstance(tags, QuerySet) and tags.model is Tag:
|
||||
return tags
|
||||
elif isinstance(tags, types.StringTypes):
|
||||
return Tag.objects.filter(name__in=parse_tag_input(tags))
|
||||
elif isinstance(tags, (types.ListType, types.TupleType)):
|
||||
if len(tags) == 0:
|
||||
return tags
|
||||
contents = set()
|
||||
for item in tags:
|
||||
if isinstance(item, types.StringTypes):
|
||||
contents.add('string')
|
||||
elif isinstance(item, Tag):
|
||||
contents.add('tag')
|
||||
elif isinstance(item, (types.IntType, types.LongType)):
|
||||
contents.add('int')
|
||||
if len(contents) == 1:
|
||||
if 'string' in contents:
|
||||
return Tag.objects.filter(name__in=[force_unicode(tag) \
|
||||
for tag in tags])
|
||||
elif 'tag' in contents:
|
||||
return tags
|
||||
elif 'int' in contents:
|
||||
return Tag.objects.filter(id__in=tags)
|
||||
else:
|
||||
raise ValueError(_('If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.'))
|
||||
else:
|
||||
raise ValueError(_('The tag input given was invalid.'))
|
||||
|
||||
def get_tag(tag):
|
||||
"""
|
||||
Utility function for accepting single tag input in a flexible
|
||||
manner.
|
||||
|
||||
If a ``Tag`` object is given it will be returned as-is; if a
|
||||
string or integer are given, they will be used to lookup the
|
||||
appropriate ``Tag``.
|
||||
|
||||
If no matching tag can be found, ``None`` will be returned.
|
||||
"""
|
||||
from tagging.models import Tag
|
||||
if isinstance(tag, Tag):
|
||||
return tag
|
||||
|
||||
try:
|
||||
if isinstance(tag, types.StringTypes):
|
||||
return Tag.objects.get(name=tag)
|
||||
elif isinstance(tag, (types.IntType, types.LongType)):
|
||||
return Tag.objects.get(id=tag)
|
||||
except Tag.DoesNotExist:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
# Font size distribution algorithms
|
||||
LOGARITHMIC, LINEAR = 1, 2
|
||||
|
||||
def _calculate_thresholds(min_weight, max_weight, steps):
|
||||
delta = (max_weight - min_weight) / float(steps)
|
||||
return [min_weight + i * delta for i in range(1, steps + 1)]
|
||||
|
||||
def _calculate_tag_weight(weight, max_weight, distribution):
|
||||
"""
|
||||
Logarithmic tag weight calculation is based on code from the
|
||||
`Tag Cloud`_ plugin for Mephisto, by Sven Fuchs.
|
||||
|
||||
.. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud
|
||||
"""
|
||||
if distribution == LINEAR or max_weight == 1:
|
||||
return weight
|
||||
elif distribution == LOGARITHMIC:
|
||||
return math.log(weight) * max_weight / math.log(max_weight)
|
||||
raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution)
|
||||
|
||||
def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC):
|
||||
"""
|
||||
Add a ``font_size`` attribute to each tag according to the
|
||||
frequency of its use, as indicated by its ``count``
|
||||
attribute.
|
||||
|
||||
``steps`` defines the range of font sizes - ``font_size`` will
|
||||
be an integer between 1 and ``steps`` (inclusive).
|
||||
|
||||
``distribution`` defines the type of font size distribution
|
||||
algorithm which will be used - logarithmic or linear. It must be
|
||||
one of ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``.
|
||||
"""
|
||||
if len(tags) > 0:
|
||||
counts = [tag.count for tag in tags]
|
||||
min_weight = float(min(counts))
|
||||
max_weight = float(max(counts))
|
||||
thresholds = _calculate_thresholds(min_weight, max_weight, steps)
|
||||
for tag in tags:
|
||||
font_set = False
|
||||
tag_weight = _calculate_tag_weight(tag.count, max_weight, distribution)
|
||||
for i in range(steps):
|
||||
if not font_set and tag_weight <= thresholds[i]:
|
||||
tag.font_size = i + 1
|
||||
font_set = True
|
||||
return tags
|
@ -1,52 +0,0 @@
|
||||
"""
|
||||
Tagging related views.
|
||||
"""
|
||||
from django.http import Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic.list_detail import object_list
|
||||
|
||||
from tagging.models import Tag, TaggedItem
|
||||
from tagging.utils import get_tag, get_queryset_and_model
|
||||
|
||||
def tagged_object_list(request, queryset_or_model=None, tag=None,
|
||||
related_tags=False, related_tag_counts=True, **kwargs):
|
||||
"""
|
||||
A thin wrapper around
|
||||
``django.views.generic.list_detail.object_list`` which creates a
|
||||
``QuerySet`` containing instances of the given queryset or model
|
||||
tagged with the given tag.
|
||||
|
||||
In addition to the context variables set up by ``object_list``, a
|
||||
``tag`` context variable will contain the ``Tag`` instance for the
|
||||
tag.
|
||||
|
||||
If ``related_tags`` is ``True``, a ``related_tags`` context variable
|
||||
will contain tags related to the given tag for the given model.
|
||||
Additionally, if ``related_tag_counts`` is ``True``, each related
|
||||
tag will have a ``count`` attribute indicating the number of items
|
||||
which have it in addition to the given tag.
|
||||
"""
|
||||
if queryset_or_model is None:
|
||||
try:
|
||||
queryset_or_model = kwargs.pop('queryset_or_model')
|
||||
except KeyError:
|
||||
raise AttributeError(_('tagged_object_list must be called with a queryset or a model.'))
|
||||
|
||||
if tag is None:
|
||||
try:
|
||||
tag = kwargs.pop('tag')
|
||||
except KeyError:
|
||||
raise AttributeError(_('tagged_object_list must be called with a tag.'))
|
||||
|
||||
tag_instance = get_tag(tag)
|
||||
if tag_instance is None:
|
||||
raise Http404(_('No Tag found matching "%s".') % tag)
|
||||
queryset = TaggedItem.objects.get_by_model(queryset_or_model, tag_instance)
|
||||
if not kwargs.has_key('extra_context'):
|
||||
kwargs['extra_context'] = {}
|
||||
kwargs['extra_context']['tag'] = tag_instance
|
||||
if related_tags:
|
||||
kwargs['extra_context']['related_tags'] = \
|
||||
Tag.objects.related_for_model(tag_instance, queryset_or_model,
|
||||
counts=related_tag_counts)
|
||||
return object_list(request, queryset, **kwargs)
|