From b013a92c434bcd26e0d16804ab653514c3c514e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Tue, 22 Jan 2019 15:44:54 +0200
Subject: [PATCH] Move project-specific settings to a new "Project Settings"
page
---
CHANGELOG.md | 6 +-
hc/accounts/forms.py | 4 +-
hc/accounts/models.py | 37 ++-
hc/accounts/tests/test_profile.py | 92 -------
hc/accounts/tests/test_project.py | 104 ++++++++
hc/accounts/views.py | 55 ++--
hc/payments/tests/test_pricing.py | 2 +-
hc/payments/views.py | 2 -
hc/urls.py | 5 +-
static/js/{profile.js => project.js} | 0
templates/accounts/billing.html | 13 -
templates/accounts/profile.html | 278 +-------------------
templates/accounts/project.html | 292 ++++++++++++++++++++++
templates/base.html | 20 +-
templates/emails/login-body-html.html | 12 +-
templates/emails/login-body-text.html | 4 +-
templates/emails/login-subject.html | 4 +-
templates/payments/pricing_not_owner.html | 6 +-
18 files changed, 482 insertions(+), 454 deletions(-)
create mode 100644 hc/accounts/tests/test_project.py
rename static/js/{profile.js => project.js} (100%)
create mode 100644 templates/accounts/project.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5fc1aab..46e16571 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py
index e3cec19c..5f84d941 100644
--- a/hc/accounts/forms.py
+++ b/hc/accounts/forms.py
@@ -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)
diff --git a/hc/accounts/models.py b/hc/accounts/models.py
index 0590cabc..a099bb8b 100644
--- a/hc/accounts/models.py
+++ b/hc/accounts/models.py
@@ -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. """
diff --git a/hc/accounts/tests/test_profile.py b/hc/accounts/tests/test_profile.py
index 90ac3864..fecc9f39 100644
--- a/hc/accounts/tests/test_profile.py
+++ b/hc/accounts/tests/test_profile.py
@@ -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")
diff --git a/hc/accounts/tests/test_project.py b/hc/accounts/tests/test_project.py
new file mode 100644
index 00000000..97f16c5c
--- /dev/null
+++ b/hc/accounts/tests/test_project.py
@@ -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")
diff --git a/hc/accounts/views.py b/hc/accounts/views.py
index a69bac07..af2a1406 100644
--- a/hc/accounts/views.py
+++ b/hc/accounts/views.py
@@ -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
diff --git a/hc/payments/tests/test_pricing.py b/hc/payments/tests/test_pricing.py
index 24728afa..d1cd9cc6 100644
--- a/hc/payments/tests/test_pricing.py
+++ b/hc/payments/tests/test_pricing.py
@@ -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)
diff --git a/hc/payments/views.py b/hc/payments/views.py
index 88472dc1..9dd6ce28 100644
--- a/hc/payments/views.py
+++ b/hc/payments/views.py
@@ -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",
diff --git a/hc/urls.py b/hc/urls.py
index 1075ebc6..219600e6 100644
--- a/hc/urls.py
+++ b/hc/urls.py
@@ -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//settings/', accounts_views.project, name="hc-project-settings"),
path('', include('hc.api.urls')),
path('', include('hc.front.urls')),
path('', include('hc.payments.urls'))
diff --git a/static/js/profile.js b/static/js/project.js
similarity index 100%
rename from static/js/profile.js
rename to static/js/project.js
diff --git a/templates/accounts/billing.html b/templates/accounts/billing.html
index fa026f72..f49c293b 100644
--- a/templates/accounts/billing.html
+++ b/templates/accounts/billing.html
@@ -58,19 +58,6 @@
{{ num_checks }} of {{ profile.check_limit }}
-
- Team Size
- = profile.team_limit %} class="at-limit" {% endif %}>
-
- {{ team_size }} of
- {% if profile.team_limit == 500 %}
- unlimited
- {% else %}
- {{ team_max }}
- {% endif %}
-
-
-
-
Settings
+
+ Settings
+ {{ request.user.email }}
+
{% if messages %}
@@ -57,141 +60,6 @@
-
-
-
API Access
- {% if project.api_key %}
- {% if show_api_keys %}
-
- API key:
- {{ project.api_key }}
-
- {% if project.api_key_readonly %}
-
- API key (read-only):
- {{ project.api_key_readonly }}
-
- {% endif %}
-
Revoke
-
- {% else %}
-
- {% endif %}
- {% else %}
-
- API access is disabled.
-
- {% endif %}
-
-
- {% if api_keys_created %}
-
- {% endif %}
-
- {% if api_keys_revoked %}
-
- {% endif %}
-
-
-
-
-
Team Access
- {% if profile.member_count %}
-
-
- {{ profile.user.email }}
- Owner
-
-
- {% for member in profile.members %}
-
- {{ member.user.email }}
- Member
-
- Remove
-
-
- {% endfor %}
-
- {% else %}
-
- Invite team members to your account.
-
-
- Share access to your checks and configured integrations
- without having to share a login.
-
- {% endif %}
-
-
-
- {% if not profile.can_invite %}
-
- {% endif %}
-
-
Set Team Name
-
- {% if profile.can_invite %}
-
Invite a Team Member
- {% endif %}
-
-
- {% if team_member_invited %}
-
- {% endif %}
-
- {% if team_member_removed %}
-
- {% endif %}
-
- {% if team_name_updated %}
-
- {% endif %}
-
-
{% csrf_token %}
@@ -210,136 +78,6 @@
-
-
-
-
-
-
-
-
{% endblock %}
-
-{% block scripts %}
-{% compress js %}
-
-
-
-{% endcompress %}
-{% endblock %}
diff --git a/templates/accounts/project.html b/templates/accounts/project.html
new file mode 100644
index 00000000..917011e9
--- /dev/null
+++ b/templates/accounts/project.html
@@ -0,0 +1,292 @@
+{% extends "base.html" %}
+{% load compress static hc_extras %}
+
+{% block title %}Project Settings - {{ project }}{% endblock %}
+
+
+{% block content %}
+
+
+
Project Settings
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+
+
+
+
Project Name
+ {{ project }}
+
Change Project Name
+
+
+ {% if project_name_updated %}
+
+ {% endif %}
+
+
+
+
+
API Access
+ {% if project.api_key %}
+ {% if show_api_keys %}
+
+ API key:
+ {{ project.api_key }}
+
+ {% if project.api_key_readonly %}
+
+ API key (read-only):
+ {{ project.api_key_readonly }}
+
+ {% endif %}
+
Revoke
+
+ {% else %}
+
+ {% endif %}
+ {% else %}
+
+ API access is disabled.
+
+ {% endif %}
+
+
+ {% if api_keys_created %}
+
+ {% endif %}
+
+ {% if api_keys_revoked %}
+
+ {% endif %}
+
+
+
+
+
Team Access
+ {% if num_members %}
+
+
+ {{ project.owner.email }}
+ Owner
+
+
+ {% for member in project.member_set.all %}
+
+ {{ member.user.email }}
+ Member
+
+ Remove
+
+
+ {% endfor %}
+
+ {% else %}
+
+ Invite team members to your project.
+ Share access to your checks and configured integrations
+ without having to share login details.
+
+ {% endif %}
+
+
+
+ {% if project.can_invite %}
+
Invite a Team Member
+ {% else %}
+
+ {% endif %}
+
+
+ {% if team_member_invited %}
+
+ {% endif %}
+
+ {% if team_member_removed %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+{% compress js %}
+
+
+
+{% endcompress %}
+{% endblock %}
diff --git a/templates/base.html b/templates/base.html
index 4996f564..5fc6ca35 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -125,28 +125,20 @@
diff --git a/templates/emails/login-body-html.html b/templates/emails/login-body-html.html
index 3109d404..2c71739a 100644
--- a/templates/emails/login-body-html.html
+++ b/templates/emails/login-body-html.html
@@ -5,9 +5,15 @@
Hello,
-{% if inviting_profile %}
- {{ inviting_profile.user.email }} invites you to their
- {% site_name %} account.
+{% if inviting_project %}
+ {% if inviting_project.name %}
+ {{ inviting_project.owner.email }} invites you to their
+ {% site_name %}
+ project {{ inviting_project }} .
+ {% else %}
+ {{ inviting_project.owner.email }} invites you to their
+ {% site_name %} account.
+ {% endif %}
You will be able to manage their
diff --git a/templates/emails/login-body-text.html b/templates/emails/login-body-text.html
index 9b238720..f17e23e1 100644
--- a/templates/emails/login-body-text.html
+++ b/templates/emails/login-body-text.html
@@ -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
diff --git a/templates/emails/login-subject.html b/templates/emails/login-subject.html
index 9e1d40d7..f07586ee 100644
--- a/templates/emails/login-subject.html
+++ b/templates/emails/login-subject.html
@@ -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 %}
diff --git a/templates/payments/pricing_not_owner.html b/templates/payments/pricing_not_owner.html
index d489f2c3..56f53476 100644
--- a/templates/payments/pricing_not_owner.html
+++ b/templates/payments/pricing_not_owner.html
@@ -14,16 +14,12 @@
You are currently viewing project {{ request.project }} .
- To manage this team, please log in as {{ request.project.owner.email }} .
+ To manage billing for this project, please log in as {{ request.project.owner.email }} .
-
- Switch to {{ request.profile }}
-
Log Out