Move project-specific settings to a new "Project Settings" page

This commit is contained in:
Pēteris Caune 2019-01-22 15:44:54 +02:00
parent 64158c83a8
commit b013a92c43
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
18 changed files with 482 additions and 454 deletions

View File

@ -4,13 +4,11 @@ All notable changes to this project will be documented in this file.
## Unreleased ## Unreleased
### Improvements ### Improvements
- Database schema: set Check.user to not null
- Database schema: add uniqueness constraint to Check.code - Database schema: add uniqueness constraint to Check.code
- Database schema: add Ping.kind field - Database schema: add Ping.kind field. Remove "start" and "fail" fields.
- Database schema: remove Ping.start and Ping.fail fields
- Add "Email Settings..." dialog and "Subject Must Contain" setting - Add "Email Settings..." dialog and "Subject Must Contain" setting
- Database schema: add the Project model - Database schema: add the Project model
- Move project-specific settings to a new "Project Settings" page
## 1.4.0 - 2018-12-25 ## 1.4.0 - 2018-12-25

View File

@ -95,5 +95,5 @@ class RemoveTeamMemberForm(forms.Form):
email = LowercaseEmailField() email = LowercaseEmailField()
class TeamNameForm(forms.Form): class ProjectNameForm(forms.Form):
team_name = forms.CharField(max_length=200, required=True) name = forms.CharField(max_length=200, required=True)

View File

@ -79,7 +79,7 @@ class Profile(models.Model):
def check_token(self, token, salt): def check_token(self, token, salt):
return salt in self.token and check_password(token, self.token) return salt in self.token and check_password(token, self.token)
def send_instant_login_link(self, inviting_profile=None, redirect_url=None): def send_instant_login_link(self, inviting_project=None, redirect_url=None):
token = self.prepare_token("login") token = self.prepare_token("login")
path = reverse("hc-check-token", args=[self.user.username, token]) path = reverse("hc-check-token", args=[self.user.username, token])
if redirect_url: if redirect_url:
@ -88,7 +88,7 @@ class Profile(models.Model):
ctx = { ctx = {
"button_text": "Sign In", "button_text": "Sign In",
"button_url": settings.SITE_ROOT + path, "button_url": settings.SITE_ROOT + path,
"inviting_profile": inviting_profile "inviting_project": inviting_project
} }
emails.login(self.user.email, ctx) emails.login(self.user.email, ctx)
@ -166,20 +166,6 @@ class Profile(models.Model):
emails.report(self.user.email, ctx, headers) emails.report(self.user.email, ctx, headers)
return True return True
def can_invite(self):
return self.member_count() < self.team_limit
def invite(self, user):
project = self.get_own_project()
Member.objects.create(user=user, project=project)
# Switch the invited user over to the new team so they
# notice the new team on next visit:
user.profile.current_project = project
user.profile.save()
user.profile.send_instant_login_link(self)
def sms_sent_this_month(self): def sms_sent_this_month(self):
# IF last_sms_date was never set, we have not sent any messages yet. # IF last_sms_date was never set, we have not sent any messages yet.
if not self.last_sms_date: if not self.last_sms_date:
@ -210,12 +196,6 @@ class Profile(models.Model):
return project return project
def member_count(self):
return Member.objects.filter(project__owner__profile=self).count()
def members(self):
return Member.objects.filter(project__owner__profile=self).all()
class Project(models.Model): class Project(models.Model):
code = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) code = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@ -242,6 +222,19 @@ class Project(models.Model):
self.api_key_readonly = urlsafe_b64encode(os.urandom(24)).decode() self.api_key_readonly = urlsafe_b64encode(os.urandom(24)).decode()
self.save() self.save()
def can_invite(self):
return self.member_set.count() < self.owner_profile.team_limit
def invite(self, user):
Member.objects.create(user=user, project=self)
# Switch the invited user over to the new team so they
# notice the new team on next visit:
user.profile.current_project = self
user.profile.save()
user.profile.send_instant_login_link(self)
def set_next_nag_date(self): def set_next_nag_date(self):
""" Set next_nag_date on profiles of all members of this project. """ """ Set next_nag_date on profiles of all members of this project. """

View File

