=============== Groups Overview =============== Groups in Pinax are simply container application/models that contain other application/models. For example, if we had something called 'Project' it might have 'Tasks' within it. The Pinax Groups system is designed to make this scenario easy to implement. It allows you to convert an application and model to become a container, henceforce called respectively a *Group App* and the model is called a '*Group Model*'. It also allows you to convert an application and associated models to become '*Group-Aware*', which if done properly makes it capable of working inside a group or as a stand-alone application. To summarize, Pinax gives you the ability to: * Create a *Group App* that can hold another application that is *Group-Aware*. * Create a *Group-Aware* application that can either stand-alone or be held inside a *Group App*. Pinax comes with a number of built-in applications of both *Group Apps* and *Group-Aware* apps detailed in this page's appendix, but its easy enough to create your own. Writing your own group aware domain objects ------------------------------------------- First lets make a Task application. Please keep in mind that our example, when complete, can be quicklybrought into any Pinax Group-App, not just projects:: import datetime from django.db import models from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType class Task(models.Model): """ We don't use anything Pinax specific in this model. In fact, we make a point with the *get_absolute_url* method of making it able to handle being in a group or as a stand-alone Django application. """ # Some sample fields below to show individual distinction of this model title = models.CharField(max_length=140) description = models.TextField(max_length=140) creator = models.ForeignKey(User, verbose_name=_("creator"), related_name="%(class)s_created" ) created = models.DateTimeField(_("created"), default=datetime.datetime.now) # The following three fields are required for being group aware. # We use a nullable generic foreign key to enable it to be optional # and we don't know what group model it will point to. object_id = models.IntegerField(null=True) content_type = models.ForeignKey(ContentType, null=True) group = generic.GenericForeignKey("content_type", "object_id") def __unicode__(self): return self.title def get_absolute_url(self, group=None): kwargs = {"id": self.id} # We check for attachment of a group. This way if the Task object # is not attached to the group the application continues to function. if group: return group.content_bridge.reverse("task_detail", group, kwargs) return reverse("task_detail", kwargs=kwargs) Forms ~~~~~ We don't want to display the special object_id, content_type, and group fields to end clients and users. So we use Django forms to control what is displayed:: from django import forms from django.utils.translation import ugettext as _ from pinax.apps.tasks.models import Task class TaskForm(forms.ModelForm): """ Our Task form is just like the Task model in that it can work with groups or independantly. """ def __init__(self, user, group=None, *args, **kwargs): self.user = user self.group = group super(TaskForm, self).__init__(*args, **kwargs) def save(self, commit=True): return super(TaskForm, self).save(commit) class Meta: model = Task fields = ["title", "description"] # only display 2 fields def clean(self): self.check_group_membership() return self.cleaned_data def check_group_membership(self): """ We only let valid group members add new tasks. If the Task is *not* attached to a group then it ignores this step. """ group = self.group if group and not self.group.user_is_member(self.user): raise forms.ValidationError(_("You must be a member to create new Tasks")) Views ~~~~~ Just like the models and forms, we build our Task views in such a way that they can handle being within or without a group:: from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 from django.template import RequestContext from django.shortcuts import render_to_response from pinax.apps.tasks.models import Task def task_list(request, group_slug=None, bridge=None): # if a bridge is supplied but does not exist in the database that # means someone is trying to look at a non-existant object. if bridge is not None: try: group = bridge.get_group(group_slug) except ObjectDoesNotExist: raise Http404 else: group = None # If we are in a group we fetch the Task list of content via a special # utility method. Otherwise we just use a standard ORM call. if group: tasks = group.content_objects(Task) else: tasks = Task.objects.all() # Is the user a member of the parent group? if not request.user.is_authenticated(): is_member = False else: if group: is_member = group.user_is_member(request.user) else: is_member = True return render_to_response("tasks/task_list.html", { "group": group, "blogs": blogs, "is_member": is_member, }, context_instance=RequestContext(request)) In many cases you might want to check if the authenticated user has membership in the group. To do this:: if not request.user.is_authenticated(): is_member = False else: is_member = group.user_is_member(request.user) URLs ~~~~ The ``urls.py`` file of your app will not need anything special. Most of that is handled by Pinax. However, URL reversal needs to be group aware. We have some helpers to help you work with this easily. Let's say you have the following code in ``tasks.urls.py``:: from django.conf.urls.defaults import * urlpatterns = patterns("", url(r"^tasks/$", "pinax.apps.tasks.views.task_list", name="task_list"), url(r"^tasks/(?P[-\w]+)/$", "pinax.apps.tasks.views.blog_detail", name="task_detail"), ) To ensure URLs to ``task_list`` are correctly generated you will need to use ``reverse`` located on the ``ContentBridge`` object:: def some_view_with_redirect(request, bridge=None): ... return HttpResponseRedirect(bridge.reverse("blog_list", group)) The ``reverse`` method work almost identical to Django's ``reverse``. It is essentially a wrapper. To reverse the ``taskdetail`` URL:: task = Task.objects.get(pk=1) bridge.reverse("task_detail", group, kwargs={"slug": task.slug}) .. note:: You should be aware that **only** ``kwargs`` work with the bridge ``reverse``. This is significant because URLs with ``args`` mapping will fail reversal. The reason behind this is because Django does not allow mixing of ``args`` and ``kwargs`` when performing URL reversal. There are some cases when you don't have easy access to the ``ContentBridge``. You may only have access to a domain object instance. You can get access to the ``ContentBridge`` from the instance. For example:: task = Task.objects.get(pk=1) task.content_bridge.reverse(...) URL reversal in templates ~~~~~~~~~~~~~~~~~~~~~~~~~ In Django you may be familiar with the ``{% url %}`` templatetag. This is basically a wrapper around ``reverse``. We provide a similar tag, but works with our ``ContentBridge.reverse``. Here is how you might use it:: {% load group_tags %} {{ task.title }} The ``{% groupurl %}`` templatetag will fall back to normal Django URL reversal if the value of the passed in ``group`` is ``None``. This enables the ability to work with no group association. Writing your own group app -------------------------- We will continue with the Project/Task corollary. Our group application will be a Project (which contain tasks, members, and more):: from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.db import models from django.utils.translation import ugettext_lazy as _ from groups.base import Group class Project(Group): """ Doesn't inherit the normal *models.Model*. Group comes with a title, description, created, creator and some glue pieces and utility methods built in for convenience. """ members = models.ManyToManyField(User, related_name = "projects", verbose_name=_("members") ) def get_absolute_url(self): return reverse("group_detail", kwargs={ "group_slug": self.slug }) def get_url_kwargs(self): return {"group_slug": self.slug} Adding in a a group-aware project ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order for a ``group-app`` to be able to display a ``group-aware`` application there needs to be a bridge. We take a sample projects.urls.py:: # Standard urlsconf import from django.conf.urls.defaults import * # Create your normal project url views urlpatterns = patterns("", url(r"^add_project$", "pinax.apps.projects.views.add", name="project_add"), url(r"^your_projects/$", "pinax.apps.projects.views.your_project", name="your_projects"), url(r"^(?P[-\w]+)$", "pinax.apps.projects.views.project_detail", name="project_detail"), url(r"^$", "pinax.apps.projects.views.projects", name="project_list"), ... ) So we can render a URL such as ``/projects/``. And then we add in the following code underneath the urlpatterns definition:: # Pinax groups special object from groups.bridge import ContentBridge # Fetch the group-aware model from pinax.apps.tasks.models import Task # Create the bridge object bridge = ContentBridge(Task, "tasks") # Add in the bridged url urlpatterns += bridge.include_urls("task.urls", r"^projects/(?P[-\w]+)/tasks") And that lets us render a URL such as ``/projects//tasks``. Appendix -------- Appendix A - Group Apps provided by Pinax ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pinax comes bundled with two types of group-apps: * tribes — used in social_project * projects — used in code_project Appendix B - Group Aware Apps provided by Pinax ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pinax includes several apps that are group aware: * photos * tasks * topics Appendix C - Group Apps available on PyPI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * pinax-dances (tutorial application) Appendix D - Group Aware Apps available on PyPI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * pinax-wall (tutorial application)