Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pinax/teams/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ def members_count(obj):
"description",
"member_access",
"manager_access",
"creator"
"creator",
"parent",
],
prepopulated_fields={"slug": ("name",)},
raw_id_fields=["creator"]
raw_id_fields=["creator", "parent"]
)


Expand Down
1 change: 1 addition & 0 deletions pinax/teams/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"on-team-blacklist": "You can not create a team by this name",
"user-member-exists": "User already on team.",
"invitee-member-exists": "Invite already sent.",
"self-referencing-parent": "A team cannot be a parent of itself.",
}


Expand Down
4 changes: 2 additions & 2 deletions pinax/teams/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class Migration(migrations.Migration):

dependencies = [
('invitations', '0001_initial'),
('pinax_invitations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

Expand All @@ -22,7 +22,7 @@ class Migration(migrations.Migration):
('state', models.CharField(max_length=20, verbose_name='state', choices=[(b'applied', 'applied'), (b'invited', 'invited'), (b'declined', 'declined'), (b'rejected', 'rejected'), (b'accepted', 'accepted'), (b'auto-joined', 'auto joined')])),
('role', models.CharField(default=b'member', max_length=20, verbose_name='role', choices=[(b'member', 'member'), (b'manager', 'manager'), (b'owner', 'owner')])),
('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')),
('invite', models.ForeignKey(related_name=b'memberships', verbose_name='invite', blank=True, to='invitations.JoinInvitation', null=True)),
('invite', models.ForeignKey(related_name=b'memberships', verbose_name='invite', blank=True, to='pinax_invitations.JoinInvitation', null=True)),
],
options={
'verbose_name': 'Team',
Expand Down
4 changes: 2 additions & 2 deletions pinax/teams/migrations/0002_add_simple_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class Migration(migrations.Migration):

dependencies = [
('invitations', '0001_initial'),
('pinax_invitations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('pinax_teams', '0001_initial'),
]
Expand All @@ -24,7 +24,7 @@ class Migration(migrations.Migration):
('state', models.CharField(choices=[(b'applied', 'applied'), (b'invited', 'invited'), (b'declined', 'declined'), (b'rejected', 'rejected'), (b'accepted', 'accepted'), (b'auto-joined', 'auto joined')], max_length=20, verbose_name='state')),
('role', models.CharField(choices=[(b'member', 'member'), (b'manager', 'manager'), (b'owner', 'owner')], default=b'member', max_length=20, verbose_name='role')),
('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')),
('invite', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='simple_memberships', to='invitations.JoinInvitation', verbose_name='invite')),
('invite', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='simple_memberships', to='pinax_invitations.JoinInvitation', verbose_name='invite')),
],
options={
'verbose_name': 'Simple Membership',
Expand Down
26 changes: 26 additions & 0 deletions pinax/teams/migrations/0005_add_team_parent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-17 21:34
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('pinax_teams', '0004_auto_20170511_0856'),
]

operations = [
migrations.AddField(
model_name='simpleteam',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pinax_teams.SimpleTeam'),
),
migrations.AddField(
model_name='team',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pinax_teams.Team'),
),
]
84 changes: 77 additions & 7 deletions pinax/teams/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
Expand All @@ -18,14 +19,23 @@
from .hooks import hookset


MESSAGE_STRINGS = hookset.get_message_strings()


