commit
687e3ace51
13 changed files with 562 additions and 0 deletions
0
symposion/teams/__init__.py
Normal file
0
symposion/teams/__init__.py
Normal file
17
symposion/teams/admin.py
Normal file
17
symposion/teams/admin.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.contrib import admin
|
||||
|
||||
import reversion
|
||||
|
||||
from symposion.teams.models import Team, Membership
|
||||
|
||||
admin.site.register(Team,
|
||||
prepopulated_fields={"slug": ("name",)},
|
||||
)
|
||||
|
||||
|
||||
class MembershipAdmin(reversion.VersionAdmin):
|
||||
list_display = ["team", "user", "state"]
|
||||
list_filter = ["team"]
|
||||
search_fields = ["user__username"]
|
||||
|
||||
admin.site.register(Membership, MembershipAdmin)
|
33
symposion/teams/backends.py
Normal file
33
symposion/teams/backends.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from django.db.models import Q
|
||||
|
||||
from .models import Team
|
||||
|
||||
|
||||
class TeamPermissionsBackend(object):
|
||||
|
||||
def authenticate(self, username=None, password=None):
|
||||
return None
|
||||
|
||||
def get_team_permissions(self, user_obj, obj=None):
|
||||
"""
|
||||
Returns a set of permission strings that this user has through his/her
|
||||
team memberships.
|
||||
"""
|
||||
if user_obj.is_anonymous() or obj is not None:
|
||||
return set()
|
||||
if not hasattr(user_obj, "_team_perm_cache"):
|
||||
memberships = Team.objects.filter(
|
||||
Q(memberships__user=user_obj),
|
||||
Q(memberships__state="manager") | Q(memberships__state="member"),
|
||||
)
|
||||
perms = memberships.values_list(
|
||||
"permissions__content_type__app_label",
|
||||
"permissions__codename"
|
||||
).order_by()
|
||||
user_obj._team_perm_cache = set(["%s.%s" % (ct, name) for ct, name in perms])
|
||||
return user_obj._team_perm_cache
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
if not user_obj.is_active:
|
||||
return False
|
||||
return perm in self.get_team_permissions(user_obj, obj)
|
50
symposion/teams/forms.py
Normal file
50
symposion/teams/forms.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
from django import forms
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from symposion.teams.models import Membership
|
||||
|
||||
|
||||
class TeamInvitationForm(forms.Form):
|
||||
|
||||
email = forms.EmailField(help_text="email address must be that of a user on the site")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.team = kwargs.pop("team")
|
||||
super(TeamInvitationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(TeamInvitationForm, self).clean()
|
||||
email = cleaned_data.get("email")
|
||||
|
||||
if email is None:
|
||||
raise forms.ValidationError("valid email address required")
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
# eventually we can invite them but for now assume they are
|
||||
# already on the site
|
||||
raise forms.ValidationError("no known user with email address %s" % email)
|
||||
|
||||
state = self.team.get_state_for_user(user)
|
||||
|
||||
if state in ["member", "manager"]:
|
||||
raise forms.ValidationError("user already in team")
|
||||
|
||||
if state in ["invited"]:
|
||||
raise forms.ValidationError("user already invited to team")
|
||||
|
||||
self.user = user
|
||||
self.state = state
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def invite(self):
|
||||
if self.state is None:
|
||||
Membership.objects.create(team=self.team, user=self.user, state="invited")
|
||||
elif self.state == "applied":
|
||||
# if they applied we shortcut invitation process
|
||||
membership = Membership.objects.get(team=self.team, user=self.user)
|
||||
membership.state = "member"
|
||||
membership.save()
|
66
symposion/teams/models.py
Normal file
66
symposion/teams/models.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
import datetime
|
||||
|
||||
from django.db import models
|
||||
|
||||
import reversion
|
||||
|
||||
from django.contrib.auth.models import Permission, User
|
||||
|
||||
|
||||
TEAM_ACCESS_CHOICES = [
|
||||
("open", "open"),
|
||||
("application", "by application"),
|
||||
("invitation", "by invitation")
|
||||
]
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
|
||||
slug = models.SlugField(unique=True)
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
access = models.CharField(max_length=20, choices=TEAM_ACCESS_CHOICES)
|
||||
permissions = models.ManyToManyField(Permission, blank=True)
|
||||
created = models.DateTimeField(default=datetime.datetime.now, editable=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_state_for_user(self, user):
|
||||
try:
|
||||
return self.memberships.get(user=user).state
|
||||
except Membership.DoesNotExist:
|
||||
return None
|
||||
|
||||
def applicants(self):
|
||||
return self.memberships.filter(state="applied")
|
||||
|
||||
def invitees(self):
|
||||
return self.memberships.filter(state="invited")
|
||||
|
||||
def members(self):
|
||||
return self.memberships.filter(state="member")
|
||||
|
||||
def managers(self):
|
||||
return self.memberships.filter(state="manager")
|
||||
|
||||
|
||||
MEMBERSHIP_STATE_CHOICES = [
|
||||
("applied", "applied"),
|
||||
("invited", "invited"),
|
||||
("declined", "declined"),
|
||||
("rejected", "rejected"),
|
||||
("member", "member"),
|
||||
("manager", "manager"),
|
||||
]
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
|
||||
user = models.ForeignKey(User, related_name="memberships")
|
||||
team = models.ForeignKey(Team, related_name="memberships")
|
||||
state = models.CharField(max_length=20, choices=MEMBERSHIP_STATE_CHOICES)
|
||||
message = models.TextField(blank=True)
|
||||
|
||||
|
||||
reversion.register(Membership)
|
0
symposion/teams/templatetags/__init__.py
Normal file
0
symposion/teams/templatetags/__init__.py
Normal file
39
symposion/teams/templatetags/teams_tags.py
Normal file
39
symposion/teams/templatetags/teams_tags.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from django import template
|
||||
|
||||
from symposion.teams.models import Team
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class AvailableTeamsNode(template.Node):
|
||||
|
||||
@classmethod
|
||||
def handle_token(cls, parser, token):
|
||||
bits = token.split_contents()
|
||||
if len(bits) == 3 and bits[1] == "as":
|
||||
return cls(bits[2])
|
||||
else:
|
||||
raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
|
||||
|
||||
def __init__(self, context_var):
|
||||
self.context_var = context_var
|
||||
|
||||
def render(self, context):
|
||||
request = context["request"]
|
||||
teams = []
|
||||
for team in Team.objects.all():
|
||||
state = team.get_state_for_user(request.user)
|
||||
if team.access == "open" and state is None:
|
||||
teams.append(team)
|
||||
elif request.user.is_staff and state is None:
|
||||
teams.append(team)
|
||||
context[self.context_var] = teams
|
||||
return u""
|
||||
|
||||
|
||||
@register.tag
|
||||
def available_teams(parser, token):
|
||||
"""
|
||||
{% available_teams as available_teams %}
|
||||
"""
|
||||
return AvailableTeamsNode.handle_token(parser, token)
|
16
symposion/teams/urls.py
Normal file
16
symposion/teams/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.conf.urls.defaults import *
|
||||
|
||||
|
||||
urlpatterns = patterns("symposion.teams.views",
|
||||
# team specific
|
||||
url(r"^(?P<slug>[\w\-]+)/$", "team_detail", name="team_detail"),
|
||||
url(r"^(?P<slug>[\w\-]+)/join/$", "team_join", name="team_join"),
|
||||
url(r"^(?P<slug>[\w\-]+)/leave/$", "team_leave", name="team_leave"),
|
||||
url(r"^(?P<slug>[\w\-]+)/apply/$", "team_apply", name="team_apply"),
|
||||
|
||||
# membership specific
|
||||
url(r"^promote/(?P<pk>\d+)/$", "team_promote", name="team_promote"),
|
||||
url(r"^demote/(?P<pk>\d+)/$", "team_demote", name="team_demote"),
|
||||
url(r"^accept/(?P<pk>\d+)/$", "team_accept", name="team_accept"),
|
||||
url(r"^reject/(?P<pk>\d+)/$", "team_reject", name="team_reject"),
|
||||
)
|
182
symposion/teams/views.py
Normal file
182
symposion/teams/views.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
from django.http import Http404
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
|
||||
from symposion.teams.forms import TeamInvitationForm
|
||||
from symposion.teams.models import Team, Membership
|
||||
|
||||
|
||||
## perm checks
|
||||
#
|
||||
# @@@ these can be moved
|
||||
|
||||
def can_join(team, user):
|
||||
state = team.get_state_for_user(user)
|
||||
if team.access == "open" and state is None:
|
||||
return True
|
||||
elif state == "invited":
|
||||
return True
|
||||
elif user.is_staff and state is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def can_leave(team, user):
|
||||
state = team.get_state_for_user(user)
|
||||
if state == "member": # managers can't leave at the moment
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def can_apply(team, user):
|
||||
state = team.get_state_for_user(user)
|
||||
if team.access == "application" and state is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def can_invite(team, user):
|
||||
state = team.get_state_for_user(user)
|
||||
if team.access == "invitation":
|
||||
if state == "manager" or user.is_staff:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
## views
|
||||
|
||||
|
||||
@login_required
|
||||
def team_detail(request, slug):
|
||||
team = get_object_or_404(Team, slug=slug)
|
||||
state = team.get_state_for_user(request.user)
|
||||
if team.access == "invitation" and state is None and not request.user.is_staff:
|
||||
raise Http404()
|
||||
|
||||
if can_invite(team, request.user):
|
||||
if request.method == "POST":
|
||||
form = TeamInvitationForm(request.POST, team=team)
|
||||
if form.is_valid():
|
||||
form.invite()
|
||||
messages.success(request, "Invitation created.")
|
||||
return redirect("team_detail", slug=slug)
|
||||
else:
|
||||
form = TeamInvitationForm(team=team)
|
||||
else:
|
||||
form = None
|
||||
|
||||
return render(request, "teams/team_detail.html", {
|
||||
"team": team,
|
||||
"state": state,
|
||||
"invite_form": form,
|
||||
"can_join": can_join(team, request.user),
|
||||
"can_leave": can_leave(team, request.user),
|
||||
"can_apply": can_apply(team, request.user),
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def team_join(request, slug):
|
||||
team = get_object_or_404(Team, slug=slug)
|
||||
state = team.get_state_for_user(request.user)
|
||||
if team.access == "invitation" and state is None and not request.user.is_staff:
|
||||
raise Http404()
|
||||
|
||||
if can_join(team, request.user) and request.method == "POST":
|
||||
membership, created = Membership.objects.get_or_create(team=team, user=request.user)
|
||||
membership.state = "member"
|
||||
membership.save()
|
||||
messages.success(request, "Joined team.")
|
||||
return redirect("team_detail", slug=slug)
|
||||
else:
|
||||
return redirect("team_detail", slug=slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def team_leave(request, slug):
|
||||
team = get_object_or_404(Team, slug=slug)
|
||||
state = team.get_state_for_user(request.user)
|
||||
if team.access == "invitation" and state is None and not request.user.is_staff:
|
||||
raise Http404()
|
||||
|
||||
if can_leave(team, request.user) and request.method == "POST":
|
||||
membership = Membership.objects.get(team=team, user=request.user)
|
||||
membership.delete()
|
||||
messages.success(request, "Left team.")
|
||||
return redirect("dashboard")
|
||||
else:
|
||||
return redirect("team_detail", slug=slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def team_apply(request, slug):
|
||||
team = get_object_or_404(Team, slug=slug)
|
||||
state = team.get_state_for_user(request.user)
|
||||
if team.access == "invitation" and state is None and not request.user.is_staff:
|
||||
raise Http404()
|
||||
|
||||
if can_apply(team, request.user) and request.method == "POST":
|
||||
membership, created = Membership.objects.get_or_create(team=team, user=request.user)
|
||||
membership.state = "applied"
|
||||
membership.save()
|
||||
messages.success(request, "Applied to join team.")
|
||||
return redirect("team_detail", slug=slug)
|
||||
else:
|
||||
return redirect("team_detail", slug=slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def team_promote(request, pk):
|
||||
if request.method == "POST":
|
||||
membership = get_object_or_404(Membership, pk=pk)
|
||||
state = membership.team.get_state_for_user(request.user)
|
||||
if request.user.is_staff or state == "manager":
|
||||
if membership.state == "member":
|
||||
membership.state = "manager"
|
||||
membership.save()
|
||||
messages.success(request, "Promoted to manager.")
|
||||
return redirect("team_detail", slug=membership.team.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def team_demote(request, pk):
|
||||
if request.method == "POST":
|
||||
membership = get_object_or_404(Membership, pk=pk)
|
||||
state = membership.team.get_state_for_user(request.user)
|
||||
if request.user.is_staff or state == "manager":
|
||||
if membership.state == "manager":
|
||||
membership.state = "member"
|
||||
membership.save()
|
||||
messages.success(request, "Demoted from manager.")
|
||||
return redirect("team_detail", slug=membership.team.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def team_accept(request, pk):
|
||||
if request.method == "POST":
|
||||
membership = get_object_or_404(Membership, pk=pk)
|
||||
state = membership.team.get_state_for_user(request.user)
|
||||
if request.user.is_staff or state == "manager":
|
||||
if membership.state == "applied":
|
||||
membership.state = "member"
|
||||
membership.save()
|
||||
messages.success(request, "Accepted application.")
|
||||
return redirect("team_detail", slug=membership.team.slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def team_reject(request, pk):
|
||||
if request.method == "POST":
|
||||
membership = get_object_or_404(Membership, pk=pk)
|
||||
state = membership.team.get_state_for_user(request.user)
|
||||
if request.user.is_staff or state == "manager":
|
||||
if membership.state == "applied":
|
||||
membership.state = "rejected"
|
||||
membership.save()
|
||||
messages.success(request, "Rejected application.")
|
||||
return redirect("team_detail", slug=membership.team.slug)
|
|
@ -167,6 +167,7 @@ INSTALLED_APPS = [
|
|||
"symposion.boxes",
|
||||
"symposion.proposals",
|
||||
"symposion.speakers",
|
||||
"symposion.teams",
|
||||
|
||||
# project
|
||||
"symposion_project.proposals",
|
||||
|
@ -193,6 +194,10 @@ ACCOUNT_LOGOUT_REDIRECT_URL = "home"
|
|||
ACCOUNT_USER_DISPLAY = lambda user: user.email
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
# Permissions Backends
|
||||
"symposion.teams.backends.TeamPermissionsBackend",
|
||||
|
||||
# Auth backends
|
||||
"account.auth_backends.EmailAuthenticationBackend",
|
||||
]
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
{% load i18n %}
|
||||
{% load proposal_tags %}
|
||||
{% load teams_tags %}
|
||||
|
||||
{% block head_title %}Dashboard{% endblock %}
|
||||
|
||||
|
@ -121,4 +122,53 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-panel">
|
||||
<div class="dashboard-panel-header">
|
||||
<i class="icon-group"></i>
|
||||
<h3>{% trans "Teams" %}</h3>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-panel-content">
|
||||
{% if user.memberships.exists %}
|
||||
<h4>Your Teams</h4>
|
||||
<table class="table table-striped">
|
||||
{% for membership in user.memberships.all %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url team_detail membership.team.slug %}">{{ membership.team.name }}</a>
|
||||
{% if membership.team.description %}<br>{{ membership.team.description }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label{% if membership.state == 'invited' %} label-info{% endif %}">{{ membership.get_state_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if membership.state == "manager" or user.is_staff %}
|
||||
{% if membership.team.applicants %}{{ membership.team.applicants.count }} applicant{{ membership.team.applicants.count|pluralize }}{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% available_teams as available_teams %}
|
||||
{% if available_teams %}
|
||||
<h4>Available Teams</h4>
|
||||
<table class="table table-striped">
|
||||
{% for team in available_teams %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url team_detail team.slug %}">{{ team }}</a>
|
||||
{% if team.description %}<br>{{ team.description }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">{{ team.get_access_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
103
symposion_project/templates/teams/team_detail.html
Normal file
103
symposion_project/templates/teams/team_detail.html
Normal file
|
@ -0,0 +1,103 @@
|
|||
{% extends "site_base.html" %}
|
||||
|
||||
{% load bootstrap_tags %}
|
||||
|
||||
{% block head_title %}{{ team.name }}{% endblock %}
|
||||
|
||||
{% block body_outer %}
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
<div class="pull-right">
|
||||
{% if can_join %}
|
||||
<form method="post" action="{% url team_join team.slug %}">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="btn btn-primary" value="join">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if can_leave %}
|
||||
<form method="post" action="{% url team_leave team.slug %}">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="btn" value="leave">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if can_apply %}
|
||||
<form method="post" action="{% url team_apply team.slug %}">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="btn btn-primary" value="apply">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h1>{{ team.name }}{% if state %} <span class="label">{{ state }}</span>{% endif %}</h1>
|
||||
|
||||
{% if team.description %}<p>{{ team.description }}</p>{% endif %}
|
||||
|
||||
{% if state == "invited" %}<p>You have been invited to join this team. Click <b>join</b> to the right to accept.</p>{% endif %}
|
||||
|
||||
{% if user.is_staff or state == "manager" %}
|
||||
{% if team.managers %}
|
||||
<h2>Managers</h2>
|
||||
<table class="table table-striped">
|
||||
{% for membership in team.managers %}
|
||||
<tr>
|
||||
<td>{{ membership.user.email }}{% if user == membership.user %} <span class="label label-info">you</span>{% endif %}</td>
|
||||
<td>
|
||||
<form style="margin: 0;" method="post" action="{% url team_demote membership.pk %}">{% csrf_token %}<button type="submit" class="btn btn-mini">demote</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if team.members %}
|
||||
<h2>Team Members</h2>
|
||||
<table class="table table-striped">
|
||||
{% for membership in team.members %}
|
||||
<tr>
|
||||
<td>{{ membership.user.email }}{% if user == membership.user %} <span class="label label-info">you</span>{% endif %}</td>
|
||||
<td>
|
||||
<form style="margin: 0;" method="post" action="{% url team_promote membership.pk %}">{% csrf_token %}<button type="submit" class="btn btn-mini">promote</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if team.applicants and team.access == "application" %}
|
||||
<h2>Applicants</h2>
|
||||
<table class="table table-striped">
|
||||
{% for membership in team.applicants %}
|
||||
<tr>
|
||||
<td>{{ membership.user.email }}</td>
|
||||
<td>
|
||||
<form style="margin: 0; float: left;" method="post" action="{% url team_accept membership.pk %}">{% csrf_token %}<button type="submit" class="btn btn-mini">accept</button></form>
|
||||
<form style="margin: 0; float: left;" method="post" action="{% url team_reject membership.pk %}">{% csrf_token %}<button type="submit" class="btn btn-mini">reject</button></form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if team.invitees %}
|
||||
<h2>Invitees</h2>
|
||||
<table class="table table-striped">
|
||||
{% for membership in team.invitees %}
|
||||
<tr>
|
||||
<td>{{ membership.user.email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if invite_form %}
|
||||
<form method="POST" action="" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<legend>Invite User to Team</legend>
|
||||
{{ invite_form|as_bootstrap }}
|
||||
<div class="form-actions">
|
||||
<input class="btn btn-primary" type="submit" value="Invite" />
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -28,6 +28,7 @@ urlpatterns = patterns("",
|
|||
url(r"^proposals/", include("symposion.proposals.urls")),
|
||||
url(r"^sponsors/", include("symposion.sponsorship.urls")),
|
||||
url(r"^boxes/", include("symposion.boxes.urls")),
|
||||
url(r"^teams/", include("symposion.teams.urls")),
|
||||
url(r"^markitup/", include("markitup.urls")),
|
||||
|
||||
url(r"^", include("symposion.cms.urls")),
|
||||
|
|
Loading…
Reference in a new issue