Browse Source

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

pull/214/head
Pēteris Caune 6 years ago
parent
commit
b013a92c43
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
18 changed files with 482 additions and 454 deletions
  1. +2
    -4
      CHANGELOG.md
  2. +2
    -2
      hc/accounts/forms.py
  3. +15
    -22
      hc/accounts/models.py
  4. +0
    -92
      hc/accounts/tests/test_profile.py
  5. +104
    -0
      hc/accounts/tests/test_project.py
  6. +39
    -16
      hc/accounts/views.py
  7. +1
    -1
      hc/payments/tests/test_pricing.py
  8. +0
    -2
      hc/payments/views.py
  9. +3
    -2
      hc/urls.py
  10. +0
    -0
      static/js/project.js
  11. +0
    -13
      templates/accounts/billing.html
  12. +4
    -274
      templates/accounts/profile.html
  13. +292
    -0
      templates/accounts/project.html
  14. +6
    -14
      templates/base.html
  15. +9
    -3
      templates/emails/login-body-html.html
  16. +2
    -2
      templates/emails/login-body-text.html
  17. +2
    -2
      templates/emails/login-subject.html
  18. +1
    -5
      templates/payments/pricing_not_owner.html

+ 2
- 4
CHANGELOG.md View File

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


+ 2
- 2
hc/accounts/forms.py View File

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

+ 15
- 22
hc/accounts/models.py View File

@ -79,7 +79,7 @@ class Profile(models.Model):
def check_token(self, token, salt):
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")
path = reverse("hc-check-token", args=[self.user.username, token])
if redirect_url:
@ -88,7 +88,7 @@ class Profile(models.Model):
ctx = {
"button_text": "Sign In",
"button_url": settings.SITE_ROOT + path,
"inviting_profile": inviting_profile
"inviting_project": inviting_project
}
emails.login(self.user.email, ctx)
@ -166,20 +166,6 @@ class Profile(models.Model):
emails.report(self.user.email, ctx, headers)
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):
# IF last_sms_date was never set, we have not sent any messages yet.
if not self.last_sms_date:
@ -210,12 +196,6 @@ class Profile(models.Model):
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):
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.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):
""" Set next_nag_date on profiles of all members of this project. """


+ 0
- 92
hc/accounts/tests/test_profile.py View File

@ -27,45 +27,6 @@ class ProfileTestCase(BaseTestCase):
expected_subject = "Set password on %s" % settings.SITE_NAME
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="[email protected]", 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="[email protected]", 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="[email protected]", 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):
check = Check(project=self.project, name="Test Check")
check.last_ping = now()
@ -132,59 +93,6 @@ class ProfileTestCase(BaseTestCase):
self.assertEqual(len(mail.outbox), 0)
def test_it_adds_team_member(self):
self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
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="[email protected]")
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'
' [email protected] 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="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 403)
def test_it_removes_team_member(self):
self.client.login(username="[email protected]", password="password")
form = {"remove_team_member": "1", "email": "[email protected]"}
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="[email protected]", 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):
self.client.login(username="[email protected]", password="password")


+ 104
- 0
hc/accounts/tests/test_project.py 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="[email protected]", 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="[email protected]", 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="[email protected]", 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="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
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="[email protected]")
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'
' [email protected] 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="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_removes_team_member(self):
self.client.login(username="[email protected]", password="password")
form = {"remove_team_member": "1", "email": "[email protected]"}
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="[email protected]", 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")

+ 39
- 16
hc/accounts/views.py View File

