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 = "*"
user-agents = "*"
emoji-country-flag = "*"
rules = "*"
[requires]
python_version = "3.6"

9
Pipfile.lock generated
View File

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

View File

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

View File

@ -1,23 +1,23 @@
<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">
{% 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 %}
<a class="button ~neutral !normal" disabled>Previous</a>
<a class="button field w-auto mr-1" disabled>Previous</a>
{% endif %}
{% 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 %}
<a class="button ~neutral !normal" disabled>Next</a>
<a class="button field w-auto" disabled>Next</a>
{% endif %}
</div>
<ul class="pagination-list w-full md:w-auto mb-2">
{% for pnum in begin %}
{% 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 %}
<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 %}
{% endfor %}
@ -25,9 +25,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in middle %}
{% 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 %}
<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 %}
{% endfor %}
{% endif %}
@ -36,9 +36,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in end %}
{% 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 %}
<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 %}
{% endfor %}
{% endif %}

View File

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

View File

@ -1,12 +1,12 @@
import uuid
import json
import uuid
from django.apps import apps
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.functions import TruncDate
from django.db.utils import NotSupportedError
from django.utils import timezone
from django.db.models.functions import TruncDate
def _default_uuid():
@ -120,7 +120,7 @@ class Service(models.Model):
"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:
avg_session_duration = sessions.annotate(
@ -133,6 +133,8 @@ class Service(models.Model):
for session in sessions
]
) / max(session_count, 1)
if session_count == 0:
avg_session_duration = None
session_chart_data = {
k["date"]: k["count"]
@ -151,7 +153,9 @@ class Service(models.Model):
"session_count": session_count,
"hit_count": hit_count,
"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_load_time": avg_load_time,
"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">
<div class="card ~neutral !low">
<div class="card ~neutral !low max-w-lg">
{% 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">
{% csrf_token %}
<fieldset class="blockLabels">
@ -36,9 +36,9 @@
{% endfor %}
<div class="block mt-4">
<button class="button ~urge 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 ~critical mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button>
<button class="button ~neutral !high mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~neutral mb-1" type="submit" name="action_send">{% trans 'Resend Verification' %}</button>
<button class="button ~neutral mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button>
</div>
</fieldset>
</form>
@ -54,10 +54,10 @@
<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 %}
{{ 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>
{% endblock %}

View File

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

View File

@ -2,7 +2,7 @@
<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>
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral cursor-pointer" readonly>
<style>
:root {
--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 %}">
{% with stats=object.stats %}
<div class="p-4 md:flex justify-between">
<div class="md:w-4/12">
<h3 class="heading text-xl mr-2 mb-1 text-purple-600">
<div class="md:w-4/12 flex items-center">
<h3 class="heading mr-2 mb-1 text-purple-600">
{{object.name}}
</h3>
{% include 'core/includes/stats_status_chip.html' %}
@ -14,23 +14,35 @@
<p>Sessions</p>
<p class="label">{{stats.session_count|intcomma}}</p>
</div>
<div>
<p>Hits</p>
<p class="label">{{stats.hit_count|intcomma}}</p>
</div>
<div>
<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>
<p>Avg. Duration</p>
<p class="label">{{stats.avg_session_duration|naturaldelta}}</p>
</div>
<div>
<p>Uptime</p>
<p class="label">99.9%</p>
<p class="label">
{% if stats.avg_session_duration != None %}
{{stats.avg_session_duration|naturaldelta}}
{% else %}
?
{% endif %}
</p>
</div>
</div>
</div>
<hr class="sep h-4">
<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>
{% endwith %}
</a>

View File

@ -1,4 +1,6 @@
{% load helpers %}
<a class="portal {% if request.get_full_path|startswith:url %}~urge active bg-gray-100{% endif %}"
href="{{url}}">{{label}}</a><br>
<div>
<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 %}
{% if stats.currently_online > 0 %}
<span class="chip ~positive">
<span class="chip ~positive !high">
{{stats.currently_online|intcomma}} online
</span>
{% elif stats.online == True %}
<span class="chip ~positive">
<span class="chip ~positive !high">
Online
</span>
{% elif stats.online == False %}
<span class="chip ~critical">
<span class="chip ~critical !high">
Offline
</span>
{% endif %}

View File

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

View File

@ -1,18 +1,26 @@
{% extends "base.html" %}
{% load rules %}
{% block content %}
<div class="md:flex justify-between">
<h4 class="heading text-5xl leading-none">Shynet Dash</h4>
<div>
{% include 'core/includes/date_range.html' %}
</div>
<div class="md:flex justify-between items-center">
<div>
<h4 class="heading">Dashboard</h4>
</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>
<hr class="sep">
{% for object in services %}
{% include 'core/includes/service_overview.html' %}
{% empty %}
<p>You don't have any services.</p>
{% endfor %}
<a href="{% url 'core:service_create' %}" class="button ~neutral my-2">+ New Service</a>
{% endblock %}

View File

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

View File

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

View File

@ -5,26 +5,25 @@
{% block head_title %}{{object.name}} Session{% endblock %}
{% 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 %}
{% block service_content %}
<article class="card ~neutral !low">
<div class="flex items-center justify-between">
<article class="card ~neutral !high">
<div class="md:flex items-center justify-between">
<div>
<h3 class="heading text-xl text-purple-600 mr-4">
{{session.identifier|default:"Anonymous"}} @ {{session.duration|naturaldelta}} <span
class="text-gray-600">({{session.ip}})</span>
<h3 class="heading text-2xl mr-4">
{{session.identifier|default:"Anonymous"}}, {{session.duration|naturaldelta}}
</h3>
<p>{{session.start_time|date:"M j Y, g:i a"}} until
{{session.last_seen|date:"g:i a"}}</p>
</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>
<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>
<p>Browser</p>
<p class="label">{{session.browser|default:"Unknown"}}</p>
@ -69,8 +68,8 @@
<div class="">
{% for hit in session.hit_set.all %}
<article class="my-12 md:flex">
<div class="md:w-2/12">
<div class="text-lg">{{hit.start_time|date:"g:i a"}}</div>
<div class="md:w-2/12 mb-2 md:mr-4 pt-4 md:text-right">
<div class="text-lg font-medium">{{hit.start_time|date:"g:i a"}}</div>
</div>
<div class="md:flex card ~neutral !low flex-grow justify-between">
<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 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 %}
{% block service_content %}
@ -15,8 +16,8 @@
<th>Time</th>
<th>Identity</th>
<th>Network</th>
<th>Duration</th>
<th>Hits</th>
<th class="rf">Duration</th>
<th class="rf">Hits</th>
</thead>
<tbody>
{% for session in object_list %}
@ -37,8 +38,8 @@
{% endif %}
</td>
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
<td>{{session.duration|naturaldelta}}</td>
<td>{{session.hit_set.count|intcomma}}</td>
<td class="rf">{{session.duration|naturaldelta}}</td>
<td class="rf">{{session.hit_set.count|intcomma}}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -5,7 +5,7 @@
{% block head_title %}{{object.name}} Management{% endblock %}
{% 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 %}
{% block service_content %}

View File

@ -1,6 +1,9 @@
from urllib.parse import urlparse
import flag
from django import template
from django.utils import timezone
from django.utils.safestring import SafeString
register = template.Library()
@ -26,8 +29,20 @@ def flag_emoji(isocode):
except:
return ""
@register.filter('startswith')
@register.filter
def startswith(text, starts):
if isinstance(text, str):
return text.startswith(starts)
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.urls import include, path
from django.views.generic import RedirectView
from . import views
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/service/new/", views.ServiceCreateView.as_view(), name="service_create"),
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.shortcuts import reverse, get_object_or_404
from django.shortcuts import get_object_or_404, reverse
from django.utils import timezone
from django.views.generic import (
CreateView,
DetailView,
TemplateView,
UpdateView,
DeleteView,
ListView,
)
from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
TemplateView, UpdateView)
from rules.contrib.views import PermissionRequiredMixin
from analytics.models import Session
from .forms import ServiceForm
from .mixins import BaseUrlMixin, DateRangeMixin
from .models import Service
from analytics.models import Session
class IndexView(TemplateView):
template_name = "core/pages/index.html"
@ -32,19 +27,26 @@ class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView):
return data
class ServiceCreateView(LoginRequiredMixin, CreateView):
class ServiceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
model = Service
form_class = ServiceForm
template_name = "core/pages/service_create.html"
permission_required = "core.create_service"
def form_valid(self, form):
form.instance.owner = self.request.user
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
template_name = "core/pages/service.html"
permission_required = "core.view_service"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
@ -52,28 +54,35 @@ class ServiceView(LoginRequiredMixin, DateRangeMixin, DetailView):
return data
class ServiceUpdateView(LoginRequiredMixin, BaseUrlMixin, UpdateView):
class ServiceUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, BaseUrlMixin, UpdateView
):
model = Service
form_class = ServiceForm
template_name = "core/pages/service_update.html"
permission_required = "core.change_service"
def get_success_url(self):
return reverse("core:service", kwargs={"uuid": self.object.uuid})
class ServiceDeleteView(LoginRequiredMixin, DeleteView):
class ServiceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
model = Service
form_class = ServiceForm
template_name = "core/pages/service_delete.html"
permission_required = "core.delete_service"
def get_success_url(self):
return reverse("core:dashboard")
class ServiceSessionsListView(LoginRequiredMixin, DateRangeMixin, ListView):
class ServiceSessionsListView(
LoginRequiredMixin, PermissionRequiredMixin, DateRangeMixin, ListView
):
model = Session
template_name = "core/pages/service_session_list.html"
paginate_by = 20
permission_required = "core.view_service"
def get_object(self):
return get_object_or_404(Service, pk=self.kwargs.get("pk"))
@ -91,11 +100,12 @@ class ServiceSessionsListView(LoginRequiredMixin, DateRangeMixin, ListView):
return data
class ServiceSessionView(LoginRequiredMixin, DetailView):
class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
model = Session
template_name = "core/pages/service_session.html"
pk_url_kwarg = "session_pk"
context_object_name = "session"
permission_required = "core.view_service"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)

View File

@ -42,6 +42,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.humanize",
"rules",
"a17t",
"core",
"analytics",
@ -129,6 +130,7 @@ STATIC_URL = "/static/"
AUTH_USER_MODEL = "core.User"
AUTHENTICATION_BACKENDS = (
"rules.permissions.ObjectPermissionBackend",
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
@ -138,6 +140,11 @@ ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
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
@ -161,3 +168,7 @@ MESSAGE_TAGS = {
messages.ERROR: "~critical",
messages.SUCCESS: "~positive",
}
# Shynet
ONLY_SUPERUSERS_CREATE = True # Can everyone create services, or only superusers?