Add ability to invite teams to an org, or request to have your team added to an org

This commit is contained in:
Michael Hall 2018-08-06 11:08:51 -04:00
parent a304da76be
commit 75feaa649a
18 changed files with 450 additions and 7 deletions

View file

@ -6,6 +6,7 @@ from .models.locale import Language, Continent, Country, SPR, City
from .models.profiles import (
UserProfile,
Organization,
OrgTeamRequest,
Team,
Member,
Category,
@ -56,6 +57,11 @@ class OrgAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'site')
admin.site.register(Organization, OrgAdmin)
class OrgRequestAdmin(admin.ModelAdmin):
list_display = ('organization', 'team', 'request_origin', 'requested_by', 'requested_date', 'accepted_by', 'joined_date')
list_filter = ('organization', 'request_origin')
admin.site.register(OrgTeamRequest, OrgRequestAdmin)
class SponsorAdmin(admin.ModelAdmin):
list_display = ('name', 'web_url')
admin.site.register(Sponsor, SponsorAdmin)

View file

@ -7,7 +7,7 @@ from django.utils import timezone
from django.contrib.auth.models import User
from .models.locale import Country, SPR, City
from .models.profiles import Team, UserProfile, Sponsor
from .models.profiles import Team, UserProfile, Sponsor, OrgTeamRequest
from .models.events import (
Event,
EventComment,
@ -381,6 +381,26 @@ class OrganizationForm(forms.ModelForm):
'cover_img',
]
class RequestToJoinOrgForm(forms.ModelForm):
class Meta:
model = OrgTeamRequest
fields = [
'team'
]
class AcceptRequestToJoinOrgForm(forms.Form):
confirm = forms.BooleanField(label="Yes, add this team to my organization", required=True)
class InviteToJoinOrgForm(forms.ModelForm):
class Meta:
model = OrgTeamRequest
fields = [
'organization'
]
class AcceptInviteToJoinOrgForm(forms.Form):
confirm = forms.BooleanField(label="Yes, add my team to this organization", required=True)
class NewCommonEventForm(forms.ModelForm):
class Meta:
model = CommonEvent

View file

@ -0,0 +1,49 @@
# Generated by Django 2.0 on 2018-08-05 17:58
import datetime
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('events', '0039_add_profile_privacy_option'),
]
operations = [
migrations.CreateModel(
name='OrgTeamRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('request_origin', models.SmallIntegerField(choices=[(0, 'Organization'), (1, 'Team')], db_index=True, default=0, verbose_name='Request from')),
('request_key', models.UUIDField(default=uuid.uuid4)),
('requested_date', models.DateTimeField(default=datetime.datetime.now)),
('joined_date', models.DateTimeField(blank=True, null=True)),
('accepted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accepted_org_memberships', to='events.UserProfile')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Organization')),
('requested_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='requested_org_memberships', to='events.UserProfile')),
],
),
migrations.AlterField(
model_name='event',
name='sponsors',
field=models.ManyToManyField(blank=True, related_name='events', to='events.Sponsor'),
),
migrations.AlterField(
model_name='team',
name='premium_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='premium_teams', to='events.UserProfile'),
),
migrations.AlterField(
model_name='team',
name='sponsors',
field=models.ManyToManyField(blank=True, related_name='teams', to='events.Sponsor'),
),
migrations.AddField(
model_name='orgteamrequest',
name='team',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Team'),
),
]

View file