@ -19,7 +19,7 @@ from django.views.decorators.http import require_POST
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
InviteTeamMemberForm, RemoveTeamMemberForm,
ReportSettingsForm, SetPasswordForm,
TeamNameForm, AvailableEmailForm,
ProjectNameForm, AvailableEmailForm,
ExistingEmailForm)
from hc.accounts.models import Profile, Project, Member
from hc.api.models import Channel, Check
@ -194,10 +194,7 @@ def profile(request):
ctx = {
"page": "profile",
"profile": profile,
"project": project,
"show_api_keys": False,
"api_status": "default",
"team_status": "default"
"project": project
}
if request.method == "POST":
@ -207,7 +204,27 @@ def profile(request):
elif "set_password" in request.POST:
profile.send_set_password_link()
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.save()
@ -224,7 +241,7 @@ def profile(request):
elif "show_api_keys" in request.POST:
ctx["show_api_keys"] = True
elif "invite_team_member" in request.POST:
if not profile.can_invite():
if not project.can_invite():
return HttpResponseForbidden()
form = InviteTeamMemberForm(request.POST)
@ -236,7 +253,7 @@ def profile(request):
except User.DoesNotExist:
user = _make_user(email)
profile.invite(user)
project.invite(user)
ctx["team_member_invited"] = email
ctx["team_status"] = "success"
@ -249,21 +266,27 @@ def profile(request):
farewell_user.profile.current_project = None
farewell_user.profile.save()
Member.objects.filter(project=request.project,
Member.objects.filter(project=project,
user=farewell_user).delete()
ctx["team_member_removed"] = email
ctx["team_status"] = "info"
elif "set_team_name" in request.POST:
form = TeamNameForm(request.POST)
elif "set_project_name" in request.POST:
form = ProjectNameForm(request.POST)
if form.is_valid():
request.project.name = form.cleaned_data["team_name"]
request.project.save()
project.name = form.cleaned_data["name"]
project.save()
ctx["team_name_updated"] = True
ctx["team_status"] = "success"
if request.project.id == project.id:
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


+ 1
- 1
hc/payments/tests/test_pricing.py View File

@ -34,7 +34,7 @@ class PricingTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password")
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):
self.sub = Subscription(user=self.alice)


+ 0
- 2
hc/payments/views.py View File

@ -60,8 +60,6 @@ def billing(request):
"profile": request.profile,
"sub": sub,
"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,
"set_plan_status": "default",
"address_status": "default",


+ 3
- 2
hc/urls.py View File

@ -1,12 +1,13 @@
from django.contrib import admin
from django.urls import include, path
from hc.accounts.views import login as hc_login
from hc.accounts import views as accounts_views
urlpatterns = [
path('admin/login/', hc_login),
path('admin/login/', accounts_views.login),
path('admin/', admin.site.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.front.urls')),
path('', include('hc.payments.urls'))


static/js/profile.js → static/js/project.js View File


+ 0
- 13
templates/accounts/billing.html View File

@ -58,19 +58,6 @@
<span>{{ num_checks }} of {{ profile.check_limit }}</span>
</td>
</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>
<button


+ 4
- 274
templates/accounts/profile.html View File

@ -7,7 +7,10 @@
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1 class="settings-title">Settings</h1>
<h1 class="settings-title">
Settings
<small>{{ request.user.email }}</small>
</h1>
</div>
{% if messages %}
<div class="col-sm-12">
@ -57,141 +60,6 @@
</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-body settings-block">
{% csrf_token %}
@ -210,136 +78,6 @@
</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="[email protected]">
</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 class="modal-dialog">
<form id="close-account-form" method="post" action="{% url 'hc-close' %}">
@ -367,11 +105,3 @@
</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/profile.js' %}"></script>
{% endcompress %}
{% endblock %}

+ 292
- 0
templates/accounts/project.html 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="[email protected]">
</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 %}

+ 6
- 14
templates/base.html View File

@ -125,28 +125,20 @@
<ul class="dropdown-menu">
{% with projects=request.get_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>
<a href="{% url 'hc-switch-team' project.owner.username %}">Checks</a>
</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 %}
<li role="separator" class="divider"></li>
{% endfor %}
{% endwith %}
<li><a href="{% url 'hc-profile' %}">Account Settings</a></li>
<li><a href="{% url 'hc-logout' %}">Log Out</a></li>
</ul>
</li>


+ 9
- 3
templates/emails/login-body-html.html View File

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


+ 2
- 2
templates/emails/login-body-text.html View File

@ -1,7 +1,7 @@
{% load hc_extras %}
{% block content %}Hello,
{% if inviting_profile %}
{{ inviting_profile.user.email }} invites you to their {% site_name %} account.
{% if inviting_project %}
{{ 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
ones. If you already have your own account on {% site_name %}, you will


+ 2
- 2
templates/emails/login-subject.html View File

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

+ 1
- 5
templates/payments/pricing_not_owner.html View File

@ -14,16 +14,12 @@
You are currently viewing project <strong>{{ request.project }}</strong>.
</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>
<br />
<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>
</p>
</div>


Loading…
Cancel
Save