Customizing your organizations¶
The use cases from which django-organizations originated included more complex ways of determining access to the views as well as additional relationships to organizations. The application is extensible with these use cases in mind.
Custom organization models¶
Let’s say you had an Account model in your app, which defined a group account to which multiple users could belong, and also had its own logo, a foreign key to a subscription plan, a website URL, and some descriptive information. Also, this client organization is related to a service provider organization.:
class ServiceProvider(Organization): """Now this model has a name field and a slug field""" url = models.URLField() class Client(Organization): """Now this model has a name field and a slug field""" service_provider = models.ForeignKey(ServiceProvider, related_name="clients") subscription_plan = models.ForeignKey(SubscriptionPlan) subscription_start = models.DateField() url = models.URLField() description = models.TextField() objects = models.Manager()
Now the ServiceProvider and Client objects you create have the attributes of the Organization model class, including access to the OrganizationUser and OrganizationOwner models. This is an indirect relationship through a join in the database - this type of inheritance is multi-table inheritance so there will be a Client table and an Organization table; the latter is what the OrganizationUser and OrganizationOwner tables are still linked to.
Custom user model¶
By default django-organizations will map User objects from django’s contrib.auth application to an Organization. However you can change this by specifying a different model in your settings using the AUTH_USER_MODEL setting. This should include the appname and model name in a string like so:
AUTH_USER_MODEL = 'auth.User'
AUTH_USER_MODEL = 'myapp.MyUser'
If you choose a different user class make sure to pay attention to the API. If it differs from the auth.User API you will likely need to use an extended backend, if you are not already.
This is worth noting here because Django Organizations is compatible with different user models in Django 1.4, which precedes the official swappable users feature in Django 1.4.
Common use cases for extending the views include updating context variable names, adding project specific functionality, or updating access controls based on your project:
class ServiceProvidersOnly(LoginRequired, OrganizationMixin): def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs self.organization = self.get_organization() self.service_provider = self.organization.provider if not self.organization.is_admin(request.user) and not \ self.service_provider.is_member(request.user): raise PermissionDenied(_("Sorry, admins only")) return super(AdminRequiredMixin, self).dispatch(request, *args, **kwargs)
This mixin implements the same restriction as the AdminRequiredMixin mixin and adds an allowance for anyone who is a member of the service provider:
class AccountUpdateView(ServiceProviderOnly, BaseOrganizationUpdate): context_object_name = "account" template_name = "myapp/account_detail.html" def get_context_data(self, **kwargs): context = super(AccountUpdateView, self).get_context_data(**kwargs) context.update(provider=self.service_provider) return context
The ServiceProvidersOnly mixin inherits from the LoginRequired class which is a mixin for applying the login_required decorator. You can write your own (it’s fairly simple) or use the convenient mixins provided by django-braces.
It would also have been possible to define the ServiceProvidersOnly without inheriting from a base class, and then defining all of the mixins in the view class definition. This has the benefit of explicitness at the expense of verbosity:
class ServiceProvidersOnly(object): def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs self.organization = self.get_organization() self.service_provider = self.organization.provider if not self.organization.is_admin(request.user) and not \ self.service_provider.is_member(request.user): raise PermissionDenied(_("Sorry, admins only")) return super(AdminRequiredMixin, self).dispatch(request, *args, **kwargs) class AccountUpdateView(LoginRequired, OrganizationMixin, ServiceProviderOnly, BaseOrganizationUpdate): context_object_name = "account" template_name = "myapp/account_detail.html" def get_context_data(self, **kwargs): context = super(AccountUpdateView, self).get_context_data(**kwargs) context.update(provider=self.service_provider) return context
While this isn’t recommended in your own project, the mixins in django-organizations itself will err on the side of depending on composition rather than inheritance from other mixins. This may require defining a mixin in your own project that combines them for simplicity in your own views, but it reduces the inheritance chain potentially making functionality more difficult ot identify.
The view mixins expressly allow superusers to access organization resources. If this is undesired behavior you will need to use your own mixins.
User registration and invitations¶
User registration and invitation plays an important role in how you will actually use Django Organizations, but it is a relatively minor aspect of the app. The default backends for both registration and invitation try to provide as little functionality to accomplish the task for most scenarios. These can be extended and customized in your own project provided that you expose a few consistent interfaces.
Creating the backend¶
Here we’ll create a slightly modified invitation backend. The default backend presumes that your user model has a username attribute. If you’re simply using the email address as your user identifier with a custom user model, this field might be missing.
The default invite_by_email method - which is part of the exposed interface - sends an invitation to the user based on the email address, and creates an inactive user account if there is no matching user. It satisfies the auth.User username’s not null condition by filling the field with the contents of a freshly generated UUID.
In the example accounts app you would create a file named backends.py.:
from organizations.backends.defaults import InvitationBackend class CustomInvitations(InvitationBackend): def invite_by_email(self, email, sender=None, request=None, **kwargs): try: user = self.user_model.objects.get(email=email) except self.user_model.DoesNotExist: user = self.user_model.objects.create(email=email, password=self.user_model.objects.make_random_password()) user.is_active = False user.save() self.send_invitation(user, sender, **kwargs) return user
This removes the username from the create method.
Configuring the backend¶
In your settings file you will need to specify that your backend should be used:
INVITATION_BACKEND = 'accounts.backends.CustomInvitations'
Your URLs can be configured as normal:
from organizations.backends import invitation_backend urlpatterns = [ ... url(r'^invite/', include(invitation_backend().get_urls())), ]
The invitation_backend function simply returns the URL patterns from the get_urls method of the specified backend.