Add more base functions

This commit is contained in:
R. Miles McCain 2020-04-12 14:19:52 -04:00
parent 565cba18e2
commit 2f06ecabd7
No known key found for this signature in database
GPG Key ID: 91CB47BDDF2671A5
20 changed files with 417 additions and 117 deletions

View File

@ -0,0 +1,46 @@
<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>
{% else %}
<a class="button ~neutral !normal" disabled>Previous</a>
{% endif %}
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button ~neutral !low">Next</a>
{% else %}
<a class="button ~neutral !normal" 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>
{% else %}
<li><a class="button ~neutral !normal" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% if middle %}
<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>
{% else %}
<li><a class="button ~neutral !normal" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% endif %}
{% if end %}
<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>
{% else %}
<li><a class="button ~neutral !normal" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% endif %}
</ul>
</nav>

View File

@ -0,0 +1,55 @@
# From https://djangosnippets.org/snippets/1441/
from django import template
from django.utils.http import urlencode
register = template.Library()
@register.inclusion_tag("a17t/includes/pagination.html")
def pagination(
page,
request,
begin_pages=2,
end_pages=2,
before_current_pages=4,
after_current_pages=4,
):
url_parameters = "".join(
[
f"&{urlencode(key)}={urlencode(value)}"
for key, value in request.GET.items()
if key != "page"
]
)
before = max(page.number - before_current_pages - 1, 0)
after = page.number + after_current_pages
begin = page.paginator.page_range[:begin_pages]
middle = page.paginator.page_range[before:after]
end = page.paginator.page_range[-end_pages:]
last_page_number = end[-1]
def collides(firstlist, secondlist):
return any(item in secondlist for item in firstlist)
if collides(middle, end):
end = range(max(page.number - before_current_pages, 1), last_page_number + 1)
middle = []
if collides(begin, middle):
begin = range(1, min(page.number + after_current_pages, last_page_number) + 1)
middle = []
if collides(begin, end):
begin = range(1, last_page_number + 1)
end = []
return {
"page": page,
"begin": begin,
"middle": middle,
"end": end,
"url_parameters": url_parameters,
}

View File

@ -70,3 +70,7 @@ class Hit(models.Model):
location = models.TextField(blank=True) location = models.TextField(blank=True)
referrer = models.TextField(blank=True) referrer = models.TextField(blank=True)
load_time = models.FloatField(null=True) load_time = models.FloatField(null=True)
@property
def duration(self):
return self.last_seen - self.start_time

View File

@ -4,27 +4,31 @@ from django.utils import timezone
class DateRangeMixin: class DateRangeMixin:
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
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(
self.request.GET.get("startDate"), "%Y-%m-%d" self.request.GET.get("startDate"), "%Y-%m-%d"
) )
found_time.replace(hour=0, minute=0) found_time.replace(hour=0, minute=0)
data["start_date"] = found_time return found_time
else: else:
data["start_date"] = timezone.now() - timezone.timedelta(days=30) return timezone.now() - timezone.timedelta(days=30)
def get_end_date(self):
if self.request.GET.get("endDate") != None: if self.request.GET.get("endDate") != None:
found_time = timezone.datetime.strptime( found_time = timezone.datetime.strptime(
self.request.GET.get("endDate"), "%Y-%m-%d" self.request.GET.get("endDate"), "%Y-%m-%d"
) )
found_time.replace(hour=23, minute=59) found_time.replace(hour=23, minute=59)
data["end_date"] = found_time return found_time
else: else:
data["end_date"] = timezone.now() return timezone.now()
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["start_date"] = self.get_start_date()
data["end_date"] = self.get_end_date()
return data return data

View File

@ -64,7 +64,6 @@ class Service(models.Model):
Session.objects.filter( Session.objects.filter(
service=self, start_time__gt=start_time, start_time__lt=end_time service=self, start_time__gt=start_time, start_time__lt=end_time
) )
.prefetch_related("hit_set")
.order_by("-start_time") .order_by("-start_time")
) )
session_count = sessions.count() session_count = sessions.count()
@ -151,6 +150,5 @@ class Service(models.Model):
"browsers": browsers, "browsers": browsers,
"devices": devices, "devices": devices,
"device_types": device_types, "device_types": device_types,
"sessions": sessions,
"online": True, "online": True,
} }

View File

@ -8,6 +8,8 @@
<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> <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/js/main.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"
integrity="sha256-R4pqcOYV8lt7snxMQO/HSbVCFRPMdrhAFMH+vr9giYI=" crossorigin="anonymous"></script>
{% block extra_head %} {% block extra_head %}
{% endblock %} {% endblock %}
</head> </head>

View File

@ -1,11 +1,11 @@
{% load humanize helpers %} {% load humanize helpers %}
<a class="card ~neutral !low service mb-6" href="{% url 'core:service' service.uuid %}"> <a class="card ~neutral !low service mb-6" href="{% url 'core:service' object.uuid %}">
{% with stats=service.stats %} {% with stats=object.stats %}
<div class="md:flex justify-between items-center"> <div class="md:flex justify-between items-center">
<div class="mr-4 md:w-4/12"> <div class="mr-4 md:w-4/12">
<h3 class="heading text-xl mr-2 mb-1"> <h3 class="heading text-xl mr-2 mb-1">
{{service.name}} {{object.name}}
</h3> </h3>
{% include 'core/includes/stats_status_chip.html' %} {% include 'core/includes/stats_status_chip.html' %}
</div> </div>

