Commits

funkybob  committed e89b710

Reorganised for shared namespace

  • Participants
  • Parent commits d818276

Comments (0)

Files changed (30)

File gnocchi/__init__.py

+__import__('pkg_resources').declare_namespace(__name__)
+

File gnocchi/blog/__init__.py

Empty file added.

File gnocchi/blog/admin.py

+from django.contrib import admin
+from gnocchi.blog import models, forms
+
+register = admin.site.register
+
+class PostAdmin(admin.ModelAdmin):
+    list_display = (
+        'title',
+        'published',
+        'post_date',
+        'kill_date',
+        'posted_by',
+        'allow_comments',
+    )
+    list_filter = ('posted_by', 'published',)
+    date_heirarchy = 'post_date'
+    search_fields = ('title', 'content',)
+    prepopulated_fields = {'slug': ('title',)}
+    exclude = ('posted_by',)
+
+    form = forms.PostAdminForm
+    fieldsets = (
+        (None, {
+            'fields': ('title', 'slug',),
+        },),
+        ('Content', {
+            'classes': ('wide',),
+            'fields': ('content',),
+        },),
+        ('Extras', {
+            'fields': (
+                ('published', 'post_date', 'kill_date',),
+                ('auto', 'tags',),
+            ),
+        },),
+    )
+
+    def queryset(self, request):
+        qset = self.model.objects.all()
+        if not request.user.is_superuser:
+            return qset.filter(posted_by=request.user)
+        return qset
+
+    def save_model(self, request, obj, form, change):
+        if not change:
+            # initial posting
+            obj.posted_by = request.user
+        obj.save()
+
+register(models.Post, PostAdmin)
+
+class CommentAdmin(admin.ModelAdmin):
+    list_display = ('post_date', 'post', 'name', 'email', 'ip_address',
+        'is_public', 'is_removed',)
+    list_filter = ('is_public', 'is_removed', 'user_name',)
+    date_heirarchy = ('post_date',)
+    search_fieklds = ('user_name', 'comment',)
+
+register(models.Comment, CommentAdmin)

File gnocchi/blog/feeds.py

+from django.contrib.syndication.views import Feed
+from gnocchi.blog.models import Post
+from taggit.models import TaggedItem
+
+class LatestBlogFeed(Feed):
+    '''Basic feed configuration
+
+    You need to derive your own class from this to set link, and you will
+    probably also want to change description and title.
+
+    See http://docs.djangoproject.com/en/1.2/ref/contrib/syndication/
+    '''
+    description = 'Blog Posts'
+    title = 'Blog'
+
+    def categories(self):
+        return TaggedItem.tags_for(Post)
+
+    def items(self):
+        return Post.objects.current()
+
+    def item_title(self, item):
+        return item.title
+
+    def item_author_name(self, item):
+        return item.posted_by.get_full_name()
+
+    def item_pubdate(self, item):
+        return item.post_date
+
+    def item_categories(self, item):
+        return item.tags
+
+class BlogTagFeed(LatestBlogFeed):
+    def get_object(self, request, tags):
+        tags = [ tag.strip() for tag in tags.split('/') ]
+        return tags
+
+    def description(self, obj):
+        return "Posts with tags %s" % (', '.join(obj))

File gnocchi/blog/forms.py

+from django import forms
+from gnocchi.blog import models, recaptcha
+from taggit.models import TaggedItem
+import re
+
+class PostAdminForm(forms.ModelForm):
+    auto = forms.BooleanField(label='Auto-tag?', required=False,
+        help_text="Automatically scan content for tags"
+    )
+    class Meta:
+        model = models.Post
+    def clean(self):
+        data = self.cleaned_data
+        if data['auto']:
+            tags = set(data['tags'])
+            tags.update([
+                tag
+                for tag in TaggedItem.tags_for(models.Post)
+                if re.search(r'\b%s\b' % tag.name, data['content'], re.I|re.M)
+            ])
+            data['tags'] = list(tags)
+        return data
+
+class CommentForm(recaptcha.BaseRecaptchaForm, forms.ModelForm):
+    captcha = recaptcha.RecaptchaField()
+    class Meta:
+        model = models.Comment
+        fields = (
+            'user_name',
+            'user_email',
+            'content',
+        )

File gnocchi/blog/models.py

