Skip to content

Commit

Permalink
Merge 02bd9e9 into 4feddb4
Browse files Browse the repository at this point in the history
  • Loading branch information
paltman committed Dec 23, 2016
2 parents 4feddb4 + 02bd9e9 commit 82b7413
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 86 deletions.
69 changes: 69 additions & 0 deletions docs/usage.md
Expand Up @@ -36,3 +36,72 @@ or without alt text:

Adjusting for the number of the image, of course.


## Scoping

The idea of scoping allows you to setup your project to have multiple blogs
partitioned by whatever domain object you would like.

### Settings

* `PINAX_BLOG_SCOPING_MODEL` - a string in the format `"app.Model"` that will set a ForeignKey on the `blog.Post` model
* `PINAX_BLOG_SCOPING_URL_VAR` - the url variable name that you use in your url prefix that will allow you to look up your scoping object
* `PINAX_BLOG_HOOKSET` - introducing the hookset pattern from other apps. just a single method: `get_scoped_object(self, **kwargs)` is defined. override this in your project to get the object to scope your posts by.
* `pinax.blog.context_processors.scoped` - add to your context processors to put `scoper_lookup` in templates for url reversing

### Example

To demonstrate how to set all this up let's walk through an example where we
will scope by `auth.User` so that each user has their own blog at `/users/:username/`.

First we will modify the `settings.py`:

```python
# ... abbreviated for clarity

TEMPLATES = [
{
# ...
"OPTIONS": {
# ...
"context_processors": [
# ...
"pinax.blog.context_processors.scoped"
],
},
},
]

PINAX_BLOG_SCOPING_URL_VAR = "username"
PINAX_BLOG_SCOPING_MODEL = "auth.User"
PINAX_BLOG_HOOKSET = "multiblog.hooks.HookSet" # where `multiblog` is the package name of our project
```

Now, we'll add the url in `urls.py`:

```python
url(r"^users/(?P<username>[-\w]+)/", include("pinax.blog.urls", namespace="pinax_blog"))
```

And finally we'll implement our hookset by adding a `hooks.py`:

```python
from django.contrib.auth.models import User


class HookSet(object):

def get_scoped_object(self, **kwargs):
username = kwargs.get("username", None)
return User.objects.get(username=username)
```

This is designed to work out of the box with templates in `pinax-theme-bootstrap`
so you can either use them directly or use them as a reference. If you need to
reverse a URL for any of the `pinax-blog` urls you can simply do:

```django
{% url "pinax_blog:blog" scoper_lookup %}
```

Now that you have the context processor installed.
6 changes: 6 additions & 0 deletions pinax/blog/admin.py
Expand Up @@ -3,6 +3,7 @@
from django.utils.functional import curry
from django.utils.translation import ugettext_lazy as _

from .conf import settings
from .forms import AdminPostForm
from .models import Post, Image, ReviewComment, Section
from .utils import can_tweet
Expand Down Expand Up @@ -78,6 +79,11 @@ def save_form(self, request, form, change):
return form.save()


if settings.PINAX_BLOG_SCOPING_MODEL:
PostAdmin.list_filter.append("scoper")
PostAdmin.fields.append("scoper")


class SectionAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}

Expand Down
8 changes: 8 additions & 0 deletions pinax/blog/conf.py
Expand Up @@ -4,6 +4,8 @@

from appconf import AppConf

from .utils import load_path_attr


def is_installed(package):
try:
Expand Down Expand Up @@ -33,14 +35,20 @@ class PinaxBlogAppConf(AppConf):
SECTION_FEED_TITLE = "Blog (%s)"
MARKUP_CHOICE_MAP = DEFAULT_MARKUP_CHOICE_MAP
MARKUP_CHOICES = DEFAULT_MARKUP_CHOICE_MAP
SCOPING_MODEL = None
SCOPING_URL_VAR = None
SLUG_UNIQUE = False
PAGINATE_BY = 10
HOOKSET = "pinax.blog.hooks.PinaxBlogDefaultHookSet"

def configure_markup_choices(self, value):
return [
(key, value[key]["label"])
for key in value.keys()
]

def configure_hookset(self, value):
return load_path_attr(value)()

class Meta:
prefix = "pinax_blog"
7 changes: 7 additions & 0 deletions pinax/blog/context_processors.py
@@ -0,0 +1,7 @@
from .conf import settings


def scoped(request):
return {
"scoper_lookup": request.resolver_match.kwargs.get(settings.PINAX_BLOG_SCOPING_URL_VAR)
}
16 changes: 16 additions & 0 deletions pinax/blog/hooks.py
@@ -0,0 +1,16 @@
from pinax.blog.conf import settings


class PinaxBlogDefaultHookSet(object):

def get_scoped_object(self, **kwargs):
return None


class HookProxy(object):

def __getattr__(self, attr):
return getattr(settings.PINAX_BLOG_HOOKSET, attr)


hookset = HookProxy()
25 changes: 25 additions & 0 deletions pinax/blog/migrations/0007_scoped.py
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-03-21 15:27
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [('blog', '0006_auto_20160321_1527')]
operations = []