@ -27,45 +27,6 @@ class ProfileTestCase(BaseTestCase):
expected_subject = "Set password on %s" % settings.SITE_NAME expected_subject = "Set password on %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, expected_subject) self.assertEqual(mail.outbox[0].subject, expected_subject)
def test_it_shows_api_keys(self):
self.project.api_key_readonly = "R" * 32
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"show_api_keys": "1"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "X" * 32)
self.assertContains(r, "R" * 32)
def test_it_creates_api_key(self):
self.client.login(username="alice@example.org", password="password")
form = {"create_api_keys": "1"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
api_key = self.project.api_key
self.assertTrue(len(api_key) > 10)
self.assertFalse("b'" in api_key)
def test_it_revokes_api_key(self):
self.project.api_key_readonly = "R" * 32
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"revoke_api_keys": "1"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 200
self.project.refresh_from_db()
self.assertEqual(self.project.api_key, "")
self.assertEqual(self.project.api_key_readonly, "")
def test_it_sends_report(self): def test_it_sends_report(self):
check = Check(project=self.project, name="Test Check") check = Check(project=self.project, name="Test Check")
check.last_ping = now() check.last_ping = now()
@ -132,59 +93,6 @@ class ProfileTestCase(BaseTestCase):
self.assertEqual(len(mail.outbox), 0) self.assertEqual(len(mail.outbox), 0)
def test_it_adds_team_member(self):
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 200)
members = self.project.member_set.all()
self.assertEqual(members.count(), 2)
member = Member.objects.get(project=self.project,
user__email="frank@example.org")
profile = member.user.profile
self.assertEqual(profile.current_project, self.project)
# And an email should have been sent
subj = ('You have been invited to join'
' alice@example.org on %s' % settings.SITE_NAME)
self.assertEqual(mail.outbox[0].subject, subj)
def test_it_checks_team_size(self):
self.profile.team_limit = 0
self.profile.save()
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 403)
def test_it_removes_team_member(self):
self.client.login(username="alice@example.org", password="password")
form = {"remove_team_member": "1", "email": "bob@example.org"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 200)
self.assertEqual(Member.objects.count(), 0)
self.bobs_profile.refresh_from_db()
self.assertEqual(self.bobs_profile.current_project, None)
def test_it_sets_team_name(self):
self.client.login(username="alice@example.org", password="password")
form = {"set_team_name": "1", "team_name": "Alpha Team"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
self.assertEqual(self.project.name, "Alpha Team")
def test_it_switches_to_own_team(self): def test_it_switches_to_own_team(self):
self.client.login(username="bob@example.org", password="password") self.client.login(username="bob@example.org", password="password")

View File

@ -0,0 +1,104 @@
from django.core import mail
from django.conf import settings
from hc.test import BaseTestCase
from hc.accounts.models import Member
class ProfileTestCase(BaseTestCase):
def setUp(self):
super(ProfileTestCase, self).setUp()
self.url = "/projects/%s/settings/" % self.project.code
def test_it_shows_api_keys(self):
self.project.api_key_readonly = "R" * 32
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"show_api_keys": "1"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "X" * 32)
self.assertContains(r, "R" * 32)
def test_it_creates_api_key(self):
self.client.login(username="alice@example.org", password="password")
form = {"create_api_keys": "1"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
api_key = self.project.api_key
self.assertTrue(len(api_key) > 10)
self.assertFalse("b'" in api_key)
def test_it_revokes_api_key(self):
self.project.api_key_readonly = "R" * 32
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"revoke_api_keys": "1"}
r = self.client.post(self.url, form)
assert r.status_code == 200
self.project.refresh_from_db()
self.assertEqual(self.project.api_key, "")
self.assertEqual(self.project.api_key_readonly, "")
def test_it_adds_team_member(self):
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
members = self.project.member_set.all()
self.assertEqual(members.count(), 2)
member = Member.objects.get(project=self.project,
user__email="frank@example.org")
profile = member.user.profile
self.assertEqual(profile.current_project, self.project)
# And an email should have been sent
subj = ('You have been invited to join'
' alice@example.org on %s' % settings.SITE_NAME)
self.assertEqual(mail.outbox[0].subject, subj)
def test_it_checks_team_size(self):
self.profile.team_limit = 0
self.profile.save()
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_removes_team_member(self):
self.client.login(username="alice@example.org", password="password")
form = {"remove_team_member": "1", "email": "bob@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.assertEqual(Member.objects.count(), 0)
self.bobs_profile.refresh_from_db()
self.assertEqual(self.bobs_profile.current_project, None)
def test_it_sets_project_name(self):
self.client.login(username="alice@example.org", password="password")
form = {"set_project_name": "1", "name": "Alpha Team"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
self.assertEqual(self.project.name, "Alpha Team")

View File

@ -19,7 +19,7 @@ from django.views.decorators.http import require_POST
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm, from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
InviteTeamMemberForm, RemoveTeamMemberForm, InviteTeamMemberForm, RemoveTeamMemberForm,
ReportSettingsForm, SetPasswordForm, ReportSettingsForm, SetPasswordForm,
TeamNameForm, AvailableEmailForm, ProjectNameForm, AvailableEmailForm,
ExistingEmailForm) ExistingEmailForm)
from hc.accounts.models import Profile, Project, Member from hc.accounts.models import Profile, Project, Member
from hc.api.models import Channel, Check from hc.api.models import Channel, Check
@ -194,10 +194,7 @@ def profile(request):
ctx = { ctx = {
"page": "profile", "page": "profile",
"profile": profile, "profile": profile,
"project": project, "project": project
"show_api_keys": False,
"api_status": "default",
"team_status": "default"
} }
if request.method == "POST": if request.method == "POST":
@ -207,7 +204,27 @@ def profile(request):
elif "set_password" in request.POST: elif "set_password" in request.POST:
profile.send_set_password_link() profile.send_set_password_link()
return redirect("hc-link-sent") return redirect("hc-link-sent")
elif "create_api_keys" in request.POST:
return render(request, "accounts/profile.html", ctx)
@login_required
def project(request, code):
project = Project.objects.get(code=code, owner_id=request.user.id)
profile = project.owner_profile
ctx = {
"page": "profile",
"project": project,
"profile": profile,
"show_api_keys": False,
"project_name_status": "default",
"api_status": "default",
"team_status": "default"
}
if request.method == "POST":
if "create_api_keys" in request.POST:
project.set_api_keys() project.set_api_keys()
project.save() project.save()
@ -224,7 +241,7 @@ def profile(request):
elif "show_api_keys" in request.POST: elif "show_api_keys" in request.POST:
ctx["show_api_keys"] = True ctx["show_api_keys"] = True
elif "invite_team_member" in request.POST: elif "invite_team_member" in request.POST:
if not profile.can_invite(): if not project.can_invite():
return HttpResponseForbidden() return HttpResponseForbidden()
form = InviteTeamMemberForm(request.POST) form = InviteTeamMemberForm(request.POST)
@ -236,7 +253,7 @@ def profile(request):
except User.DoesNotExist: except User.DoesNotExist:
user = _make_user(email) user = _make_user(email)
profile.invite(user) project.invite(user)
ctx["team_member_invited"] = email ctx["team_member_invited"] = email
ctx["team_status"] = "success" ctx["team_status"] = "success"
@ -249,21 +266,27 @@ def profile(request):
farewell_user.profile.current_project = None farewell_user.profile.current_project = None
farewell_user.profile.save() farewell_user.profile.save()
Member.objects.filter(project=request.project, Member.objects.filter(project=project,
user=farewell_user).delete() user=farewell_user).delete()
ctx["team_member_removed"] = email ctx["team_member_removed"] = email
ctx["team_status"] = "info" ctx["team_status"] = "info"
elif "set_team_name" in request.POST: elif "set_project_name" in request.POST:
form = TeamNameForm(request.POST) form = ProjectNameForm(request.POST)
if form.is_valid(): if form.is_valid():
request.project.name = form.cleaned_data["team_name"] project.name = form.cleaned_data["name"]
request.project.save() project.save()
ctx["team_name_updated"] = True if request.project.id == project.id:
ctx["team_status"] = "success" request.project = project
return render(request, "accounts/profile.html", ctx) ctx["project_name_updated"] = True
ctx["project_name_status"] = "success"
# Count members right before rendering the template, in case
# we just invited or removed someone
ctx["num_members"] = project.member_set.count()
return render(request, "accounts/project.html", ctx)
@login_required @login_required