+from django.db import models
+
+from gnocchi.blog import settings
+from taggit.managers import TaggableManager
+
+from datetime import datetime
+
+class BlogManager(models.Manager):
+    def current(self):
+        now = datetime.now()
+        return self.get_query_set().filter(
+            published=True,
+            post_date__lte=now,
+        ).exclude(
+            kill_date__lte=now
+        )
+
+class Post(models.Model):
+    title = models.CharField(max_length=200)
+    slug = models.SlugField()
+    content = models.TextField(blank=True)
+
+    post_date = models.DateTimeField(default=datetime.now)
+    kill_date = models.DateTimeField(null=True, blank=True, default=None)
+    posted_by = models.ForeignKey('auth.User', blank=True, null=True,
+        related_name='blog_posts')
+    published = models.BooleanField(default=False)
+
+    allow_comments = models.BooleanField(default=True)
+
+    tags = TaggableManager(blank=True)
+
+    objects = BlogManager()
+
+    class Meta:
+        ordering = ('-post_date',)
+    def __unicode__(self):
+        return self.title
+    @models.permalink
+    def get_absolute_url(self):
+        kwargs = {'slug': self.slug,}
+        if settings.BLOG_DATEBASED_URLS:
+            kwargs.update({
+                'year': '%04d' % self.post_date.year,
+                'month': self.post_date.strftime('%b').lower(),
+                'day': '%02d' % self.post_date.day,
+            })
+        if settings.BLOG_USERNAME_URLS:
+            kwargs['username'] = self.posted_by.username
+        return 'blog-detail', (), kwargs
+
+class Comment(models.Model):
+    '''Mostly a copy of django.contrib.comments Comment model'''
+    post = models.ForeignKey(Post)
+    content = models.TextField()
+
+    post_date = models.DateTimeField(default=datetime.now)
+    ip_address = models.IPAddressField(blank=True, null=True)
+
+    user = models.ForeignKey('auth.User', null=True, blank=True)
+    user_name = models.CharField(max_length=50)
+    user_email  = models.EmailField(blank=True)
+
+    is_public = models.BooleanField(default=True)
+    is_removed = models.BooleanField(default=True)
+
+    class Meta:
+        ordering = ('post_date',)
+    def __unicode__(self):
+        return u'%s...' % self.content[:50]
+
+    # Properties for variable values
+
+    def get_name(self):
+        if self.user_id:
+            return self.user.get_full_name() or self.user.username
+        return self.user_name
+    def set_name(self, value):
+        if self.user_id:
+            raise AttributeError("This comment was posted by an authenticated "\
+                "user and thus the name is read-only.")
+        self.user_name = value
+    name = property(get_name, set_name)
+
+    def get_email(self):
+        if self.user_id:
+            return self.user.email
+        return self.user_email
+    def set_email(self, value):
+        if self.user_id:
+            raise AttributeError("This comment was posted by an authenticated "\
+                "user and thus the email is read-only.")
+        self.user_email = value
+    email = property(get_email, set_email)

File gnocchi/blog/recaptcha.py

