General expansions

This commit is contained in:
R. Miles McCain 2020-04-12 23:30:20 -04:00
parent b7d84085a3
commit c5deaeaa92
No known key found for this signature in database
GPG Key ID: 91CB47BDDF2671A5
25 changed files with 348 additions and 196 deletions

View File

@ -16,6 +16,7 @@ pyyaml = "*"
ua-parser = "*" ua-parser = "*"
user-agents = "*" user-agents = "*"
emoji-country-flag = "*" emoji-country-flag = "*"
rules = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

9
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "93fe2edcab5c588173dcaa4799fd66873e47014f417920ad5a969544a33e5cb5" "sha256": "14b7afb8af8c07320e7c765ae013966a5e95dd708a23f056981378beca52846d"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -184,6 +184,13 @@
], ],
"version": "==1.3.0" "version": "==1.3.0"
}, },
"rules": {
"hashes": [
"sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"
],
"index": "pypi",
"version": "==2.2"
},
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",

View File

@ -38,5 +38,4 @@
{% if field.help_text %} {% if field.help_text %}
<p class="support">{{field.help_text|safe}}</p> <p class="support">{{field.help_text|safe}}</p>
{% endif %} {% endif %}
</field> </field>

View File

@ -1,23 +1,23 @@
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination"> <nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
<div class="w-full md:w-auto mb-2"> <div class="w-full md:w-auto mb-2">
{% if page.has_previous %} {% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button ~neutral !low">Previous</a> <a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field w-auto mr-1">Previous</a>
{% else %} {% else %}
<a class="button ~neutral !normal" disabled>Previous</a> <a class="button field w-auto mr-1" disabled>Previous</a>
{% endif %} {% endif %}
{% if page.has_next %} {% if page.has_next %}
<a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button ~neutral !low">Next</a> <a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button field w-auto">Next</a>
{% else %} {% else %}
<a class="button ~neutral !normal" disabled>Next</a> <a class="button field w-auto" disabled>Next</a>
{% endif %} {% endif %}
</div> </div>
<ul class="pagination-list w-full md:w-auto mb-2"> <ul class="pagination-list w-full md:w-auto mb-2">
{% for pnum in begin %} {% for pnum in begin %}
{% ifequal page.number pnum %} {% ifequal page.number pnum %}
<li><a class="button ~neutral !high">{{ pnum }}</a></li> <li><a class="button field w-auto text-white bg-gray-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button ~neutral !normal" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field w-auto" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
@ -25,9 +25,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li> <li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in middle %} {% for pnum in middle %}
{% ifequal page.number pnum %} {% ifequal page.number pnum %}
<li><a class="button ~neutral !high">{{ pnum }}</a></li> <li><a class="button field w-auto text-white bg-gray-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button ~neutral !normal" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field w-auto" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -36,9 +36,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li> <li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in end %} {% for pnum in end %}
{% ifequal page.number pnum %} {% ifequal page.number pnum %}
<li><a class="button ~neutral !high">{{ pnum }}</a></li> <li><a class="button field w-auto text-white bg-gray-700">{{ pnum }}</a></li>
{% else %} {% else %}
<li><a class="button ~neutral !normal" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li> <li><a class="button field w-auto" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %} {% endifequal %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -4,7 +4,6 @@ from django.utils import timezone
class DateRangeMixin: class DateRangeMixin:
def get_start_date(self): def get_start_date(self):
if self.request.GET.get("startDate") != None: if self.request.GET.get("startDate") != None:
found_time = timezone.datetime.strptime( found_time = timezone.datetime.strptime(

View File

@ -1,12 +1,12 @@
import uuid
import json import json
import uuid
from django.apps import apps 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.db.models.functions import TruncDate
from django.db.utils import NotSupportedError from django.db.utils import NotSupportedError
from django.utils import timezone from django.utils import timezone
from django.db.models.functions import TruncDate
def _default_uuid(): def _default_uuid():
@ -120,7 +120,7 @@ class Service(models.Model):
"load_time__avg" "load_time__avg"
] ]
avg_hits_per_session = hit_count / max(session_count, 1) avg_hits_per_session = hit_count / session_count if session_count > 0 else None
try: try:
avg_session_duration = sessions.annotate( avg_session_duration = sessions.annotate(
@ -133,6 +133,8 @@ class Service(models.Model):
for session in sessions for session in sessions
] ]
) / max(session_count, 1) ) / max(session_count, 1)
if session_count == 0:
avg_session_duration = None
session_chart_data = { session_chart_data = {
k["date"]: k["count"] k["date"]: k["count"]
@ -151,7 +153,9 @@ class Service(models.Model):
"session_count": session_count, "session_count": session_count,
"hit_count": 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 / (max(session_count, 1)), "bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration, "avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time, "avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session, "avg_hits_per_session": avg_hits_per_session,

25
shynet/core/rules.py Normal file
View File

@ -0,0 +1,25 @@
import rules
from django.conf import settings
@rules.predicate
def is_service_creator(user):
if settings.ONLY_SUPERUSERS_CREATE:
return user.is_superuser
return True
@rules.predicate
def is_service_owner(service, user):
return service.owner == user
@rules.predicate
def is_service_collaborator(service, user):
return user in service.collaborators.all()
rules.add_perm("core.view_service", is_service_owner | is_service_collaborator)
rules.add_perm("core.delete_service", is_service_owner)
rules.add_perm("core.change_service", is_service_owner)
rules.add_perm("core.create_service", is_service_creator)

View File

@ -0,0 +1,12 @@
.table tbody tr:nth-child(2n) {
background-color: transparent;
}
.limited-height {
overflow-y: scroll;
max-height: 400px;
}
.rf {
text-align: right !important;
}

View File

@ -9,10 +9,10 @@
<hr class="sep"> <hr class="sep">
<div class="card ~neutral !low"> <div class="card ~neutral !low max-w-lg">
{% if user.emailaddress_set.all %} {% if user.emailaddress_set.all %}
<p>{% trans 'The following email addresses are associated with your account:' %}</p> <p>{% trans 'These are your known email addresses:' %}</p>
<form action="{% url 'account_email' %}" class="email_list" method="post"> <form action="{% url 'account_email' %}" class="email_list" method="post">
{% csrf_token %} {% csrf_token %}
<fieldset class="blockLabels"> <fieldset class="blockLabels">
@ -36,9 +36,9 @@
{% endfor %} {% endfor %}
<div class="block mt-4"> <div class="block mt-4">
<button class="button ~urge mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button> <button class="button ~neutral !high mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~info mb-1" type="submit" name="action_send">{% trans 'Resend Verification' %}</button> <button class="button ~neutral mb-1" type="submit" name="action_send">{% trans 'Resend Verification' %}</button>
<button class="button ~critical mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button> <button class="button ~neutral mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button>
</div> </div>
</fieldset> </fieldset>
</form> </form>
@ -54,10 +54,10 @@
<hr class="sep"> <hr class="sep">
<form method="post" action="{% url 'account_email' %}" class="add_email max-w-lg"> <form method="post" action="{% url 'account_email' %}" class="card ~neutral !low max-w-lg">
{% csrf_token %} {% csrf_token %}
{{ form|a17t }} {{ form|a17t }}
<button name="action_add" class="button ~neutral !high" type="submit">{% trans "Add Email" %}</button> <button name="action_add" class="button ~neutral !high" type="submit">{% trans "Add Address" %}</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,3 +1,4 @@
{% load static rules %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -10,27 +11,45 @@
<script src="https://cdn.jsdelivr.net/npm/litepicker/dist/js/main.js"></script> <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/js/main.js"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.18.1/dist/apexcharts.min.js" <script src="https://cdn.jsdelivr.net/npm/apexcharts@3.18.1/dist/apexcharts.min.js"
integrity="sha256-RalQXBZdisB04aaBsm+6YZ0b/iRYjX1MZn90m19AnCY=" crossorigin="anonymous"></script> integrity="sha256-RalQXBZdisB04aaBsm+6YZ0b/iRYjX1MZn90m19AnCY=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
{% block extra_head %} {% block extra_head %}
{% endblock %} {% endblock %}
</head> </head>
<body class="bg-gray-100 min-h-full"> <body class="bg-gray-200 min-h-full">
{% block body %} {% block body %}
<section class="max-w-screen-lg mx-auto px-6 md:py-12 md:flex"> <section class="max-w-screen-xl mx-auto px-6 md:px-0 md:py-12 md:flex">
<aside class="w-2/12 mr-8 block"> <aside class="md:w-2/12 md:pr-6">
<a class="icon ~urge ml-6 mb-12 mt-3" href="{% url 'core:dashboard' %}"> <a class="icon ~urge ml-6 mb-8 mt-3" href="{% url 'core:dashboard' %}">
<i class="fas fa-low-vision fa-3x text-purple-600"></i> <i class="fas fa-book-reader fa-3x text-purple-600"></i>
</a> </a>
{% if user.owning_services.all %}
<p class="ml-2 mt-8 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 'core:service' service.uuid 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 %}
{% endif %}
{% has_perm 'core.create_service' user as can_create %}
{% if can_create %}
{% url 'core:service_create' as url %} {% url 'core:service_create' as url %}
{% include 'core/includes/sidebar_portal.html' with label="+ Create" url=url %} {% include 'core/includes/sidebar_portal.html' with label="+ Create" url=url %}
{% endif %}
{% if user.collaborating_services.all %}
<p class="ml-2 mt-8 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p>
{% for service in user.collaborating_services.all %}
{% url 'core:service' service.uuid as url %}
{% include 'core/includes/sidebar_portal.html' with label=service.name url=url %}
{% endfor %}
{% endif %}
<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>
@ -52,7 +71,7 @@
{% endif %} {% endif %}
</aside> </aside>
<div class="flex-grow"> <div class="md:w-10/12">
{% if messages %} {% if messages %}
<div> <div>
{% for message in messages %} {% for message in messages %}
@ -60,6 +79,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<hr class="sep">
{% endif %} {% endif %}
<main> <main>
{% block content %} {% block content %}

View File

@ -2,7 +2,7 @@
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate"> <input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate"> <input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
</form> </form>
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral !low cursor-pointer" readonly> <input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral cursor-pointer" readonly>
<style> <style>
:root { :root {
--litepickerMonthButtonHover: var(--color-urge); --litepickerMonthButtonHover: var(--color-urge);

View File

@ -3,8 +3,8 @@
<a class="card ~neutral !low service mb-6 p-0" href="{% url 'core:service' object.uuid %}"> <a class="card ~neutral !low service mb-6 p-0" href="{% url 'core:service' object.uuid %}">
{% with stats=object.stats %} {% with stats=object.stats %}
<div class="p-4 md:flex justify-between"> <div class="p-4 md:flex justify-between">
<div class="md:w-4/12"> <div class="md:w-4/12 flex items-center">
<h3 class="heading text-xl mr-2 mb-1 text-purple-600"> <h3 class="heading mr-2 mb-1 text-purple-600">
{{object.name}} {{object.name}}
</h3> </h3>
{% include 'core/includes/stats_status_chip.html' %} {% include 'core/includes/stats_status_chip.html' %}
@ -14,23 +14,35 @@
<p>Sessions</p> <p>Sessions</p>
<p class="label">{{stats.session_count|intcomma}}</p> <p class="label">{{stats.session_count|intcomma}}</p>
</div> </div>
<div>
<p>Hits</p>
<p class="label">{{stats.hit_count|intcomma}}</p>
</div>
<div> <div>
<p>Bounce Rate</p> <p>Bounce Rate</p>
<p class="label">{{stats.bounce_rate_pct|floatformat:"-1"}}%</p> <p class="label">
{% if stats.bounce_rate_pct != None %}
{{stats.bounce_rate_pct|floatformat:"-1"}}%
{% else %}
?
{% endif %}
</p>
</div> </div>
<div> <div>
<p>Avg. Duration</p> <p>Avg. Duration</p>
<p class="label">{{stats.avg_session_duration|naturaldelta}}</p> <p class="label">
</div> {% if stats.avg_session_duration != None %}
<div> {{stats.avg_session_duration|naturaldelta}}
<p>Uptime</p> {% else %}
<p class="label">99.9%</p> ?
{% endif %}
</p>
</div> </div>
</div> </div>
</div> </div>
<hr class="sep h-4"> <hr class="sep h-4">
<div style="bottom: -1px;"> <div style="bottom: -1px;">
{% include 'core/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 %} {% include 'core/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %}
</div> </div>
{% endwith %} {% endwith %}
</a> </a>

View File

@ -1,4 +1,6 @@
{% load helpers %} {% load helpers %}
<a class="portal {% if request.get_full_path|startswith:url %}~urge active bg-gray-100{% endif %}" <div>
href="{{url}}">{{label}}</a><br> <a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-gray-100{% endif %}"
href="{{url}}">{{label}}</a>
</div>

View File

@ -2,15 +2,15 @@
{% with stats=object.get_daily_stats %} {% with stats=object.get_daily_stats %}
{% if stats.currently_online > 0 %} {% if stats.currently_online > 0 %}
<span class="chip ~positive"> <span class="chip ~positive !high">
{{stats.currently_online|intcomma}} online {{stats.currently_online|intcomma}} online
</span> </span>
{% elif stats.online == True %} {% elif stats.online == True %}
<span class="chip ~positive"> <span class="chip ~positive !high">
Online Online
</span> </span>
{% elif stats.online == False %} {% elif stats.online == False %}
<span class="chip ~critical"> <span class="chip ~critical !high">
Offline Offline
</span> </span>
{% endif %} {% endif %}

View File

@ -1,76 +1,74 @@
<div id="{{name|default:'timeChart'}}"></div> <div id="chart{{name|default:'Main'}}"></div>
<script> <script>
var triggerMatchesChartOptions = { var triggerMatchesChartOptions = {
dataLabels: { dataLabels: {
enabled: false
},
tooltip: {
shared: false,
},
colors: ["#805AD5"],
chart: {
toolbar: {
show: false,
},
type: 'area',
height: {{height|default:"200"}},
offsetY: -1,
animations: {
enabled: false enabled: false
}, },
sparkline: { tooltip: {
enabled: {% if sparkline %}true{% else %}false{% endif %}, shared: false,
}, },
fill: { colors: ["#805AD5"],
type: 'gradient', chart: {
gradient: { toolbar: {
shadeIntensity: 1, show: false,
inverseColors: false, },
opacityFrom: 0.8, type: 'area',
opacityTo: 0, height: {{height|default:"200"}},
stops: [0, 75, 100] offsetY: -1,
animations: {
enabled: false
},
sparkline: {
enabled: {% if sparkline %}true{% else %}false{% endif %},
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
inverseColors: false,
opacityFrom: 0.8,
opacityTo: 0,
stops: [0, 75, 100]
},
}, },
}, },
}, grid: {
grid: { padding: {
padding: { right: 0,
right: 0, left: -8,
left: -8, },
}, xaxis: {
{% if not sparkline %} lines: {
xaxis: { show: true,
lines: { }
show: true, },
} yaxis: {
lines: {
show: false,
}
},
show: true
}, },
yaxis: { yaxis: {
lines: { labels: {
show: false, show: false,
formatter: val => val.toFixed(0)
},
padding: {
left: 0,
} }
}, },
show: true xaxis: {
{% endif %} type: "datetime",
},
yaxis: {
labels: {
show: false,
formatter: val => val.toFixed(0)
}, },
padding: { stroke: {
left: 0, width: 1.5,
} },
}, series: [{
xaxis: { name: "{{unit|default:'Sessions'}}",
type: "datetime", data: {{data|safe}}
}, }]
stroke: { };
width: 1.5, var triggerMatchesChart = new ApexCharts(document.querySelector("#chart{{name|default:'Main'}}"), triggerMatchesChartOptions);
}, triggerMatchesChart.render();
series: [{
name: "{{unit|default:'Sessions'}}",
data: {{data|safe}}
}]
};
var triggerMatchesChart = new ApexCharts(document.querySelector("#{{name|default:'timeChart'}}"), triggerMatchesChartOptions);
triggerMatchesChart.render();
</script> </script>

View File

@ -1,18 +1,26 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load rules %}
{% block content %} {% block content %}
<div class="md:flex justify-between"> <div class="md:flex justify-between items-center">
<h4 class="heading text-5xl leading-none">Shynet Dash</h4> <div>
<div> <h4 class="heading">Dashboard</h4>
{% include 'core/includes/date_range.html' %} </div>
</div> <div class="flex items-center">
<div class="mr-1">
{% include 'core/includes/date_range.html' %}
</div>
{% has_perm "core.create_service" user as can_create %}
{% if can_create %}
<a href="{% url 'core:service_create' %}" class="button field w-auto">+ New Service</a>
{% endif %}
</div>
</div> </div>
<hr class="sep"> <hr class="sep">
{% for object in services %} {% for object 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 %}

View File

@ -1,155 +1,183 @@
{% extends "core/service_base.html" %} {% extends "core/service_base.html" %}
{% load humanize helpers %} {% load humanize helpers rules %}
{% block service_actions %} {% block service_actions %}
<div class="mr-2">{% include 'core/includes/date_range.html' %}</div> <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 &rarr;</a> {% has_perm 'core.change_service' user object as can_update %}
{% if can_update %}
<a href="{% url 'core:service_update' service.uuid %}" class="button field ~neutral w-auto">Manage &rarr;</a>
{% endif %}
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
{% include 'core/includes/time_chart.html' with data=stats.session_chart_data %} <div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="stats"> <article class="">
<article class="card ~neutral !low"> <p class="label text-gray-400">Sessions</p>
<p class="label">Sessions</p> <p class="heading">
<p class="heading text-purple-600">
{{stats.session_count|intcomma}} {{stats.session_count|intcomma}}
</p> </p>
</article> </article>
<article class="card ~neutral !low"> <article class="">
<p class="label">Hits</p> <p class="label text-gray-400">Hits</p>
<p class="heading text-purple-600"> <p class="heading">
{{stats.hit_count|intcomma}} {{stats.hit_count|intcomma}}
</p> </p>
</article> </article>
<article class="card ~neutral !low"> <article class="">
<p class="label">Avg. Load Time</p> <p class="label text-gray-400">Load Time</p>
<p class="heading text-purple-600"> <p class="heading">
{% if stats.avg_load_time %}
{{stats.avg_load_time|floatformat:"0"}}ms {{stats.avg_load_time|floatformat:"0"}}ms
{% else %}
?
{% endif %}
</p> </p>
</article> </article>
<article class="card ~neutral !low"> <article class="">
<p class="label">Bounce Rate</p> <p class="label text-gray-400">Bounce Rate</p>
<p class="heading text-purple-600">{{stats.bounce_rate_pct|floatformat:"-1"}}%</p> <p class="heading">
{% if stats.bounce_rate_pct %}
{{stats.bounce_rate_pct|floatformat:"-1"}}%
{% else %}
?
{% endif %}
</p>
</article> </article>
<article class="card ~neutral !low"> <article class="">
<p class="label">Avg. Duration</p> <p class="label text-gray-400">Duration</p>
<p class="heading text-purple-600">{{stats.avg_session_duration|naturaldelta}}</p> <p class="heading">
{% if stats.avg_session_duration %}
{{stats.avg_session_duration|naturaldelta}}
{% else %}
?
{% endif %}
</p>
</article> </article>
<article class="card ~neutral !low"> <article class="">
<p class="label">Avg. Hits/Session</p> <p class="label text-gray-400">Hits/Session</p>
<p class="heading text-purple-600">{{stats.avg_hits_per_session|floatformat:"-1"}}</p> <p class="heading">
{% if stats.avg_hits_per_session %}
{{stats.avg_hits_per_session|floatformat:"-1"}}
{% else %}
?
{% endif %}
</p>
</article> </article>
</div> </div>
<div class="card ~neutral !low py-0 mb-6">
{% include 'core/includes/time_chart.html' with data=stats.session_chart_data %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card ~neutral !low limited-height"> <div class="card ~neutral !low limited-height pt-1">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Location</th> <th>Location</th>
<th>Hits</th> <th class="rf">Hits</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for location in stats.locations %} {% for location in stats.locations %}
<tr> <tr>
<td>{{location.location|urlize}}</td> <td>{{location.location|urldisplay}}</td>
<td>{{location.count|intcomma}}</td> <td class="rf">{{location.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card ~neutral !low limited-height"> <div class="card ~neutral !low limited-height pt-1">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Referrer</th> <th>Referrer</th>
<th>Sessions</th> <th class="rf">Sessions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for referrer in stats.referrers %} {% for referrer in stats.referrers %}
<tr> <tr>
<td>{{referrer.referrer|default:"Direct"|urlize}}</td> <td>{{referrer.referrer|default:"Direct"|urldisplay}}</td>
<td>{{referrer.count|intcomma}}</td> <td class="rf">{{referrer.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card ~neutral !low limited-height"> <div class="card ~neutral !low limited-height pt-1">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Operating System</th> <th>Operating System</th>
<th>Sessions</th> <th class="rf">Sessions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for os in stats.operating_systems %} {% for os in stats.operating_systems %}
<tr> <tr>
<td>{{os.os|default:"Unknown"}}</td> <td>{{os.os|default:"Unknown"}}</td>
<td>{{os.count|intcomma}}</td> <td class="rf">{{os.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card ~neutral !low limited-height"> <div class="card ~neutral !low limited-height pt-1">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Browser</th> <th>Browser</th>
<th>Sessions</th> <th class="rf">Sessions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for browser in stats.browsers %} {% for browser in stats.browsers %}
<tr> <tr>
<td>{{browser.browser|default:"Unknown"}}</td> <td>{{browser.browser|default:"Unknown"}}</td>
<td>{{browser.count|intcomma}}</td> <td class="rf">{{browser.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card ~neutral !low limited-height"> <div class="card ~neutral !low limited-height pt-1">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Device</th> <th>Device</th>
<th>Sessions</th> <th class="rf">Sessions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for device in stats.devices %} {% for device in stats.devices %}
<tr> <tr>
<td>{{device.device|default:"Unknown"}}</td> <td>{{device.device|default:"Unknown"}}</td>
<td>{{device.count|intcomma}}</td> <td class="rf">{{device.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card ~neutral !low limited-height"> <div class="card ~neutral !low limited-height pt-1">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Device Type</th> <th>Device Type</th>
<th>Sessions</th> <th class="rf">Sessions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for device_type in stats.device_types %} {% for device_type in stats.device_types %}
<tr> <tr>
<td>{{device_type.device_type|default:"Unknown"|title}}</td> <td>{{device_type.device_type|default:"Unknown"|title}}</td>
<td>{{device_type.count|intcomma}}</td> <td class="rf">{{device_type.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<a href="{% url 'core:service_session_list' service.uuid %}" class="button">View individual sessions &rarr;</a> <a href="{% url 'core:service_session_list' service.uuid %}" class="button field w-auto">View individual sessions
&rarr;</a>
{% endblock %} {% endblock %}

View File

@ -5,7 +5,7 @@
{% block head_title %}Create Service{% endblock %} {% block head_title %}Create Service{% endblock %}
{% block content %} {% block content %}
<h4 class="heading text-5xl leading-none">Create Service</h4> <h4 class="heading leading-none">Create Service</h4>
<hr class="sep"> <hr class="sep">
<form class="card ~neutral !low p-0 max-w-xl" method="POST"> <form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %} {% csrf_token %}

View File

@ -5,26 +5,25 @@
{% block head_title %}{{object.name}} Session{% endblock %} {% block head_title %}{{object.name}} Session{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'core:service' object.uuid %}" class="button field ~neutral !low w-auto">Analytics &rarr;</a> <a href="{% url 'core:service' object.uuid %}" class="button field ~neutral w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
<article class="card ~neutral !low"> <article class="card ~neutral !high">
<div class="flex items-center justify-between"> <div class="md:flex items-center justify-between">
<div> <div>
<h3 class="heading text-xl text-purple-600 mr-4"> <h3 class="heading text-2xl mr-4">
{{session.identifier|default:"Anonymous"}} @ {{session.duration|naturaldelta}} <span {{session.identifier|default:"Anonymous"}}, {{session.duration|naturaldelta}}
class="text-gray-600">({{session.ip}})</span>
</h3> </h3>
<p>{{session.start_time|date:"M j Y, g:i a"}} until
{{session.last_seen|date:"g:i a"}}</p>
</div> </div>
<div> <div>
{% if session.is_currently_active %}<span class="chip ~positive text-base">Online</span>{% endif %} <p class="font-medium text-lg">{{session.start_time|date:"M j Y, g:i a"}} to
{{session.last_seen|date:"g:i a"}}</p>
{% if session.is_currently_active %}<span class="chip ~positive !high text-base">Online</span>{% endif %}
</div> </div>
</div> </div>
<hr class="sep h-8"> <hr class="sep h-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
<div> <div>
<p>Browser</p> <p>Browser</p>
<p class="label">{{session.browser|default:"Unknown"}}</p> <p class="label">{{session.browser|default:"Unknown"}}</p>
@ -69,8 +68,8 @@
<div class=""> <div class="">
{% for hit in session.hit_set.all %} {% for hit in session.hit_set.all %}
<article class="my-12 md:flex"> <article class="my-12 md:flex">
<div class="md:w-2/12"> <div class="md:w-2/12 mb-2 md:mr-4 pt-4 md:text-right">
<div class="text-lg">{{hit.start_time|date:"g:i a"}}</div> <div class="text-lg font-medium">{{hit.start_time|date:"g:i a"}}</div>
</div> </div>
<div class="md:flex card ~neutral !low flex-grow justify-between"> <div class="md:flex card ~neutral !low flex-grow justify-between">
<div class="mb-4 md:mb-0 md:w-1/2"> <div class="mb-4 md:mb-0 md:w-1/2">

View File

@ -5,7 +5,8 @@
{% block head_title %}{{object.name}} Sessions{% endblock %} {% block head_title %}{{object.name}} Sessions{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'core:service' object.uuid %}" class="button field ~neutral !low w-auto">Analytics &rarr;</a> <div class="mr-2">{% include 'core/includes/date_range.html' %}</div>
<a href="{% url 'core:service' object.uuid %}" class="button field ~neutral w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
@ -15,8 +16,8 @@
<th>Time</th> <th>Time</th>
<th>Identity</th> <th>Identity</th>
<th>Network</th> <th>Network</th>
<th>Duration</th> <th class="rf">Duration</th>
<th>Hits</th> <th class="rf">Hits</th>
</thead> </thead>
<tbody> <tbody>
{% for session in object_list %} {% for session in object_list %}
@ -37,8 +38,8 @@
{% endif %} {% endif %}
</td> </td>
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td> <td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
<td>{{session.duration|naturaldelta}}</td> <td class="rf">{{session.duration|naturaldelta}}</td>
<td>{{session.hit_set.count|intcomma}}</td> <td class="rf">{{session.hit_set.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -5,7 +5,7 @@
{% block head_title %}{{object.name}} Management{% endblock %} {% block head_title %}{{object.name}} Management{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'core:service' object.uuid %}" class="button field ~neutral !low w-auto">View &rarr;</a> <a href="{% url 'core:service' object.uuid %}" class="button field ~neutral w-auto">View &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}

View File

@ -1,6 +1,9 @@
from urllib.parse import urlparse
import flag import flag
from django import template from django import template
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import SafeString
register = template.Library() register = template.Library()
@ -26,8 +29,20 @@ def flag_emoji(isocode):
except: except:
return "" return ""
@register.filter('startswith')
@register.filter
def startswith(text, starts): def startswith(text, starts):
if isinstance(text, str): if isinstance(text, str):
return text.startswith(starts) return text.startswith(starts)
return False return False
@register.filter
def urldisplay(url):
try:
parsed = urlparse(url)
return SafeString(
f"<a href='{url}' title='{url}' rel='nofollow'>{parsed.path if len(parsed.path) < 32 else parsed.path[:32] + '&hellip;'}</a>"
)
except:
return url

View File

@ -1,10 +1,11 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.IndexView.as_view(), name="index"), path("", RedirectView.as_view(url="/dash"), 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/new/", views.ServiceCreateView.as_view(), name="service_create"),
path("dash/service/<pk>/", views.ServiceView.as_view(), name="service"), path("dash/service/<pk>/", views.ServiceView.as_view(), name="service"),

View File

@ -1,21 +1,16 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import reverse, get_object_or_404 from django.shortcuts import get_object_or_404, reverse
from django.utils import timezone from django.utils import timezone
from django.views.generic import ( from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
CreateView, TemplateView, UpdateView)
DetailView, from rules.contrib.views import PermissionRequiredMixin
TemplateView,
UpdateView, from analytics.models import Session
DeleteView,
ListView,
)
from .forms import ServiceForm from .forms import ServiceForm
from .mixins import BaseUrlMixin, DateRangeMixin from .mixins import BaseUrlMixin, DateRangeMixin
from .models import Service from .models import Service
from analytics.models import Session
class IndexView(TemplateView): class IndexView(TemplateView):
template_name = "core/pages/index.html" template_name = "core/pages/index.html"
@ -32,19 +27,26 @@ class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView):
return data return data
class ServiceCreateView(LoginRequiredMixin, CreateView): class ServiceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Service model = Service
form_class = ServiceForm form_class = ServiceForm
template_name = "core/pages/service_create.html" template_name = "core/pages/service_create.html"
permission_required = "core.create_service"
def form_valid(self, form): def form_valid(self, form):
form.instance.owner = self.request.user form.instance.owner = self.request.user
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self):
return reverse("core:service", kwargs={"pk": self.object.uuid})
class ServiceView(LoginRequiredMixin, DateRangeMixin, DetailView):
class ServiceView(
LoginRequiredMixin, PermissionRequiredMixin, DateRangeMixin, DetailView
):
model = Service model = Service
template_name = "core/pages/service.html" template_name = "core/pages/service.html"
permission_required = "core.view_service"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
@ -52,28 +54,35 @@ class ServiceView(LoginRequiredMixin, DateRangeMixin, DetailView):
return data return data
class ServiceUpdateView(LoginRequiredMixin, BaseUrlMixin, UpdateView): class ServiceUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, BaseUrlMixin, UpdateView
):
model = Service model = Service
form_class = ServiceForm form_class = ServiceForm
template_name = "core/pages/service_update.html" template_name = "core/pages/service_update.html"
permission_required = "core.change_service"
def get_success_url(self): def get_success_url(self):
return reverse("core:service", kwargs={"uuid": self.object.uuid}) return reverse("core:service", kwargs={"uuid": self.object.uuid})
class ServiceDeleteView(LoginRequiredMixin, DeleteView): class ServiceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
model = Service model = Service
form_class = ServiceForm form_class = ServiceForm
template_name = "core/pages/service_delete.html" template_name = "core/pages/service_delete.html"
permission_required = "core.delete_service"
def get_success_url(self): def get_success_url(self):
return reverse("core:dashboard") return reverse("core:dashboard")
class ServiceSessionsListView(LoginRequiredMixin, DateRangeMixin, ListView): class ServiceSessionsListView(
LoginRequiredMixin, PermissionRequiredMixin, DateRangeMixin, ListView
):
model = Session model = Session
template_name = "core/pages/service_session_list.html" template_name = "core/pages/service_session_list.html"
paginate_by = 20 paginate_by = 20
permission_required = "core.view_service"
def get_object(self): def get_object(self):
return get_object_or_404(Service, pk=self.kwargs.get("pk")) return get_object_or_404(Service, pk=self.kwargs.get("pk"))
@ -91,11 +100,12 @@ class ServiceSessionsListView(LoginRequiredMixin, DateRangeMixin, ListView):
return data return data
class ServiceSessionView(LoginRequiredMixin, DetailView): class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
model = Session model = Session
template_name = "core/pages/service_session.html" template_name = "core/pages/service_session.html"
pk_url_kwarg = "session_pk" pk_url_kwarg = "session_pk"
context_object_name = "session" context_object_name = "session"
permission_required = "core.view_service"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)

View File

@ -42,6 +42,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.sites", "django.contrib.sites",
"django.contrib.humanize", "django.contrib.humanize",
"rules",
"a17t", "a17t",
"core", "core",
"analytics", "analytics",
@ -129,6 +130,7 @@ STATIC_URL = "/static/"
AUTH_USER_MODEL = "core.User" AUTH_USER_MODEL = "core.User"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
"rules.permissions.ObjectPermissionBackend",
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend", "allauth.account.auth_backends.AuthenticationBackend",
) )
@ -138,6 +140,11 @@ ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_SUBJECT_PREFIX = ""
ACCOUNT_USER_DISPLAY = lambda k: k.email
LOGIN_REDIRECT_URL = "/dash"
SITE_ID = 1 SITE_ID = 1
@ -161,3 +168,7 @@ MESSAGE_TAGS = {
messages.ERROR: "~critical", messages.ERROR: "~critical",
messages.SUCCESS: "~positive", messages.SUCCESS: "~positive",
} }
# Shynet
ONLY_SUPERUSERS_CREATE = True # Can everyone create services, or only superusers?