View File

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

View File

@ -1,5 +1,6 @@
{% load humanize %} {% load humanize %}
{% with stats=object.get_daily_stats %}
{% if stats.currently_online > 0 %} {% if stats.currently_online > 0 %}
<span class="chip ~positive"> <span class="chip ~positive">
{{stats.currently_online|intcomma}} online {{stats.currently_online|intcomma}} online
@ -13,3 +14,4 @@
Offline Offline
</span> </span>
{% endif %} {% endif %}
{% endwith %}

View File

@ -9,7 +9,7 @@
</div> </div>
<hr class="sep"> <hr class="sep">
{% for service 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>

View File

@ -1,31 +1,13 @@
{% extends "base.html" %} {% extends "core/service_base.html" %}
{% load humanize helpers %} {% load humanize helpers %}
{% block head_title %}{{service.name}}{% endblock %} {% block service_actions %}
{% 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> <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> <a href="{% url 'core:service_update' service.uuid %}" class="button field ~neutral !low w-auto">Manage &rarr;</a>
</div> {% endblock %}
</div>
<hr class="sep h-8"> {% block service_content %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="stats"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6" id="stats">
<article class="card ~neutral !low"> <article class="card ~neutral !low">
<p class="label">Sessions</p> <p class="label">Sessions</p>
@ -70,7 +52,7 @@
<tbody> <tbody>
{% for location in stats.locations %} {% for location in stats.locations %}
<tr> <tr>
<td>{{location.location}}</td> <td>{{location.location|urlize}}</td>
<td>{{location.count|intcomma}}</td> <td>{{location.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -88,7 +70,7 @@
<tbody> <tbody>
{% for referrer in stats.referrers %} {% for referrer in stats.referrers %}
<tr> <tr>
<td>{{referrer.referrer|default:"Direct"}}</td> <td>{{referrer.referrer|default:"Direct"|urlize}}</td>
<td>{{referrer.count|intcomma}}</td> <td>{{referrer.count|intcomma}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -168,63 +150,5 @@
</table> </table>
</div> </div>
</div> </div>
<div class="card ~neutral !low limited-height"> <a href="{% url 'core:service_session_list' service.uuid %}" class="button">View individual sessions &rarr;</a>
<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 %} {% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "core/service_base.html" %}
{% load a17t_tags %}
{% block head_title %}Delete {{object.name}}{% endblock %}
{% block service_content %}
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %}
<div class="p-4">
<p>Are you sure you want to delete this service? All of its
analytics and associated data will be permanently deleted.</p>
{{form|a17t}}
</div>
<div class="section ~critical !normal p-4">
<button type="submit" class="button ~critical !high">Delete</button>
<a href="{% url 'core:service' object.uuid %}" class="button ~critical !low">Cancel</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,100 @@
{% extends "core/service_base.html" %}
{% load a17t_tags pagination humanize helpers %}
{% 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>
{% endblock %}
{% block service_content %}
<article class="card ~neutral !low">
<div class="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>
<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 %}
</div>
</div>
<hr class="sep h-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p>Browser</p>
<p class="label">{{session.browser|default:"Unknown"}}</p>
</div>
<div>
<p>Device</p>
<p class="label">{{session.device|default:"Unknown"}}</p>
</div>
<div>
<p>Device Type</p>
<p class="label">{{session.device_type|title}}</p>
</div>
<div>
<p>OS</p>
<p class="label">{{session.os|default:"Unknown"}}</p>
</div>
<div>
<p>Network</p>
<p class="label">{{session.asn|default:"Unknown"}}</p>
</div>
<div>
<p>Country</p>
<p class="label">{{session.country|flag_emoji}} {{session.country|default:"Unknown"}}</p>
</div>
<div>
<p>Location</p>
<p class="label">
{% if session.latitude %}
<a href="https://www.google.com/maps/search/?api=1&query={{session.latitude}},{{session.longitude}}">Open
in Maps &nearr;</a>
{% else %}
Unknown
{% endif %}
</p>
</div>
<div>
<p>Time Zone</p>
<p class="label">{{session.time_zone|default:"Unknown"}}</p>
</div>
</div>
</article>
<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>
<div class="md:flex card ~neutral !low flex-grow justify-between">
<div class="mb-4 md:mb-0 md:w-1/2">
<p class="label font-medium text-lg">{{hit.location|urlize}}</p>
{% if hit.referrer %}
<p>via {{hit.referrer|urlize}}<p>
{% endif %}
</div>
<div class="grid grid-cols-3 gap-3 md:pl-8 md:w-1/2">
<div>
<p>Duration</p>
<p class="label">{{hit.duration|naturaldelta}}</p>
</div>
<div>
<p>Load</p>
<p class="label">{{hit.load_time|floatformat:"0"}}ms</p>
</div>
<div>
<p>Tracker</p>
<p class="label">{{hit.tracker}}</p>
</div>
</div>
<div>
</article>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "core/service_base.html" %}
{% load a17t_tags pagination humanize helpers %}
{% 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>
{% endblock %}
{% block service_content %}
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-scroll">
<table class="table">
<thead>
<th>Time</th>
<th>Identity</th>
<th>Network</th>
<th>Duration</th>
<th>Hits</th>
</thead>
<tbody>
{% for session in object_list %}
<tr>
<td>
<a href="{% url 'core:service_session' object.pk session.pk %}" class="font-medium text-purple-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
{% if session.is_currently_active %}
<span class="badge ~positive">Online</span>
{% endif %}
</a>
</td>
<td>
{% if session.identifier %}
<span class="chip ~neutral">{{session.identifier}}</span>
{% else %}
<span class="text-gray-600">&mdash;</span>
{% endif %}
</td>
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
<td>{{session.duration|naturaldelta}}</td>
<td>{{session.hit_set.count|intcomma}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% pagination page_obj request %}
{% endblock %}

View File

@ -1,17 +1,14 @@
{% extends "base.html" %} {% extends "core/service_base.html" %}
{% load a17t_tags %} {% load a17t_tags %}
{% block head_title %}{{object.name}} Management{% endblock %} {% block head_title %}{{object.name}} Management{% endblock %}
{% block content %} {% block service_actions %}
<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 &rarr;</a> <a href="{% url 'core:service' object.uuid %}" class="button field ~neutral !low w-auto">View &rarr;</a>
</div> {% endblock %}
<hr class="sep">
{% block service_content %}
<div class="max-w-xl content"> <div class="max-w-xl content">
<h5>Analytics Installation</h5> <h5>Analytics Installation</h5>
<p>(At the end of <code>&lt;body&gt;</code>) <p>(At the end of <code>&lt;body&gt;</code>)
@ -25,9 +22,14 @@
<div class="p-4"> <div class="p-4">
{{form|a17t}} {{form|a17t}}
</div> </div>
<div class="section ~neutral !normal p-4"> <div class="section ~neutral !normal p-4 flex justify-between">
<div>
<button type="submit" class="button ~neutral !high">Save</button> <button type="submit" class="button ~neutral !high">Save</button>
<a href="{% url 'core:service' object.uuid %}" class="button ~neutral !low">Cancel</a> <a href="{% url 'core:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
</div> </div>
<div>
<a href="{% url 'core:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
</div>
</div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load humanize helpers %}
{% block head_title %}{{service.name}}{% endblock %}
{% block content %}
<div class="md:flex justify-between items-center" id="heading">
<a class="flex items-center" href="{% url 'core:service' object.uuid %}">
<h3 class="heading leading-none mr-4">
{{object.name}}
</h3>
<div class='text-3xl'>
{% include 'core/includes/stats_status_chip.html' %}
</div>
</a>
<div class="flex items-center">
{% block service_actions %}
{% endblock %}
</div>
</div>
<hr class="sep h-8">
{% block service_content %}
{% endblock %}
{% endblock %}

View File

@ -25,3 +25,9 @@ def flag_emoji(isocode):
return flag.flag(isocode) return flag.flag(isocode)
except: except:
return "" return ""
@register.filter('startswith')
def startswith(text, starts):
if isinstance(text, str):
return text.startswith(starts)
return False

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -13,4 +13,19 @@ urlpatterns = [
views.ServiceUpdateView.as_view(), views.ServiceUpdateView.as_view(),
name="service_update", name="service_update",
), ),
path(
"dash/service/<pk>/delete/",
views.ServiceDeleteView.as_view(),
name="service_delete",
),
path(
"dash/service/<pk>/sessions/",
views.ServiceSessionsListView.as_view(),
name="service_session_list",
),
path(
"dash/service/<pk>/sessions/<session_pk>/",
views.ServiceSessionView.as_view(),
name="service_session",
),
] ]

View File

@ -1,13 +1,21 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import reverse from django.shortcuts import reverse, get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.views.generic import (CreateView, DetailView, TemplateView, from django.views.generic import (
UpdateView) CreateView,
DetailView,
TemplateView,
UpdateView,
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"
@ -51,3 +59,45 @@ class ServiceUpdateView(LoginRequiredMixin, BaseUrlMixin, UpdateView):
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):
model = Service
form_class = ServiceForm
template_name = "core/pages/service_delete.html"
def get_success_url(self):
return reverse("core:dashboard")
class ServiceSessionsListView(LoginRequiredMixin, DateRangeMixin, ListView):
model = Session
template_name = "core/pages/service_session_list.html"
paginate_by = 20
def get_object(self):
return get_object_or_404(Service, pk=self.kwargs.get("pk"))
def get_queryset(self):
return Session.objects.filter(
service=self.get_object(),
start_time__lt=self.get_end_date(),
start_time__gt=self.get_start_date(),
)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["object"] = self.get_object()
return data
class ServiceSessionView(LoginRequiredMixin, DetailView):
model = Session
template_name = "core/pages/service_session.html"
pk_url_kwarg = "session_pk"
context_object_name = "session"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
return data