+"""
+An easy-to-use Django forms integration of the reCaptcha service.
+v1.0.1
+
+To use, simply base your form off the ``RecaptchaForm`` class. This class adds
+a new argument that must be provided to the form, ``remote_ip``.
+
+Two settings which must be set in your project's ``settings`` module are
+``RECAPTCHA_PUBLIC_KEY`` and ``RECAPTCHA_SECRET_KEY``, the public and private
+keys for your domain, respectively.
+
+Following is an example of creating a basic comment form class and then
+using an instance of the form in a view::
+
+    from django import forms
+    from mysite.utils import recaptcha
+
+    class CommentForm(recaptcha.RecaptchaForm):
+        name = forms.CharField()
+        comment = forms.CharField(widget=Textarea())
+        captcha = recaptcha.RecaptchaField()
+
+    def comment(request):
+        comment_form = CommentForm(remote_ip=request.META['REMOTE_ADDR'])
+        ...
+
+If you need to use a different base form (such as ``ModelForm``), use multiple
+inheritance like so::
+
+    class MyModelForm(BaseRecaptchaForm, ModelForm):
+        ...
+"""
+
+import httplib
+from django import forms
+from django.conf import settings
+from django.utils.safestring import mark_safe
+from django.utils.http import urlencode
+
+OPTIONS_SCRIPT_HTML = u'''<script type="text/javascript">
+   var RecaptchaOptions = %r;
+</script>
+'''
+RECAPTCHA_HTML = u'''%(options)s<script type="text/javascript" src="http://api.recaptcha.net/challenge?k=%(public_key)s"></script>
+<noscript>
+   <iframe src="http://api.recaptcha.net/noscript?k=%(public_key)s"
+       height="300" width="500" frameborder="0"></iframe><br />
+   <textarea name="recaptcha_challenge_field" rows="3" cols="40">
+   </textarea>
+   <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
+</noscript>'''
+
+
+class RecaptchaWidget(forms.Widget):
+    def __init__(self, theme=None, tabindex=None, public_key=None):
+        '''
+        From http://recaptcha.net/apidocs/captcha/client.html#look-n-feel::
+
+            theme:      'red' | 'white' | 'blackglass' | 'clean'
+
+                Defines which theme to use for reCAPTCHA.
+
+            tabindex:   any integer
+
+                Sets a tabindex for the reCAPTCHA text box. If other elements
+                in the form use a tabindex, this should be set so that
+                navigation is easier for the user.
+
+        The optional ``public_key`` argument can be used to override the
+        default use of the project-wide ``RECAPTCHA_PUBLIC_KEY`` setting.
+        '''
+        options = {}
+        if theme:
+            options['theme'] = theme
+        if tabindex:
+            options['tabindex'] = tabindex
+        self.options = options
+        self.public_key = public_key or settings.RECAPTCHA_PUBLIC_KEY
+        super(RecaptchaWidget, self).__init__()
+
+    def render(self, name, value, attrs=None):
+        args = dict(public_key=self.public_key, options='')
+        if self.options:
+            args['options'] = OPTIONS_SCRIPT_HTML % self.options
+        return mark_safe(RECAPTCHA_HTML % args)
+
+    def value_from_datadict(self, data, files, name):
+        challenge = data.get('recaptcha_challenge_field')
+        response = data.get('recaptcha_response_field')
+        return (challenge, response)
+
+    def id_for_label(self, id_):
+        return None
+
+
+class RecaptchaField(forms.Field):
+    widget = RecaptchaWidget
+    default_error_messages = {
+        'required': u'Please enter the CAPTCHA solution.',
+        'invalid': u'An incorrect CAPTCHA solution was entered.',
+        'no-remote-ip': u'CAPTCHA failed due to no visible IP address.',
+        'challenge-error': u'An error occurred with the CAPTCHA service - try '
+            'refreshing.',
+        'unknown-error': u'The CAPTCHA service returned the following error: '
+            '%(code)s.',
+    }
+
+    def __init__(self, private_key=None, *args, **kwargs):
+        """
+        The optional ``private_key`` argument can be used to override the
+        default use of the project-wide ``RECAPTCHA_SECRET_KEY`` setting.
+        """
+        self.remote_ip = None
+        self.private_key = private_key or settings.RECAPTCHA_SECRET_KEY
+        super(RecaptchaField, self).__init__(*args, **kwargs)
+
+    def clean(self, value):
+        if not self.remote_ip:
+            raise forms.ValidationError(self.error_messages['no-remote-ip'])
+        value = super(RecaptchaField, self).clean(value)
+        challenge, response = value
+        if not challenge:
+            raise forms.ValidationError(self.error_messages['challenge-error'])
+        if not response:
+            raise forms.ValidationError(self.error_messages['required'])
+        try:
+            value = validate_recaptcha(self.remote_ip, challenge, response,
+                                       self.private_key)
+        except RecaptchaError, e:
+            if e.code == 'incorrect-captcha-sol':
+                raise forms.ValidationError(self.error_messages['invalid'])
+            raise forms.ValidationError(self.error_messages['unknown-error'] %
+                {'code': e.code}
+            )
+        return value
+
+
+class BaseRecaptchaForm(forms.BaseForm):
+    def __init__(self, remote_ip, *args, **kwargs):
+        super(BaseRecaptchaForm, self).__init__(*args, **kwargs)
+        for field in self.fields.values():
+            if isinstance(field, RecaptchaField):
+                field.remote_ip = remote_ip
+
+
+class RecaptchaForm(BaseRecaptchaForm, forms.Form):
+    pass
+
+
+class RecaptchaError(Exception):
+    def __init__(self, code):
+        self.code = code
+
+    def __str__(self):
+        return self.code
+
+def validate_recaptcha(remote_ip, challenge, response, private_key):
+    assert challenge, 'No challenge was provided for reCaptcha validation'
+    # Request validation from recaptcha.net
+    params = dict(privatekey=private_key, remoteip=remote_ip,
+                  challenge=challenge, response=response)
+    params = urlencode(params)
+    headers = {"Content-type": "application/x-www-form-urlencoded",
+               "Accept": "text/plain"}
+    conn = httplib.HTTPConnection("api-verify.recaptcha.net")
+    conn.request("POST", "/verify", params, headers)
+    response = conn.getresponse()
+    if response.status == 200:
+        data = response.read()
+    else:
+        data = ''
+    conn.close()
+    # Validate based on response data
+    result = data.startswith('true')
+    if not result:
+        bits = data.split('\n', 2)
+        error_code = ''
+        if len(bits) > 1:
+            error_code = bits[1]
+        raise RecaptchaError(error_code)
+    # Return dictionary
+    return result

File gnocchi/blog/search_indexes.py