View File

@ -34,7 +34,7 @@ class PricingTestCase(BaseTestCase):
self.client.login(username="bob@example.org", password="password") self.client.login(username="bob@example.org", password="password")
r = self.client.get("/pricing/") r = self.client.get("/pricing/")
self.assertContains(r, "To manage this team") self.assertContains(r, "To manage billing for this project")
def test_it_shows_active_plan(self): def test_it_shows_active_plan(self):
self.sub = Subscription(user=self.alice) self.sub = Subscription(user=self.alice)

View File

@ -60,8 +60,6 @@ def billing(request):
"profile": request.profile, "profile": request.profile,
"sub": sub, "sub": sub,
"num_checks": Check.objects.filter(project__owner=request.user).count(), "num_checks": Check.objects.filter(project__owner=request.user).count(),
"team_size": request.profile.member_count() + 1,
"team_max": request.profile.team_limit + 1,
"send_invoices_status": send_invoices_status, "send_invoices_status": send_invoices_status,
"set_plan_status": "default", "set_plan_status": "default",
"address_status": "default", "address_status": "default",

View File

@ -1,12 +1,13 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from hc.accounts.views import login as hc_login from hc.accounts import views as accounts_views
urlpatterns = [ urlpatterns = [
path('admin/login/', hc_login), path('admin/login/', accounts_views.login),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('accounts/', include('hc.accounts.urls')), path('accounts/', include('hc.accounts.urls')),
path('projects/<uuid:code>/settings/', accounts_views.project, name="hc-project-settings"),
path('', include('hc.api.urls')), path('', include('hc.api.urls')),
path('', include('hc.front.urls')), path('', include('hc.front.urls')),
path('', include('hc.payments.urls')) path('', include('hc.payments.urls'))

View File

@ -58,19 +58,6 @@
<span>{{ num_checks }} of {{ profile.check_limit }}</span> <span>{{ num_checks }} of {{ profile.check_limit }}</span>
</td> </td>
</tr> </tr>
<tr>
<td>Team Size</td>
<td {% if team_size >= profile.team_limit %} class="at-limit" {% endif %}>
<span>
{{ team_size }} of
{% if profile.team_limit == 500 %}
unlimited
{% else %}
{{ team_max }}
{% endif %}
</span>
</td>
</tr>
</table> </table>
<button <button

View File

@ -7,7 +7,10 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h1 class="settings-title">Settings</h1> <h1 class="settings-title">
Settings
<small>{{ request.user.email }}</small>
</h1>
</div> </div>
{% if messages %} {% if messages %}
<div class="col-sm-12"> <div class="col-sm-12">
@ -57,141 +60,6 @@
</div> </div>
</div> </div>
<div class="panel panel-{{ api_status }}">
<div class="panel-body settings-block">
<h2>API Access</h2>
{% if project.api_key %}
{% if show_api_keys %}
<p>
API key: <br />
<code>{{ project.api_key }}</code>
</p>
{% if project.api_key_readonly %}
<p>
API key (read-only): <br />
<code>{{ project.api_key_readonly }}</code>
</p>
{% endif %}
<button
data-toggle="modal"
data-target="#revoke-api-key-modal"
class="btn btn-danger pull-right">Revoke</button>
{% else %}
<form method="post">
<span class="icon-ok"></span>
API access is enabled.
{% csrf_token %}
<button
type="submit"
name="show_api_keys"
class="btn btn-default pull-right">Show API keys</button>
</form>
{% endif %}
{% else %}
<span class="icon-cancel"></span>
API access is disabled.
<form method="post">
{% csrf_token %}
<button
type="submit"
name="create_api_keys"
class="btn btn-default pull-right">Create API keys</button>
</form>
{% endif %}
</div>
{% if api_keys_created %}
<div class="panel-footer">
API keys created
</div>
{% endif %}
{% if api_keys_revoked %}
<div class="panel-footer">
API keys revoked
</div>
{% endif %}
</div>
<div class="panel panel-{{ team_status }}">
<div class="panel-body settings-block">
<h2>Team Access</h2>
{% if profile.member_count %}
<table class="table">
<tr>
<td>{{ profile.user.email }}</td>
<td>Owner</td>
<td></td>
</tr>
{% for member in profile.members %}
<tr>
<td>{{ member.user.email }} </td>
<td>Member</td>
<td>
<a
href="#"
data-email="{{ member.user.email }}"
class="pull-right member-remove">Remove</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>
<strong>Invite team members to your account.</strong>
</p>
<p>
Share access to your checks and configured integrations
without having to share a login.
</p>
{% endif %}
<br />
{% if not profile.can_invite %}
<div class="alert alert-info">
<strong>Team size limit reached.</strong>
To invite more members to your team, please
<a href="{% url 'hc-pricing' %}">upgrade your account!</a>
</div>
{% endif %}
<a
href="#"
class="btn btn-default"
data-toggle="modal"
data-target="#set-team-name-modal">Set Team Name</a>
{% if profile.can_invite %}
<a
href="#"
class="btn btn-primary pull-right"
data-toggle="modal"
data-target="#invite-team-member-modal">Invite a Team Member</a>
{% endif %}
</div>
{% if team_member_invited %}
<div class="panel-footer">
{{ team_member_invited }} invited to team
</div>
{% endif %}
{% if team_member_removed %}
<div class="panel-footer">
{{ team_member_removed }} removed from team
</div>
{% endif %}
{% if team_name_updated %}
<div class="panel-footer">
Team name updated
</div>
{% endif %}
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-body settings-block"> <div class="panel-body settings-block">
{% csrf_token %} {% csrf_token %}
@ -210,136 +78,6 @@
</div> </div>
</div> </div>
<div id="revoke-api-key-modal" class="modal">
<div class="modal-dialog">
<form id="revoke-api-key-form" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Revoke API Keys?</h4>
</div>
<div class="modal-body">
<p>You are about to revoke your current API keys.</p>
<p>Afterwards, you can create new API keys, but there will
be <strong>no way of getting the current API
keys back</strong>.
</p>
<p>Are you sure?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="revoke_api_keys"
class="btn btn-danger">Revoke API Keys</button>
</div>
</div>
</form>
</div>
</div>
<div id="remove-team-member-modal" class="modal">
<div class="modal-dialog">
<form id="remove-team-member-form" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Remove Team Member</h4>
</div>
<div class="modal-body">
<p>You are about to remove <span id="rtm-email"></span> from the team.</p>
<p>Are you sure?</p>
<input
type="hidden"
name="email"
id="remove-team-member-email" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="remove_team_member"
class="btn btn-danger">Remove Member from Team</button>
</div>
</div>
</form>
</div>
</div>
<div id="invite-team-member-modal" class="modal">
<div class="modal-dialog">
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Invite a Team Member</h4>
</div>
<div class="modal-body">
<ul>
<li>Team Members can create and manage Checks and Integrations</li>
<li>Only the team owner (you) can view and edit billing settings</li>
</ul>
<div class="form-group">
<label for="itm-email" class="col-sm-2 control-label">Email</label>
<div class="col-sm-9">
<input
type="email"
class="form-control"
id="itm-email"
name="email"
placeholder="friend@example.org">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="invite_team_member"
class="btn btn-primary">Send Invite</button>
</div>
</div>
</form>
</div>
</div>
<div id="set-team-name-modal" class="modal">
<div class="modal-dialog">
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Set Team Name</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="team-name" class="col-sm-4 control-label">Team Name</label>
<div class="col-sm-7">
<input
type="text"
class="form-control"
id="team-name"
name="team_name"
value="{{ project }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="set_team_name"
class="btn btn-primary">Set Team Name</button>
</div>
</div>
</form>
</div>
</div>
<div id="close-account-modal" class="modal"> <div id="close-account-modal" class="modal">
<div class="modal-dialog"> <div class="modal-dialog">
<form id="close-account-form" method="post" action="{% url 'hc-close' %}"> <form id="close-account-form" method="post" action="{% url 'hc-close' %}">
@ -367,11 +105,3 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/profile.js' %}"></script>
{% endcompress %}
{% endblock %}

View File

@ -0,0 +1,292 @@
{% extends "base.html" %}
{% load compress static hc_extras %}
{% block title %}Project Settings - {{ project }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-9 col-md-6">
<h1 class="settings-title">Project Settings</h1>
{% for message in messages %}
<p class="alert alert-{{ message.tags }}">{{ message }}</p>
{% endfor %}
<div class="panel panel-{{ project_name_status }}">
<div class="panel-body settings-block">
<h2>Project Name</h2>
{{ project }}
<a
href="#"
class="btn btn-default pull-right"
data-toggle="modal"
data-target="#set-project-name-modal">Change Project Name</a>
</div>
{% if project_name_updated %}
<div class="panel-footer">
Project name updated
</div>
{% endif %}
</div>
<div class="panel panel-{{ api_status }}">
<div class="panel-body settings-block">
<h2>API Access</h2>
{% if project.api_key %}
{% if show_api_keys %}
<p>
API key: <br />
<code>{{ project.api_key }}</code>
</p>
{% if project.api_key_readonly %}
<p>
API key (read-only): <br />
<code>{{ project.api_key_readonly }}</code>
</p>
{% endif %}
<button
data-toggle="modal"
data-target="#revoke-api-key-modal"
class="btn btn-danger pull-right">Revoke</button>
{% else %}
<form method="post">
<span class="icon-ok"></span>
API access is enabled.
{% csrf_token %}
<button
type="submit"
name="show_api_keys"
class="btn btn-default pull-right">Show API keys</button>
</form>
{% endif %}
{% else %}
<span class="icon-cancel"></span>
API access is disabled.
<form method="post">
{% csrf_token %}
<button
type="submit"
name="create_api_keys"
class="btn btn-default pull-right">Create API keys</button>
</form>
{% endif %}
</div>
{% if api_keys_created %}
<div class="panel-footer">
API keys created
</div>
{% endif %}
{% if api_keys_revoked %}
<div class="panel-footer">
API keys revoked
</div>
{% endif %}
</div>
<div class="panel panel-{{ team_status }}">
<div class="panel-body settings-block">
<h2>Team Access</h2>
{% if num_members %}
<table class="table">
<tr>
<td>{{ project.owner.email }}</td>
<td>Owner</td>
<td></td>
</tr>
{% for member in project.member_set.all %}
<tr>
<td>{{ member.user.email }} </td>
<td>Member</td>
<td>
<a
href="#"
data-email="{{ member.user.email }}"
class="pull-right member-remove">Remove</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>
<strong>Invite team members to your project.</strong>
Share access to your checks and configured integrations
without having to share login details.
</p>
{% endif %}
<br />
{% if project.can_invite %}
<a
href="#"
class="btn btn-primary pull-right"
data-toggle="modal"
data-target="#invite-team-member-modal">Invite a Team Member</a>
{% else %}
<div class="alert alert-info">
<strong>Team size limit reached.</strong>
To invite more members, please
<a href="{% url 'hc-pricing' %}">upgrade your account!</a>
</div>
{% endif %}
</div>
{% if team_member_invited %}
<div class="panel-footer">
{{ team_member_invited }} invited to team
</div>
{% endif %}
{% if team_member_removed %}
<div class="panel-footer">
{{ team_member_removed }} removed from team
</div>
{% endif %}
</div>
</div>
</div>
<div id="revoke-api-key-modal" class="modal">
<div class="modal-dialog">
<form id="revoke-api-key-form" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Revoke API Keys?</h4>
</div>
<div class="modal-body">
<p>You are about to revoke your current API keys.</p>
<p>Afterwards, you can create new API keys, but there will
be <strong>no way of getting the current API
keys back</strong>.
</p>
<p>Are you sure?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="revoke_api_keys"
class="btn btn-danger">Revoke API Keys</button>
</div>
</div>
</form>
</div>
</div>
<div id="remove-team-member-modal" class="modal">
<div class="modal-dialog">
<form id="remove-team-member-form" method="post">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Remove Team Member</h4>
</div>
<div class="modal-body">
<p>You are about to remove <strong id="rtm-email"></strong> from the project.</p>
<p>Are you sure?</p>
<input
type="hidden"
name="email"
id="remove-team-member-email" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="remove_team_member"
class="btn btn-danger">Remove Member from Project</button>
</div>
</div>
</form>
</div>
</div>
<div id="invite-team-member-modal" class="modal">
<div class="modal-dialog">
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="remove-check-title">Invite a Team Member</h4>
</div>
<div class="modal-body">
<ul>
<li>Team Members can create and manage Checks and Integrations</li>
<li>Only the project owner (you) can view and edit billing settings</li>
</ul>
<div class="form-group">
<label for="itm-email" class="col-sm-2 control-label">Email</label>
<div class="col-sm-9">
<input
type="email"
class="form-control"
id="itm-email"
name="email"
placeholder="friend@example.org">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="invite_team_member"
class="btn btn-primary">Send Invite</button>
</div>
</div>
</form>
</div>
</div>
<div id="set-project-name-modal" class="modal">
<div class="modal-dialog">
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>Change Project Name</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="project-name" class="col-sm-4 control-label">Project Name</label>
<div class="col-sm-7">
<input
type="text"
class="form-control"
id="project-name"
name="name"
value="{{ project }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button
type="submit"
name="set_project_name"
class="btn btn-primary">Set Project Name</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/project.js' %}"></script>
{% endcompress %}
{% endblock %}

View File

@ -125,28 +125,20 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% with projects=request.get_projects %} {% with projects=request.get_projects %}
{% for project in projects %} {% for project in projects %}
{% if project.owner == request.user %}
<li class="dropdown-header">{{ project }}</li>
<li>
<a href="{% url 'hc-switch-team' project.owner.username %}">Checks</a>
</li>
<li><a href="{% url 'hc-profile' %}">Account Settings</a></li>
<li role="separator" class="divider"></li>
{% endif %}
{% endfor %}
{% for project in projects %}
{% if project.owner == request.user %}
{% else %}
<li class="dropdown-header">{{ project }}</li> <li class="dropdown-header">{{ project }}</li>
<li> <li>
<a href="{% url 'hc-switch-team' project.owner.username %}">Checks</a> <a href="{% url 'hc-switch-team' project.owner.username %}">Checks</a>
</li> </li>
<li role="separator" class="divider"></li> {% if project.owner == request.user %}
<li>
<a href="{% url 'hc-project-settings' project.code %}">Project Settings</a>
</li>
{% endif %} {% endif %}
<li role="separator" class="divider"></li>
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
<li><a href="{% url 'hc-profile' %}">Account Settings</a></li>
<li><a href="{% url 'hc-logout' %}">Log Out</a></li> <li><a href="{% url 'hc-logout' %}">Log Out</a></li>
</ul> </ul>
</li> </li>

View File

@ -5,9 +5,15 @@
Hello, Hello,
<br /> <br />
{% if inviting_profile %} {% if inviting_project %}
<strong>{{ inviting_profile.user.email }}</strong> invites you to their {% if inviting_project.name %}
<a href="{% site_root %}">{% site_name %}</a> account. <strong>{{ inviting_project.owner.email }}</strong> invites you to their
<a href="{% site_root %}">{% site_name %}</a>
project <strong>{{ inviting_project }}</strong>.
{% else %}
<strong>{{ inviting_project.owner.email }}</strong> invites you to their
<a href="{% site_root %}">{% site_name %}</a> account.
{% endif %}
<br /><br /> <br /><br />
You will be able to manage their You will be able to manage their

View File

@ -1,7 +1,7 @@
{% load hc_extras %} {% load hc_extras %}
{% block content %}Hello, {% block content %}Hello,
{% if inviting_profile %} {% if inviting_project %}
{{ inviting_profile.user.email }} invites you to their {% site_name %} account. {{ inviting_project.owner.email }} invites you to their {% site_name %} account.
You will be able to manage their existing monitoring checks and set up new You will be able to manage their existing monitoring checks and set up new
ones. If you already have your own account on {% site_name %}, you will ones. If you already have your own account on {% site_name %}, you will

View File

@ -1,6 +1,6 @@
{% load hc_extras %} {% load hc_extras %}
{% if inviting_profile %} {% if inviting_project %}
You have been invited to join {{ inviting_profile.user.email }} on {% site_name %} You have been invited to join {{ inviting_project }} on {% site_name %}
{% else %} {% else %}
Log in to {% site_name %} Log in to {% site_name %}
{% endif %} {% endif %}

View File

@ -14,16 +14,12 @@
You are currently viewing project <strong>{{ request.project }}</strong>. You are currently viewing project <strong>{{ request.project }}</strong>.
</p> </p>
<p> <p>
To manage this team, please log in as <strong>{{ request.project.owner.email }}</strong>. To manage billing for this project, please log in as <strong>{{ request.project.owner.email }}</strong>.
</p> </p>
<br /> <br />
<p> <p>
<a class="btn btn-default"
href="{% url 'hc-switch-team' request.user.username %}">
Switch to {{ request.profile }}
</a>
<a class="btn btn-default" href="{% url 'hc-logout' %}">Log Out</a> <a class="btn btn-default" href="{% url 'hc-logout' %}">Log Out</a>
</p> </p>
</div> </div>