@ -83,13 +83,22 @@ class UserProfile(models.Model):
local = self.timezone.localize(dt)
return local.astimezone(pytz.utc)
@property
def is_a_team_admin(self):
return Member.objects.filter(user=self, role=Member.ADMIN).count() > 0
@property
def administering(self):
return [member.team for member in Member.objects.filter(user=self, role=Member.ADMIN)]
return [member.team for member in Member.objects.filter(user=self, role=Member.ADMIN).order_by('team__name')]
@property
def is_a_team_moderator(self):
return Member.objects.filter(user=self, role__in=(Member.ADMIN, Member.MODERATOR)).count() > 0
@property
def moderating(self):
return [member.team for member in Member.objects.filter(user=self, role__in=(Member.ADMIN, Member.MODERATOR))]
return [member.team for member in Member.objects.filter(user=self, role__in=(Member.ADMIN, Member.MODERATOR)).order_by('team__name')]
def can_create_event(self, team):
try:
@ -241,6 +250,32 @@ class Organization(models.Model):
def __str__(self):
return u'%s' % (self.name)
class OrgTeamRequest(models.Model):
ORG=0
TEAM=1
ORIGINS = [
(ORG, _("Organization")),
(TEAM, _("Team")),
]
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
team = models.ForeignKey('Team', on_delete=models.CASCADE)
request_origin = models.SmallIntegerField(_("Request from"), choices=ORIGINS, default=ORG, db_index=True)
request_key = models.UUIDField(default=uuid.uuid4, editable=True)
requested_by = models.ForeignKey(UserProfile, related_name='requested_org_memberships', on_delete=models.SET_NULL, null=True, blank=False)
requested_date = models.DateTimeField(default=datetime.datetime.now)
accepted_by = models.ForeignKey(UserProfile, related_name='accepted_org_memberships', on_delete=models.SET_NULL, null=True, blank=True)
joined_date = models.DateTimeField(null=True, blank=True)
@property
def origin_name(self):
return OrgTeamRequest.ORIGINS[self.request_origin][1]
def __str__(self):
return '%s in %s' % (self.team, self.organization)
class Sponsor(models.Model):
name = models.CharField(_("Sponsor Name"), max_length=256, null=False, blank=False)
description = models.TextField(blank=True, null=True)

View file

@ -0,0 +1,3 @@
<table>
{{ invite_form }}
</table>

View file

@ -0,0 +1,3 @@
<table>
{{ request_form }}
</table>

View file

@ -0,0 +1,12 @@
{% extends "get_together/emails/base.html" %}
{% block content %}
<h3>You've been invited to join {{org.name|striptags}}</h3>
<p>{{ sender }} has invited your team, <b>{{team.name}}</b>, to join their Get Together organization: <b>{{org.name}}</b>.
</p>
<br>
<a href="https://{{site.domain}}{% url 'confirm-request-to-join-org' req.request_key %}" title="{{ org.name|striptags }} invitation.">Click here to view and confirm this invitation.</a>
</p>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends 'get_together/emails/base.txt' %}
{% block content %}
== You've been invited to join {{org.name}} ==
{{ sender }} has invited your team, {{team.name}}, to join their Get Together organization: {{org.name}}.
Click here to view and confirm this invitation: https://{{site.domain}}{% url 'confirm-request-to-join-org' req.request_key %}
{% endblock %}

View file