def avatar_upload(instance, filename):
ext = filename.split(".")[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
return os.path.join("avatars", filename)


def create_slug(name):
return slugify(name)[:50]
def create_slug(name, parent=None):
slug = slugify(name)
if parent:
slug = "{parent_pk}-{slug}".format(
parent_pk=parent.pk,
slug=slug,
)
return slug[:50]


class BaseTeam(models.Model):
Expand All @@ -50,12 +60,17 @@ class BaseTeam(models.Model):

member_access = models.CharField(max_length=20, choices=MEMBER_ACCESS_CHOICES, verbose_name=_("member access"))
manager_access = models.CharField(max_length=20, choices=MANAGER_ACCESS_CHOICES, verbose_name=_("manager access"))
parent = models.ForeignKey("self", blank=True, null=True, related_name="children")

class Meta:
abstract = True
verbose_name = _("Base")
verbose_name_plural = _("Bases")

def clean(self):
if self.pk and self.pk == self.parent_id:
raise ValidationError({"parent": MESSAGE_STRINGS["self-referencing-parent"]})

def can_join(self, user):
state = self.state_for(user)
if self.member_access == BaseTeam.MEMBER_ACCESS_OPEN and state is None:
Expand All @@ -74,6 +89,44 @@ def can_apply(self, user):
state = self.state_for(user)
return self.member_access == BaseTeam.MEMBER_ACCESS_APPLICATION and state is None

def get_root_team(self):
"""
Returns the top-most parent for a team
"""
team = self
while getattr(team, "parent"):
team = team.parent
return team

@property
def ancestors(self):
"""
Returns the parent(s) of a team
"""
team = self
chain = []
while getattr(team, "parent"):
chain.append(team)
team = team.parent
chain.append(team)
# first in, last out
chain.reverse()
return chain

@property
def descendants(self):
"""
Return descendants of a team
"""
_descendants = []
for child in self.children.all():
_descendants.extend(child.descendants)
return _descendants

@property
def full_name(self):
return " : ".join([a.name for a in self.ancestors])

@property
def applicants(self):
return self.memberships.filter(state=BaseMembership.STATE_APPLIED)
Expand Down Expand Up @@ -173,10 +226,27 @@ def invite_user(self, from_user, to_email, role, message=None):
return membership

def for_user(self, user):
try:
return self.memberships.get(user=user)
except ObjectDoesNotExist:
pass
"""
Return the first membership found for the current team and user
or for any of the team's parents and the user

@@@ we may decide to explicitly add membership for "children" if a
user is a manager or member of a parent org
"""
attr = "_membership_for_user"

if hasattr(self, attr) is False:
team = self
membership = None
while team:
try:
membership = team.memberships.get(user=user)
break
except ObjectDoesNotExist:
team = team.parent
# @@@ care about the type of membership if retrieved from a parent
setattr(self, attr, membership)
return getattr(self, attr)

def state_for(self, user):
membership = self.for_user(user=user)
Expand Down Expand Up @@ -221,7 +291,7 @@ def __str__(self):

def save(self, *args, **kwargs):
if not self.id:
self.slug = create_slug(self.name)
self.slug = create_slug(self.name, self.parent)
self.full_clean()
super(Team, self).save(*args, **kwargs)

Expand Down
55 changes: 54 additions & 1 deletion pinax/teams/templatetags/pinax_teams_tags.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django import template

from ..models import Team
from ..models import Team, Membership


register = template.Library()
Expand Down Expand Up @@ -38,3 +38,56 @@ def available_teams(parser, token):
{% available_teams as available_teams %}
"""
return AvailableTeamsNode.handle_token(parser, token)


@register.assignment_tag(takes_context=True)
def ancestors_for(context, team=None):
"""
Retrives the ancestors for a given team and indicates
if the user can manage each ancestor
"""
if team is None:
team = context["team"]

ancestors = []
for ancestor in team.ancestors:
ancestors.append({
"team": ancestor,
"can_manage": is_managed_by(ancestor, context["user"])
})
context["ancestors"] = ancestors
return ancestors


@register.assignment_tag(takes_context=True)
def children_for(context, team=None):
"""
Retrieves the children of a given team and indicates
if the user can manage each child
"""
if team is None:
team = context["team"]

children = []
for child in team.children.order_by("slug"):
children.append({
"team": child,
"can_manage": is_managed_by(child, context["user"])
})
return children


# @@@ document template
@register.inclusion_tag("pinax/teams/_breadcrumbs.html", takes_context=True)
def get_team_breadcrumbs(context):
context["ancestors"] = ancestors_for(context)
return context


def is_managed_by(team, user):
return team.role_for(user) in [Membership.ROLE_MANAGER, Membership.ROLE_OWNER]


@register.filter(name="is_managed_by")
def is_managed_by_as_filter(team, user):
return is_managed_by(team, user)
Loading