forked from GithubBackups/healthchecks
Move project-specific settings to a new "Project Settings" page
This commit is contained in:
parent
64158c83a8
commit
b013a92c43
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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. """
|
||||
|
||||
|
@ -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="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):
|
||||
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="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):
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
|
||||
|
104
hc/accounts/tests/test_project.py
Normal file
104
hc/accounts/tests/test_project.py
Normal 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")
|
@ -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
|
||||
|
@ -34,7 +34,7 @@ class PricingTestCase(BaseTestCase):
|
||||
self.client.login(username="bob@example.org", 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)
|
||||
|
@ -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",
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
|
@ -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">×</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">×</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">×</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">×</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
templates/accounts/project.html
Normal file
292
templates/accounts/project.html
Normal 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">×</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">×</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">×</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">×</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 %}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
@ -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…
x
Reference in New Issue
Block a user