@ -0,0 +1,12 @@
{% extends "get_together/emails/base.html" %}
{% block content %}
<h3>{{team.name|striptags}} has requested to join your organization.</h3>
<p>{{ sender }} has requested to have their team, <b>{{team.name}}</b>, added to your Get Together organization: <b>{{org.name}}</b>.
</p>
<br>
<a href="https://{{site.domain}}{% url 'confirm-request-to-join-org' req.request_key %}" title="{{ team.name|striptags }} request.">Click here to view and confirm this request.</a>
</p>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends 'get_together/emails/base.txt' %}
{% block content %}
== {{team.name}} has requested to join your organization. ==
{{ sender }} has requested to have their team, {{team.name}}, added to your Get Together organization: {{org.name}}.
Click here to view and confirm this request: https://{{site.domain}}{% url 'confirm-request-to-join-org' req.request_key %}
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "get_together/base.html" %}
{% load static %}
{% block content %}
<h2><b>{{team.name}}</b> has been invited to join <b>{{org.name}}</b></h2>
<p>Would you like to join this organization?</p>
<form action="{% url "confirm-request-to-join-org" invite.request_key %}" method="post">
{% csrf_token %}
{% include "events/org_invite_form.html" %}
<br />
<button type="submit" class="btn btn-primary">Accept</button>
<a class="btn btn-secondary" href="{% url 'show-team' team.id %}">Decline</a>
</form>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "get_together/base.html" %}
{% load static %}
{% block content %}
<h2><b>{{team.name}}</b> has asked to join <b>{{org.name}}</b></h2>
<p>Would you like to add this team?</p>
<form action="{% url "confirm-request-to-join-org" invite.request_key %}" method="post">
{% csrf_token %}
{% include "events/org_request_form.html" %}
<br />
<button type="submit" class="btn btn-primary">Accept</button>
<a class="btn btn-secondary" href="{% url 'show-org' org.slug %}">Decline</a>
</form>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "get_together/base.html" %}
{% load static %}
{% block content %}
<h2>Invite: {{team.name}}</h2>
<p>Select which of your organizations you would like to invite this team to.</p>
<form action="{% url "invite-to-join-org" team.id %}" enctype="multipart/form-data" method="post">
{% csrf_token %}
{% include "events/org_invite_form.html" %}
<br />
<button type="submit" class="btn btn-primary">Invite</button>
</form>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "get_together/base.html" %}
{% load static %}
{% block content %}
<h2>Request to join: {{org.name}}</h2>
<p>Select which of your teams you would like to add to this organization.</p>
<form action="{% url "request-to-join-org" org.slug %}" enctype="multipart/form-data" method="post">
{% csrf_token %}
{% include "events/org_request_form.html" %}
<br />
<button type="submit" class="btn btn-primary">Request</button>
</form>
{% endblock %}

View file

@ -23,7 +23,7 @@
<div class="col-sm-9">
{% if can_edit_org %}
<div id="admin_buttons" class="mb-2">
<a href="{% url 'edit-org' org.slug %}" class="btn btn-secondary btn-sm"><i class="fa fa-pencil"></i> Edit Org</a>
<a href="{% url 'edit-org' org.slug %}" class="btn btn-secondary btn-sm"><i class="fa fa-pencil"></i> Edit Org</a>
<a href="#" class="btn btn-secondary btn-sm disabled"><i class="fa fa-users"></i> Manage Members</a>
</div>
{% endif %}
@ -35,6 +35,11 @@
{% else %}
<h2>Welcome to {{ org.name }}</h2>
{% endif %}
<div id="action_buttons" class="mb-2">
{% if request.user.profile.is_a_team_admin %}
<a href="{% url 'request-to-join-org' org.slug %}" class="btn btn-secondary btn-sm"><i class="fa fa-globe"></i> Join Organization</a>
{% endif %}
</div>
{% if org.description %}
<div class="container container-secondary mb-3">

View file

@ -23,8 +23,11 @@
<div class="col-md-9">
{% if can_edit_team %}
<div id="admin_buttons" class="mb-2">
<a href="{% url 'edit-team' team.id %}" class="btn btn-secondary btn-sm"><i class="fa fa-pencil"></i> Edit Team</a>
<a href="{% url 'edit-team' team.id %}" class="btn btn-secondary btn-sm"><i class="fa fa-pencil"></i> Edit Team</a>
<a href="{% url 'manage-members' team.id %}" class="btn btn-secondary btn-sm"><i class="fa fa-users"></i> Manage Members</a>
{% if request.user.profile.owned_orgs.count > 0 %}
<a href="{% url 'invite-to-join-org' team.id %}" class="btn btn-secondary btn-sm"><i class="fa fa-globe"></i> Add to Organization</a>
{% endif %}
</div>
{% endif %}
{% if team.banner_img %}

View file