if settings.PINAX_BLOG_SCOPING_MODEL is not None:
dependencies.append(
migrations.swappable_dependency(settings.PINAX_BLOG_SCOPING_MODEL)
)

operations.append(
migrations.AddField(
model_name='post',
name='scoper',
field=models.ForeignKey(related_name='blog_posts', to=settings.PINAX_BLOG_SCOPING_MODEL)
)
)
15 changes: 14 additions & 1 deletion pinax/blog/models.py
Expand Up @@ -38,6 +38,7 @@ def ig(L, i):
for x in L:
yield x[i]


STATES = settings.PINAX_BLOG_UNPUBLISHED_STATES + ["Published"]
PINAX_BLOG_STATE_CHOICES = list(zip(range(1, 1 + len(STATES)), STATES))

Expand All @@ -61,6 +62,9 @@ class Post(models.Model):

STATE_CHOICES = PINAX_BLOG_STATE_CHOICES

if settings.PINAX_BLOG_SCOPING_MODEL is not None:
scoper = models.ForeignKey(settings.PINAX_BLOG_SCOPING_MODEL, related_name="blog_posts")

section = models.ForeignKey(Section)

title = models.CharField(_("Title"), max_length=90)
Expand Down Expand Up @@ -188,6 +192,12 @@ def save(self, **kwargs):
self.published = timezone.now()
super(Post, self).save(**kwargs)

@property
def scoping_url_kwargs(self):
if getattr(self, "scoper", None) is not None:
return {settings.PINAX_BLOG_SCOPING_URL_VAR: self.scoper}
return {}

@property
def sharable_url(self):
"""
Expand All @@ -196,7 +206,9 @@ def sharable_url(self):
"""
if not self.is_published or self.is_future_published:
if self.secret_key:
return reverse("pinax_blog:blog_post_secret", kwargs={"post_secret_key": self.secret_key})
kwargs = self.scoping_url_kwargs
kwargs.update({"post_secret_key": self.secret_key})
return reverse("pinax_blog:blog_post_secret", kwargs=kwargs)
else:
return "A secret sharable url for non-authenticated users is generated when you save this post."
else:
Expand Down Expand Up @@ -226,6 +238,7 @@ def get_absolute_url(self):
kwargs = {
"post_pk": self.pk,
}
kwargs.update(self.scoping_url_kwargs)
return reverse(name, kwargs=kwargs)

def inc_views(self):
Expand Down
97 changes: 21 additions & 76 deletions pinax/blog/templatetags/pinax_blog_tags.py
Expand Up @@ -6,85 +6,30 @@
register = template.Library()


class LatestBlogPostsNode(template.Node):
@register.assignment_tag
def latest_blog_posts(scoper=None):
qs = Post.objects.current()
if scoper:
qs = qs.filter(scoper=scoper)
return qs[:5]

def __init__(self, context_var):
self.context_var = context_var

def render(self, context):
latest_posts = Post.objects.current()[:5]
context[self.context_var] = latest_posts
return ""
@register.assignment_tag
def latest_blog_post(scoper=None):
qs = Post.objects.current()
if scoper:
qs = qs.filter(scoper=scoper)
return qs[0]


@register.tag
def latest_blog_posts(parser, token):
bits = token.split_contents()
return LatestBlogPostsNode(bits[2])
@register.assignment_tag
def latest_section_post(section, scoper=None):
qs = Post.objects.published().filter(section__name=section).order_by("-published")
if scoper:
qs = qs.filter(scoper=scoper)
return qs[0] if qs.count() > 0 else None


class LatestBlogPostNode(template.Node):

def __init__(self, context_var):
self.context_var = context_var

def render(self, context):
try:
latest_post = Post.objects.current()[0]
except IndexError:
latest_post = None
context[self.context_var] = latest_post
return ""


@register.tag
def latest_blog_post(parser, token):
bits = token.split_contents()
return LatestBlogPostNode(bits[2])


class LatestSectionPostNode(template.Node):

def __init__(self, section, context_var):
self.section = template.Variable(section)
self.context_var = context_var

def render(self, context):
section = self.section.resolve(context)

post = Post.objects.published().filter(section__name=section).order_by("-published")
try:
post = post[0]
except IndexError:
post = None
context[self.context_var] = post
return ""


@register.tag
def latest_section_post(parser, token):
"""
{% latest_section_post "articles" as latest_article_post %}
"""
bits = token.split_contents()
return LatestSectionPostNode(bits[1], bits[3])


class BlogSectionsNode(template.Node):

def __init__(self, context_var):
self.context_var = context_var

def render(self, context):
sections = Section.objects.filter(enabled=True)
context[self.context_var] = sections
return ""


@register.tag
def blog_sections(parser, token):
"""
{% blog_sections as blog_sections %}
"""
bits = token.split_contents()
return BlogSectionsNode(bits[2])
@register.assignment_tag
def blog_sections():
return Section.objects.filter(enabled=True)

0 comments on commit 82b7413

Please sign in to comment.