1. Curtis Maloney
  2. gnocchi-blog

Commits

cur...@tinbrain.net  committed c3a1a61

Initial import from gnocchi project

  • Participants
  • Branches default

Comments (0)

Files changed (15)

File .hgignore

View file
  • Ignore whitespace
+syntax: glob
+*~
+*.pyc

File MANIFEST.in

View file
  • Ignore whitespace
+recursive-include blog/ *.html *.txt

File blog/__init__.py

  • Ignore whitespace
Empty file added.

File blog/admin.py

View file
  • Ignore whitespace
+from django.contrib import admin
+from django.views.generic.simple import direct_to_template
+from django.conf.urls.defaults import patterns, url
+from django.http import HttpResponseRedirect
+
+from gnocchi2.blog import models
+
+register = admin.site.register
+
+class BlogPostAdmin(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',)
+    actions = ('publish_newsletter',)
+
+    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:
+            obj.posted_by = request.user
+        obj.save()
+
+register( models.BlogPost, BlogPostAdmin )
+

File blog/feeds.py

View file
  • Ignore whitespace
+from django.contrib.syndication.views import Feed
+from gnocchi2.blog.models import BlogPost
+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(BlogPost)
+
+    def items(self):
+        return BlogPost.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
+
+

File blog/models.py

View file
  • Ignore whitespace
+from django.db import models
+from django.core.urlresolvers import reverse
+
+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 BlogPost(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)
+    published = models.BooleanField(default=False)
+
+    allow_comments = models.BooleanField(default=True)
+
+    tags = TaggableManager()
+
+    objects = BlogManager()
+
+    class Meta:
+        ordering = ('-post_date',)
+    def __unicode__(self):
+        return self.title
+    def get_absolute_url(self):
+        return reverse('blog_detail', kwargs={
+            'slug': self.slug,
+            'year': '%04d' % self.post_date.year,
+            'month': self.post_date.strftime('%b').lower(),
+            'day': '%02d' % self.post_date.day,
+        })
+

File blog/recaptcha.py

View file
  • Ignore whitespace
+"""
+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 blog/search_indexes.py

View file
  • Ignore whitespace
+
+from haystack.indexes import SearchIndex, CharField
+from haystack import site
+from gnocchi2.blog.models import BlogPost
+
+class BlogPostIndex(SearchIndex):
+    text = CharField(document=True, use_template=True)
+    title = CharField(model_attr='title')
+
+site.register(BlogPost, BlogPostIndex)
+

File blog/sitemap.py

  • Ignore whitespace
Empty file added.

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

View file
  • Ignore whitespace
+{{ object.title }}
+{{ object.content }}
+

File blog/templatetags/__init__.py

  • Ignore whitespace
Empty file added.

File blog/templatetags/blog.py

View file
  • Ignore whitespace
+from django import template
+from gnocchi2.blog.models import BlogPost
+from gnocchi2 import tools
+
+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 = BlogPost.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 = tools.template.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 = BlogPost.objects.current()
+    if username:
+        qset.filter(posted_by__username=username)
+    try:
+        return qset[:int(value)]
+    except BlogPost.DoesNotExist:
+        return []
+

File blog/urls.py

View file
  • Ignore whitespace
+from django.conf.urls.defaults import patterns, url
+
+urlpatterns = patterns('gnocchi2.blog.views',
+    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[\w-]+)/$',
+        'blog_detail',
+        name='blog_detail',
+    ),
+    url(r'^$', 'blog_list', ),
+)
+
+

File blog/views.py

View file
  • Ignore whitespace
+from django.views.generic import date_based, list_detail
+
+from gnocchi2.blog.models import BlogPost
+
+def _build_queryset(request, username=None, **kw):
+    qset = BlogPost.objects.current()
+    if username:
+        qset = qset.filter( user__username=username )
+    tags = request.GET.getlist('tags')
+    if tags:
+        return qset.filter(tags__in=tags)
+    return qset
+
+def blog_index(request, *a, **kw):
+    return date_based.archive_index(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date',
+        *a, **kw)
+
+def blog_year(request, *a, **kw):
+    return date_based.archive_year(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date', *a, **kw)
+
+def blog_month(request, *a, **kw):
+    return date_based.archive_month(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date',
+        *a, **kw)
+
+def blog_week(request, *a, **kw):
+    return date_based.archive_week(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date',
+        *a, **kw)
+
+def blog_day(request, *a, **kw):
+    return date_based.archive_day(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date',
+        *a, **kw)
+
+def blog_today(request, *a, **kw):
+    return date_based.archive_today(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date',
+        *a, **kw)
+
+def blog_detail(request, *a, **kw):
+    return date_based.object_detail(request,
+        queryset=_build_queryset(request, *a, **kw),
+        date_field='post_date',
+        slug_field='slug',
+        *a, **kw)
+
+def blog_list(request, *a, **kw):
+    return list_detail.object_list(request,
+        queryset=_build_queryset(request, *a, **kw),
+        *a, **kw)
+

File setup.py

View file
  • Ignore whitespace
+from distutils.core import setup
+
+setup( name='gnocchi-blog',
+    version = '1.0',
+    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 = [
+        'gnocchi2.blog',
+        'gnocchi2.blog.templatetags',
+    ],
+    package_dir = {
+        'gnocchi2.blog': 'blog',
+    },
+    package_data = {
+        'gnocchi2.blog': [
+            'templates/search/indexes/blog/blogpost_text.txt',
+        ]
+    },
+    zip_safe = False,
+    classifiers = [
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+    ],
+    requires = [
+        'Django (>1.2)',
+        'taggit (>=0.8.0)',
+    ]
+)