From 32adb64dc0fc57bfa1bcc89c201d5f46c66859f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Mon, 11 Oct 2021 11:33:18 +0200
Subject: [PATCH 01/29] Add api app with ApiToken model
---
shynet/api/__init__.py | 0
shynet/api/admin.py | 3 +++
shynet/api/apps.py | 6 ++++++
shynet/api/migrations/0001_initial.py | 30 +++++++++++++++++++++++++++
shynet/api/migrations/__init__.py | 0
shynet/api/models.py | 19 +++++++++++++++++
shynet/api/tests.py | 3 +++
shynet/api/views.py | 3 +++
shynet/shynet/settings.py | 1 +
9 files changed, 65 insertions(+)
create mode 100644 shynet/api/__init__.py
create mode 100644 shynet/api/admin.py
create mode 100644 shynet/api/apps.py
create mode 100644 shynet/api/migrations/0001_initial.py
create mode 100644 shynet/api/migrations/__init__.py
create mode 100644 shynet/api/models.py
create mode 100644 shynet/api/tests.py
create mode 100644 shynet/api/views.py
diff --git a/shynet/api/__init__.py b/shynet/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shynet/api/admin.py b/shynet/api/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/shynet/api/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/shynet/api/apps.py b/shynet/api/apps.py
new file mode 100644
index 0000000..66656fd
--- /dev/null
+++ b/shynet/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'api'
diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py
new file mode 100644
index 0000000..b6ebdc0
--- /dev/null
+++ b/shynet/api/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.2.5 on 2021-10-11 09:31
+
+import api.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ApiToken',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=64)),
+ ('value', models.TextField(default=api.models._default_token_value, unique=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ['name', 'value'],
+ },
+ ),
+ ]
diff --git a/shynet/api/migrations/__init__.py b/shynet/api/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shynet/api/models.py b/shynet/api/models.py
new file mode 100644
index 0000000..800f726
--- /dev/null
+++ b/shynet/api/models.py
@@ -0,0 +1,19 @@
+from django.db import models
+from core.models import User
+from secrets import token_urlsafe
+
+
+def _default_token_value():
+ return token_urlsafe(32)
+
+
+class ApiToken(models.Model):
+ name = models.CharField(max_length=64)
+ value = models.TextField(default=_default_token_value, unique=True)
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens")
+
+ class Meta:
+ ordering = ["name", "value"]
+
+ def __str__(self):
+ return self.name
diff --git a/shynet/api/tests.py b/shynet/api/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/shynet/api/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/shynet/api/views.py b/shynet/api/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/shynet/api/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/shynet/shynet/settings.py b/shynet/shynet/settings.py
index 709ee75..1774342 100644
--- a/shynet/shynet/settings.py
+++ b/shynet/shynet/settings.py
@@ -59,6 +59,7 @@ INSTALLED_APPS = [
"core",
"dashboard.apps.DashboardConfig",
"analytics",
+ "api",
"allauth",
"allauth.account",
"allauth.socialaccount",
From bec4b193668c1ccb468f05428f1ba7126c3154ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Mon, 11 Oct 2021 12:37:01 +0200
Subject: [PATCH 02/29] Add ApiToken to admin
---
shynet/api/admin.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/shynet/api/admin.py b/shynet/api/admin.py
index 8c38f3f..4b08f5a 100644
--- a/shynet/api/admin.py
+++ b/shynet/api/admin.py
@@ -1,3 +1,9 @@
from django.contrib import admin
+from api.models import ApiToken
-# Register your models here.
+
+class ApiTokenAdmin(admin.ModelAdmin):
+ list_display = ("name", "user", "value")
+
+
+admin.site.register(ApiToken, ApiTokenAdmin)
From 90b2896ded7926660de8b32d3cefe2b0dedc80bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 13 Oct 2021 16:01:31 +0200
Subject: [PATCH 03/29] Add ApiTokenRequiredMixin
---
shynet/api/mixins.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 shynet/api/mixins.py
diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py
new file mode 100644
index 0000000..380e009
--- /dev/null
+++ b/shynet/api/mixins.py
@@ -0,0 +1,24 @@
+from django.http import JsonResponse
+from django.contrib.auth.models import AnonymousUser
+from .models import ApiToken
+
+
+class ApiTokenRequiredMixin:
+ def _get_user_by_token(self, request):
+ token = request.headers.get('Authorization')
+ if not token or not token.startswith('Token '):
+ return AnonymousUser()
+
+ token = token.split(' ')[1]
+ api_token = ApiToken.objects.filter(value=token).first()
+ if not api_token:
+ return AnonymousUser()
+
+ return api_token.user
+
+ def dispatch(self, request, *args, **kwargs):
+ request.user = self._get_user_by_token(request)
+ if not request.user.is_authenticated:
+ return JsonResponse(data={}, status=403)
+
+ return super().dispatch(request, *args, **kwargs)
From a963694fd0958df65ecdd422034076a2123b2004 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 13 Oct 2021 19:21:52 +0200
Subject: [PATCH 04/29] Add DashboardApiView
---
shynet/api/urls.py | 7 +++++++
shynet/api/views.py | 30 ++++++++++++++++++++++++++++--
shynet/shynet/urls.py | 1 +
3 files changed, 36 insertions(+), 2 deletions(-)
create mode 100644 shynet/api/urls.py
diff --git a/shynet/api/urls.py b/shynet/api/urls.py
new file mode 100644
index 0000000..11d877f
--- /dev/null
+++ b/shynet/api/urls.py
@@ -0,0 +1,7 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("dashboard/", views.DashboardApiView.as_view(), name="services"),
+]
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 91ea44a..e3b5290 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -1,3 +1,29 @@
-from django.shortcuts import render
+from django.http import JsonResponse
+from django.db.models import Q
+from django.db.models.query import QuerySet
+from django.views.generic import View
-# Create your views here.
+from dashboard.mixins import DateRangeMixin
+from core.models import Service
+
+from .mixins import ApiTokenRequiredMixin
+
+
+class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
+ def get(self, request, *args, **kwargs):
+ services = Service.objects.filter(
+ Q(owner=request.user) | Q(collaborators__in=[request.user])
+ ).distinct()
+
+ start = self.get_start_date()
+ end = self.get_end_date()
+ services_data = [s.get_core_stats(start, end) for s in services]
+ for service_data in services_data:
+ for key, value in service_data.items():
+ if isinstance(value, QuerySet):
+ service_data[key] = list(value)
+ for key, value in service_data['compare'].items():
+ if isinstance(value, QuerySet):
+ service_data['compare'][key] = list(value)
+
+ return JsonResponse(data={'services': services_data})
diff --git a/shynet/shynet/urls.py b/shynet/shynet/urls.py
index 7264261..71aabe6 100644
--- a/shynet/shynet/urls.py
+++ b/shynet/shynet/urls.py
@@ -25,4 +25,5 @@ urlpatterns = [
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
path("healthz/", include("health_check.urls")),
path("", include(("core.urls", "core"), namespace="core")),
+ path("api/", include(("api.urls", "api"), namespace="api")),
]
From 2f8891a8437a924daf261f97c8ae80291dcd3de5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 13 Oct 2021 19:52:36 +0200
Subject: [PATCH 05/29] Add minimal argument to get_core_stats
---
shynet/api/views.py | 3 ++-
shynet/core/models.py | 39 +++++++++++++++++++++++++++------------
2 files changed, 29 insertions(+), 13 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index e3b5290..7968ede 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -15,9 +15,10 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
Q(owner=request.user) | Q(collaborators__in=[request.user])
).distinct()
+ minimal = request.GET.get('minimal').lower() in ('1', 'true')
start = self.get_start_date()
end = self.get_end_date()
- services_data = [s.get_core_stats(start, end) for s in services]
+ services_data = [s.get_core_stats(start, end, minimal) for s in services]
for service_data in services_data:
for key, value in service_data.items():
if isinstance(value, QuerySet):
diff --git a/shynet/core/models.py b/shynet/core/models.py
index 9568f58..9f78b00 100644
--- a/shynet/core/models.py
+++ b/shynet/core/models.py
@@ -107,21 +107,21 @@ class Service(models.Model):
start_time=timezone.now() - timezone.timedelta(days=1)
)
- def get_core_stats(self, start_time=None, end_time=None):
+ def get_core_stats(self, start_time=None, end_time=None, minimal=False):
if start_time is None:
start_time = timezone.now() - timezone.timedelta(days=30)
if end_time is None:
end_time = timezone.now()
- main_data = self.get_relative_stats(start_time, end_time)
+ main_data = self.get_relative_stats(start_time, end_time, minimal)
comparison_data = self.get_relative_stats(
- start_time - (end_time - start_time), start_time
+ start_time - (end_time - start_time), start_time, minimal
)
main_data["compare"] = comparison_data
return main_data
- def get_relative_stats(self, start_time, end_time):
+ def get_relative_stats(self, start_time, end_time, minimal=False):
Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit")
@@ -146,6 +146,28 @@ class Service(models.Model):
bounces = sessions.filter(is_bounce=True)
bounce_count = bounces.count()
+ avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
+ "load_time__avg"
+ ]
+
+ avg_hits_per_session = hit_count / session_count if session_count > 0 else None
+
+ avg_session_duration = self._get_avg_session_duration(sessions, session_count)
+ if minimal:
+ return {
+ "currently_online": currently_online,
+ "session_count": session_count,
+ "hit_count": hit_count,
+ "has_hits": has_hits,
+ "bounce_rate_pct": bounce_count * 100 / session_count
+ if session_count > 0
+ else None,
+ "avg_session_duration": avg_session_duration,
+ "avg_load_time": avg_load_time,
+ "avg_hits_per_session": avg_hits_per_session,
+ "online": True,
+ }
+
locations = (
hits.values("location")
.annotate(count=models.Count("location"))
@@ -192,17 +214,10 @@ class Service(models.Model):
.order_by("-count")
)
- avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
- "load_time__avg"
- ]
-
- avg_hits_per_session = hit_count / session_count if session_count > 0 else None
-
- avg_session_duration = self._get_avg_session_duration(sessions, session_count)
-
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now
)
+
return {
"currently_online": currently_online,
"session_count": session_count,
From d62d48c7b402cf80f4c1cb85baa1dd9d6ad4fece Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Thu, 14 Oct 2021 07:38:21 +0200
Subject: [PATCH 06/29] Add uuid filter and service uuid filter
---
shynet/api/views.py | 37 ++++++++++++++++++++++++++++---------
1 file changed, 28 insertions(+), 9 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 7968ede..77b6142 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -15,16 +15,35 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
Q(owner=request.user) | Q(collaborators__in=[request.user])
).distinct()
- minimal = request.GET.get('minimal').lower() in ('1', 'true')
+ uuid = request.GET.get('uuid')
+ if uuid:
+ services = services.filter(uuid=uuid)
+
+ minimal = request.GET.get('minimal', '0').lower() in ('1', 'true')
start = self.get_start_date()
end = self.get_end_date()
- services_data = [s.get_core_stats(start, end, minimal) for s in services]
- for service_data in services_data:
- for key, value in service_data.items():
- if isinstance(value, QuerySet):
- service_data[key] = list(value)
- for key, value in service_data['compare'].items():
- if isinstance(value, QuerySet):
- service_data['compare'][key] = list(value)
+ services_data = [
+ {
+ 'name': s.name,
+ 'uuid': s.uuid,
+ 'link': s.link,
+ 'stats': s.get_core_stats(start, end, minimal),
+ }
+ for s in services
+ ]
+
+ if not minimal:
+ services_data = self._convert_querysets_to_lists(services_data)
return JsonResponse(data={'services': services_data})
+
+ def _convert_querysets_to_lists(self, services_data):
+ for service_data in services_data:
+ for key, value in service_data['stats'].items():
+ if isinstance(value, QuerySet):
+ service_data['stats'][key] = list(value)
+ for key, value in service_data['stats']['compare'].items():
+ if isinstance(value, QuerySet):
+ service_data['stats']['compare'][key] = list(value)
+
+ return service_data
From 787ce1775f5bd56991a47eb6218e29afcd93d813 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 17 Nov 2021 11:00:52 +0100
Subject: [PATCH 07/29] Move token to User model + add API setting view
---
shynet/api/admin.py | 10 +------
shynet/api/migrations/0001_initial.py | 30 -------------------
shynet/api/mixins.py | 9 +++---
shynet/api/models.py | 20 +------------
.../migrations/0009_auto_20211117_0217.py | 24 +++++++++++++++
shynet/core/models.py | 8 ++++-
shynet/dashboard/templates/base.html | 3 ++
.../dashboard/pages/api_settings.html | 22 ++++++++++++++
.../dashboard/pages/service_update.html | 7 +++++
shynet/dashboard/urls.py | 14 +++++++--
shynet/dashboard/views.py | 17 +++++++++--
11 files changed, 94 insertions(+), 70 deletions(-)
delete mode 100644 shynet/api/migrations/0001_initial.py
create mode 100644 shynet/core/migrations/0009_auto_20211117_0217.py
create mode 100644 shynet/dashboard/templates/dashboard/pages/api_settings.html
diff --git a/shynet/api/admin.py b/shynet/api/admin.py
index 4b08f5a..d3c1eff 100644
--- a/shynet/api/admin.py
+++ b/shynet/api/admin.py
@@ -1,9 +1 @@
-from django.contrib import admin
-from api.models import ApiToken
-
-
-class ApiTokenAdmin(admin.ModelAdmin):
- list_display = ("name", "user", "value")
-
-
-admin.site.register(ApiToken, ApiTokenAdmin)
+# from django.contrib import admin
diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py
deleted file mode 100644
index b6ebdc0..0000000
--- a/shynet/api/migrations/0001_initial.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 3.2.5 on 2021-10-11 09:31
-
-import api.models
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='ApiToken',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=64)),
- ('value', models.TextField(default=api.models._default_token_value, unique=True)),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'ordering': ['name', 'value'],
- },
- ),
- ]
diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py
index 380e009..47dc9a8 100644
--- a/shynet/api/mixins.py
+++ b/shynet/api/mixins.py
@@ -1,6 +1,7 @@
from django.http import JsonResponse
from django.contrib.auth.models import AnonymousUser
-from .models import ApiToken
+
+from core.models import User
class ApiTokenRequiredMixin:
@@ -10,11 +11,9 @@ class ApiTokenRequiredMixin:
return AnonymousUser()
token = token.split(' ')[1]
- api_token = ApiToken.objects.filter(value=token).first()
- if not api_token:
- return AnonymousUser()
+ user = User.objects.filter(api_token=token).first()
- return api_token.user
+ return user if user else AnonymousUser()
def dispatch(self, request, *args, **kwargs):
request.user = self._get_user_by_token(request)
diff --git a/shynet/api/models.py b/shynet/api/models.py
index 800f726..24e1689 100644
--- a/shynet/api/models.py
+++ b/shynet/api/models.py
@@ -1,19 +1 @@
-from django.db import models
-from core.models import User
-from secrets import token_urlsafe
-
-
-def _default_token_value():
- return token_urlsafe(32)
-
-
-class ApiToken(models.Model):
- name = models.CharField(max_length=64)
- value = models.TextField(default=_default_token_value, unique=True)
- user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens")
-
- class Meta:
- ordering = ["name", "value"]
-
- def __str__(self):
- return self.name
+# from django.db import models
diff --git a/shynet/core/migrations/0009_auto_20211117_0217.py b/shynet/core/migrations/0009_auto_20211117_0217.py
new file mode 100644
index 0000000..461cd1e
--- /dev/null
+++ b/shynet/core/migrations/0009_auto_20211117_0217.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.5 on 2021-11-17 07:17
+
+import core.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0008_auto_20200628_1403'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='api_token',
+ field=models.TextField(default=core.models._default_api_token, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/shynet/core/models.py b/shynet/core/models.py
index 9f78b00..875faba 100644
--- a/shynet/core/models.py
+++ b/shynet/core/models.py
@@ -1,8 +1,9 @@
import ipaddress
-import json
import re
import uuid
+from secrets import token_urlsafe
+
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractUser
@@ -43,9 +44,14 @@ def _parse_network_list(networks: str):
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
+def _default_api_token():
+ return token_urlsafe(32)
+
+
class User(AbstractUser):
username = models.TextField(default=_default_uuid, unique=True)
email = models.EmailField(unique=True)
+ api_token = models.TextField(default=_default_api_token, unique=True)
def __str__(self):
return self.email
diff --git a/shynet/dashboard/templates/base.html b/shynet/dashboard/templates/base.html
index f4d2f67..113d602 100644
--- a/shynet/dashboard/templates/base.html
+++ b/shynet/dashboard/templates/base.html
@@ -88,6 +88,9 @@
{% url 'account_set_password' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %}
+ {% url 'dashboard:api_settings' as url %}
+ {% include 'dashboard/includes/sidebar_portal.html' with label="API" url=url %}
+
{% url 'account_logout' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %}
diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html
new file mode 100644
index 0000000..fed6882
--- /dev/null
+++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 097335f..90c56eb 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -30,5 +30,12 @@
+
+
+
API Token
+
+ {{request.user.api_token}}
+
+
{% endblock %}
diff --git a/shynet/dashboard/urls.py b/shynet/dashboard/urls.py
index 328ce58..e107a13 100644
--- a/shynet/dashboard/urls.py
+++ b/shynet/dashboard/urls.py
@@ -1,6 +1,4 @@
-from django.contrib import admin
-from django.urls import include, path
-from django.views.generic import RedirectView
+from django.urls import path
from . import views
@@ -28,4 +26,14 @@ urlpatterns = [
views.ServiceSessionView.as_view(),
name="service_session",
),
+ path(
+ "api-settings/",
+ views.ApiSettingsView.as_view(),
+ name="api_settings",
+ ),
+ path(
+ "api-token-refresh/",
+ views.RefreshApiTokenView.as_view(),
+ name="api_token_refresh",
+ ),
]
diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py
index 9ce02ce..96a59a8 100644
--- a/shynet/dashboard/views.py
+++ b/shynet/dashboard/views.py
@@ -3,8 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.db.models import Q
-from django.shortcuts import get_object_or_404, reverse
-from django.utils import timezone
+from django.shortcuts import get_object_or_404, reverse, redirect
from django.views.generic import (
CreateView,
DeleteView,
@@ -12,11 +11,12 @@ from django.views.generic import (
ListView,
TemplateView,
UpdateView,
+ View,
)
from rules.contrib.views import PermissionRequiredMixin
from analytics.models import Session
-from core.models import Service
+from core.models import Service, _default_api_token
from .forms import ServiceForm
from .mixins import DateRangeMixin
@@ -155,3 +155,14 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
data = super().get_context_data(**kwargs)
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
return data
+
+
+class ApiSettingsView(LoginRequiredMixin, TemplateView):
+ template_name = "dashboard/pages/api_settings.html"
+
+
+class RefreshApiTokenView(LoginRequiredMixin, View):
+ def get(self, request):
+ request.user.api_token = _default_api_token()
+ request.user.save()
+ return redirect('dashboard:api_settings')
From 8302aedaa793a678c3dc24a6326706839e057e64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 17 Nov 2021 11:46:35 +0100
Subject: [PATCH 08/29] Fix problem with whitespaces in copied token
---
shynet/dashboard/templates/dashboard/pages/api_settings.html | 4 +---
.../dashboard/templates/dashboard/pages/service_update.html | 4 +---
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html
index fed6882..b242766 100644
--- a/shynet/dashboard/templates/dashboard/pages/api_settings.html
+++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html
@@ -8,9 +8,7 @@
Token
-
- {{request.user.api_token}}
-
+
{{request.user.api_token}}
Refresh token
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 90c56eb..c63a176 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -33,9 +33,7 @@
API Token
-
- {{request.user.api_token}}
-
+
{{request.user.api_token}}
{% endblock %}
From ff6933b4de360d4e72d10e46aef5d210bcb34be5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Mon, 11 Oct 2021 11:33:18 +0200
Subject: [PATCH 09/29] Add api app with ApiToken model
---
shynet/api/__init__.py | 0
shynet/api/admin.py | 3 +++
shynet/api/apps.py | 6 ++++++
shynet/api/migrations/0001_initial.py | 30 +++++++++++++++++++++++++++
shynet/api/migrations/__init__.py | 0
shynet/api/models.py | 19 +++++++++++++++++
shynet/api/tests.py | 3 +++
shynet/api/views.py | 3 +++
shynet/shynet/settings.py | 1 +
9 files changed, 65 insertions(+)
create mode 100644 shynet/api/__init__.py
create mode 100644 shynet/api/admin.py
create mode 100644 shynet/api/apps.py
create mode 100644 shynet/api/migrations/0001_initial.py
create mode 100644 shynet/api/migrations/__init__.py
create mode 100644 shynet/api/models.py
create mode 100644 shynet/api/tests.py
create mode 100644 shynet/api/views.py
diff --git a/shynet/api/__init__.py b/shynet/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shynet/api/admin.py b/shynet/api/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/shynet/api/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/shynet/api/apps.py b/shynet/api/apps.py
new file mode 100644
index 0000000..66656fd
--- /dev/null
+++ b/shynet/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'api'
diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py
new file mode 100644
index 0000000..b6ebdc0
--- /dev/null
+++ b/shynet/api/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.2.5 on 2021-10-11 09:31
+
+import api.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ApiToken',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=64)),
+ ('value', models.TextField(default=api.models._default_token_value, unique=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ['name', 'value'],
+ },
+ ),
+ ]
diff --git a/shynet/api/migrations/__init__.py b/shynet/api/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/shynet/api/models.py b/shynet/api/models.py
new file mode 100644
index 0000000..800f726
--- /dev/null
+++ b/shynet/api/models.py
@@ -0,0 +1,19 @@
+from django.db import models
+from core.models import User
+from secrets import token_urlsafe
+
+
+def _default_token_value():
+ return token_urlsafe(32)
+
+
+class ApiToken(models.Model):
+ name = models.CharField(max_length=64)
+ value = models.TextField(default=_default_token_value, unique=True)
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens")
+
+ class Meta:
+ ordering = ["name", "value"]
+
+ def __str__(self):
+ return self.name
diff --git a/shynet/api/tests.py b/shynet/api/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/shynet/api/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/shynet/api/views.py b/shynet/api/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/shynet/api/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/shynet/shynet/settings.py b/shynet/shynet/settings.py
index e461481..9232d65 100644
--- a/shynet/shynet/settings.py
+++ b/shynet/shynet/settings.py
@@ -59,6 +59,7 @@ INSTALLED_APPS = [
"core",
"dashboard.apps.DashboardConfig",
"analytics",
+ "api",
"allauth",
"allauth.account",
"allauth.socialaccount",
From 1dec03c724dd5c7129ebd51025989b8ac026a9be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Mon, 11 Oct 2021 12:37:01 +0200
Subject: [PATCH 10/29] Add ApiToken to admin
---
shynet/api/admin.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/shynet/api/admin.py b/shynet/api/admin.py
index 8c38f3f..4b08f5a 100644
--- a/shynet/api/admin.py
+++ b/shynet/api/admin.py
@@ -1,3 +1,9 @@
from django.contrib import admin
+from api.models import ApiToken
-# Register your models here.
+
+class ApiTokenAdmin(admin.ModelAdmin):
+ list_display = ("name", "user", "value")
+
+
+admin.site.register(ApiToken, ApiTokenAdmin)
From a7248cd54b7f4979e0cf21e8ebbdf16f7f36d4f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 13 Oct 2021 16:01:31 +0200
Subject: [PATCH 11/29] Add ApiTokenRequiredMixin
---
shynet/api/mixins.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 shynet/api/mixins.py
diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py
new file mode 100644
index 0000000..380e009
--- /dev/null
+++ b/shynet/api/mixins.py
@@ -0,0 +1,24 @@
+from django.http import JsonResponse
+from django.contrib.auth.models import AnonymousUser
+from .models import ApiToken
+
+
+class ApiTokenRequiredMixin:
+ def _get_user_by_token(self, request):
+ token = request.headers.get('Authorization')
+ if not token or not token.startswith('Token '):
+ return AnonymousUser()
+
+ token = token.split(' ')[1]
+ api_token = ApiToken.objects.filter(value=token).first()
+ if not api_token:
+ return AnonymousUser()
+
+ return api_token.user
+
+ def dispatch(self, request, *args, **kwargs):
+ request.user = self._get_user_by_token(request)
+ if not request.user.is_authenticated:
+ return JsonResponse(data={}, status=403)
+
+ return super().dispatch(request, *args, **kwargs)
From 5966ea2f84c38e33697e441cd2f95d94f00d945e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 13 Oct 2021 19:21:52 +0200
Subject: [PATCH 12/29] Add DashboardApiView
---
shynet/api/urls.py | 7 +++++++
shynet/api/views.py | 30 ++++++++++++++++++++++++++++--
shynet/shynet/urls.py | 1 +
3 files changed, 36 insertions(+), 2 deletions(-)
create mode 100644 shynet/api/urls.py
diff --git a/shynet/api/urls.py b/shynet/api/urls.py
new file mode 100644
index 0000000..11d877f
--- /dev/null
+++ b/shynet/api/urls.py
@@ -0,0 +1,7 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("dashboard/", views.DashboardApiView.as_view(), name="services"),
+]
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 91ea44a..e3b5290 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -1,3 +1,29 @@
-from django.shortcuts import render
+from django.http import JsonResponse
+from django.db.models import Q
+from django.db.models.query import QuerySet
+from django.views.generic import View
-# Create your views here.
+from dashboard.mixins import DateRangeMixin
+from core.models import Service
+
+from .mixins import ApiTokenRequiredMixin
+
+
+class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
+ def get(self, request, *args, **kwargs):
+ services = Service.objects.filter(
+ Q(owner=request.user) | Q(collaborators__in=[request.user])
+ ).distinct()
+
+ start = self.get_start_date()
+ end = self.get_end_date()
+ services_data = [s.get_core_stats(start, end) for s in services]
+ for service_data in services_data:
+ for key, value in service_data.items():
+ if isinstance(value, QuerySet):
+ service_data[key] = list(value)
+ for key, value in service_data['compare'].items():
+ if isinstance(value, QuerySet):
+ service_data['compare'][key] = list(value)
+
+ return JsonResponse(data={'services': services_data})
diff --git a/shynet/shynet/urls.py b/shynet/shynet/urls.py
index 7264261..71aabe6 100644
--- a/shynet/shynet/urls.py
+++ b/shynet/shynet/urls.py
@@ -25,4 +25,5 @@ urlpatterns = [
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
path("healthz/", include("health_check.urls")),
path("", include(("core.urls", "core"), namespace="core")),
+ path("api/", include(("api.urls", "api"), namespace="api")),
]
From e577aa4997bcc693e9e98ca160aff946e3dbf965 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 13 Oct 2021 19:52:36 +0200
Subject: [PATCH 13/29] Add minimal argument to get_core_stats
---
shynet/api/views.py | 3 ++-
shynet/core/models.py | 39 +++++++++++++++++++++++++++------------
2 files changed, 29 insertions(+), 13 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index e3b5290..7968ede 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -15,9 +15,10 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
Q(owner=request.user) | Q(collaborators__in=[request.user])
).distinct()
+ minimal = request.GET.get('minimal').lower() in ('1', 'true')
start = self.get_start_date()
end = self.get_end_date()
- services_data = [s.get_core_stats(start, end) for s in services]
+ services_data = [s.get_core_stats(start, end, minimal) for s in services]
for service_data in services_data:
for key, value in service_data.items():
if isinstance(value, QuerySet):
diff --git a/shynet/core/models.py b/shynet/core/models.py
index 9568f58..9f78b00 100644
--- a/shynet/core/models.py
+++ b/shynet/core/models.py
@@ -107,21 +107,21 @@ class Service(models.Model):
start_time=timezone.now() - timezone.timedelta(days=1)
)
- def get_core_stats(self, start_time=None, end_time=None):
+ def get_core_stats(self, start_time=None, end_time=None, minimal=False):
if start_time is None:
start_time = timezone.now() - timezone.timedelta(days=30)
if end_time is None:
end_time = timezone.now()
- main_data = self.get_relative_stats(start_time, end_time)
+ main_data = self.get_relative_stats(start_time, end_time, minimal)
comparison_data = self.get_relative_stats(
- start_time - (end_time - start_time), start_time
+ start_time - (end_time - start_time), start_time, minimal
)
main_data["compare"] = comparison_data
return main_data
- def get_relative_stats(self, start_time, end_time):
+ def get_relative_stats(self, start_time, end_time, minimal=False):
Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit")
@@ -146,6 +146,28 @@ class Service(models.Model):
bounces = sessions.filter(is_bounce=True)
bounce_count = bounces.count()
+ avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
+ "load_time__avg"
+ ]
+
+ avg_hits_per_session = hit_count / session_count if session_count > 0 else None
+
+ avg_session_duration = self._get_avg_session_duration(sessions, session_count)
+ if minimal:
+ return {
+ "currently_online": currently_online,
+ "session_count": session_count,
+ "hit_count": hit_count,
+ "has_hits": has_hits,
+ "bounce_rate_pct": bounce_count * 100 / session_count
+ if session_count > 0
+ else None,
+ "avg_session_duration": avg_session_duration,
+ "avg_load_time": avg_load_time,
+ "avg_hits_per_session": avg_hits_per_session,
+ "online": True,
+ }
+
locations = (
hits.values("location")
.annotate(count=models.Count("location"))
@@ -192,17 +214,10 @@ class Service(models.Model):
.order_by("-count")
)
- avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
- "load_time__avg"
- ]
-
- avg_hits_per_session = hit_count / session_count if session_count > 0 else None
-
- avg_session_duration = self._get_avg_session_duration(sessions, session_count)
-
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now
)
+
return {
"currently_online": currently_online,
"session_count": session_count,
From d809ec82d9888618f8f7a74f920409233552f4f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Thu, 14 Oct 2021 07:38:21 +0200
Subject: [PATCH 14/29] Add uuid filter and service uuid filter
---
shynet/api/views.py | 37 ++++++++++++++++++++++++++++---------
1 file changed, 28 insertions(+), 9 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 7968ede..77b6142 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -15,16 +15,35 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
Q(owner=request.user) | Q(collaborators__in=[request.user])
).distinct()
- minimal = request.GET.get('minimal').lower() in ('1', 'true')
+ uuid = request.GET.get('uuid')
+ if uuid:
+ services = services.filter(uuid=uuid)
+
+ minimal = request.GET.get('minimal', '0').lower() in ('1', 'true')
start = self.get_start_date()
end = self.get_end_date()
- services_data = [s.get_core_stats(start, end, minimal) for s in services]
- for service_data in services_data:
- for key, value in service_data.items():
- if isinstance(value, QuerySet):
- service_data[key] = list(value)
- for key, value in service_data['compare'].items():
- if isinstance(value, QuerySet):
- service_data['compare'][key] = list(value)
+ services_data = [
+ {
+ 'name': s.name,
+ 'uuid': s.uuid,
+ 'link': s.link,
+ 'stats': s.get_core_stats(start, end, minimal),
+ }
+ for s in services
+ ]
+
+ if not minimal:
+ services_data = self._convert_querysets_to_lists(services_data)
return JsonResponse(data={'services': services_data})
+
+ def _convert_querysets_to_lists(self, services_data):
+ for service_data in services_data:
+ for key, value in service_data['stats'].items():
+ if isinstance(value, QuerySet):
+ service_data['stats'][key] = list(value)
+ for key, value in service_data['stats']['compare'].items():
+ if isinstance(value, QuerySet):
+ service_data['stats']['compare'][key] = list(value)
+
+ return service_data
From 66b841fd86a398e3a738ece90d74ea6f5b11f83a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 17 Nov 2021 11:00:52 +0100
Subject: [PATCH 15/29] Move token to User model + add API setting view
---
shynet/api/admin.py | 10 +------
shynet/api/migrations/0001_initial.py | 30 -------------------
shynet/api/mixins.py | 9 +++---
shynet/api/models.py | 20 +------------
.../migrations/0009_auto_20211117_0217.py | 24 +++++++++++++++
shynet/core/models.py | 8 ++++-
shynet/dashboard/templates/base.html | 3 ++
.../dashboard/pages/api_settings.html | 22 ++++++++++++++
.../dashboard/pages/service_update.html | 7 +++++
shynet/dashboard/urls.py | 14 +++++++--
shynet/dashboard/views.py | 17 +++++++++--
11 files changed, 94 insertions(+), 70 deletions(-)
delete mode 100644 shynet/api/migrations/0001_initial.py
create mode 100644 shynet/core/migrations/0009_auto_20211117_0217.py
create mode 100644 shynet/dashboard/templates/dashboard/pages/api_settings.html
diff --git a/shynet/api/admin.py b/shynet/api/admin.py
index 4b08f5a..d3c1eff 100644
--- a/shynet/api/admin.py
+++ b/shynet/api/admin.py
@@ -1,9 +1 @@
-from django.contrib import admin
-from api.models import ApiToken
-
-
-class ApiTokenAdmin(admin.ModelAdmin):
- list_display = ("name", "user", "value")
-
-
-admin.site.register(ApiToken, ApiTokenAdmin)
+# from django.contrib import admin
diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py
deleted file mode 100644
index b6ebdc0..0000000
--- a/shynet/api/migrations/0001_initial.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 3.2.5 on 2021-10-11 09:31
-
-import api.models
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='ApiToken',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=64)),
- ('value', models.TextField(default=api.models._default_token_value, unique=True)),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'ordering': ['name', 'value'],
- },
- ),
- ]
diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py
index 380e009..47dc9a8 100644
--- a/shynet/api/mixins.py
+++ b/shynet/api/mixins.py
@@ -1,6 +1,7 @@
from django.http import JsonResponse
from django.contrib.auth.models import AnonymousUser
-from .models import ApiToken
+
+from core.models import User
class ApiTokenRequiredMixin:
@@ -10,11 +11,9 @@ class ApiTokenRequiredMixin:
return AnonymousUser()
token = token.split(' ')[1]
- api_token = ApiToken.objects.filter(value=token).first()
- if not api_token:
- return AnonymousUser()
+ user = User.objects.filter(api_token=token).first()
- return api_token.user
+ return user if user else AnonymousUser()
def dispatch(self, request, *args, **kwargs):
request.user = self._get_user_by_token(request)
diff --git a/shynet/api/models.py b/shynet/api/models.py
index 800f726..24e1689 100644
--- a/shynet/api/models.py
+++ b/shynet/api/models.py
@@ -1,19 +1 @@
-from django.db import models
-from core.models import User
-from secrets import token_urlsafe
-
-
-def _default_token_value():
- return token_urlsafe(32)
-
-
-class ApiToken(models.Model):
- name = models.CharField(max_length=64)
- value = models.TextField(default=_default_token_value, unique=True)
- user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens")
-
- class Meta:
- ordering = ["name", "value"]
-
- def __str__(self):
- return self.name
+# from django.db import models
diff --git a/shynet/core/migrations/0009_auto_20211117_0217.py b/shynet/core/migrations/0009_auto_20211117_0217.py
new file mode 100644
index 0000000..461cd1e
--- /dev/null
+++ b/shynet/core/migrations/0009_auto_20211117_0217.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.5 on 2021-11-17 07:17
+
+import core.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0008_auto_20200628_1403'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='api_token',
+ field=models.TextField(default=core.models._default_api_token, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/shynet/core/models.py b/shynet/core/models.py
index 9f78b00..875faba 100644
--- a/shynet/core/models.py
+++ b/shynet/core/models.py
@@ -1,8 +1,9 @@
import ipaddress
-import json
import re
import uuid
+from secrets import token_urlsafe
+
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractUser
@@ -43,9 +44,14 @@ def _parse_network_list(networks: str):
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
+def _default_api_token():
+ return token_urlsafe(32)
+
+
class User(AbstractUser):
username = models.TextField(default=_default_uuid, unique=True)
email = models.EmailField(unique=True)
+ api_token = models.TextField(default=_default_api_token, unique=True)
def __str__(self):
return self.email
diff --git a/shynet/dashboard/templates/base.html b/shynet/dashboard/templates/base.html
index f4d2f67..113d602 100644
--- a/shynet/dashboard/templates/base.html
+++ b/shynet/dashboard/templates/base.html
@@ -88,6 +88,9 @@
{% url 'account_set_password' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %}
+ {% url 'dashboard:api_settings' as url %}
+ {% include 'dashboard/includes/sidebar_portal.html' with label="API" url=url %}
+
{% url 'account_logout' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %}
diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html
new file mode 100644
index 0000000..fed6882
--- /dev/null
+++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 9036de8..7a9713b 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -30,5 +30,12 @@
+
+
+
API Token
+
+ {{request.user.api_token}}
+
+
{% endblock %}
diff --git a/shynet/dashboard/urls.py b/shynet/dashboard/urls.py
index 328ce58..e107a13 100644
--- a/shynet/dashboard/urls.py
+++ b/shynet/dashboard/urls.py
@@ -1,6 +1,4 @@
-from django.contrib import admin
-from django.urls import include, path
-from django.views.generic import RedirectView
+from django.urls import path
from . import views
@@ -28,4 +26,14 @@ urlpatterns = [
views.ServiceSessionView.as_view(),
name="service_session",
),
+ path(
+ "api-settings/",
+ views.ApiSettingsView.as_view(),
+ name="api_settings",
+ ),
+ path(
+ "api-token-refresh/",
+ views.RefreshApiTokenView.as_view(),
+ name="api_token_refresh",
+ ),
]
diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py
index 9ce02ce..96a59a8 100644
--- a/shynet/dashboard/views.py
+++ b/shynet/dashboard/views.py
@@ -3,8 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.db.models import Q
-from django.shortcuts import get_object_or_404, reverse
-from django.utils import timezone
+from django.shortcuts import get_object_or_404, reverse, redirect
from django.views.generic import (
CreateView,
DeleteView,
@@ -12,11 +11,12 @@ from django.views.generic import (
ListView,
TemplateView,
UpdateView,
+ View,
)
from rules.contrib.views import PermissionRequiredMixin
from analytics.models import Session
-from core.models import Service
+from core.models import Service, _default_api_token
from .forms import ServiceForm
from .mixins import DateRangeMixin
@@ -155,3 +155,14 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
data = super().get_context_data(**kwargs)
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
return data
+
+
+class ApiSettingsView(LoginRequiredMixin, TemplateView):
+ template_name = "dashboard/pages/api_settings.html"
+
+
+class RefreshApiTokenView(LoginRequiredMixin, View):
+ def get(self, request):
+ request.user.api_token = _default_api_token()
+ request.user.save()
+ return redirect('dashboard:api_settings')
From bcf94147c92c1331e1c10846bb9c68277025fb4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 17 Nov 2021 11:46:35 +0100
Subject: [PATCH 16/29] Fix problem with whitespaces in copied token
---
shynet/dashboard/templates/dashboard/pages/api_settings.html | 4 +---
.../dashboard/templates/dashboard/pages/service_update.html | 4 +---
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html
index fed6882..b242766 100644
--- a/shynet/dashboard/templates/dashboard/pages/api_settings.html
+++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html
@@ -8,9 +8,7 @@
Token
-
- {{request.user.api_token}}
-
+
{{request.user.api_token}}
Refresh token
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 7a9713b..010db09 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -33,9 +33,7 @@
API Token
-
- {{request.user.api_token}}
-
+
{{request.user.api_token}}
{% endblock %}
From 069b218828892d84213e9bf9edc1b30c1f805aa2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Tue, 4 Jan 2022 08:52:39 +0100
Subject: [PATCH 17/29] Move api token info to security tab
---
.../templates/account/password_change.html | 15 ++++++++++++--
shynet/dashboard/templates/base.html | 3 ---
.../dashboard/pages/api_settings.html | 20 -------------------
shynet/dashboard/views.py | 2 +-
4 files changed, 14 insertions(+), 26 deletions(-)
delete mode 100644 shynet/dashboard/templates/dashboard/pages/api_settings.html
diff --git a/shynet/dashboard/templates/account/password_change.html b/shynet/dashboard/templates/account/password_change.html
index ca8f276..2a68a1d 100644
--- a/shynet/dashboard/templates/account/password_change.html
+++ b/shynet/dashboard/templates/account/password_change.html
@@ -2,8 +2,8 @@
{% load i18n a17t_tags %}
-{% block head_title %}{% trans "Change Password" %}{% endblock %}
-{% block page_title %}{% trans "Change Password" %}{% endblock %}
+{% block head_title %}{% trans "Change authentication info" %}{% endblock %}
+{% block page_title %}{% trans "Change authentication info" %}{% endblock %}
{% block card %}
+
+
+
{% endblock %}
diff --git a/shynet/dashboard/templates/base.html b/shynet/dashboard/templates/base.html
index 113d602..f4d2f67 100644
--- a/shynet/dashboard/templates/base.html
+++ b/shynet/dashboard/templates/base.html
@@ -88,9 +88,6 @@
{% url 'account_set_password' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %}
- {% url 'dashboard:api_settings' as url %}
- {% include 'dashboard/includes/sidebar_portal.html' with label="API" url=url %}
-
{% url 'account_logout' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %}
diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html
deleted file mode 100644
index b242766..0000000
--- a/shynet/dashboard/templates/dashboard/pages/api_settings.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py
index 96a59a8..080e05d 100644
--- a/shynet/dashboard/views.py
+++ b/shynet/dashboard/views.py
@@ -165,4 +165,4 @@ class RefreshApiTokenView(LoginRequiredMixin, View):
def get(self, request):
request.user.api_token = _default_api_token()
request.user.save()
- return redirect('dashboard:api_settings')
+ return redirect('account_change_password')
From 7f60b3abff5ec2f44bc28b7307db22c87a85b13c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 5 Jan 2022 08:53:46 +0100
Subject: [PATCH 18/29] Rename minimal parameter to basic
---
shynet/api/views.py | 6 +++---
shynet/core/models.py | 10 +++++-----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 77b6142..cd0181e 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -19,7 +19,7 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
if uuid:
services = services.filter(uuid=uuid)
- minimal = request.GET.get('minimal', '0').lower() in ('1', 'true')
+ basic = request.GET.get('basic', '0').lower() in ('1', 'true')
start = self.get_start_date()
end = self.get_end_date()
services_data = [
@@ -27,12 +27,12 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
'name': s.name,
'uuid': s.uuid,
'link': s.link,
- 'stats': s.get_core_stats(start, end, minimal),
+ 'stats': s.get_core_stats(start, end, basic),
}
for s in services
]
- if not minimal:
+ if not basic:
services_data = self._convert_querysets_to_lists(services_data)
return JsonResponse(data={'services': services_data})
diff --git a/shynet/core/models.py b/shynet/core/models.py
index 875faba..426c8f8 100644
--- a/shynet/core/models.py
+++ b/shynet/core/models.py
@@ -113,21 +113,21 @@ class Service(models.Model):
start_time=timezone.now() - timezone.timedelta(days=1)
)
- def get_core_stats(self, start_time=None, end_time=None, minimal=False):
+ def get_core_stats(self, start_time=None, end_time=None, basic=False):
if start_time is None:
start_time = timezone.now() - timezone.timedelta(days=30)
if end_time is None:
end_time = timezone.now()
- main_data = self.get_relative_stats(start_time, end_time, minimal)
+ main_data = self.get_relative_stats(start_time, end_time, basic)
comparison_data = self.get_relative_stats(
- start_time - (end_time - start_time), start_time, minimal
+ start_time - (end_time - start_time), start_time, basic
)
main_data["compare"] = comparison_data
return main_data
- def get_relative_stats(self, start_time, end_time, minimal=False):
+ def get_relative_stats(self, start_time, end_time, basic=False):
Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit")
@@ -159,7 +159,7 @@ class Service(models.Model):
avg_hits_per_session = hit_count / session_count if session_count > 0 else None
avg_session_duration = self._get_avg_session_duration(sessions, session_count)
- if minimal:
+ if basic:
return {
"currently_online": currently_online,
"session_count": session_count,
From 2aaadfe81cc3587f52a4832490ad39e1e5937450 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 5 Jan 2022 09:02:23 +0100
Subject: [PATCH 19/29] Display api urls on service management page
---
.../dashboard/pages/service_update.html | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 010db09..03ff1c0 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -32,8 +32,21 @@
-
API Token
-
{{request.user.api_token}}
+
API
+
Service data can be accessed via API on url:
+
{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}
+
+ There are 3 optional query parameters:
+
+ startDate - to set start date in format YYYY-MM-DD
+ endDate - to set end date in format YYYY-MM-DD
+ basic - to get only basic data set to '1' or 'true'
+
+
+
Example using HTTPie:
+
http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{request.user.api_token}}'
+
Example using cURL:
+
curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'
{% endblock %}
From ba91ed561d733bea8598300ad917c208053e4f0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 5 Jan 2022 09:47:14 +0100
Subject: [PATCH 20/29] Add uuid validation
---
shynet/api/views.py | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index cd0181e..bad7dae 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -1,3 +1,4 @@
+import uuid
from django.http import JsonResponse
from django.db.models import Q
from django.db.models.query import QuerySet
@@ -9,6 +10,14 @@ from core.models import Service
from .mixins import ApiTokenRequiredMixin
+def is_valid_uuid(value):
+ try:
+ uuid.UUID(value)
+ return True
+ except ValueError:
+ return False
+
+
class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
def get(self, request, *args, **kwargs):
services = Service.objects.filter(
@@ -16,7 +25,7 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
).distinct()
uuid = request.GET.get('uuid')
- if uuid:
+ if uuid and is_valid_uuid(uuid):
services = services.filter(uuid=uuid)
basic = request.GET.get('basic', '0').lower() in ('1', 'true')
From 6d84f6313081de7c58a1a6b0f0d49d24f7c1c3ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Wed, 5 Jan 2022 10:27:14 +0100
Subject: [PATCH 21/29] Add API documentation to GUIDE.md
---
GUIDE.md | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/GUIDE.md b/GUIDE.md
index 90153ed..40e2916 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -208,6 +208,25 @@ In a single-page application, the page never reloads. (That's the entire point o
Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go.
+
+### API
+
+All data displayed on dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```.
+By default this url return full data from all services from last 30 days.
+Authentication header should be set to use user's parsonal API token (```'Authorization:Token '```).
+
+There are 4 optional query parameters:
+ * uuid - to get data only from one service
+ * startDate - to set start date in format YYYY-MM-DD
+ * endDate - to set end date in format YYYY-MM-DD
+ * basic - to get only basic data set to '1' or 'true'
+
+Example in HTTPie:
+```http get '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{user_api_token}}'```
+
+Example in cURL:
+```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'```
+
---
## Troubleshooting
From 4a6af187651f27e8898febf4206ac754ba6b3795 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Thu, 14 Apr 2022 19:41:14 +0200
Subject: [PATCH 22/29] Add django-cors-headers
---
poetry.lock | 17 ++++++++++++++++-
pyproject.toml | 1 +
shynet/shynet/settings.py | 5 +++++
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/poetry.lock b/poetry.lock
index 42436dc..0e41f22 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -300,6 +300,17 @@ python3-openid = ">=3.0.8"
requests = "*"
requests-oauthlib = ">=0.3.0"
+[[package]]
+name = "django-cors-headers"
+version = "3.11.0"
+description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+Django = ">=2.2"
+
[[package]]
name = "django-coverage-plugin"
version = "2.0.2"
@@ -949,7 +960,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
-content-hash = "009655b041e17f83ac3f1b423241ee299b0e706274b5b77519d3c087a4a032f8"
+content-hash = "9fa33a531809239cfa88d8d896484af7d8be765b2a0f4878ab5434d5af6b30d2"
[metadata.files]
aiohttp = [
@@ -1220,6 +1231,10 @@ django = [
django-allauth = [
{file = "django-allauth-0.45.0.tar.gz", hash = "sha256:6d46be0e1480316ccd45476db3aefb39db70e038d2a543112d314b76bb999a4e"},
]
+django-cors-headers = [
+ {file = "django-cors-headers-3.11.0.tar.gz", hash = "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456"},
+ {file = "django_cors_headers-3.11.0-py3-none-any.whl", hash = "sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b"},
+]
django-coverage-plugin = [
{file = "django_coverage_plugin-2.0.2-py3-none-any.whl", hash = "sha256:4206c85ffba0301f83aecc38e5b01b1b9a4b45a545d9456a827e3fabea18d952"},
{file = "django_coverage_plugin-2.0.2.tar.gz", hash = "sha256:e91e3a0c8de2b3766a144cdd30dbbf7a79e5c532a5dcc1373ce7eaad83b358b3"},
diff --git a/pyproject.toml b/pyproject.toml
index e32781b..00564c7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,7 @@ django-health-check = "^3.16.4"
django-npm = "^1.0.0"
python-dotenv = "^0.18.0"
django-debug-toolbar = "^3.2.1"
+django-cors-headers = "^3.11.0"
[tool.poetry.dev-dependencies]
pytest-sugar = "^0.9.4"
diff --git a/shynet/shynet/settings.py b/shynet/shynet/settings.py
index ec0ade3..976e1d9 100644
--- a/shynet/shynet/settings.py
+++ b/shynet/shynet/settings.py
@@ -64,12 +64,14 @@ INSTALLED_APPS = [
"allauth.account",
"allauth.socialaccount",
"debug_toolbar",
+ "corsheaders",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
+ "corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -371,3 +373,6 @@ DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
)
+
+CORS_ALLOW_ALL_ORIGINS = True
+CORS_ALLOW_METHODS = ["GET", "OPTIONS"]
From b87b158aabb8e82da68652740d164f6d8be6bbda Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Fri, 22 Apr 2022 08:28:09 +0200
Subject: [PATCH 23/29] Fix typo
---
shynet/api/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index bad7dae..0cea315 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -55,4 +55,4 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
if isinstance(value, QuerySet):
service_data['stats']['compare'][key] = list(value)
- return service_data
+ return services_data
From ca97453c3e431482a346be0488a89dd0a8037720 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Tue, 26 Apr 2022 10:13:52 +0200
Subject: [PATCH 24/29] Return 400 if date format is invalid
---
shynet/api/views.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 0cea315..5ee4900 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -29,8 +29,12 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
services = services.filter(uuid=uuid)
basic = request.GET.get('basic', '0').lower() in ('1', 'true')
- start = self.get_start_date()
- end = self.get_end_date()
+ try:
+ start = self.get_start_date()
+ end = self.get_end_date()
+ except ValueError:
+ return JsonResponse(status=400, data={'error': 'Invalid date format'})
+
services_data = [
{
'name': s.name,
From d9bbeea89278b814e6b5faad14a3d9102e2686bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Thu, 12 May 2022 12:10:44 +0200
Subject: [PATCH 25/29] Remove basic option from API
For simplicity
---
shynet/core/models.py | 38 ++++++-------------
.../dashboard/pages/service_update.html | 7 ++--
2 files changed, 15 insertions(+), 30 deletions(-)
diff --git a/shynet/core/models.py b/shynet/core/models.py
index 426c8f8..dac7e60 100644
--- a/shynet/core/models.py
+++ b/shynet/core/models.py
@@ -113,21 +113,21 @@ class Service(models.Model):
start_time=timezone.now() - timezone.timedelta(days=1)
)
- def get_core_stats(self, start_time=None, end_time=None, basic=False):
+ def get_core_stats(self, start_time=None, end_time=None):
if start_time is None:
start_time = timezone.now() - timezone.timedelta(days=30)
if end_time is None:
end_time = timezone.now()
- main_data = self.get_relative_stats(start_time, end_time, basic)
+ main_data = self.get_relative_stats(start_time, end_time)
comparison_data = self.get_relative_stats(
- start_time - (end_time - start_time), start_time, basic
+ start_time - (end_time - start_time), start_time
)
main_data["compare"] = comparison_data
return main_data
- def get_relative_stats(self, start_time, end_time, basic=False):
+ def get_relative_stats(self, start_time, end_time):
Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit")
@@ -152,28 +152,6 @@ class Service(models.Model):
bounces = sessions.filter(is_bounce=True)
bounce_count = bounces.count()
- avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
- "load_time__avg"
- ]
-
- avg_hits_per_session = hit_count / session_count if session_count > 0 else None
-
- avg_session_duration = self._get_avg_session_duration(sessions, session_count)
- if basic:
- return {
- "currently_online": currently_online,
- "session_count": session_count,
- "hit_count": hit_count,
- "has_hits": has_hits,
- "bounce_rate_pct": bounce_count * 100 / session_count
- if session_count > 0
- else None,
- "avg_session_duration": avg_session_duration,
- "avg_load_time": avg_load_time,
- "avg_hits_per_session": avg_hits_per_session,
- "online": True,
- }
-
locations = (
hits.values("location")
.annotate(count=models.Count("location"))
@@ -220,6 +198,14 @@ class Service(models.Model):
.order_by("-count")
)
+ avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
+ "load_time__avg"
+ ]
+
+ avg_hits_per_session = hit_count / session_count if session_count > 0 else None
+
+ avg_session_duration = self._get_avg_session_duration(sessions, session_count)
+
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now
)
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 03ff1c0..67ba6e5 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -36,17 +36,16 @@
Service data can be accessed via API on url:
{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}
- There are 3 optional query parameters:
+ There are 2 optional query parameters:
startDate - to set start date in format YYYY-MM-DD
endDate - to set end date in format YYYY-MM-DD
- basic - to get only basic data set to '1' or 'true'
Example using HTTPie:
- http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{request.user.api_token}}'
+ http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{request.user.api_token}}'
Example using cURL:
- curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'
+ curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'
{% endblock %}
From 77cb1fb37c0da5bad39b3905f7a48cd3f176bac7 Mon Sep 17 00:00:00 2001
From: "R. Miles McCain"
Date: Sat, 27 Aug 2022 14:52:02 -0700
Subject: [PATCH 26/29] Improve language
---
GUIDE.md | 12 +++++-----
.../templates/account/password_change.html | 5 +++--
.../dashboard/pages/service_update.html | 22 +++++++++----------
3 files changed, 18 insertions(+), 21 deletions(-)
diff --git a/GUIDE.md b/GUIDE.md
index 40e2916..be2ef7f 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -211,15 +211,13 @@ Fortunately, Shynet offers a simple method you can call from anywhere within you
### API
-All data displayed on dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```.
-By default this url return full data from all services from last 30 days.
-Authentication header should be set to use user's parsonal API token (```'Authorization:Token '```).
+All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token '```).
There are 4 optional query parameters:
- * uuid - to get data only from one service
- * startDate - to set start date in format YYYY-MM-DD
- * endDate - to set end date in format YYYY-MM-DD
- * basic - to get only basic data set to '1' or 'true'
+ * `uuid` - to get data only from one service
+ * `startDate` - to set start date in format YYYY-MM-DD
+ * `endDate` - to set end date in format YYYY-MM-DD
+ * `basic` - to get only basic data set to '1' or 'true'
Example in HTTPie:
```http get '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{user_api_token}}'```
diff --git a/shynet/dashboard/templates/account/password_change.html b/shynet/dashboard/templates/account/password_change.html
index 2a68a1d..6bb4a6e 100644
--- a/shynet/dashboard/templates/account/password_change.html
+++ b/shynet/dashboard/templates/account/password_change.html
@@ -13,13 +13,14 @@
-
Personal API token
+
Personal API token
+
To learn more about the API, see our API guide .
{% endblock %}
diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html
index 67ba6e5..aeb2c58 100644
--- a/shynet/dashboard/templates/dashboard/pages/service_update.html
+++ b/shynet/dashboard/templates/dashboard/pages/service_update.html
@@ -31,21 +31,19 @@
-
-
API
-
Service data can be accessed via API on url:
-
{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}
+
API
+
+
Shynet provides a simple API that you can use to pull data programmatically. You can access this data via this URL:
+
{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}
- There are 2 optional query parameters:
-
- startDate - to set start date in format YYYY-MM-DD
- endDate - to set end date in format YYYY-MM-DD
-
+ There are 2 optional query parameters:
+
+ startDate
— to set the start date (in format YYYY-MM-DD)
+ endDate
— to set the end date (in format YYYY-MM-DD)
+
-
Example using HTTPie:
-
http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{request.user.api_token}}'
Example using cURL:
-
curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'
+
curl -H 'Authorization: Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'
{% endblock %}
From b7f2e9cfe656be1860264704d48e89c9429205f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Thu, 12 May 2022 12:40:33 +0200
Subject: [PATCH 27/29] Remove basic option from api view
---
shynet/api/views.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/shynet/api/views.py b/shynet/api/views.py
index 5ee4900..a5dc895 100644
--- a/shynet/api/views.py
+++ b/shynet/api/views.py
@@ -28,7 +28,6 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
if uuid and is_valid_uuid(uuid):
services = services.filter(uuid=uuid)
- basic = request.GET.get('basic', '0').lower() in ('1', 'true')
try:
start = self.get_start_date()
end = self.get_end_date()
@@ -40,13 +39,12 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
'name': s.name,
'uuid': s.uuid,
'link': s.link,
- 'stats': s.get_core_stats(start, end, basic),
+ 'stats': s.get_core_stats(start, end),
}
for s in services
]
- if not basic:
- services_data = self._convert_querysets_to_lists(services_data)
+ services_data = self._convert_querysets_to_lists(services_data)
return JsonResponse(data={'services': services_data})
From b286c80754e7973a9487c3f218205780777546b0 Mon Sep 17 00:00:00 2001
From: "R. Miles McCain"
Date: Sun, 28 Aug 2022 15:07:05 -0700
Subject: [PATCH 28/29] Remove unneeded views
---
GUIDE.md | 9 ++++-----
shynet/dashboard/urls.py | 5 -----
shynet/dashboard/views.py | 4 ----
shynet/shynet/urls.py | 2 +-
4 files changed, 5 insertions(+), 15 deletions(-)
diff --git a/GUIDE.md b/GUIDE.md
index be2ef7f..2b67ce3 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -211,19 +211,18 @@ Fortunately, Shynet offers a simple method you can call from anywhere within you
### API
-All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token '```).
+All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/v1/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token '```).
-There are 4 optional query parameters:
+There are 3 optional query parameters:
* `uuid` - to get data only from one service
* `startDate` - to set start date in format YYYY-MM-DD
* `endDate` - to set end date in format YYYY-MM-DD
- * `basic` - to get only basic data set to '1' or 'true'
Example in HTTPie:
-```http get '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{user_api_token}}'```
+```http get '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{user_api_token}}'```
Example in cURL:
-```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'```
+```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01'```
---
diff --git a/shynet/dashboard/urls.py b/shynet/dashboard/urls.py
index e107a13..7aa917b 100644
--- a/shynet/dashboard/urls.py
+++ b/shynet/dashboard/urls.py
@@ -26,11 +26,6 @@ urlpatterns = [
views.ServiceSessionView.as_view(),
name="service_session",
),
- path(
- "api-settings/",
- views.ApiSettingsView.as_view(),
- name="api_settings",
- ),
path(
"api-token-refresh/",
views.RefreshApiTokenView.as_view(),
diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py
index 080e05d..8039430 100644
--- a/shynet/dashboard/views.py
+++ b/shynet/dashboard/views.py
@@ -157,10 +157,6 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
return data
-class ApiSettingsView(LoginRequiredMixin, TemplateView):
- template_name = "dashboard/pages/api_settings.html"
-
-
class RefreshApiTokenView(LoginRequiredMixin, View):
def get(self, request):
request.user.api_token = _default_api_token()
diff --git a/shynet/shynet/urls.py b/shynet/shynet/urls.py
index 71aabe6..cbec472 100644
--- a/shynet/shynet/urls.py
+++ b/shynet/shynet/urls.py
@@ -25,5 +25,5 @@ urlpatterns = [
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
path("healthz/", include("health_check.urls")),
path("", include(("core.urls", "core"), namespace="core")),
- path("api/", include(("api.urls", "api"), namespace="api")),
+ path("api/v1/", include(("api.urls", "api"), namespace="api")),
]
From 5e48e2dcf54c080607da962eb2d79de8b3bcf0d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?=
Date: Mon, 29 Aug 2022 08:44:17 +0200
Subject: [PATCH 29/29] Use POST to api token refresh
---
shynet/dashboard/templates/account/password_change.html | 7 ++++---
shynet/dashboard/views.py | 2 +-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/shynet/dashboard/templates/account/password_change.html b/shynet/dashboard/templates/account/password_change.html
index 6bb4a6e..3fe2bbf 100644
--- a/shynet/dashboard/templates/account/password_change.html
+++ b/shynet/dashboard/templates/account/password_change.html
@@ -16,9 +16,10 @@
Personal API token
To learn more about the API, see our API guide .
diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py
index 8039430..97360d0 100644
--- a/shynet/dashboard/views.py
+++ b/shynet/dashboard/views.py
@@ -158,7 +158,7 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
class RefreshApiTokenView(LoginRequiredMixin, View):
- def get(self, request):
+ def post(self, request):
request.user.api_token = _default_api_token()
request.user.save()
return redirect('account_change_password')