Add base dashboard view
This commit is contained in:
parent
170a8ec00b
commit
565cba18e2
3
Pipfile
3
Pipfile
@ -15,8 +15,7 @@ django-ipware = "*"
|
|||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
ua-parser = "*"
|
ua-parser = "*"
|
||||||
user-agents = "*"
|
user-agents = "*"
|
||||||
django-humanize = "*"
|
emoji-country-flag = "*"
|
||||||
anonymizeip = "*"
|
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.6"
|
python_version = "3.6"
|
||||||
|
32
Pipfile.lock
generated
32
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "5c4a63923138fea970c8851ac72d6d6a391db28090a02d738e675775d2c6f26b"
|
"sha256": "93fe2edcab5c588173dcaa4799fd66873e47014f417920ad5a969544a33e5cb5"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -23,14 +23,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.5.2"
|
"version": "==2.5.2"
|
||||||
},
|
},
|
||||||
"anonymizeip": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:491cb94a31bae23294c5b93a13dd5c9ed55be98003c622e76e2fe64d6a4f3e91",
|
|
||||||
"sha256:e0d446b06b2bbf236394a90b971de403f90f805d14db3a405f8731716acad1fe"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.0.0"
|
|
||||||
},
|
|
||||||
"asgiref": {
|
"asgiref": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
|
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
|
||||||
@ -89,13 +81,6 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.41.0"
|
"version": "==0.41.0"
|
||||||
},
|
},
|
||||||
"django-humanize": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:32491bf0209b89a277f7bfdab7fd6d4cc7944bb037f742d62e8e447a575c0028"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.1.2"
|
|
||||||
},
|
|
||||||
"django-ipware": {
|
"django-ipware": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"
|
"sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"
|
||||||
@ -103,6 +88,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.1.0"
|
"version": "==2.1.0"
|
||||||
},
|
},
|
||||||
|
"emoji-country-flag": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:67c0cb6a3765fb53f31b34160d6b1c8a5f44b297bc278d1835c6f2e5b0a9a592",
|
||||||
|
"sha256:ae7edb38077b0840210fa9e37673f481f2b9c032446e13ad6dab2b1108cd7ad6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
"geoip2": {
|
"geoip2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5869e987bc54c0d707264fec4710661332cc38d2dca5a7f9bb5362d0308e2ce0",
|
"sha256:5869e987bc54c0d707264fec4710661332cc38d2dca5a7f9bb5362d0308e2ce0",
|
||||||
@ -111,13 +104,6 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.0.0"
|
"version": "==3.0.0"
|
||||||
},
|
},
|
||||||
"humanize": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:98b7ac9d1a70ad62175c8e0dd44beebbd92418727fc4e214468dfb2baa8ebfb5",
|
|
||||||
"sha256:bc2a1ff065977011de2bc36197a4b14730c54bfc46ab12a153376684573a2dab"
|
|
||||||
],
|
|
||||||
"version": "==2.3.0"
|
|
||||||
},
|
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||||
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class A17TConfig(AppConfig):
|
class A17TConfig(AppConfig):
|
||||||
name = 'a17t'
|
name = "a17t"
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{% load a17t_tags %}
|
{% load a17t_tags %}
|
||||||
|
|
||||||
<field role="field" class="block mb-2">
|
<field role="field" class="block mb-4">
|
||||||
|
|
||||||
{% if field|is_checkbox %}
|
{% if field|is_checkbox %}
|
||||||
<label class="switch {{ classes.label }}">
|
<label class="switch {{ classes.label }}">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
@ -12,25 +11,25 @@
|
|||||||
<label class="label my-1" for="{{field.auto_id}}">{{ field.label }}</label>
|
<label class="label my-1" for="{{field.auto_id}}">{{ field.label }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for choice in field %}
|
{% for choice in field %}
|
||||||
<label class="switch">
|
<label class="switch my-1">
|
||||||
{{ choice.tag }}
|
{{ choice.tag }}
|
||||||
<span>{{ choice.choice_label }}</span>
|
<span>{{ choice.choice_label }}</span>
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif field|is_input %}
|
{% elif field|is_input %}
|
||||||
<label class="label" for="{{field.auto_id}}">{{ field.label }}</label>
|
{% include 'a17t/includes/label.html' %}
|
||||||
{{field|add_class:"input"}}
|
{{field|add_class:"input my-1"}}
|
||||||
{% elif field|is_textarea %}
|
{% elif field|is_textarea %}
|
||||||
<label class="label" for="{{field.auto_id}}">{{ field.label }}</label>
|
{% include 'a17t/includes/label.html' %}
|
||||||
{{ field|add_class:'textarea' }}
|
{{ field|add_class:'textarea my-1' }}
|
||||||
{% elif field|is_select %}
|
{% elif field|is_select %}
|
||||||
<label class="label" for="{{field.auto_id}}">{{ field.label }}</label>
|
{% include 'a17t/includes/label.html' %}
|
||||||
<div class="select {% if field.errors|length > 0 %}~critical{% endif %}">
|
<div class="select {% if field.errors|length > 0 %}~critical{% endif %} my-1">
|
||||||
{{field}}
|
{{field}}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<label class="label" for="{{field.auto_id}}">{{ field.label }}</label>
|
{% include 'a17t/includes/label.html' %}
|
||||||
{{field|add_class:"field"}}
|
{{field|add_class:"field my-1"}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
|
1
shynet/a17t/templates/a17t/includes/label.html
Normal file
1
shynet/a17t/templates/a17t/includes/label.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<label class="label" for="{{field.auto_id}}">{{ field.label }} {% if not field.field.required %}<span class="badge ~neutral">Optional</span>{% endif %}</label>
|
@ -1,5 +1,4 @@
|
|||||||
from django import forms
|
from django import forms, template
|
||||||
from django import template
|
|
||||||
from django.forms import BoundField
|
from django.forms import BoundField
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -6,16 +6,10 @@ from django.db import migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('analytics', '0002_auto_20200410_0258'),
|
("analytics", "0002_auto_20200410_0258"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(model_name="hit", name="metadata_raw",),
|
||||||
model_name='hit',
|
migrations.RemoveField(model_name="session", name="metadata_raw",),
|
||||||
name='metadata_raw',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='session',
|
|
||||||
name='metadata_raw',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -1,34 +1,29 @@
|
|||||||
# Generated by Django 3.0.5 on 2020-04-11 19:41
|
# Generated by Django 3.0.5 on 2020-04-11 19:41
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('analytics', '0003_auto_20200410_1325'),
|
("analytics", "0003_auto_20200410_1325"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='hit',
|
model_name="hit", old_name="start", new_name="start_time",
|
||||||
old_name='start',
|
|
||||||
new_name='start_time',
|
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='session',
|
model_name="session", old_name="first_seen", new_name="start_time",
|
||||||
old_name='first_seen',
|
|
||||||
new_name='start_time',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='hit',
|
|
||||||
name='duration',
|
|
||||||
),
|
),
|
||||||
|
migrations.RemoveField(model_name="hit", name="duration",),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='hit',
|
model_name="hit",
|
||||||
name='last_seen',
|
name="last_seen",
|
||||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, default=django.utils.timezone.now
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
16
shynet/analytics/migrations/0005_hit_initial.py
Normal file
16
shynet/analytics/migrations/0005_hit_initial.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-04-12 03:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("analytics", "0004_auto_20200411_1541"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="hit", name="initial", field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
29
shynet/analytics/migrations/0006_auto_20200412_0003.py
Normal file
29
shynet/analytics/migrations/0006_auto_20200412_0003.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-04-12 04:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("analytics", "0005_hit_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="hit", name="httpStatus",),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="session",
|
||||||
|
name="device_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("PHONE", "Phone"),
|
||||||
|
("TABLET", "Tablet"),
|
||||||
|
("DESK", "Desktop / Laptop"),
|
||||||
|
("ROBOT", "Robot"),
|
||||||
|
("OTHER", "Other"),
|
||||||
|
],
|
||||||
|
default="OTHER",
|
||||||
|
max_length=6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
28
shynet/analytics/migrations/0007_auto_20200412_0010.py
Normal file
28
shynet/analytics/migrations/0007_auto_20200412_0010.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-04-12 04:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("analytics", "0006_auto_20200412_0003"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="session",
|
||||||
|
name="device_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("PHONE", "Phone"),
|
||||||
|
("TABLET", "Tablet"),
|
||||||
|
("DESKTOP", "Desktop"),
|
||||||
|
("ROBOT", "Robot"),
|
||||||
|
("OTHER", "Other"),
|
||||||
|
],
|
||||||
|
default="OTHER",
|
||||||
|
max_length=7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
16
shynet/analytics/migrations/0008_auto_20200412_0015.py
Normal file
16
shynet/analytics/migrations/0008_auto_20200412_0015.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-04-12 04:15
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("analytics", "0007_auto_20200412_0010"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="hit", old_name="loadTime", new_name="load_time",
|
||||||
|
),
|
||||||
|
]
|
@ -2,6 +2,7 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.models import Service
|
from core.models import Service
|
||||||
|
|
||||||
@ -25,6 +26,17 @@ class Session(models.Model):
|
|||||||
user_agent = models.TextField()
|
user_agent = models.TextField()
|
||||||
browser = models.TextField()
|
browser = models.TextField()
|
||||||
device = models.TextField()
|
device = models.TextField()
|
||||||
|
device_type = models.CharField(
|
||||||
|
max_length=7,
|
||||||
|
choices=[
|
||||||
|
("PHONE", "Phone"),
|
||||||
|
("TABLET", "Tablet"),
|
||||||
|
("DESKTOP", "Desktop"),
|
||||||
|
("ROBOT", "Robot"),
|
||||||
|
("OTHER", "Other"),
|
||||||
|
],
|
||||||
|
default="OTHER",
|
||||||
|
)
|
||||||
os = models.TextField()
|
os = models.TextField()
|
||||||
ip = models.GenericIPAddressField()
|
ip = models.GenericIPAddressField()
|
||||||
|
|
||||||
@ -35,9 +47,18 @@ class Session(models.Model):
|
|||||||
latitude = models.FloatField(null=True)
|
latitude = models.FloatField(null=True)
|
||||||
time_zone = models.TextField(blank=True)
|
time_zone = models.TextField(blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_currently_active(self):
|
||||||
|
return timezone.now() - self.last_seen < timezone.timedelta(seconds=10)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duration(self):
|
||||||
|
return self.last_seen - self.start_time
|
||||||
|
|
||||||
|
|
||||||
class Hit(models.Model):
|
class Hit(models.Model):
|
||||||
session = models.ForeignKey(Session, on_delete=models.CASCADE)
|
session = models.ForeignKey(Session, on_delete=models.CASCADE)
|
||||||
|
initial = models.BooleanField(default=True)
|
||||||
|
|
||||||
# Base request information
|
# Base request information
|
||||||
start_time = models.DateTimeField(auto_now_add=True)
|
start_time = models.DateTimeField(auto_now_add=True)
|
||||||
@ -48,6 +69,4 @@ class Hit(models.Model):
|
|||||||
# Advanced page information
|
# Advanced page information
|
||||||
location = models.TextField(blank=True)
|
location = models.TextField(blank=True)
|
||||||
referrer = models.TextField(blank=True)
|
referrer = models.TextField(blank=True)
|
||||||
loadTime = models.FloatField(null=True)
|
load_time = models.FloatField(null=True)
|
||||||
httpStatus = models.IntegerField(null=True)
|
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ from celery import shared_task
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from anonymizeip import anonymize_ip
|
|
||||||
|
|
||||||
from core.models import Service
|
from core.models import Service
|
||||||
|
|
||||||
@ -51,13 +50,10 @@ def ingress_request(
|
|||||||
ip_data = _geoip2_lookup(ip)
|
ip_data = _geoip2_lookup(ip)
|
||||||
log.debug(f"Found geoip2 data")
|
log.debug(f"Found geoip2 data")
|
||||||
|
|
||||||
if service.anonymize_ips:
|
|
||||||
ip = anonymize_ip(ip)
|
|
||||||
|
|
||||||
# Create or update session
|
# Create or update session
|
||||||
session = Session.objects.filter(
|
session = Session.objects.filter(
|
||||||
service=service,
|
service=service,
|
||||||
last_seen__gt=timezone.now() - timezone.timedelta(minutes=30),
|
last_seen__gt=timezone.now() - timezone.timedelta(minutes=10),
|
||||||
ip=ip,
|
ip=ip,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
@ -65,15 +61,25 @@ def ingress_request(
|
|||||||
if session is None:
|
if session is None:
|
||||||
log.debug("Cannot link to existing session; creating a new one...")
|
log.debug("Cannot link to existing session; creating a new one...")
|
||||||
ua = user_agents.parse(user_agent)
|
ua = user_agents.parse(user_agent)
|
||||||
|
initial = True
|
||||||
|
device_type = "OTHER"
|
||||||
|
if ua.is_mobile:
|
||||||
|
device_type = "PHONE"
|
||||||
|
elif ua.is_tablet:
|
||||||
|
device_type = "TABLET"
|
||||||
|
elif ua.is_pc:
|
||||||
|
device_type = "DESKTOP"
|
||||||
|
elif ua.is_bot:
|
||||||
|
device_type = "ROBOT"
|
||||||
session = Session.objects.create(
|
session = Session.objects.create(
|
||||||
service=service,
|
service=service,
|
||||||
ip=ip,
|
ip=ip,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
browser=f"{ua.browser.family or ''} {ua.browser.version_string or ''}".strip(),
|
browser=ua.browser.family or "",
|
||||||
device=f"{ua.device.model or ''}",
|
device=ua.device.model or "",
|
||||||
os=f"{ua.os.family or ''} {ua.os.version_string or ''}".strip(),
|
device_type=device_type,
|
||||||
|
os=ua.os.family or "",
|
||||||
asn=ip_data.get("asn", ""),
|
asn=ip_data.get("asn", ""),
|
||||||
country=ip_data.get("country", ""),
|
country=ip_data.get("country", ""),
|
||||||
longitude=ip_data.get("longitude"),
|
longitude=ip_data.get("longitude"),
|
||||||
@ -82,6 +88,7 @@ def ingress_request(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.debug("Updating old session with new data...")
|
log.debug("Updating old session with new data...")
|
||||||
|
initial = False
|
||||||
# Update last seen time
|
# Update last seen time
|
||||||
session.last_seen = timezone.now()
|
session.last_seen = timezone.now()
|
||||||
session.save()
|
session.save()
|
||||||
@ -92,6 +99,7 @@ def ingress_request(
|
|||||||
hit = None
|
hit = None
|
||||||
if idempotency is not None:
|
if idempotency is not None:
|
||||||
if cache.get(idempotency_path) is not None:
|
if cache.get(idempotency_path) is not None:
|
||||||
|
cache.touch(idempotency_path, 10 * 60)
|
||||||
hit = Hit.objects.filter(
|
hit = Hit.objects.filter(
|
||||||
pk=cache.get(idempotency_path), session=session
|
pk=cache.get(idempotency_path), session=session
|
||||||
).first()
|
).first()
|
||||||
@ -107,14 +115,15 @@ def ingress_request(
|
|||||||
# There is no existing hit; create a new one
|
# There is no existing hit; create a new one
|
||||||
hit = Hit.objects.create(
|
hit = Hit.objects.create(
|
||||||
session=session,
|
session=session,
|
||||||
|
initial=initial,
|
||||||
tracker=tracker,
|
tracker=tracker,
|
||||||
location=location,
|
location=location,
|
||||||
referrer=payload.get("referrer", ""),
|
referrer=payload.get("referrer", ""),
|
||||||
loadTime=payload.get("loadTime"),
|
load_time=payload.get("loadTime"),
|
||||||
)
|
)
|
||||||
# Set idempotency (if applicable)
|
# Set idempotency (if applicable)
|
||||||
if idempotency is not None:
|
if idempotency is not None:
|
||||||
cache.set(idempotency_path, hit.pk, timeout=30 * 60)
|
cache.set(idempotency_path, hit.pk, timeout=10 * 60)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise e
|
raise e
|
||||||
|
24
shynet/core/forms.py
Normal file
24
shynet/core/forms.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import Service
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Service
|
||||||
|
fields = ["name", "link", "origins"]
|
||||||
|
widgets = {
|
||||||
|
"name": forms.TextInput(),
|
||||||
|
"origins": forms.TextInput(),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
"origins": "Allowed Hostnames",
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
"name": _("What should the service be called?"),
|
||||||
|
"link": _("What's the service's primary URL?"),
|
||||||
|
"origins": _(
|
||||||
|
"At what hostnames does the service operate? This sets CORS headers, so use '*' if you're not sure (or don't care)."
|
||||||
|
),
|
||||||
|
}
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('core', '0001_initial'),
|
("core", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='service',
|
model_name="service",
|
||||||
name='anonymize_ips',
|
name="anonymize_ips",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
14
shynet/core/migrations/0003_remove_service_anonymize_ips.py
Normal file
14
shynet/core/migrations/0003_remove_service_anonymize_ips.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 3.0.5 on 2020-04-11 23:25
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0002_service_anonymize_ips"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="service", name="anonymize_ips",),
|
||||||
|
]
|
36
shynet/core/mixins.py
Normal file
36
shynet/core/mixins.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class DateRangeMixin:
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
if self.request.GET.get("startDate") != None:
|
||||||
|
found_time = timezone.datetime.strptime(
|
||||||
|
self.request.GET.get("startDate"), "%Y-%m-%d"
|
||||||
|
)
|
||||||
|
found_time.replace(hour=0, minute=0)
|
||||||
|
data["start_date"] = found_time
|
||||||
|
else:
|
||||||
|
data["start_date"] = timezone.now() - timezone.timedelta(days=30)
|
||||||
|
|
||||||
|
if self.request.GET.get("endDate") != None:
|
||||||
|
found_time = timezone.datetime.strptime(
|
||||||
|
self.request.GET.get("endDate"), "%Y-%m-%d"
|
||||||
|
)
|
||||||
|
found_time.replace(hour=23, minute=59)
|
||||||
|
data["end_date"] = found_time
|
||||||
|
else:
|
||||||
|
data["end_date"] = timezone.now()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUrlMixin:
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
url_data = urlparse(self.request.build_absolute_uri())
|
||||||
|
data["base_url"] = f"{url_data.scheme}://{url_data.netloc}"
|
||||||
|
return data
|
@ -1,10 +1,10 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.apps import apps
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.db.utils import NotSupportedError
|
from django.db.utils import NotSupportedError
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
def _default_uuid():
|
def _default_uuid():
|
||||||
@ -39,9 +39,6 @@ class Service(models.Model):
|
|||||||
max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True
|
max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Analytics settings
|
|
||||||
anonymize_ips = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@ -63,8 +60,12 @@ class Service(models.Model):
|
|||||||
service=self, start_time__gt=timezone.now() - timezone.timedelta(seconds=10)
|
service=self, start_time__gt=timezone.now() - timezone.timedelta(seconds=10)
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
sessions = Session.objects.filter(
|
sessions = (
|
||||||
service=self, start_time__gt=start_time, start_time__lt=end_time
|
Session.objects.filter(
|
||||||
|
service=self, start_time__gt=start_time, start_time__lt=end_time
|
||||||
|
)
|
||||||
|
.prefetch_related("hit_set")
|
||||||
|
.order_by("-start_time")
|
||||||
)
|
)
|
||||||
session_count = sessions.count()
|
session_count = sessions.count()
|
||||||
|
|
||||||
@ -76,28 +77,80 @@ class Service(models.Model):
|
|||||||
bounces = sessions.annotate(hit_count=models.Count("hit")).filter(hit_count=1)
|
bounces = sessions.annotate(hit_count=models.Count("hit")).filter(hit_count=1)
|
||||||
bounce_count = bounces.count()
|
bounce_count = bounces.count()
|
||||||
|
|
||||||
|
locations = (
|
||||||
|
hits.values("location")
|
||||||
|
.annotate(count=models.Count("location"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
referrers = (
|
||||||
|
hits.filter(initial=True)
|
||||||
|
.values("referrer")
|
||||||
|
.annotate(count=models.Count("referrer"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
operating_systems = (
|
||||||
|
sessions.values("os").annotate(count=models.Count("os")).order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
browsers = (
|
||||||
|
sessions.values("browser")
|
||||||
|
.annotate(count=models.Count("browser"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
device_types = (
|
||||||
|
sessions.values("device_type")
|
||||||
|
.annotate(count=models.Count("device_type"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
sessions.values("device")
|
||||||
|
.annotate(count=models.Count("device"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
device_types = (
|
||||||
|
sessions.values("device_type")
|
||||||
|
.annotate(count=models.Count("device_type"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
|
||||||
|
avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[
|
||||||
|
"load_time__avg"
|
||||||
|
]
|
||||||
|
|
||||||
|
avg_hits_per_session = hit_count / max(session_count, 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
avg_session_duration = sessions.annotate(
|
avg_session_duration = sessions.annotate(
|
||||||
duration=models.F("last_seen") - models.F("start_time")
|
duration=models.F("last_seen") - models.F("start_time")
|
||||||
).aggregate(duration=models.Avg("duration"))["duration"]
|
).aggregate(duration=models.Avg("duration"))["duration"]
|
||||||
except NotSupportedError:
|
except NotSupportedError:
|
||||||
avg_session_duration = (
|
avg_session_duration = sum(
|
||||||
sum(
|
[
|
||||||
[
|
(session.last_seen - session.start_time).total_seconds()
|
||||||
(session.last_seen - session.start_time).total_seconds()
|
for session in sessions
|
||||||
for session in sessions
|
]
|
||||||
]
|
) / max(session_count, 1)
|
||||||
)
|
|
||||||
/ session_count
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"currently_online": currently_online,
|
"currently_online": currently_online,
|
||||||
"sessions": session_count,
|
"session_count": session_count,
|
||||||
"hits": hit_count,
|
"hit_count": hit_count,
|
||||||
"avg_hits_per_session": hit_count / (max(session_count, 1)),
|
"avg_hits_per_session": hit_count / (max(session_count, 1)),
|
||||||
"bounce_rate_pct": bounce_count * 100 / session_count,
|
"bounce_rate_pct": bounce_count * 100 / (max(session_count, 1)),
|
||||||
"avg_session_duration": avg_session_duration,
|
"avg_session_duration": avg_session_duration,
|
||||||
"uptime": 99.9,
|
"avg_load_time": avg_load_time,
|
||||||
|
"avg_hits_per_session": avg_hits_per_session,
|
||||||
|
"locations": locations,
|
||||||
|
"referrers": referrers,
|
||||||
|
"operating_systems": operating_systems,
|
||||||
|
"browsers": browsers,
|
||||||
|
"devices": devices,
|
||||||
|
"device_types": device_types,
|
||||||
|
"sessions": sessions,
|
||||||
"online": True,
|
"online": True,
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% include 'a17t/head.html' %}
|
{% include 'a17t/head.html' %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/litepicker/dist/js/main.js"></script>
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
@ -15,18 +16,20 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<section class="max-w-screen-lg mx-auto px-6 md:py-12 md:flex">
|
<section class="max-w-screen-lg mx-auto px-6 md:py-12 md:flex">
|
||||||
<aside class="w-2/12 mr-4 block">
|
<aside class="w-2/12 mr-8 block">
|
||||||
<a class="icon ~urge ml-6 mb-8 mt-3" href="{% url 'core:dashboard' %}">
|
<a class="icon ~urge ml-6 mb-12 mt-3" href="{% url 'core:dashboard' %}">
|
||||||
<i class="fas fa-low-vision fa-3x text-purple-600"></i>
|
<i class="fas fa-low-vision fa-3x text-purple-600"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p class="ml-2 mt-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p>
|
|
||||||
|
|
||||||
{% for service in user.owning_services.all %}
|
{% for service in user.owning_services.all %}
|
||||||
{% url 'account_email' as url %}
|
{% url 'core:service' service.uuid as url %}
|
||||||
{% include 'core/includes/sidebar_portal.html' with label=service.name url=url %}
|
{% include 'core/includes/sidebar_portal.html' with label=service.name url=url %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% url 'core:service_create' as url %}
|
||||||
|
{% include 'core/includes/sidebar_portal.html' with label="+ Create" url=url %}
|
||||||
|
|
||||||
<p class="ml-2 mt-8 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p>
|
<p class="ml-2 mt-8 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p>
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
33
shynet/core/templates/core/includes/date_range.html
Normal file
33
shynet/core/templates/core/includes/date_range.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<form method="GET" id="datePicker">
|
||||||
|
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
|
||||||
|
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
|
||||||
|
</form>
|
||||||
|
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral !low cursor-pointer" readonly>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--litepickerMonthButtonHover: var(--color-urge);
|
||||||
|
--litepickerDayColorHover: var(--color-urge);
|
||||||
|
--litepickerDayIsTodayColor: var(--color-urge);
|
||||||
|
--litepickerDayIsInRange: var(--color-urge-normal-fill);
|
||||||
|
--litepickerDayIsStartBg: var(--color-urge);
|
||||||
|
--litepickerDayIsEndBg: var(--color-urge);
|
||||||
|
--litepickerButtonApplyBg: var(--color-urge);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
var picker = new Litepicker({
|
||||||
|
element: document.getElementById('rangePicker'),
|
||||||
|
singleMode: false,
|
||||||
|
format: 'MMM D, YYYY',
|
||||||
|
maxDate: new Date(),
|
||||||
|
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
|
||||||
|
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),
|
||||||
|
onSelect: function (startDate, endDate) {
|
||||||
|
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() +
|
||||||
|
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate());
|
||||||
|
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" +
|
||||||
|
(endDate.getMonth() + 1) + "-" + endDate.getDate());
|
||||||
|
document.getElementById("datePicker").submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,45 +1,30 @@
|
|||||||
{% load humanizelib %}
|
{% load humanize helpers %}
|
||||||
<article class="card ~neutral !low service mb-4">
|
|
||||||
{% with stats=service.get_daily_stats %}
|
<a class="card ~neutral !low service mb-6" href="{% url 'core:service' service.uuid %}">
|
||||||
<div class="md:flex justify-between">
|
{% with stats=service.stats %}
|
||||||
<div class="mr-4 md:w-1/4">
|
<div class="md:flex justify-between items-center">
|
||||||
<h3 class="heading text-2xl mr-2">
|
<div class="mr-4 md:w-4/12">
|
||||||
|
<h3 class="heading text-xl mr-2 mb-1">
|
||||||
{{service.name}}
|
{{service.name}}
|
||||||
</h3>
|
</h3>
|
||||||
{% if stats.currently_online > 0 %}
|
{% include 'core/includes/stats_status_chip.html' %}
|
||||||
<span class="badge ~positive">
|
|
||||||
{{stats.currently_online|intcomma}} online
|
|
||||||
</span>
|
|
||||||
{% elif stats.online == True %}
|
|
||||||
<span class="badge ~positive">
|
|
||||||
Online
|
|
||||||
</span>
|
|
||||||
{% elif stats.online == False %}
|
|
||||||
<span class="badge ~critical">
|
|
||||||
Offline
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<p class="font-medium text-sm">Sessions</p>
|
<p class="font-medium text-sm">Sessions</p>
|
||||||
<p class="text-xl text-purple-700 font-medium">{{stats.sessions|intcomma}}</p>
|
<p class="text-xl text-purple-700 font-medium">{{stats.session_count|intcomma}}</p>
|
||||||
</div>
|
|
||||||
<div class="mr-2">
|
|
||||||
<p class="font-medium text-sm">Pages/Session</p>
|
|
||||||
<p class="text-xl text-purple-700 font-medium">{{stats.avg_hits_per_session|floatformat:"-1"}}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<p class="font-medium text-sm">Bounce Rate</p>
|
<p class="font-medium text-sm">Bounce Rate</p>
|
||||||
<p class="text-xl text-purple-700 font-medium">{{stats.bounce_rate_pct|floatformat:"-1"}}%</p>
|
<p class="text-xl text-purple-700 font-medium">{{stats.bounce_rate_pct|floatformat:"-1"}}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<p class="font-medium text-sm">Avg. Session</p>
|
<p class="font-medium text-sm">Avg. Duration</p>
|
||||||
<p class="text-xl text-purple-700 font-medium">{{stats.avg_session_duration|naturaldelta}}</p>
|
<p class="text-xl text-purple-700 font-medium">{{stats.avg_session_duration|naturaldelta}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
<p class="font-medium text-sm">Uptime</p>
|
<p class="font-medium text-sm">Uptime</p>
|
||||||
<p class="text-xl text-purple-700 font-medium">{{stats.uptime|floatformat:"-1"}}%</p>
|
<p class="text-xl text-purple-700 font-medium">99.9%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</article>
|
</a>
|
@ -1 +1,2 @@
|
|||||||
<a class="portal !low w-full {% if url == request.get_full_path %}~urge active bg-gray-100{% endif %}" href="{{url}}">{{label}}</a>
|
<a class="portal {% if url == request.get_full_path %}~urge active bg-gray-100{% endif %}"
|
||||||
|
href="{{url}}">{{label}}</a><br>
|
15
shynet/core/templates/core/includes/stats_status_chip.html
Normal file
15
shynet/core/templates/core/includes/stats_status_chip.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% if stats.currently_online > 0 %}
|
||||||
|
<span class="chip ~positive">
|
||||||
|
{{stats.currently_online|intcomma}} online
|
||||||
|
</span>
|
||||||
|
{% elif stats.online == True %}
|
||||||
|
<span class="chip ~positive">
|
||||||
|
Online
|
||||||
|
</span>
|
||||||
|
{% elif stats.online == False %}
|
||||||
|
<span class="chip ~critical">
|
||||||
|
Offline
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
@ -1,13 +1,18 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h4 class="heading text-xl leading-none text-purple-600">Shynet</h4>
|
<div class="md:flex justify-between">
|
||||||
<h4 class="heading text-5xl leading-none">Dashboard</h4>
|
<h4 class="heading text-5xl leading-none">Shynet Dash</h4>
|
||||||
|
<div>
|
||||||
|
{% include 'core/includes/date_range.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr class="sep">
|
<hr class="sep">
|
||||||
|
|
||||||
{% for service in user.owning_services.all %}
|
{% for service in services %}
|
||||||
{% include 'core/includes/service_overview.html' %}
|
{% include 'core/includes/service_overview.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p>You don't have any services.</p>
|
<p>You don't have any services.</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<a href="{% url 'core:service_create' %}" class="button ~neutral my-2">+ New Service</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
230
shynet/core/templates/core/pages/service.html
Normal file
230
shynet/core/templates/core/pages/service.html
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load humanize helpers %}
|
||||||
|
|
||||||
|
{% block head_title %}{{service.name}}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.limited-height {
|
||||||
|
max-height: 400px !important;
|
||||||
|
overflow: scroll !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="md:flex justify-between items-center" id="heading">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h3 class="heading leading-none mr-4">
|
||||||
|
{{service.name}}
|
||||||
|
</h3>
|
||||||
|
<div class='text-3xl'>
|
||||||
|
{% include 'core/includes/stats_status_chip.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-2">{% include 'core/includes/date_range.html' %}</div>
|
||||||
|
<a href="{% url 'core:service_update' service.uuid %}" class="button field ~neutral !low w-auto">Manage →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="sep h-8">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="stats">
|
||||||
|
<article class="card ~neutral !low">
|
||||||
|
<p class="label">Sessions</p>
|
||||||
|
<p class="heading text-purple-600">
|
||||||
|
{{stats.session_count|intcomma}}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="card ~neutral !low">
|
||||||
|
<p class="label">Hits</p>
|
||||||
|
<p class="heading text-purple-600">
|
||||||
|
{{stats.hit_count|intcomma}}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="card ~neutral !low">
|
||||||
|
<p class="label">Avg. Load Time</p>
|
||||||
|
<p class="heading text-purple-600">
|
||||||
|
{{stats.avg_load_time|floatformat:"0"}}ms
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="card ~neutral !low">
|
||||||
|
<p class="label">Bounce Rate</p>
|
||||||
|
<p class="heading text-purple-600">{{stats.bounce_rate_pct|floatformat:"-1"}}%</p>
|
||||||
|
</article>
|
||||||
|
<article class="card ~neutral !low">
|
||||||
|
<p class="label">Avg. Duration</p>
|
||||||
|
<p class="heading text-purple-600">{{stats.avg_session_duration|naturaldelta}}</p>
|
||||||
|
</article>
|
||||||
|
<article class="card ~neutral !low">
|
||||||
|
<p class="label">Avg. Hits/Session</p>
|
||||||
|
<p class="heading text-purple-600">{{stats.avg_hits_per_session|floatformat:"-1"}}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Hits</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for location in stats.locations %}
|
||||||
|
<tr>
|
||||||
|
<td>{{location.location}}</td>
|
||||||
|
<td>{{location.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Referrer</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for referrer in stats.referrers %}
|
||||||
|
<tr>
|
||||||
|
<td>{{referrer.referrer|default:"Direct"}}</td>
|
||||||
|
<td>{{referrer.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Operating System</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for os in stats.operating_systems %}
|
||||||
|
<tr>
|
||||||
|
<td>{{os.os|default:"Unknown"}}</td>
|
||||||
|
<td>{{os.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Browser</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for browser in stats.browsers %}
|
||||||
|
<tr>
|
||||||
|
<td>{{browser.browser|default:"Unknown"}}</td>
|
||||||
|
<td>{{browser.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for device in stats.devices %}
|
||||||
|
<tr>
|
||||||
|
<td>{{device.device|default:"Unknown"}}</td>
|
||||||
|
<td>{{device.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Device Type</th>
|
||||||
|
<th>Sessions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for device_type in stats.device_types %}
|
||||||
|
<tr>
|
||||||
|
<td>{{device_type.device_type|default:"Unknown"|title}}</td>
|
||||||
|
<td>{{device_type.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low limited-height">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Identity</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Network</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Hits</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for session in stats.sessions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
|
||||||
|
{% if session.is_currently_active %}
|
||||||
|
<span class="badge ~positive">Online</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{session.identifier|default:"Anonymous"}}</td>
|
||||||
|
<td>{{session.duration|naturaldelta}}</td>
|
||||||
|
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
|
||||||
|
<td>{{session.ip}}</td>
|
||||||
|
<td>{{session.hit_set.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% for session in stats.sessions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
|
||||||
|
{% if session.is_currently_active %}
|
||||||
|
<span class="badge ~positive">Online</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{session.identifier|default:"Anonymous"}}</td>
|
||||||
|
<td>{{session.duration|naturaldelta}}</td>
|
||||||
|
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
|
||||||
|
<td>{{session.ip}}</td>
|
||||||
|
<td>{{session.hit_set.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% for session in stats.sessions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
|
||||||
|
{% if session.is_currently_active %}
|
||||||
|
<span class="badge ~positive">Online</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{session.identifier|default:"Anonymous"}}</td>
|
||||||
|
<td>{{session.duration|naturaldelta}}</td>
|
||||||
|
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
|
||||||
|
<td>{{session.ip}}</td>
|
||||||
|
<td>{{session.hit_set.count|intcomma}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
20
shynet/core/templates/core/pages/service_create.html
Normal file
20
shynet/core/templates/core/pages/service_create.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load a17t_tags %}
|
||||||
|
|
||||||
|
{% block head_title %}Create Service{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h4 class="heading text-5xl leading-none">Create Service</h4>
|
||||||
|
<hr class="sep">
|
||||||
|
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="p-4">
|
||||||
|
{{form|a17t}}
|
||||||
|
</div>
|
||||||
|
<div class="section ~urge !normal p-4">
|
||||||
|
<button type="submit" class="button ~urge !high">Create</button>
|
||||||
|
<a href="{% url 'core:dashboard' %}" class="button ~urge !low">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
33
shynet/core/templates/core/pages/service_update.html
Normal file
33
shynet/core/templates/core/pages/service_update.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load a17t_tags %}
|
||||||
|
|
||||||
|
{% block head_title %}{{object.name}} Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="md:flex justify-between items-center" id="heading">
|
||||||
|
<h3 class="heading leading-none mr-4">
|
||||||
|
{{object.name}}
|
||||||
|
</h3>
|
||||||
|
<a href="{% url 'core:service' object.uuid %}" class="button field ~neutral !low w-auto">View →</a>
|
||||||
|
</div>
|
||||||
|
<hr class="sep">
|
||||||
|
<div class="max-w-xl content">
|
||||||
|
<h5>Analytics Installation</h5>
|
||||||
|
<p>(At the end of <code><body></code>)
|
||||||
|
<pre
|
||||||
|
class="text-sm"><code>{% filter force_escape %}<noscript><img src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/pixel.gif"></noscript>
|
||||||
|
<script src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/identifier/script.js"></script>{% endfilter %}</pre></code>
|
||||||
|
</div>
|
||||||
|
<hr class="sep">
|
||||||
|
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="p-4">
|
||||||
|
{{form|a17t}}
|
||||||
|
</div>
|
||||||
|
<div class="section ~neutral !normal p-4">
|
||||||
|
<button type="submit" class="button ~neutral !high">Save</button>
|
||||||
|
<a href="{% url 'core:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
0
shynet/core/templatetags/__init__.py
Normal file
0
shynet/core/templatetags/__init__.py
Normal file
27
shynet/core/templatetags/helpers.py
Normal file
27
shynet/core/templatetags/helpers.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import flag
|
||||||
|
from django import template
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def naturaldelta(timedelta):
|
||||||
|
if isinstance(timedelta, timezone.timedelta):
|
||||||
|
seconds = timedelta.seconds
|
||||||
|
else:
|
||||||
|
seconds = timedelta
|
||||||
|
string = ""
|
||||||
|
if seconds // 3600 > 0:
|
||||||
|
string += "{:02.0f}:".format(seconds // 3600)
|
||||||
|
string += "{:02.0f}:".format((seconds % 3600) // 60)
|
||||||
|
string += "{:02.0f}".format(seconds % 60)
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def flag_emoji(isocode):
|
||||||
|
try:
|
||||||
|
return flag.flag(isocode)
|
||||||
|
except:
|
||||||
|
return ""
|
@ -6,4 +6,11 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.IndexView.as_view(), name="index"),
|
path("", views.IndexView.as_view(), name="index"),
|
||||||
path("dash/", views.DashboardView.as_view(), name="dashboard"),
|
path("dash/", views.DashboardView.as_view(), name="dashboard"),
|
||||||
|
path("dash/service/new/", views.ServiceCreateView.as_view(), name="service_create"),
|
||||||
|
path("dash/service/<pk>/", views.ServiceView.as_view(), name="service"),
|
||||||
|
path(
|
||||||
|
"dash/service/<pk>/manage/",
|
||||||
|
views.ServiceUpdateView.as_view(),
|
||||||
|
name="service_update",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,10 +1,53 @@
|
|||||||
from django.views.generic import TemplateView
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.generic import (CreateView, DetailView, TemplateView,
|
||||||
|
UpdateView)
|
||||||
|
|
||||||
|
from .forms import ServiceForm
|
||||||
|
from .mixins import BaseUrlMixin, DateRangeMixin
|
||||||
|
from .models import Service
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
template_name = "core/pages/index.html"
|
template_name = "core/pages/index.html"
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(LoginRequiredMixin, TemplateView):
|
class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView):
|
||||||
template_name = "core/pages/dashboard.html"
|
template_name = "core/pages/dashboard.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
data["services"] = Service.objects.filter(owner=self.request.user)
|
||||||
|
for service in data["services"]:
|
||||||
|
service.stats = service.get_core_stats(data["start_date"], data["end_date"])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
model = Service
|
||||||
|
form_class = ServiceForm
|
||||||
|
template_name = "core/pages/service_create.html"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.owner = self.request.user
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceView(LoginRequiredMixin, DateRangeMixin, DetailView):
|
||||||
|
model = Service
|
||||||
|
template_name = "core/pages/service.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUpdateView(LoginRequiredMixin, BaseUrlMixin, UpdateView):
|
||||||
|
model = Service
|
||||||
|
form_class = ServiceForm
|
||||||
|
template_name = "core/pages/service_update.html"
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("core:service", kwargs={"uuid": self.object.uuid})
|
||||||
|
@ -12,6 +12,9 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# Messages
|
||||||
|
from django.contrib.messages import constants as messages
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@ -38,7 +41,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.sites",
|
"django.contrib.sites",
|
||||||
"django_humanize",
|
"django.contrib.humanize",
|
||||||
"a17t",
|
"a17t",
|
||||||
"core",
|
"core",
|
||||||
"analytics",
|
"analytics",
|
||||||
@ -151,12 +154,10 @@ CELERY_REDIS_SOCKET_TIMEOUT = 15
|
|||||||
MAXMIND_CITY_DB = os.getenv("MAXMIND_CITY_DB")
|
MAXMIND_CITY_DB = os.getenv("MAXMIND_CITY_DB")
|
||||||
MAXMIND_ASN_DB = os.getenv("MAXMIND_ASN_DB")
|
MAXMIND_ASN_DB = os.getenv("MAXMIND_ASN_DB")
|
||||||
|
|
||||||
# Messages
|
|
||||||
from django.contrib.messages import constants as messages
|
|
||||||
MESSAGE_TAGS = {
|
MESSAGE_TAGS = {
|
||||||
messages.INFO: '~info',
|
messages.INFO: "~info",
|
||||||
messages.WARNING: "~warning",
|
messages.WARNING: "~warning",
|
||||||
messages.ERROR: "~critical",
|
messages.ERROR: "~critical",
|
||||||
messages.SUCCESS: "~positive",
|
messages.SUCCESS: "~positive",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,14 +10,6 @@
|
|||||||
<noscript>
|
<noscript>
|
||||||
<img src="http://localhost:8000/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/pixel.gif">
|
<img src="http://localhost:8000/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/pixel.gif">
|
||||||
</noscript>
|
</noscript>
|
||||||
<script>
|
|
||||||
var shynetSessionMetadata = {
|
|
||||||
session: "this is some session metadata",
|
|
||||||
};
|
|
||||||
var shynetHitMetadata = {
|
|
||||||
hit: "this is some hit metadata",
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script src="http://localhost:8000/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/identifier/script.js"></script>
|
<script src="http://localhost:8000/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/identifier/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user