Separate core from dashboard
This commit is contained in:
0
shynet/dashboard/__init__.py
Normal file
0
shynet/dashboard/__init__.py
Normal file
5
shynet/dashboard/apps.py
Normal file
5
shynet/dashboard/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DashboardConfig(AppConfig):
|
||||
name = 'dashboard'
|
||||
25
shynet/dashboard/forms.py
Normal file
25
shynet/dashboard/forms.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import Service
|
||||
|
||||
|
||||
class ServiceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = ["name", "link", "origins", "collaborators"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"origins": forms.TextInput(),
|
||||
"collaborators": forms.CheckboxSelectMultiple(),
|
||||
}
|
||||
labels = {
|
||||
"origins": "Allowed Hostnames",
|
||||
}
|
||||
help_texts = {
|
||||
"name": _("What should the service be called?"),
|
||||
"link": _("What's the service's primary URL?"),
|
||||
"origins": _(
|
||||
"At what hostnames does the service operate? This sets CORS headers, so use '*' if you're not sure (or don't care)."
|
||||
),
|
||||
}
|
||||
0
shynet/dashboard/migrations/__init__.py
Normal file
0
shynet/dashboard/migrations/__init__.py
Normal file
39
shynet/dashboard/mixins.py
Normal file
39
shynet/dashboard/mixins.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class DateRangeMixin:
|
||||
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)
|
||||
return found_time
|
||||
else:
|
||||
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)
|
||||
return found_time
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
class BaseUrlMixin:
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
url_data = urlparse(self.request.build_absolute_uri())
|
||||
data["base_url"] = f"{url_data.scheme}://{url_data.netloc}"
|
||||
return data
|
||||
16
shynet/dashboard/static/dashboard/css/global.css
Normal file
16
shynet/dashboard/static/dashboard/css/global.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.table tbody tr:nth-child(2n) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.table {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.limited-height {
|
||||
overflow: scroll;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.rf {
|
||||
text-align: right !important;
|
||||
}
|
||||
10
shynet/dashboard/templates/account/account_inactive.html
Normal file
10
shynet/dashboard/templates/account/account_inactive.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Account Inactive" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Account Inactive" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<p>{% trans "This account is inactive." %}</p>
|
||||
{% endblock %}
|
||||
14
shynet/dashboard/templates/account/base.html
Normal file
14
shynet/dashboard/templates/account/base.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<h4 class="heading">{% block page_title %}{% endblock %}</h4>
|
||||
</div>
|
||||
<hr class="sep">
|
||||
{% block main %}
|
||||
<div class="card ~neutral !low max-w-lg content">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
77
shynet/dashboard/templates/account/email.html
Normal file
77
shynet/dashboard/templates/account/email.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Email Addresses" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Email Addresses" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="card ~neutral !low max-w-lg">
|
||||
|
||||
{% if user.emailaddress_set.all %}
|
||||
<p>{% trans 'These are your known email addresses:' %}</p>
|
||||
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset class="blockLabels">
|
||||
{% for emailaddress in user.emailaddress_set.all %}
|
||||
<div class="ctrlHolder my-2">
|
||||
<label for="email_radio_{{forloop.counter}}" class="switch">
|
||||
|
||||
<input id="email_radio_{{forloop.counter}}" type="radio" name="email"
|
||||
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked" {%endif %}
|
||||
value="{{emailaddress.email}}" />
|
||||
|
||||
{{ emailaddress.email }}
|
||||
{% if emailaddress.verified %}
|
||||
<span class="badge ~positive">{% trans "Verified" %}</span>
|
||||
{% else %}
|
||||
<span class="badge ~critical">{% trans "Unverified" %}</span>
|
||||
{% endif %}
|
||||
{% if emailaddress.primary %}<span class="badge ~urge">{% trans "Primary" %}</span>{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="block mt-4">
|
||||
<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>
|
||||
|
||||
{% else %}
|
||||
<aside class="aside ~warning">
|
||||
<p>
|
||||
{% trans "You currently do not have an email address associated with your account. Without one, you won't be able to reset your password, receive notifications, etc." %}
|
||||
</p>
|
||||
</aside>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr class="sep">
|
||||
|
||||
<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 Address" %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block extra_body %}
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
var message = "{% trans 'Do you really want to remove the selected email address?' %}";
|
||||
var actions = document.getElementsByName('action_remove');
|
||||
if (actions.length) {
|
||||
actions[0].addEventListener("click", function (e) {
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hi there,
|
||||
|
||||
You're receiving this email because {{ user_display }} has listed this email as a valid contact address for their account.
|
||||
|
||||
To confirm this is correct, go to {{ activate_url }}
|
||||
{% endblocktrans %}
|
||||
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you,
|
||||
{{ site_name }}
|
||||
{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1 @@
|
||||
{% include "account/email/email_confirmation_message.txt" %}
|
||||
@@ -0,0 +1 @@
|
||||
{% include "account/email/email_confirmation_subject.txt" %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Confirm Email Address{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hi there,
|
||||
|
||||
You're receiving this email because you or someone else has requested a password for your account.
|
||||
|
||||
This message can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %}
|
||||
|
||||
{{ password_reset_url }}
|
||||
|
||||
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you,
|
||||
{{ site_name }}
|
||||
{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Password Reset Email{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
33
shynet/dashboard/templates/account/email_confirm.html
Normal file
33
shynet/dashboard/templates/account/email_confirm.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load account %}
|
||||
|
||||
{% block head_title %}{% trans "Confirm Email Address" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Confirm Email Address" %}{% endblock %}
|
||||
|
||||
|
||||
{% block card %}
|
||||
{% if confirmation %}
|
||||
|
||||
{% user_display confirmation.email_address.user as user_display %}
|
||||
|
||||
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a
|
||||
href="mailto:{{ email }}">{{ email }}</a> is a valid email where we can reach you.{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button ~positive !high">{% trans 'Confirm' %}</button>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% url 'account_email' as email_url %}
|
||||
|
||||
<p>{% blocktrans %}This email confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new
|
||||
email confirmation request</a>.{% endblocktrans %}</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
20
shynet/dashboard/templates/account/login.html
Normal file
20
shynet/dashboard/templates/account/login.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
{% load account socialaccount %}
|
||||
|
||||
{% block head_title %}{% trans "Sign In" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Sign In" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="login" method="POST" action="{% url 'account_login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form|a17t }}
|
||||
{% if redirect_field_value %}
|
||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
||||
{% endif %}
|
||||
<button class="button ~urge !high mr-2" type="submit">{% trans "Sign In" %}</button>
|
||||
<a href="{% url 'account_reset_password' %}" class="button ~neutral mr-2">{% trans "Reset Password" %}</a>
|
||||
<a href="{{ signup_url }}" class="button ~neutral">Sign Up</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
18
shynet/dashboard/templates/account/logout.html
Normal file
18
shynet/dashboard/templates/account/logout.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Sign Out" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Sign Out" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<p>{% trans 'Are you sure you want to sign out?' %}</p>
|
||||
|
||||
<form method="post" action="{% url 'account_logout' %}" class="max-w-lg">
|
||||
{% csrf_token %}
|
||||
{% if redirect_field_value %}
|
||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
|
||||
{% endif %}
|
||||
<button type="submit" class="button ~neutral !high">{% trans 'Sign Out' %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}You cannot remove your primary email address ({{email}}).{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Confirmation email sent to {{email}}.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Confirmed {{email}}.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Removed email address {{email}}.{% endblocktrans %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load account %}
|
||||
{% load i18n %}
|
||||
{% user_display user as name %}
|
||||
{% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}You have signed out.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Password successfully changed.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Password successfully set.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}New primary email address set.{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Your primary email address must be verified.{% endblocktrans %}
|
||||
14
shynet/dashboard/templates/account/password_change.html
Normal file
14
shynet/dashboard/templates/account/password_change.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{% url 'account_change_password' %}" class="password_change max-w-lg">
|
||||
{% csrf_token %}
|
||||
{{ form|a17t }}
|
||||
<button type="submit" name="action" class="button ~urge !high">{% trans "Change Password" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
23
shynet/dashboard/templates/account/password_reset.html
Normal file
23
shynet/dashboard/templates/account/password_reset.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
{% load account %}
|
||||
|
||||
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Password Reset" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{% trans "Forgotten your password? Enter your email address below, and we'll send you an email to reset it." %}
|
||||
</p>
|
||||
|
||||
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset max-w-lg">
|
||||
{% csrf_token %}
|
||||
{{ form|a17t }}
|
||||
<button type="submit" class="button ~urge !high">{% trans 'Reset Password' %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
15
shynet/dashboard/templates/account/password_reset_done.html
Normal file
15
shynet/dashboard/templates/account/password_reset_done.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load account %}
|
||||
|
||||
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Password Reset" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
{% if user.is_authenticated %}
|
||||
{% include "account/snippets/already_logged_in.html" %}
|
||||
{% endif %}
|
||||
|
||||
<p>{% blocktrans %}We have sent you an email with a password reset link. Please try again if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
{% if token_fail %}
|
||||
{% url 'account_reset_password' as passwd_reset_url %}
|
||||
<p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
{% if form %}
|
||||
<form method="POST" action="{{ action_url }}" class="max-w-lg">
|
||||
{% csrf_token %}
|
||||
{{ form|a17t }}
|
||||
<button type="submit" name="action" class="button ~urge !high">{% trans 'Change Password' %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% trans 'Your password is now changed.' %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block head_title %}{% trans "Change Password" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<p>{% trans 'Your password is now changed.' %}</p>
|
||||
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
|
||||
{% endblock %}
|
||||
14
shynet/dashboard/templates/account/password_set.html
Normal file
14
shynet/dashboard/templates/account/password_set.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Set Password" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Set Password" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{% url 'account_set_password' %}" class="password_set">
|
||||
{% csrf_token %}
|
||||
{{ form|a17t }}
|
||||
<button type="submit" name="action" class="button ~urge !high">{% trans 'Set Password' %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
20
shynet/dashboard/templates/account/signup.html
Normal file
20
shynet/dashboard/templates/account/signup.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n a17t_tags %}
|
||||
|
||||
{% block head_title %}{% trans "Sign Up" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Sign Up" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<aside class="aside ~info">{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a> instead.{% endblocktrans %}</aside>
|
||||
|
||||
<form class="signup max-w-lg" id="signup_form" method="post" action="{% url 'account_signup' %}">
|
||||
{% csrf_token %}
|
||||
{{ form|a17t }}
|
||||
{% if redirect_field_value %}
|
||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
||||
{% endif %}
|
||||
<button type="submit" class="button ~urge !high">{% trans "Sign Up" %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
10
shynet/dashboard/templates/account/signup_closed.html
Normal file
10
shynet/dashboard/templates/account/signup_closed.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Sign Up Closed" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<p>{% trans "Public sign-ups are not allowed at this time." %}</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load account %}
|
||||
|
||||
{% user_display user as user_display %}
|
||||
<aside class="aside ~info">
|
||||
<p><strong>{% trans "Note" %}:</strong> {% blocktrans %}you are already logged in as {{ user_display }}.{% endblocktrans %}</p>
|
||||
</aside>
|
||||
10
shynet/dashboard/templates/account/verification_sent.html
Normal file
10
shynet/dashboard/templates/account/verification_sent.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Verify Email Address" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Verify Email Address" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<p>{% blocktrans %}We have sent an email to you for verification. Follow the link provided to finalize the signup process. Please try to log in again if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "account/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "Verify Email Address" %}{% endblock %}
|
||||
{% block page_title %}{% trans "Verify Email Address" %}{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
{% url 'account_email' as email_url %}
|
||||
|
||||
<p>{% blocktrans %}This part of the site requires us to verify that
|
||||
you are who you claim to be. For this purpose, we require that you
|
||||
verify ownership of your email address. {% endblocktrans %}</p>
|
||||
|
||||
<p>{% blocktrans %}We have sent an email to you for
|
||||
verification. Please click on the link inside this email. Please
|
||||
try again if you do not receive it within a few minutes.{% endblocktrans %}</p>
|
||||
|
||||
<aside class="aside ~info">{% blocktrans %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your email address</a>.{% endblocktrans %}</aside>
|
||||
{% endblock %}
|
||||
112
shynet/dashboard/templates/base.html
Normal file
112
shynet/dashboard/templates/base.html
Normal file
@@ -0,0 +1,112 @@
|
||||
{% load static rules %}
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{% block head_title %}Privacy-oriented analytics{% endblock %} | Shynet</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<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/apexcharts@3.18.1/dist/apexcharts.min.js"
|
||||
integrity="sha256-RalQXBZdisB04aaBsm+6YZ0b/iRYjX1MZn90m19AnCY=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-200 min-h-full">
|
||||
{% block body %}
|
||||
|
||||
<section class="max-w-screen-xl mx-auto px-4 md:px-0 py-4 md:py-12 md:flex">
|
||||
<aside class="mb-8 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center">
|
||||
<a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}">
|
||||
<i class="fas fa-binoculars fa-3x text-purple-600 hidden md:block"></i>
|
||||
<i class="fas fa-binoculars fa-2x text-purple-600 md:hidden"></i>
|
||||
</a>
|
||||
|
||||
<button class="button ~neutral !low md:hidden"
|
||||
onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')">
|
||||
<span class="icon">
|
||||
<i class="fas fa-bars"></i>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="sep h-4 md:h-8 w-full">
|
||||
<div id="navMenuExpanded" class="bg-white shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
|
||||
{% if user.owning_services.all %}
|
||||
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p>
|
||||
|
||||
{% for service in user.owning_services.all %}
|
||||
{% url 'dashboard:service' service.uuid as url %}
|
||||
{% include 'dashboard/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 'dashboard:service_create' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="+ Create" url=url %}
|
||||
|
||||
<hr class="sep h-8">
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if user.collaborating_services.all %}
|
||||
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p>
|
||||
|
||||
{% for service in user.collaborating_services.all %}
|
||||
{% url 'dashboard:service' service.uuid as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url %}
|
||||
{% endfor %}
|
||||
|
||||
<hr class="sep h-8">
|
||||
{% endif %}
|
||||
|
||||
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
{% url 'account_email' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Emails" url=url %}
|
||||
|
||||
{% url 'account_set_password' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %}
|
||||
|
||||
{% url 'account_logout' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% url 'account_login' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Log In" url=url %}
|
||||
|
||||
{% url 'account_signup' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Sign Up" url=url %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</aside>
|
||||
<div class="md:w-10/12">
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<article class="card {{message.tags}} !high mb-2 w-full">{{message}}</article>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<hr class="sep">
|
||||
{% endif %}
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extra_body %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<form method="GET" id="datePicker">
|
||||
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
|
||||
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
|
||||
</form>
|
||||
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral cursor-pointer" readonly>
|
||||
<style>
|
||||
:root {
|
||||
--litepickerMonthButtonHover: var(--color-urge);
|
||||
--litepickerDayColorHover: var(--color-urge);
|
||||
--litepickerDayIsTodayColor: var(--color-urge);
|
||||
--litepickerDayIsInRange: var(--color-urge-normal-fill);
|
||||
--litepickerDayIsStartBg: var(--color-urge);
|
||||
--litepickerDayIsEndBg: var(--color-urge);
|
||||
--litepickerButtonApplyBg: var(--color-urge);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var picker = new Litepicker({
|
||||
element: document.getElementById('rangePicker'),
|
||||
singleMode: false,
|
||||
format: 'MMM D, YYYY',
|
||||
maxDate: new Date(),
|
||||
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
|
||||
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),
|
||||
onSelect: function (startDate, endDate) {
|
||||
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() +
|
||||
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate());
|
||||
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" +
|
||||
(endDate.getMonth() + 1) + "-" + endDate.getDate());
|
||||
document.getElementById("datePicker").submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
{% load humanize helpers %}
|
||||
|
||||
<a class="card ~neutral !low service mb-6 p-0" href="{% url 'dashboard:service' object.uuid %}">
|
||||
{% with stats=object.stats %}
|
||||
<div class="p-4 md:flex justify-between">
|
||||
<div class="md:w-4/12 flex items-center mb-4 md:mb-0">
|
||||
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-purple-600">
|
||||
{{object.name}}
|
||||
</h3>
|
||||
{% include 'dashboard/includes/stats_status_chip.html' %}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<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">
|
||||
{% if stats.bounce_rate_pct != None %}
|
||||
{{stats.bounce_rate_pct|floatformat:"-1"}}%
|
||||
{% else %}
|
||||
?
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Avg. Duration</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 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
</a>
|
||||
@@ -0,0 +1,35 @@
|
||||
{% load humanize helpers %}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Session Start</th>
|
||||
<th>Identity</th>
|
||||
<th>Network</th>
|
||||
<th class="rf">Duration</th>
|
||||
<th class="rf">Hits</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'dashboard: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">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
|
||||
<td class="rf">{{session.duration|naturaldelta}}</td>
|
||||
<td class="rf">{{session.hit_set.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,6 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div>
|
||||
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-gray-100{% endif %}"
|
||||
href="{{url}}">{{label}}</a>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load humanize %}
|
||||
|
||||
{% with stats=object.get_daily_stats %}
|
||||
{% if stats.currently_online > 0 %}
|
||||
<span class="chip ~positive !high">
|
||||
{{stats.currently_online|intcomma}} online
|
||||
</span>
|
||||
{% elif stats.online == True %}
|
||||
<span class="chip ~positive !high">
|
||||
Online
|
||||
</span>
|
||||
{% elif stats.online == False %}
|
||||
<span class="chip ~critical !high">
|
||||
Offline
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
@@ -0,0 +1,77 @@
|
||||
<div id="chart{{name|default:'Main'}}"></div>
|
||||
<script>
|
||||
var triggerMatchesChartOptions = {
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
tooltip: {
|
||||
shared: false,
|
||||
},
|
||||
colors: ["#805AD5"],
|
||||
chart: {
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
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,
|
||||
},
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
show: true
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
show: false,
|
||||
formatter: val => val.toFixed(0)
|
||||
},
|
||||
padding: {
|
||||
left: 0,
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
},
|
||||
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>
|
||||
26
shynet/dashboard/templates/dashboard/pages/dashboard.html
Normal file
26
shynet/dashboard/templates/dashboard/pages/dashboard.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load rules %}
|
||||
|
||||
{% block content %}
|
||||
<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 'dashboard/includes/date_range.html' %}
|
||||
</div>
|
||||
{% has_perm "core.create_service" user as can_create %}
|
||||
{% if can_create %}
|
||||
<a href="{% url 'dashboard:service_create' %}" class="button field w-auto">+ New Service</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="sep">
|
||||
{% for object in services %}
|
||||
{% include 'dashboard/includes/service_overview.html' %}
|
||||
{% empty %}
|
||||
<p>You don't have any services on this Shynet instance yet.</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
9
shynet/dashboard/templates/dashboard/pages/index.html
Normal file
9
shynet/dashboard/templates/dashboard/pages/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<h2>Shynet Analytics</h2>
|
||||
<p>Eventually, more information about Shynet will be available here.</p>
|
||||
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
|
||||
</section>
|
||||
{% endblock %}
|
||||
187
shynet/dashboard/templates/dashboard/pages/service.html
Normal file
187
shynet/dashboard/templates/dashboard/pages/service.html
Normal file
@@ -0,0 +1,187 @@
|
||||
{% extends "dashboard/service_base.html" %}
|
||||
|
||||
{% load humanize helpers rules %}
|
||||
|
||||
{% block service_actions %}
|
||||
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
|
||||
{% has_perm 'core.change_service' user object as can_update %}
|
||||
{% if can_update %}
|
||||
<a href="{% url 'dashboard:service_update' service.uuid %}" class="button field ~neutral w-auto">Manage →</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
<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="">
|
||||
<p class="label text-gray-400">Hits</p>
|
||||
<p class="heading">
|
||||
{{stats.hit_count|intcomma}}
|
||||
</p>
|
||||
</article>
|
||||
<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="">
|
||||
<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="">
|
||||
<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="">
|
||||
<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 'dashboard/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 py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th class="rf">Hits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for location in stats.locations %}
|
||||
<tr>
|
||||
<td>{{location.location|urldisplay}}</td>
|
||||
<td class="rf">{{location.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Referrer</th>
|
||||
<th class="rf">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for referrer in stats.referrers %}
|
||||
<tr>
|
||||
<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 py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Operating System</th>
|
||||
<th class="rf">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for os in stats.operating_systems %}
|
||||
<tr>
|
||||
<td>{{os.os|default:"Unknown"}}</td>
|
||||
<td class="rf">{{os.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Browser</th>
|
||||
<th class="rf">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for browser in stats.browsers %}
|
||||
<tr>
|
||||
<td>{{browser.browser|default:"Unknown"}}</td>
|
||||
<td class="rf">{{browser.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th class="rf">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for device in stats.devices %}
|
||||
<tr>
|
||||
<td>{{device.device|default:"Unknown"}}</td>
|
||||
<td class="rf">{{device.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Device Type</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 class="rf">{{device_type.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral !low">
|
||||
{% include 'dashboard/includes/session_list.html' %}
|
||||
<hr class="sep h-8">
|
||||
<a href="{% url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto">View more sessions
|
||||
→</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load a17t_tags %}
|
||||
|
||||
{% block head_title %}Create Service{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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 %}
|
||||
<div class="p-4">
|
||||
{{form|a17t}}
|
||||
</div>
|
||||
<div class="section ~urge !normal p-4">
|
||||
<button type="submit" class="button ~urge !high">Create</button>
|
||||
<a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "dashboard/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 'dashboard:service' object.uuid %}" class="button ~critical !low">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,99 @@
|
||||
{% extends "dashboard/service_base.html" %}
|
||||
|
||||
{% load a17t_tags pagination humanize helpers %}
|
||||
|
||||
{% block head_title %}{{object.name}} Session{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
<article class="card ~neutral !high">
|
||||
<div class="md:flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="heading text-2xl mr-4">
|
||||
{{session.identifier|default:"Anonymous"}}, {{session.duration|naturaldelta}}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<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 text-gray-400 font-medium">
|
||||
<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 ↗</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 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">
|
||||
<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 %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "dashboard/service_base.html" %}
|
||||
|
||||
{% load a17t_tags pagination humanize helpers %}
|
||||
|
||||
{% block head_title %}{{object.name}} Sessions{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-scroll">
|
||||
{% include 'dashboard/includes/session_list.html' %}
|
||||
</div>
|
||||
{% pagination page_obj request %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "dashboard/service_base.html" %}
|
||||
|
||||
{% load a17t_tags %}
|
||||
|
||||
{% block head_title %}{{object.name}} Management{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral w-auto">View →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
<div class="max-w-xl content">
|
||||
<h5>Installation</h5>
|
||||
<p>Place the following snippet at the end of the <code><body></code> tag on any page you'd like to track.</p>
|
||||
<div class="card ~neutral !high font-mono text-sm">
|
||||
{% filter force_escape %}<noscript><img
|
||||
src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/pixel.gif"></noscript>
|
||||
<script src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/identifier/script.js"></script>
|
||||
{% endfilter %}
|
||||
</div>
|
||||
<hr class="sep h-4">
|
||||
<h5>Settings</h5>
|
||||
<form class="card ~neutral !low p-0" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="p-4">
|
||||
{{form|a17t}}
|
||||
</div>
|
||||
<div class="section ~neutral !normal p-4 flex justify-between">
|
||||
<div>
|
||||
<button type="submit" class="button ~neutral !high">Save</button>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
shynet/dashboard/templates/dashboard/service_base.html
Normal file
25
shynet/dashboard/templates/dashboard/service_base.html
Normal 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 mb-4 md:mb-0" href="{% url 'dashboard:service' object.uuid %}">
|
||||
<h3 class="heading leading-none mr-4">
|
||||
{{object.name}}
|
||||
</h3>
|
||||
<div class='text-3xl'>
|
||||
{% include 'dashboard/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 %}
|
||||
0
shynet/dashboard/templatetags/__init__.py
Normal file
0
shynet/dashboard/templatetags/__init__.py
Normal file
48
shynet/dashboard/templatetags/helpers.py
Normal file
48
shynet/dashboard/templatetags/helpers.py
Normal file
@@ -0,0 +1,48 @@
|
||||
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()
|
||||
|
||||
|
||||
@register.filter
|
||||
def naturaldelta(timedelta):
|
||||
if isinstance(timedelta, timezone.timedelta):
|
||||
seconds = timedelta.seconds
|
||||
else:
|
||||
seconds = timedelta
|
||||
string = ""
|
||||
if seconds // 3600 > 0:
|
||||
string += "{:02.0f}:".format(seconds // 3600)
|
||||
string += "{:02.0f}:".format((seconds % 3600) // 60)
|
||||
string += "{:02.0f}".format(seconds % 60)
|
||||
return string
|
||||
|
||||
|
||||
@register.filter
|
||||
def flag_emoji(isocode):
|
||||
try:
|
||||
return flag.flag(isocode)
|
||||
except:
|
||||
return ""
|
||||
|
||||
|
||||
@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] + '…'}</a>"
|
||||
)
|
||||
except:
|
||||
return url
|
||||
31
shynet/dashboard/urls.py
Normal file
31
shynet/dashboard/urls.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.DashboardView.as_view(), name="dashboard"),
|
||||
path("service/new/", views.ServiceCreateView.as_view(), name="service_create"),
|
||||
path("service/<pk>/", views.ServiceView.as_view(), name="service"),
|
||||
path(
|
||||
"service/<pk>/manage/",
|
||||
views.ServiceUpdateView.as_view(),
|
||||
name="service_update",
|
||||
),
|
||||
path(
|
||||
"service/<pk>/delete/",
|
||||
views.ServiceDeleteView.as_view(),
|
||||
name="service_delete",
|
||||
),
|
||||
path(
|
||||
"service/<pk>/sessions/",
|
||||
views.ServiceSessionsListView.as_view(),
|
||||
name="service_session_list",
|
||||
),
|
||||
path(
|
||||
"service/<pk>/sessions/<session_pk>/",
|
||||
views.ServiceSessionView.as_view(),
|
||||
name="service_session",
|
||||
),
|
||||
]
|
||||
122
shynet/dashboard/views.py
Normal file
122
shynet/dashboard/views.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic import (
|
||||
CreateView,
|
||||
DeleteView,
|
||||
DetailView,
|
||||
ListView,
|
||||
TemplateView,
|
||||
UpdateView,
|
||||
)
|
||||
from rules.contrib.views import PermissionRequiredMixin
|
||||
from django.db.models import Q
|
||||
|
||||
from analytics.models import Session
|
||||
|
||||
from .forms import ServiceForm
|
||||
from .mixins import BaseUrlMixin, DateRangeMixin
|
||||
from core.models import Service
|
||||
|
||||
class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView):
|
||||
template_name = "dashboard/pages/dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["services"] = Service.objects.filter(
|
||||
Q(owner=self.request.user) | Q(collaborators__in=[self.request.user])
|
||||
)
|
||||
for service in data["services"]:
|
||||
service.stats = service.get_core_stats(data["start_date"], data["end_date"])
|
||||
return data
|
||||
|
||||
|
||||
class ServiceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
model = Service
|
||||
form_class = ServiceForm
|
||||
template_name = "dashboard/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("dashboard:service", kwargs={"pk": self.object.uuid})
|
||||
|
||||
|
||||
class ServiceView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DateRangeMixin, DetailView
|
||||
):
|
||||
model = Service
|
||||
template_name = "dashboard/pages/service.html"
|
||||
permission_required = "core.view_service"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"])
|
||||
data["object_list"] = Session.objects.filter(
|
||||
service=self.get_object(),
|
||||
start_time__lt=self.get_end_date(),
|
||||
start_time__gt=self.get_start_date(),
|
||||
).order_by("-start_time")[:10]
|
||||
return data
|
||||
|
||||
|
||||
class ServiceUpdateView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, BaseUrlMixin, UpdateView
|
||||
):
|
||||
model = Service
|
||||
form_class = ServiceForm
|
||||
template_name = "dashboard/pages/service_update.html"
|
||||
permission_required = "core.change_service"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("dashboard:service", kwargs={"pk": self.object.uuid})
|
||||
|
||||
|
||||
class ServiceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
model = Service
|
||||
form_class = ServiceForm
|
||||
template_name = "dashboard/pages/service_delete.html"
|
||||
permission_required = "core.delete_service"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("dashboard:dashboard")
|
||||
|
||||
|
||||
class ServiceSessionsListView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, DateRangeMixin, ListView
|
||||
):
|
||||
model = Session
|
||||
template_name = "dashboard/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"))
|
||||
|
||||
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(),
|
||||
).order_by("-start_time")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["object"] = self.get_object()
|
||||
return data
|
||||
|
||||
|
||||
class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
model = Session
|
||||
template_name = "dashboard/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)
|
||||
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
|
||||
return data
|
||||
Reference in New Issue
Block a user