diff --git a/CHANGELOG.md b/CHANGELOG.md index 1108b714..b590cade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ### Improvements - Add the EMAIL_USE_VERIFICATION configuration setting (#232) +- Show "Badges" and "Settings" in top navigation (#234) ## 1.6.0 - 2019-04-01 diff --git a/hc/accounts/tests/test_badges.py b/hc/accounts/tests/test_badges.py index 37eaf808..0f86407d 100644 --- a/hc/accounts/tests/test_badges.py +++ b/hc/accounts/tests/test_badges.py @@ -9,7 +9,7 @@ class BadgesTestCase(BaseTestCase): Check.objects.create(project=self.bobs_project, tags="bobs-tag") self.client.login(username="alice@example.org", password="password") - r = self.client.get("/accounts/profile/badges/") + r = self.client.get("/projects/%s/badges/" % self.project.code) self.assertContains(r, "foo.svg") self.assertContains(r, "a-B_1.svg") @@ -27,6 +27,6 @@ class BadgesTestCase(BaseTestCase): self.project.save() self.client.login(username="alice@example.org", password="password") - r = self.client.get("/accounts/profile/badges/") + r = self.client.get("/projects/%s/badges/" % self.project.code) self.assertContains(r, "badge/alices-badge-key/") self.assertContains(r, "badge/alices-badge-key/") diff --git a/hc/accounts/tests/test_project.py b/hc/accounts/tests/test_project.py index a7595083..a2032d58 100644 --- a/hc/accounts/tests/test_project.py +++ b/hc/accounts/tests/test_project.py @@ -5,17 +5,22 @@ from hc.test import BaseTestCase from hc.accounts.models import Member -class ProfileTestCase(BaseTestCase): +class ProjectTestCase(BaseTestCase): def setUp(self): - super(ProfileTestCase, self).setUp() + super(ProjectTestCase, self).setUp() self.url = "/projects/%s/settings/" % self.project.code def test_it_checks_access(self): - self.client.login(username="bob@example.org", password="password") + self.client.login(username="charlie@example.org", password="password") r = self.client.get(self.url) self.assertEqual(r.status_code, 404) + def test_it_allows_team_access(self): + self.client.login(username="bob@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "Change Project Name") + def test_it_shows_api_keys(self): self.project.api_key_readonly = "R" * 32 self.project.save() @@ -78,6 +83,13 @@ class ProfileTestCase(BaseTestCase): " Alice's Project on %s" % settings.SITE_NAME) self.assertEqual(mail.outbox[0].subject, subj) + def test_it_requires_owner_to_add_team_member(self): + self.client.login(username="bob@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_checks_team_size(self): self.profile.team_limit = 0 self.profile.save() @@ -100,6 +112,13 @@ class ProfileTestCase(BaseTestCase): self.bobs_profile.refresh_from_db() self.assertEqual(self.bobs_profile.current_project, None) + def test_it_requires_owner_to_remove_team_member(self): + self.client.login(username="bob@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, 403) + def test_it_checks_membership_when_removing_team_member(self): self.client.login(username="charlie@example.org", password="password") diff --git a/hc/accounts/urls.py b/hc/accounts/urls.py index a85de966..02dbd8ed 100644 --- a/hc/accounts/urls.py +++ b/hc/accounts/urls.py @@ -16,7 +16,6 @@ urlpatterns = [ path('profile/', views.profile, name="hc-profile"), path('profile/notifications/', views.notifications, name="hc-notifications"), - path('profile/badges/', views.badges, name="hc-badges"), path('close/', views.close, name="hc-close"), path('unsubscribe_reports//', diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 8c6ade44..1bf7a06d 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -1,6 +1,5 @@ from datetime import timedelta as td import uuid -import re from django.conf import settings from django.contrib import messages @@ -10,7 +9,8 @@ from django.contrib.auth import authenticate from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.core import signing -from django.http import HttpResponseForbidden, HttpResponseBadRequest +from django.http import (HttpResponseForbidden, HttpResponseBadRequest, + HttpResponseNotFound) from django.shortcuts import get_object_or_404, redirect, render from django.utils.timezone import now from django.urls import resolve, Resolver404 @@ -23,7 +23,6 @@ from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm, ExistingEmailForm) from hc.accounts.models import Profile, Project, Member from hc.api.models import Channel, Check -from hc.lib.badges import get_badge_url from hc.payments.models import Subscription NEXT_WHITELIST = ("hc-checks", @@ -238,12 +237,22 @@ def add_project(request): @login_required def project(request, code): - project = get_object_or_404(Project, code=code, owner=request.user) + if request.user.is_superuser: + q = Project.objects + else: + q = request.profile.projects() + try: + project = q.get(code=code) + except Project.DoesNotExist: + return HttpResponseNotFound() + + is_owner = project.owner_id == request.user.id ctx = { "page": "project", "project": project, - "show_api_keys": False, + "is_owner": is_owner, + "show_api_keys": "show_api_keys" in request.GET, "project_name_status": "default", "api_status": "default", "team_status": "default" @@ -267,7 +276,7 @@ def project(request, code): elif "show_api_keys" in request.POST: ctx["show_api_keys"] = True elif "invite_team_member" in request.POST: - if not project.can_invite(): + if not is_owner or not project.can_invite(): return HttpResponseForbidden() form = InviteTeamMemberForm(request.POST) @@ -284,6 +293,9 @@ def project(request, code): ctx["team_status"] = "success" elif "remove_team_member" in request.POST: + if not is_owner: + return HttpResponseForbidden() + form = RemoveTeamMemberForm(request.POST) if form.is_valid(): q = User.objects @@ -354,37 +366,6 @@ def notifications(request): return render(request, "accounts/notifications.html", ctx) -@login_required -def badges(request): - badge_sets = [] - for project in request.profile.projects(): - tags = set() - for check in Check.objects.filter(project=project): - tags.update(check.tags_list()) - - sorted_tags = sorted(tags, key=lambda s: s.lower()) - sorted_tags.append("*") # For the "overall status" badge - - urls = [] - for tag in sorted_tags: - if not re.match("^[\w-]+$", tag) and tag != "*": - continue - - urls.append({ - "svg": get_badge_url(project.badge_key, tag), - "json": get_badge_url(project.badge_key, tag, format="json"), - }) - - badge_sets.append({"project": project, "urls": urls}) - - ctx = { - "page": "profile", - "badges": badge_sets - } - - return render(request, "accounts/badges.html", ctx) - - @login_required def set_password(request, token): if not request.profile.check_token(token, "set-password"): diff --git a/hc/front/urls.py b/hc/front/urls.py index 4b9df356..d9785dd8 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -50,6 +50,7 @@ channel_urls = [ urlpatterns = [ path('', views.index, name="hc-index"), path('projects//checks/', views.my_checks, name="hc-checks"), + path('projects//badges/', views.badges, name="hc-badges"), path('projects//checks/add/', views.add_check, name="hc-add-check"), path('checks/cron_preview/', views.cron_preview), path('projects//checks/status/', views.status, name="hc-status"), diff --git a/hc/front/views.py b/hc/front/views.py index f41bb324..ad2ebfab 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta as td import json +import re from urllib.parse import urlencode from croniter import croniter @@ -29,6 +30,7 @@ from hc.front.schemas import telegram_callback from hc.front.templatetags.hc_extras import (num_down_title, down_title, sortchecks) from hc.lib import jsonschema +from hc.lib.badges import get_badge_url import pytz from pytz.exceptions import UnknownTimeZoneError import requests @@ -441,6 +443,7 @@ def log(request, code): limit = check.project.owner_profile.ping_log_limit ctx = { + "project": check.project, "check": check, "events": _get_events(check, limit), "limit": limit, @@ -459,6 +462,7 @@ def details(request, code): ctx = { "page": "details", + "project": check.project, "check": check, "channels": channels, "timezones": pytz.all_timezones @@ -515,6 +519,38 @@ def status_single(request, code): return JsonResponse(doc) +@login_required +def badges(request, code): + project = _get_project_for_user(request, code) + + tags = set() + for check in Check.objects.filter(project=project): + tags.update(check.tags_list()) + + sorted_tags = sorted(tags, key=lambda s: s.lower()) + sorted_tags.append("*") # For the "overall status" badge + + urls = [] + for tag in sorted_tags: + if not re.match("^[\w-]+$", tag) and tag != "*": + continue + + urls.append({ + "tag": tag, + "svg": get_badge_url(project.badge_key, tag), + "json": get_badge_url(project.badge_key, tag, format="json"), + }) + + ctx = { + "have_tags": len(urls) > 1, + "page": "badges", + "project": project, + "badges": urls + } + + return render(request, "front/badges.html", ctx) + + @login_required def channels(request): if request.method == "POST": @@ -547,6 +583,7 @@ def channels(request): ctx = { "page": "channels", + "project": request.project, "profile": request.project.owner_profile, "channels": channels, "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, diff --git a/static/css/base.css b/static/css/base.css index 5b5c2291..8a8dd8ca 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -41,6 +41,10 @@ body { font-size: small; } +#global-links > li > a { + font-size: small; +} + @media (min-width: 768px) { .navbar-default .navbar-nav > li.active > a, .navbar-default .navbar-nav > li > a:hover { border-bottom: 5px solid #eee; @@ -48,6 +52,11 @@ body { } } +.navbar-default .navbar-brand, .navbar-default .navbar-brand:hover { + color: #333; + font-weight: bold; +} + .navbar-text { font-size: small; } @@ -115,3 +124,16 @@ pre { .badge-down { background-color: #d9534f; } + +.dropdown-menu > li.project-item a { + min-width: 200px; + padding: 6px 20px 3px 20px; +} + +.project-item .name { + display: inline-block; + max-width: 130px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/templates/accounts/badges.html b/templates/accounts/badges.html deleted file mode 100644 index 4c7bd9d9..00000000 --- a/templates/accounts/badges.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "base.html" %} -{% load compress static hc_extras %} - -{% block title %}Account Settings - {% site_name %}{% endblock %} - -{% block content %} -
-
-