@ -101,6 +101,9 @@ urlpatterns = [
path('org/<str:org_slug>/', views.show_org, name='show-org'),
path('org/<str:org_slug>/+edit/', views.edit_org, name='edit-org'),
path('team/<int:team_id>/+invite_to_join_org/', views.invite_to_join_org, name='invite-to-join-org'),
path('org/<str:org_slug>/+request_to_join_org/', views.request_to_join_org, name='request-to-join-org'),
path('org/+confirm_request/<str:request_key>/', views.confirm_request_to_join_org, name='confirm-request-to-join-org'),
path('org/<str:org_slug>/+create-event/', views.create_common_event, name='create-common-event'),
path('common/<int:event_id>/+create-event/', views.create_common_event_team_select, name='create-common-event-team-select'),
path('common/<int:event_id>/<str:event_slug>/', views.show_common_event, name='show-common-event'),

View file

@ -11,9 +11,9 @@ from django.template.loader import get_template, render_to_string
from django.conf import settings
from events.models.profiles import Organization, Team, UserProfile, Member
from events.models.profiles import Organization, Team, UserProfile, Member, OrgTeamRequest
from events.models.events import Event, CommonEvent, Place, Attendee
from events.forms import OrganizationForm, NewCommonEventForm
from events.forms import OrganizationForm, NewCommonEventForm, RequestToJoinOrgForm, InviteToJoinOrgForm, AcceptRequestToJoinOrgForm, AcceptInviteToJoinOrgForm
from events import location
from events.utils import slugify
@ -69,6 +69,222 @@ def edit_org(request, org_slug):
return redirect('home')
@login_required
def request_to_join_org(request, org_slug):
org = get_object_or_404(Organization, slug=org_slug)
if not len(request.user.profile.administering) > 0:
messages.add_message(request, messages.WARNING, message=_('You are not the administrator for any teams.'))
return redirect('show-org', org_slug=org.slug)
req = OrgTeamRequest(organization=org, request_origin=OrgTeamRequest.TEAM, requested_by=request.user.profile)
if request.method == 'GET':
form = RequestToJoinOrgForm(instance=req)
form.fields['team'].queryset = Team.objects.filter(member__user=request.user.profile, member__role=Member.ADMIN).order_by('name')
context = {
'org': org,
'request_form': form,
}
return render(request, 'get_together/orgs/request_to_join.html', context)
elif request.method == 'POST':
form = RequestToJoinOrgForm(request.POST, instance=req)
form.fields['team'].queryset = Team.objects.filter(member__user=request.user.profile, member__role=Member.ADMIN).order_by('name')
if form.is_valid():
req = form.save()
send_org_request(req)
messages.add_message(request, messages.SUCCESS, message=_('Your request has been send to the organization administrators.'))
return redirect('show-org', org_slug=org.slug)
else:
context = {
'org': org,
'request_form': form,
}
return render(request, 'get_together/orgs/request_to_join.html', context)
else:
return redirect('home')
def send_org_request(req):
context = {
'sender': req.requested_by,
'req': req,
'org': req.organization,
'team': req.team,
'site': Site.objects.get(id=1),
}
email_subject = 'Request to join: %s' % req.team.name
email_body_text = render_to_string('get_together/emails/orgs/request_to_org.txt', context)
email_body_html = render_to_string('get_together/emails/orgs/request_to_org.html', context)
email_from = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@gettogether.community')
admin = req.organization.owner_profile
success = send_mail(
from_email=email_from,
html_message=email_body_html,
message=email_body_text,
recipient_list=[admin.user.email],
subject=email_subject,
fail_silently=True,
)
EmailRecord.objects.create(
sender=req.requested_by.user,
recipient=admin.user,
email=admin.user.email,
subject=email_subject,
body=email_body_text,
ok=success
)
@login_required
def invite_to_join_org(request, team_id):
team = get_object_or_404(Team, id=team_id)
if not request.user.profile.owned_orgs.count() > 0:
messages.add_message(request, messages.WARNING, message=_('You are not the administrator for any organizations.'))
return redirect('show-team', team_id=team_id)
invite = OrgTeamRequest(team=team, request_origin=OrgTeamRequest.ORG, requested_by=request.user.profile)
if request.method == 'GET':
form = InviteToJoinOrgForm(instance=invite)
form.fields['organization'].queryset = Organization.objects.filter(owner_profile=request.user.profile).order_by('name')
context = {
'team': team,
'invite_form': form,
}
return render(request, 'get_together/orgs/invite_to_join.html', context)
elif request.method == 'POST':
form = InviteToJoinOrgForm(request.POST, instance=invite)
if form.is_valid():
invite = form.save()
send_org_invite(invite)
messages.add_message(request, messages.SUCCESS, message=_('Your request has been send to the team administrators.'))
return redirect('show-team', team_id=team_id)
else:
context = {
'team': team,
'invite_form': form,
}
return render(request, 'get_together/orgs/invite_to_join.html', context)
else:
return redirect('home')
def send_org_invite(req):
context = {
'sender': req.requested_by,
'req': req,
'org': req.organization,
'team': req.team,
'site': Site.objects.get(id=1),
}
email_subject = 'Invitation to join: %s' % req.organization.name
email_body_text = render_to_string('get_together/emails/orgs/invite_to_org.txt', context)
email_body_html = render_to_string('get_together/emails/orgs/invite_to_org.html', context)
email_from = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@gettogether.community')
for admin in Member.objects.filter(team=req.team, role=Member.ADMIN, user__user__account__is_email_confirmed=True):
success = send_mail(
from_email=email_from,
html_message=email_body_html,
message=email_body_text,
recipient_list=[admin.user.user.email],
subject=email_subject,
fail_silently=True,
)
EmailRecord.objects.create(
sender=req.requested_by.user,
recipient=admin.user.user,
email=admin.user.user.email,
subject=email_subject,
body=email_body_text,
ok=success
)
@login_required
def confirm_request_to_join_org(request, request_key):
req = get_object_or_404(OrgTeamRequest, request_key=request_key)
if req.request_origin == req.ORG:
return accept_invite_to_join_org(request, req)
else:
return accept_request_to_join_org(request, req)
@login_required
def accept_request_to_join_org(request, req):
if not request.user.profile.can_edit_org(req.organization):
messages.add_message(request, messages.WARNING, message=_('You do not have permission to accept new teams to this organization.'))
return redirect('show-org', org_slug=req.organization.slug)
if request.method == 'GET':
form = AcceptRequestToJoinOrgForm()
context = {
'invite': req,
'org': req.organization,
'team': req.team,
'request_form': form,
}
return render(request, 'get_together/orgs/accept_request.html', context)
elif request.method == 'POST':
form = AcceptRequestToJoinOrgForm(request.POST)
if form.is_valid() and form.cleaned_data['confirm']:
req.accepted_by = request.user.profile
req.joined_date = datetime.datetime.now()
req.save()
req.team.organization = req.organization
req.team.save()
messages.add_message(request, messages.SUCCESS, message=_('%s has been added to your organization.' % req.team.name))
return redirect('show-org', org_slug=req.organization.slug)
else:
context = {
'invite': req,
'org': req.organization,
'team': req.team,
'request_form': form,
}
return render(request, 'get_together/orgs/accept_request.html', context)
else:
return redirect('home')
@login_required
def accept_invite_to_join_org(request, req):
if not request.user.profile.can_edit_team(req.team):
messages.add_message(request, messages.WARNING, message=_('You do not have permission to add this team to an orgnization.'))
return redirect('show-team-by-slug', team_slug=req.team.slug)
if request.method == 'GET':
form = AcceptInviteToJoinOrgForm()
context = {
'invite': req,
'org': req.organization,
'team': req.team,
'invite_form': form,
}
return render(request, 'get_together/orgs/accept_invite.html', context)
elif request.method == 'POST':
form = AcceptInviteToJoinOrgForm(request.POST)
if form.is_valid() and form.cleaned_data['confirm']:
req.accepted_by = request.user.profile
req.joined_date = datetime.datetime.now()
req.save()
req.team.organization = req.organization
req.team.save()
messages.add_message(request, messages.SUCCESS, message=_('You team has been added to %s.' % req.organization.name))
return redirect('show-team-by-slug', team_slug=req.team.slug)
else:
context = {
'invite': req,
'org': req.organization,
'team': req.team,
'invite_form': form,
}
return render(request, 'get_together/orgs/accept_invite.html', context)
else:
return redirect('home')
def show_common_event(request, event_id, event_slug):
event = get_object_or_404(CommonEvent, id=event_id)
context = {