+
+from haystack.indexes import SearchIndex, CharField
+from haystack import site
+from gnocchi.blog import models
+
+class PostIndex(SearchIndex):
+    text = CharField(document=True, use_template=True)
+    title = CharField(model_attr='title')
+
+site.register(Post, PostIndex)
+
+class CommentIndex(SearchIndex):
+    text = CharField(document=True, use_template=True)
+
+site.register(Comment, CommentIndex)

File gnocchi/blog/settings.py

+from django.conf import settings
+
+BLOG_USERNAME_URLS = getattr(settings, 'BLOG_USERNAME_URLS', False)
+BLOG_DATEBASED_URLS = getattr(settings, 'BLOG_DATEBASED_URLS', True)

File gnocchi/blog/sitemap.py

Empty file added.

File gnocchi/blog/templates/search/indexes/blog/blogpost_text.txt

+{{ object.title }}
+{{ object.content }}
+

File gnocchi/blog/templatetags/__init__.py

Empty file added.

File gnocchi/blog/templatetags/blog.py

+from django import template
+from gnocchi.blog.models import Post
+from gnocchi.tools.template import parse_args
+
+register = template.Library()
+
+class RecentPostsNode(template.Node):
+    def __init__(self, count, varname, username=None, *tags):
+        self.count = count
+        self.varname = varname
+        self.tags = tags
+        self.username = username
+    def render(self, context):
+        tags = filter(None, [tag.resolve(context) for tag in self.tags])
+        varname = self.varname
+        if not isinstance(varname, basestring):
+            varname = varname.resolve(context)
+        username = self.username
+        if username and not isinstance(username, basestring):
+            username = username.resolve(context)
+
+        qset = Post.objects.current()
+        if tags:
+            qset = qset.filter(tag__in=tags)
+        if username:
+            qset = qset.filter(posted_by__username=username)
+        context[varname] = qset
+        return ''
+
+@register.tag
+def get_recent_posts(parser, token):
+    args, kwargs, as_name = parse_args(parser, token, 'posts')
+    count = int(args.pop(0))
+    return RecentPostsNode(count, as_name, *args, **kwargs)
+
+@register.filter
+def recent_posts(value, username=None):
+    qset = Post.objects.current()
+    if username:
+        qset.filter(posted_by__username=username)
+    try:
+        return qset[:int(value)]
+    except Post.DoesNotExist:
+        return []

File gnocchi/blog/urls.py

+from django.conf.urls.defaults import patterns, url
+
+from gnocchi.blog.views import PostList, PostDetail
+
+urlpatterns = patterns('gnocchi.blog.views',
+    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\w-]+)/$',
+        PostDetail.as_view(),
+        name='blog-detail',
+    ),
+    url(r'^$', PostList.as_view(),),
+)

File gnocchi/blog/views.py

+from django.views.generic.dates import ArchiveIndexView, DateDetailView
+from django.http import HttpResponseRedirect
+
+from gnocchi.blog import models, forms
+
+# This will roll for django-1.3
+class PostMixin(object):
+    date_field = 'post_date'
+    slug_field = 'slug'
+    model = models.Post
+    username = None
+
+    def get_queryset(self):
+        qset = models.Post.objects.current()
+        if self.username:
+            qset = qset.filter(posted_by__username=self.username)
+        tags = self.request.GET.getlist('tags')
+        if tags:
+            qset = qset.filter(tags__in=tags)
+        return qset
+
+class PostList(PostMixin, ArchiveIndexView):
+    template_name = 'blog/post_list.html'
+    allow_empty = True
+
+class PostDetail(PostMixin, DateDetailView):
+    template_name = 'blog/post_detail.html'
+    context_object_name = 'post'
+
+    def _get_remote_ip(self, request):
+        try:
+            return request.META['HTTP_X_FORWARDED_FOR']
+        except KeyError:
+            return request.META['REMOTE_ADDR']
+
+    def post(self, request, *args, **kwargs):
+        self.object = post = self.get_object()
+        form = forms.CommentForm(self._get_remote_ip(self.request), request.POST)
+        if form.is_valid():
+            comment = form.save(commit=False)
+            comment.post = post
+            comment.ip_address = self._get_remote_ip(self.request)
+            if request.user.is_authenticated():
+                comment.user = request.user
+            comment.save()
+            return HttpResponseRedirect(post.get_absolute_url())
+        context = self.get_context_data(object=post)
+        context['comment_form'] = form
+        return self.render_to_response(context)
+
+    def get_context_data(self, **kwargs):
+        context = super(PostDetail, self).get_context_data(**kwargs)
+        context['comment_form'] = forms.CommentForm(self._get_remote_ip(self.request))
+        return context

File gnocchi_blog/__init__.py

Empty file removed.

File gnocchi_blog/admin.py

