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 ## 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: 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 - 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


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

@ -95,5 +95,5 @@ class RemoveTeamMemberForm(forms.Form):
email = LowercaseEmailField() 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): 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. """


+ 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 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="[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): 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="[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): def test_it_switches_to_own_team(self):
self.client.login(username="[email protected]", password="password") 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, 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,
"show_api_keys": False,
"api_status": "default",
"team_status": "default"
"project": project
} }
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:
form = TeamNameForm(request.POST)
elif "set_project_name" in request.POST:
form = ProjectNameForm(request.POST)
if form.is_valid(): 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 @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") self.client.login(username="[email protected]", 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)


+ 0
- 2
hc/payments/views.py 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",


+ 3
- 2
hc/urls.py 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'))


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> <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


+ 4
- 274
templates/accounts/profile.html 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="[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 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 %}

+ 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"> <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>


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

@ -5,9 +5,15 @@
Hello, Hello,
<br /> <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 /> <br /><br />
You will be able to manage their You will be able to manage their


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

@ -1,7 +1,7 @@
{% load hc_extras %} {% load hc_extras %}
{% block content %}Hello, {% 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 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


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

@ -1,6 +1,6 @@
{% load hc_extras %} {% 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 %} {% else %}
Log in to {% site_name %} Log in to {% site_name %}
{% endif %} {% endif %}

+ 1
- 5
templates/payments/pricing_not_owner.html 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>


Loading…
Cancel
Save