<commit_before>
===============
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<slug>[-\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)<commit_msg>blog -> task refactoring of docs/groups.txt<commit_after>
===============
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,
            "tasks": tasks,
            "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<slug>[-\w]+)/$", "pinax.apps.tasks.views.task_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("task_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)
{'fraction_non_alphanumeric': 0.08247697851327906, 'fraction_numerical': 0.0018684105164820499, 'mean_word_length': 2.4038155802861687, 'pattern_counts': {'":': 8, '<': 5, '<?xml version=': 0, '>': 6, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7100280', 'n_tokens_mistral': 3912, 'n_tokens_neox': 3816, 'n_words': 1569}