-from django.contrib import admin
-from gnocchi_blog import models, forms
-
-register = admin.site.register
-
-class PostAdmin(admin.ModelAdmin):
-    list_display = (
-        'title',
-        'published',
-        'post_date',
-        'kill_date',
-        'posted_by',
-        'allow_comments',
-    )
-    list_filter = ('posted_by', 'published',)
-    date_heirarchy = 'post_date'
-    search_fields = ('title', 'content',)
-    prepopulated_fields = {'slug': ('title',)}
-    exclude = ('posted_by',)
-
-    form = forms.PostAdminForm
-    fieldsets = (
-        (None, {
-            'fields': ('title', 'slug',),
-        },),
-        ('Content', {
-            'classes': ('wide',),
-            'fields': ('content',),
-        },),
-        ('Extras', {
-            'fields': (
-                ('published', 'post_date', 'kill_date',),
-                ('auto', 'tags',),
-            ),
-        },),
-    )
-
-    def queryset(self, request):
-        qset = self.model.objects.all()
-        if not request.user.is_superuser:
-            return qset.filter(posted_by=request.user)
-        return qset
-
-    def save_model(self, request, obj, form, change):
-        if not change:
-            # initial posting
-            obj.posted_by = request.user
-        obj.save()
-
-register(models.Post, PostAdmin)
-
-class CommentAdmin(admin.ModelAdmin):
-    list_display = ('post_date', 'post', 'name', 'email', 'ip_address',
-        'is_public', 'is_removed',)
-    list_filter = ('is_public', 'is_removed', 'user_name',)
-    date_heirarchy = ('post_date',)
-    search_fieklds = ('user_name', 'comment',)
-
-register(models.Comment, CommentAdmin)

File gnocchi_blog/feeds.py

-from django.contrib.syndication.views import Feed
-from gnocchi_blog.models import Post
-from taggit.models import TaggedItem
-
-class LatestBlogFeed(Feed):
-    '''Basic feed configuration
-
-    You need to derive your own class from this to set link, and you will
-    probably also want to change description and title.
-
-    See http://docs.djangoproject.com/en/1.2/ref/contrib/syndication/
-    '''
-    description = 'Blog Posts'
-    title = 'Blog'
-
-    def categories(self):
-        return TaggedItem.tags_for(Post)
-
-    def items(self):
-        return Post.objects.current()
-
-    def item_title(self, item):
-        return item.title
-
-    def item_author_name(self, item):
-        return item.posted_by.get_full_name()
-
-    def item_pubdate(self, item):
-        return item.post_date
-
-    def item_categories(self, item):
-        return item.tags
-
-class BlogTagFeed(LatestBlogFeed):
-    def get_object(self, request, tags):
-        tags = [ tag.strip() for tag in tags.split('/') ]
-        return tags
-
-    def description(self, obj):
-        return "Posts with tags %s" % (', '.join(obj))

File gnocchi_blog/forms.py

-from django import forms
-from gnocchi_blog import models
-from taggit.models import TaggedItem
-import re
-from gnocchi_blog import recaptcha
-
-class PostAdminForm(forms.ModelForm):
-    auto = forms.BooleanField(label='Auto-tag?', required=False,
-        help_text="Automatically scan content for tags"
-    )
-    class Meta:
-        model = models.Post
-    def clean(self):
-        data = self.cleaned_data
-        if data['auto']:
-            tags = set(data['tags'])
-            tags.update([
-                tag
-                for tag in TaggedItem.tags_for(models.Post)
-                if re.search(r'\b%s\b' % tag.name, data['content'], re.I|re.M)
-            ])
-            data['tags'] = list(tags)
-        return data
-
-class CommentForm(recaptcha.BaseRecaptchaForm, forms.ModelForm):
-    captcha = recaptcha.RecaptchaField()
-    class Meta:
-        model = models.Comment
-        fields = (
-            'user_name',
-            'user_email',
-            'content',
-        )

File gnocchi_blog/models.py

