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)
referrer = models.TextField(blank=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:
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
def get_start_date(self):
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
return found_time
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:
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
return found_time
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

View File

@ -64,7 +64,6 @@ class Service(models.Model):
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()
@ -151,6 +150,5 @@ class Service(models.Model):
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"sessions": sessions,
"online": True,
}

View File

@ -8,6 +8,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% include 'a17t/head.html' %}
<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 %}
{% endblock %}
</head>

View File

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

View File

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

View File

@ -9,7 +9,7 @@
</div>
<hr class="sep">
{% for service in services %}
{% for object in services %}
{% include 'core/includes/service_overview.html' %}
{% empty %}
<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 %}
{% block head_title %}{{service.name}}{% endblock %}
{% 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>
{% 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 &rarr;</a>
</div>
</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">
<article class="card ~neutral !low">
<p class="label">Sessions</p>
@ -70,7 +52,7 @@
<tbody>
{% for location in stats.locations %}
<tr>
<td>{{location.location}}</td>
<td>{{location.location|urlize}}</td>
<td>{{location.count|intcomma}}</td>
</tr>
{% endfor %}
@ -88,7 +70,7 @@
<tbody>
{% for referrer in stats.referrers %}
<tr>
<td>{{referrer.referrer|default:"Direct"}}</td>
<td>{{referrer.referrer|default:"Direct"|urlize}}</td>
<td>{{referrer.count|intcomma}}</td>
</tr>
{% endfor %}
@ -168,63 +150,5 @@
</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>
<a href="{% url 'core:service_session_list' service.uuid %}" class="button">View individual sessions &rarr;</a>
{% 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 %}
{% 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 &rarr;</a>
</div>
<hr class="sep">
{% block service_actions %}
<a href="{% url 'core:service' object.uuid %}" class="button field ~neutral !low w-auto">View &rarr;</a>
{% endblock %}
{% block service_content %}
<div class="max-w-xl content">
<h5>Analytics Installation</h5>
<p>(At the end of <code>&lt;body&gt;</code>)
@ -25,9 +22,14 @@
<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 class="section ~neutral !normal p-4 flex justify-between">
<div>
<button type="submit" class="button ~neutral !high">Save</button>
<a href="{% url 'core:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
</div>
<div>
<a href="{% url 'core:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
</div>
</div>
</form>
{% 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)
except:
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(),
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.shortcuts import reverse
from django.shortcuts import reverse, get_object_or_404
from django.utils import timezone
from django.views.generic import (CreateView, DetailView, TemplateView,
UpdateView)
from django.views.generic import (
CreateView,
DetailView,
TemplateView,
UpdateView,
DeleteView,
ListView,
)
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"
@ -51,3 +59,45 @@ class ServiceUpdateView(LoginRequiredMixin, BaseUrlMixin, UpdateView):
def get_success_url(self):
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