Settings {{ request.user.email }}

-
-
- -
-
- -
- -
-
-
-

Status Badges

-

- {% site_name %} provides status badges for each of the tags - you have used. Additionally, the "{% site_name %}" - badge shows the overall status of all checks in a - project. The badges have public, but hard-to-guess - URLs. You can use them in your READMEs, - dashboards or status pages. -

- -
- - -
- - - {% for badge_set in badges %} - - - - {% for urldict in badge_set.urls %} - - - - - {% endfor %} - {% endfor %} -
{{ badge_set.project }}
- - - {{ urldict.svg }} -
- - {% for badge_set in badges %} - - - - - {% for urldict in badge_set.urls %} - - - - - {% endfor %} - {% endfor %} -
{{ badge_set.project }}
- - {{ urldict.json }} -
-
-
-
-
-{% endblock %} - -{% block scripts %} -{% compress js %} - - - -{% endcompress %} -{% endblock %} diff --git a/templates/accounts/billing.html b/templates/accounts/billing.html index 23d6f724..2933f843 100644 --- a/templates/accounts/billing.html +++ b/templates/accounts/billing.html @@ -19,7 +19,6 @@
  • Account
  • Billing
  • Email Reports
  • -
  • Badges
  • diff --git a/templates/accounts/notifications.html b/templates/accounts/notifications.html index b52f405f..dcea4e06 100644 --- a/templates/accounts/notifications.html +++ b/templates/accounts/notifications.html @@ -21,7 +21,6 @@
  • Billing
  • {% endif %}
  • Email Reports
  • -
  • Badges
  • diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 6b8eff74..3b821ede 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -29,7 +29,6 @@
  • Billing
  • {% endif %}
  • Email Reports
  • -
  • Badges
  • diff --git a/templates/accounts/project.html b/templates/accounts/project.html index b32eee0a..e6a72bf1 100644 --- a/templates/accounts/project.html +++ b/templates/accounts/project.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "base_project.html" %} {% load compress static hc_extras %} {% block title %}Project Settings - {{ project }}{% endblock %} @@ -7,7 +7,6 @@ {% block content %}
    -

    Project Settings

    {% for message in messages %}

    {{ message }}

    {% endfor %} @@ -103,10 +102,12 @@ {{ member.user.email }} Member + {% if is_owner %} Remove + {% endif %} {% endfor %} @@ -121,7 +122,8 @@
    - {% if project.can_invite %} + {% if is_owner %} + {% if project.can_invite%} upgrade your account!
    {% endif %} + {% endif %}
    {% if team_member_invited %} @@ -149,6 +152,7 @@ {% endif %} + {% if is_owner %}
    {% csrf_token %} @@ -163,6 +167,7 @@
    + {% endif %} diff --git a/templates/base.html b/templates/base.html index 1576710b..35da84fe 100644 --- a/templates/base.html +++ b/templates/base.html @@ -88,18 +88,7 @@