-from django.db import models
-
-from gnocchi_blog import settings
-from taggit.managers import TaggableManager
-
-from datetime import datetime
-
-class BlogManager(models.Manager):
-    def current(self):
-        now = datetime.now()
-        return self.get_query_set().filter(
-            published=True,
-            post_date__lte=now,
-        ).exclude(
-            kill_date__lte=now
-        )
-
-class Post(models.Model):
-    title = models.CharField(max_length=200)
-    slug = models.SlugField()
-    content = models.TextField(blank=True)
-
-    post_date = models.DateTimeField(default=datetime.now)
-    kill_date = models.DateTimeField(null=True, blank=True, default=None)
-    posted_by = models.ForeignKey('auth.User', blank=True, null=True,
-        related_name='blog_posts')
-    published = models.BooleanField(default=False)
-
-    allow_comments = models.BooleanField(default=True)
-
-    tags = TaggableManager(blank=True)
-
-    objects = BlogManager()
-
-    class Meta:
-        ordering = ('-post_date',)
-    def __unicode__(self):
-        return self.title
-    @models.permalink
-    def get_absolute_url(self):
-        kwargs = {'slug': self.slug,}
-        if settings.BLOG_DATEBASED_URLS:
-            kwargs.update({
-                'year': '%04d' % self.post_date.year,
-                'month': self.post_date.strftime('%b').lower(),
-                'day': '%02d' % self.post_date.day,
-            })
-        if settings.BLOG_USERNAME_URLS:
-            kwargs['username'] = self.posted_by.username
-        return 'blog-detail', (), kwargs
-
-class Comment(models.Model):
-    '''Mostly a copy of django.contrib.comments Comment model'''
-    post = models.ForeignKey(Post)
-    content = models.TextField()
-
-    post_date = models.DateTimeField(default=datetime.now)
-    ip_address = models.IPAddressField(blank=True, null=True)
-
-    user = models.ForeignKey('auth.User', null=True, blank=True)
-    user_name = models.CharField(max_length=50)
-    user_email  = models.EmailField(blank=True)
-
-    is_public = models.BooleanField(default=True)
-    is_removed = models.BooleanField(default=True)
-
-    class Meta:
-        ordering = ('post_date',)
-    def __unicode__(self):
-        return u'%s...' % self.content[:50]
-
-    # Properties for variable values
-
-    def get_name(self):
-        if self.user_id:
-            return self.user.get_full_name() or self.user.username
-        return self.user_name
-    def set_name(self, value):
-        if self.user_id:
-            raise AttributeError("This comment was posted by an authenticated "\
-                "user and thus the name is read-only.")
-        self.user_name = value
-    name = property(get_name, set_name)
-
-    def get_email(self):
-        if self.user_id:
-            return self.user.email
-        return self.user_email
-    def set_email(self, value):
-        if self.user_id:
-            raise AttributeError("This comment was posted by an authenticated "\
-                "user and thus the email is read-only.")
-        self.user_email = value
-    email = property(get_email, set_email)

File gnocchi_blog/recaptcha.py

