Using Teams
Teams are designed to provide sandboxes for groups of users collaborating on single project. Users can join one or more teams, invite other users to their teams, and give different team members different roles.
Pegasus provides the building blocks to setup a team-based application. Some of those building blocks are documented here.
Note: all of the following examples assume you have setup Pegasus with teams enabled.
Example App
Section titled “Example App”As of version 0.17, Pegasus ships with a built-in example application demonstrating the basics of working with team-based models and views.
The example app includes:
- A data model that belongs to a team.
- A set of class based views for working with that data model, limited to the context of a team.
Third party examples
Section titled “Third party examples”A Pegasus user Peter Cherna has created some more example applications that demonstrate additional team-based examples, including functional views, pagination, APIs and working with “global” objects.
They are a great place to start for inspiration and getting something up and running quickly!
Note: the example apps are not officially sanctioned/supported by Pegasus---though features from them will be continually incorporated into future releases.
Data Models
Section titled “Data Models”Teams use three primary models - apps.users.CustomUser, apps.teams.Team, and apps.teams.Membership.
The Membership model uses Django’s “through” support
to extend the User/Team relationship with additional fields.
By default, a role field is added to represent the User’s role in the Team (admin or member).
Team-based models
Section titled “Team-based models”Data models that “belong” to a Team can subclass BaseTeamModel.
See the example app for usage.
Team-based Views
Section titled “Team-based Views”At its core, all Team-based views need the following:
See apps.team.urls for an example of how to set these up in your apps, and
your main apps.{project}.urls file for how to add them to your site’s URLs.
Anything that goes into team_urlpatterns in apps.{project}.urls will automatically be added under the
URL https://example.com/a/<team_slug>/. The team_slug is a human-readable, URL-friendly version
of the team name that is auto-generated for you.
Middleware
Section titled “Middleware”The apps.teams.middleware.TeamsMiddleware must be included in the list of middleware. It must be placed
after django.contrib.auth.middleware.AuthenticationMiddleware. The purpose of this middleware is to
set request.team and request.team_membership based on the current request. It will attempt to load
the team as follows:
- From the
team_slugin the request path if available - From the current session if available
- From the user’s list of teams if available
If the team_slug is available from the request path but it does not match a team that the user has access to
then the request will terminate with a 404. Apart from this the middleware does not do any validation of the
team or the team membership. That is left to the decorators described below.
See apps.team.views for example team views.
All views that are referenced under team_urlpatterns must contain team_slug as the first argument.
In addition to adding this field, you will likely want to use one of the built-in permission decorators (see below) to ensure the logged-in user can access the selected team.
Additionally, you will have to scope any data model access to the relevant Team in any Database/ORM queries you make inside your views.
Permission Control
Section titled “Permission Control”Pegasus includes two convenience decorators for use in team views.
These can be found in apps.teams.decorators.
The login_and_team_required decorator
Section titled “The login_and_team_required decorator”This decorator can be used to ensure that the logged in user has access to the team in the view.
It requires your view takes in a team_slug, as in the example views.
It can be used in functional views like this:
@login_and_team_requireddef a_team_view(request, team_slug): # other view logic here return render(request, 'web/my_template.html', context={ 'team': request.team, })Or in class-based views like this:
@method_decorator(login_and_team_required, name='dispatch')class ATeamView(View): # other view details go hereIf the current user does not have access to the team they will see a 404 page.
If no user is logged in they’ll be redirected to a login view, just like the login_required decorator.
The team_admin_required decorator
Section titled “The team_admin_required decorator”The team_admin_required decorator works just like the login_and_team_required decorator, except
in addition to checking team membership the role is also checked and if the user doesn’t have
“admin” access they will not be able to access the view.
The LoginAndTeamRequiredMixin and TeamAdminRequiredMixin classes
Section titled “The LoginAndTeamRequiredMixin and TeamAdminRequiredMixin classes”These mixins provide the same functionality as the decorators, but are designed to work with Django’s generic class-based views. They can be used like this:
class ATeamModelListView(LoginAndTeamRequiredMixin, ListView): model = MyModelSee the example app for more details.
Template tags
Section titled “Template tags”In addition to the decorators, you can also use template tags to check user / team access from a template.
This can be useful for hiding/showing certain content based on a user’s team role.
The is_member_of filter can be used to check team membership, and the is_admin_of filter can be used
to check if a user is a team admin. For example, the following will show only if the logged in user
is an admin of the associated team:
{% load team_tags %}{% if team and request.user|is_admin_of:team %} <p>You're an admin of {{team.name}}.</p>{% elif team and request.user|is_member_of:team %} <p>You're a member of {{team.name}}.</p>{% else %} <p>Sorry you don't have access to {{team.name}}.</p>{% endif %}Adding Roles
Section titled “Adding Roles”The permission system is designed to be simple enough to easily use, but extensible enough that you can customize it to match your project’s needs.
Here’s how you can add a new role to your app:
1. Define the New Role in roles.py
Section titled “1. Define the New Role in roles.py”First, you need to add your new role constant and update the choices in apps/teams/roles.py:
ROLE_ADMIN = 'admin'ROLE_MEMBER = 'member'ROLE_MODERATOR = 'moderator' # Add your new role here
ROLE_CHOICES = ( # customize roles here (ROLE_ADMIN, 'Administrator'), (ROLE_MEMBER, 'Member'), (ROLE_MODERATOR, 'Moderator'), # Add your new role choice here)Technically, this is all that’s needed, as this will cause the role to show up in the invitation UI and allow it to be used in team memberships.
However, you’ll probably also want to use the role in your app permissions system. To do that, you should also add a helper function for the new role if you want to use it in permission checks:
def is_moderator(user: CustomUser, team) -> bool: if not team: return False
from .models import Membership return Membership.objects.filter(team=team, user=user, role=ROLE_MODERATOR).exists()2. Update the Membership Model (if needed)
Section titled “2. Update the Membership Model (if needed)”The Membership model in models.py already uses roles.ROLE_CHOICES for its role field,
so it will automatically pick up your new role.
However, you might want to add a helper method to the Membership model:
class Membership(BaseModel): # ... existing code ...
def is_moderator(self) -> bool: return self.role == roles.ROLE_MODERATOR3. Add a new decorator (if needed)
Section titled “3. Add a new decorator (if needed)”If you’d like to use the role in decorators, similar to @team_admin_required you can do so by adding
a new function to apps/teams/decorators.py:
# import the and use function you created in step 1from .roles import is_admin, is_member, is_moderator
def team_moderator_required(view_func): return _get_decorated_function(view_func, is_moderator)4. Use the role
Section titled “4. Use the role”You’ll need to update any views or logic that handle role-based permissions, by calling the helper functions and decorators you’ve defined above.
The specifics here will depend on the role you’ve added and the goals you’re trying to achieve with it.
Cookbooks
Section titled “Cookbooks”Partially using teams
Section titled “Partially using teams”Many projects might want to use teams in the background but hide them from users. This can be useful in certain scenarios:
- If you know you want to use teams eventually, but haven’t built out support for them yet.
- If your application has different user types, some of which belong to teams and some of which don’t.
The recommended way to handle this situation is to enable teams in Pegasus, but hide/restrict them in the UI. In this world, all users will still belong to a default team (which is created for them automatically), and all models are associated with teams. However, the concept of teams will be hidden from users until you decide to make them visible.
This allows you to easily “turn on” teams when you are ready, or migrate a user from a “non-team” to a “team” account, while being able to use the same underlying business logic and not have to deal with complicated data migrations.
To achieve this, you should do something like the following:
- Build your application with teams enabled. This will provide the data models and URL scaffolding to work with teams, and make using teams later much easier.
- Hide the
team_namefield from the signup form (signup.html), and let teams be auto-created for new users. - Hide the team-related items from the application navigation (entry point is
app_nav.html). - (Optional) Hide/remove out the team-based url mappings (entry point is
teams/urls.py). Do this if you don’t want people to be able to access team-related functionality even if they navigate to the right URL in the browser. - Continue building your application using the team-based models and URL patterns, but don’t expose them to the user.
If you follow these steps it should be relatively easy to expose teams down the line instead of having to deal with a complicated migration. That process will mostly involve un-hidng the team functionality that was hidden.