-"""
-An easy-to-use Django forms integration of the reCaptcha service.
-v1.0.1
-
-To use, simply base your form off the ``RecaptchaForm`` class. This class adds
-a new argument that must be provided to the form, ``remote_ip``.
-
-Two settings which must be set in your project's ``settings`` module are
-``RECAPTCHA_PUBLIC_KEY`` and ``RECAPTCHA_SECRET_KEY``, the public and private
-keys for your domain, respectively.
-
-Following is an example of creating a basic comment form class and then
-using an instance of the form in a view::
-
-    from django import forms
-    from mysite.utils import recaptcha
-
-    class CommentForm(recaptcha.RecaptchaForm):
-        name = forms.CharField()
-        comment = forms.CharField(widget=Textarea())
-        captcha = recaptcha.RecaptchaField()
-
-    def comment(request):
-        comment_form = CommentForm(remote_ip=request.META['REMOTE_ADDR'])
-        ...
-
-If you need to use a different base form (such as ``ModelForm``), use multiple
-inheritance like so::
-
-    class MyModelForm(BaseRecaptchaForm, ModelForm):
-        ...
-"""
-
-import httplib
-from django import forms
-from django.conf import settings
-from django.utils.safestring import mark_safe
-from django.utils.http import urlencode
-
-OPTIONS_SCRIPT_HTML = u'''<script type="text/javascript">
-   var RecaptchaOptions = %r;
-</script>
-'''
-RECAPTCHA_HTML = u'''%(options)s<script type="text/javascript" src="http://api.recaptcha.net/challenge?k=%(public_key)s"></script>
-<noscript>
-   <iframe src="http://api.recaptcha.net/noscript?k=%(public_key)s"
-       height="300" width="500" frameborder="0"></iframe><br />
-   <textarea name="recaptcha_challenge_field" rows="3" cols="40">
-   </textarea>
-   <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
-</noscript>'''
-
-
-class RecaptchaWidget(forms.Widget):
-    def __init__(self, theme=None, tabindex=None, public_key=None):
-        '''
-        From http://recaptcha.net/apidocs/captcha/client.html#look-n-feel::
-
-            theme:      'red' | 'white' | 'blackglass' | 'clean'
-
-                Defines which theme to use for reCAPTCHA.
-
-            tabindex:   any integer
-
-                Sets a tabindex for the reCAPTCHA text box. If other elements
-                in the form use a tabindex, this should be set so that
-                navigation is easier for the user.
-
-        The optional ``public_key`` argument can be used to override the
-        default use of the project-wide ``RECAPTCHA_PUBLIC_KEY`` setting.
-        '''
-        options = {}
-        if theme:
-            options['theme'] = theme
-        if tabindex:
-            options['tabindex'] = tabindex
-        self.options = options
-        self.public_key = public_key or settings.RECAPTCHA_PUBLIC_KEY
-        super(RecaptchaWidget, self).__init__()
-
-    def render(self, name, value, attrs=None):
-        args = dict(public_key=self.public_key, options='')
-        if self.options:
-            args['options'] = OPTIONS_SCRIPT_HTML % self.options
-        return mark_safe(RECAPTCHA_HTML % args)
-
-    def value_from_datadict(self, data, files, name):
-        challenge = data.get('recaptcha_challenge_field')
-        response = data.get('recaptcha_response_field')
-        return (challenge, response)
-
-    def id_for_label(self, id_):
-        return None
-
-
-class RecaptchaField(forms.Field):
-    widget = RecaptchaWidget
-    default_error_messages = {
-        'required': u'Please enter the CAPTCHA solution.',
-        'invalid': u'An incorrect CAPTCHA solution was entered.',
-        'no-remote-ip': u'CAPTCHA failed due to no visible IP address.',
-        'challenge-error': u'An error occurred with the CAPTCHA service - try '
-            'refreshing.',
-        'unknown-error': u'The CAPTCHA service returned the following error: '
-            '%(code)s.',
-    }
-
-    def __init__(self, private_key=None, *args, **kwargs):
-        """
-        The optional ``private_key`` argument can be used to override the
-        default use of the project-wide ``RECAPTCHA_SECRET_KEY`` setting.
-        """
-        self.remote_ip = None
-        self.private_key = private_key or settings.RECAPTCHA_SECRET_KEY
-        super(RecaptchaField, self).__init__(*args, **kwargs)
-
-    def clean(self, value):
-        if not self.remote_ip:
-            raise forms.ValidationError(self.error_messages['no-remote-ip'])
-        value = super(RecaptchaField, self).clean(value)
-        challenge, response = value
-        if not challenge:
-            raise forms.ValidationError(self.error_messages['challenge-error'])
-        if not response:
-            raise forms.ValidationError(self.error_messages['required'])
-        try:
-            value = validate_recaptcha(self.remote_ip, challenge, response,
-                                       self.private_key)
-        except RecaptchaError, e:
-            if e.code == 'incorrect-captcha-sol':
-                raise forms.ValidationError(self.error_messages['invalid'])
-            raise forms.ValidationError(self.error_messages['unknown-error'] %
-                {'code': e.code}
-            )
-        return value
-
-
-class BaseRecaptchaForm(forms.BaseForm):
-    def __init__(self, remote_ip, *args, **kwargs):
-        super(BaseRecaptchaForm, self).__init__(*args, **kwargs)
-        for field in self.fields.values():
-            if isinstance(field, RecaptchaField):
-                field.remote_ip = remote_ip
-
-
-class RecaptchaForm(BaseRecaptchaForm, forms.Form):
-    pass
-
-
-class RecaptchaError(Exception):
-    def __init__(self, code):
-        self.code = code
-
-    def __str__(self):
-        return self.code
-
-def validate_recaptcha(remote_ip, challenge, response, private_key):
-    assert challenge, 'No challenge was provided for reCaptcha validation'
-    # Request validation from recaptcha.net
-    params = dict(privatekey=private_key, remoteip=remote_ip,
-                  challenge=challenge, response=response)
-    params = urlencode(params)
-    headers = {"Content-type": "application/x-www-form-urlencoded",
-               "Accept": "text/plain"}
-    conn = httplib.HTTPConnection("api-verify.recaptcha.net")
-    conn.request("POST", "/verify", params, headers)
-    response = conn.getresponse()
-    if response.status == 200:
-        data = response.read()
-    else:
-        data = ''
-    conn.close()
-    # Validate based on response data
-    result = data.startswith('true')
-    if not result:
-        bits = data.split('\n', 2)
-        error_code = ''
-        if len(bits) > 1:
-            error_code = bits[1]
-        raise RecaptchaError(error_code)
-    # Return dictionary
-    return result

File gnocchi_blog/search_indexes.py

-
-from haystack.indexes import SearchIndex, CharField
-from haystack import site
-from gnocchi_blog import models
-
-class PostIndex(SearchIndex):
-    text = CharField(document=True, use_template=True)
-    title = CharField(model_attr='title')
-
-site.register(Post, PostIndex)
-
-class CommentIndex(SearchIndex):
-    text = CharField(document=True, use_template=True)
-
-site.register(Comment, CommentIndex)

File gnocchi_blog/settings.py

-from django.conf import settings
-
-BLOG_USERNAME_URLS = getattr(settings, 'BLOG_USERNAME_URLS', False)
-BLOG_DATEBASED_URLS = getattr(settings, 'BLOG_DATEBASED_URLS', True)

File gnocchi_blog/sitemap.py

Empty file removed.

File gnocchi_blog/templates/search/indexes/blog/blogpost_text.txt

-{{ object.title }}
-{{ object.content }}
-

File gnocchi_blog/templatetags/__init__.py

Empty file removed.

File gnocchi_blog/templatetags/blog.py

-from django import template
-from gnocchi_blog.models import Post
-from gnocchi_tools.template import parse_args
-
-register = template.Library()
-
-class RecentPostsNode(template.Node):
-    def __init__(self, count, varname, username=None, *tags):
-        self.count = count
-        self.varname = varname
-        self.tags = tags
-        self.username = username
-    def render(self, context):
-        tags = filter(None, [tag.resolve(context) for tag in self.tags])
-        varname = self.varname
-        if not isinstance(varname, basestring):
-            varname = varname.resolve(context)
-        username = self.username
-        if username and not isinstance(username, basestring):
-            username = username.resolve(context)
-
-        qset = Post.objects.current()
-        if tags:
-            qset = qset.filter(tag__in=tags)
-        if username:
-            qset = qset.filter(posted_by__username=username)
-        context[varname] = qset
-        return ''
-
-@register.tag
-def get_recent_posts(parser, token):
-    args, kwargs, as_name = parse_args(parser, token, 'posts')
-    count = int(args.pop(0))
-    return RecentPostsNode(count, as_name, *args, **kwargs)
-
-@register.filter
-def recent_posts(value, username=None):
-    qset = Post.objects.current()
-    if username:
-        qset.filter(posted_by__username=username)
-    try:
-        return qset[:int(value)]
-    except Post.DoesNotExist:
-        return []

File gnocchi_blog/urls.py

-from django.conf.urls.defaults import patterns, url
-
-from gnocchi_blog.views import PostList, PostDetail
-
-urlpatterns = patterns('gnocchi_blog.views',
-    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\w-]+)/$',
-        PostDetail.as_view(),
-        name='blog-detail',
-    ),
-    url(r'^$', PostList.as_view(),),
-)

File gnocchi_blog/views.py

-from django.views.generic.dates import ArchiveIndexView, DateDetailView
-from django.http import HttpResponseRedirect
-
-from gnocchi_blog import models, forms
-
-# This will roll for django-1.3
-class PostMixin(object):
-    date_field = 'post_date'
-    slug_field = 'slug'
-    model = models.Post
-    username = None
-
-    def get_queryset(self):
-        qset = models.Post.objects.current()
-        if self.username:
-            qset = qset.filter(posted_by__username=self.username)
-        tags = self.request.GET.getlist('tags')
-        if tags:
-            qset = qset.filter(tags__in=tags)
-        return qset
-
-class PostList(PostMixin, ArchiveIndexView):
-    template_name = 'blog/post_list.html'
-    allow_empty = True
-
-class PostDetail(PostMixin, DateDetailView):
-    template_name = 'blog/post_detail.html'
-    context_object_name = 'post'
-
-    def _get_remote_ip(self, request):
-        try:
-            return request.META['HTTP_X_FORWARDED_FOR']
-        except KeyError:
-            return request.META['REMOTE_ADDR']
-
-    def post(self, request, *args, **kwargs):
-        self.object = post = self.get_object()
-        form = forms.CommentForm(self._get_remote_ip(self.request), request.POST)
-        if form.is_valid():
-            comment = form.save(commit=False)
-            comment.post = post
-            comment.ip_address = self._get_remote_ip(self.request)
-            if request.user.is_authenticated():
-                comment.user = request.user
-            comment.save()
-            return HttpResponseRedirect(post.get_absolute_url())
-        context = self.get_context_data(object=post)
-        context['comment_form'] = form
-        return self.render_to_response(context)
-
-    def get_context_data(self, **kwargs):
-        context = super(PostDetail, self).get_context_data(**kwargs)
-        context['comment_form'] = forms.CommentForm(self._get_remote_ip(self.request))
-        return context
 from setuptools import setup, find_packages
 
 setup( name='gnocchi-blog',
-    version = '1.3',
+    version = '1.4',
     description = 'A simple multi-user Blog',
     author = 'Curtis Maloney',
     author_email = 'curtis@tinbrain.net',
     url = 'http://bitbucket.org/funkybob/gnocchi-blog/',
     keywords = [ 'django', 'blog', ],
     packages = find_packages(),
+    namespace_packages = ['gnocchi',],
     zip_safe = False,
     classifiers = [
         'Environment :: Web Environment',