Skip to content
Snippets Groups Projects
Verified Commit d210dd40 authored by Andrej Ramašeuski's avatar Andrej Ramašeuski
Browse files

Statistika a log

parent e15cf498
No related branches found
No related tags found
No related merge requests found
Pipeline #10929 passed
......@@ -9,6 +9,8 @@ RUN apt-get update && apt-get install -y \
libcrypt-openssl-x509-perl \
libdata-guid-perl \
libdata-random-perl \
libdatetime-perl \
libdatetime-event-recurrence-perl \
libdata-validate-uri-perl \
libdbix-class-perl \
libdbd-pg-perl \
......
1.2.0
1.3.0
......@@ -103,7 +103,9 @@ sub startup {
$r->get('/')->to(cb => sub { shift->render('index'); });
$r->post('/')->to('Shortcut#create');
$r->get('/shortcuts')->to(cb => sub { shift->render('shortcuts'); });
$r->get('/shortcuts')->to(cb => sub { shift->render('shortcuts-vue'); });
$r->get('/shortcuts/:id')->to('Shortcut#get1');
$r->get('/shortcuts/:id/log.csv')->to('Log#csv');
$r->get('/:shortcut')->to('Shortcut#redirect');
$r->get('/:shortcut/qr.png')->to('Shortcut#qr');
......
package PZ::Controller::Log;
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub main ($c) {
$c->openapi->valid_input or return;
my $shortcut = $c->stash->{user}->shortcuts({
id => $c->stash->{id}
})->first;
return $c->error(404, 'NOT_FOUND') if ! $shortcut;
my ($log, @result);
$log = $shortcut->log_items({},
{
order_by => { -desc => 'time'},
rows => $c->param('page_size'),
offset => $c->param('page_size') * ( $c->param('page_index') - 1 ),
}
);
ITEM:
while ( my $item = $log->next ) {
push @result, {
time => $item->time,
ip => $item->ip,
ua => $item->ua,
referrer => $item->referrer,
};
}
$c->render(json => \@result );
}
sub csv ($c) {
my $shortcut = $c->current_user->shortcuts({
id => $c->stash->{id}
})->first;
return $c->error(404, 'NOT_FOUND') if ! $shortcut;
my ($log, @result);
$log = $shortcut->log_items({},
{
order_by => { 'time'},
}
);
$c->res->headers->content_type('text/csv');
$c->res->headers->content_disposition(
'attachment; filename=log.csv',
);
$c->render(
template =>'log.csv',
log => $log,
);
}
1;
......@@ -85,7 +85,10 @@ sub list ($c) {
my @shortcuts = ();
SHORTCUT:
foreach my $shortcut ( $c->stash->{user}->shortcuts({ deleted => undef }) ) {
foreach my $shortcut ( $c->stash->{user}->shortcuts(
{ deleted => undef },
{ order_by => 'shortcut' },
) ) {
push @shortcuts, $c->spec_filter(
{ $shortcut->get_columns }, 'Shortcut'
);
......@@ -94,6 +97,34 @@ sub list ($c) {
$c->render(json => \@shortcuts, );
}
sub get ($c) {
my $shortcut = $c->stash->{user}->shortcuts({
id => $c->stash->{id}
})->first;
if ( ! $shortcut ) {
$c->render( status => 404, text => 'not found' );
return;
}
$c->render(json => { $shortcut->get_columns } );
}
sub get1 ($c) {
my $shortcut = $c->current_user->shortcuts({
id => $c->stash->{id}
})->first;
if ( ! $shortcut ) {
$c->render( status => 404, text => 'not found' );
return;
}
$c->stash->{shortcut} = $shortcut;
$c->stash->{template} = 'shortcut';
}
sub update ($c) {
my $shortcut = $c->stash->{user}->shortcuts({
......
package PZ::Controller::Stat;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use DateTime;
sub main ($c) {
my $shortcut = $c->stash->{user}->shortcuts({
id => $c->stash->{id}
})->first;
return $c->error(404, 'NOT_FOUND') if ! $shortcut;
my %tz = (time_zone => 'Europe/Prague');
my $begin = DateTime->from_epoch(%tz, epoch => $c->param('begin') / 1000);
my $end = DateTime->from_epoch(%tz, epoch => $c->param('end') / 1000);
my ($stat, @result);
$stat = $shortcut->stat_daily_items(period => {
between => [$begin, $end]
});
ITEM:
while ( my $item = $stat->next ) {
push @result, {
period => $item->period,
count => $item->count,
};
}
$c->render(json => \@result );
}
1;
......@@ -48,7 +48,7 @@ __PACKAGE__->has_many(
);
__PACKAGE__->has_many(
stat_dayly_items => 'PZ::Schema::Result::StatDayly',
stat_daily_items => 'PZ::Schema::Result::StatDaily',
{ 'foreign.shortcut_id' => 'self.id', },
);
......
......@@ -38,6 +38,31 @@ components:
type: integer
description: Kód přesměrování
enum: [301, 302]
StatisticItem:
type: object
properties:
period:
type: string
description: Obdobi
count:
type: integer
description: Počet
LogItem:
type: object
properties:
timestamp:
type: string
description: Datum a cas
ip:
type: string
description: IP adresa
ua:
type: string
description: User Agent
referrer:
type: string
description: HTTP Referrer
securitySchemes:
Token:
type: apiKey
......@@ -82,6 +107,21 @@ paths:
$ref: '#/components/schemas/Shortcut'
/shortcuts/{id}:
get:
tags:
- shortcuts
security:
- Token: []
summary: "Zkratka"
operationId: ShortcutGet
x-mojo-to: shortcut#get
responses:
204:
description: Shortcut
content:
application/json:
schema:
$ref: '#/components/schemas/Shortcut'
put:
tags:
- shortcuts
......@@ -110,3 +150,85 @@ paths:
responses:
204:
description: Shortcut deleted
/shortcuts/{id}/stat/:
get:
tags:
- shortcut
- statistic
security:
- Token: []
summary: "Statistika pro zkratku"
operationId: ShortcutStatistic
x-mojo-to: Stat#main
parameters:
- name: id
in: path
required: true
example: 100345
description: "Identifikator zkratky"
schema:
type: integer
- name: begin
in: query
required: true
description: "Zacatek"
schema:
type: integer
- name: end
in: query
required: true
description: "Konec"
schema:
type: integer
responses:
200:
description: Statistika
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/StatisticItem'
/shortcuts/{id}/log:
get:
tags:
- shortcuts
- statistic
security:
- Token: []
summary: "Log pro zkratku"
operationId: ShortcutLog
x-mojo-to: Log#main
parameters:
- name: id
in: path
required: true
example: 100345
description: "Identifikator zkratky"
schema:
type: integer
- name: page_size
in: query
example: 100
description: "Počet zaznamu na strance"
schema:
type: integer
default: 1000
- name: page_index
in: query
example: 1
description: "Stranka"
schema:
type: integer
default: 1
responses:
200:
description: Log navstev pro zkratku
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/LogItem'
......@@ -30,7 +30,6 @@
<script type="text/javascript" src="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.js"></script>
<script src="https://cdn-unpkg.pirati.cz/vue@2.7.8/dist/vue.min.js"></script>
<script src="<%= config->{styleguide} %>js/main.bundle.js"></script>
<link rel="stylesheet" href="/custom.css"/>
</head>
......
"datum a čas","ip adresa","browser","referrer"<%= "\x0D\x0A" =%><% while (my $entry = $log->next) { =%>"<%= $entry->time =%>","<%= $entry->ip =%>","<%= $entry->ua =%>","<%= $entry->referrer =%>"<%= "\x0D\x0A" =%><% } =%>
% layout 'default';
<h1 class="head-alt-md md:head-alt-lg max-w-5xl mb-8">Zkratka "<%= $shortcut->shortcut %>"</h1>
% if (0) {
<h2 class="head-alt-sm my-4">Editace zkratky</h2>
<form id="Edit" class="mb-8">
<div class="form-field form-field--error form-field--required mb-4">
<label class="form-field__label" for="url">URL</label>
<div class="form-field__wrapper form-field__wrapper--shadowed">
<input type="text" class="text-input form-field__control form-field--required" value="" v-model="shortcut.url" id="url" />
</div>
<div class="form-field__error" v-if="shortcut.url === ''"></div>
</div>
<div class="form-field form-field--error form-field--required mb-4">
<label class="form-field__label" for="code">Typ přesměrování</label>
<div class="form-field__wrapper form-field__wrapper--shadowed select">
<select v-model="shortcut.code" class="select__control form-field__control ">
<option value="301">301 trvalé</option>
<option value="302">302 dočasné</option>
</select>
</div>
<div class="form-field__error" v-if="shortcut.url === ''"></div>
</div>
<button class="btn Xbtn--blue-300 btn--hoveractive" @click.prevent="updateShortcut">
<div class="btn__body">Uložit změny</div>
</button>
<button class="btn Xbtn--red-600 btn--hoveractive" @click.prevent="delete_confirm_visible=true">
<div class="btn__body">Smazat zkratku</div>
</button>
<div class="modal__overlay toggle-modal-sample-1" id="modal-sample-1" v-if="delete_confirm_visible" v-cloak>
<div class="modal__content" role="dialog">
<div class="modal__container w-full max-w-2xl" role="dialog">
<div class="modal__container-body elevation-10">
<button class="modal__close" title="Zavřít" @click.prevent="delete_confirm_visible = false"><i class="ico--cross"></i></button>
<div class="card ">
<div class="card__body ">
<h1 class="card-headline mb-2">Smazat zratku</h1>
<p class="card-body-text my-8">
Opravdu chcete smazat zkratku?
</p>
<p class="card-body-text">
<button class="btn btn--green-300 btn--hoveractive text-sm" @click.prevent="deleteMeet()">
<div class="btn__body"><i class="btn__inline-icon ico--bin"></i>ANO</div>
</button>
<button class="btn btn--orange-400 btn--hoveractive text-sm" @click.prevent="delete_confirm_visible = false">
<div class="btn__body"><i class="btn__inline-icon ico--cross"></i>NE</div>
</button>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
% }
<h2 class="head-alt-sm my-4">Statistika navštěv</h2>
<div class="mb-8" style="width:100%; height: 600px;"><canvas id="stat"></canvas></div>
<div class="mb-4 flex justify-between">
<h2 class="head-alt-sm">Log navštěv</h2>
<div>
<button class="btn btn--orange-400 btn--hoveractive text-sm">
<div class="btn__body"><i class="btn__inline-icon ico--download"></i><a href="/shortcuts/<%= $shortcut->id %>/log.csv">Stahnout CSV</a></div>
</button>
</div>
</div>
<div id="log"></div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script>
const end = Date.now();
const begin = Date.now() - 31 * 24 * 3600 * 1000;
const ctx = document.getElementById('stat');
var chart = new Chart(ctx, {
type: 'bar',
data: {
datasets: [{
label: 'Počet kliků',
data: [],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
},
},
x: {
min: begin,
max: end,
type: 'time',
display: true,
offset: true,
time: {
unit: 'day',
tooltipFormat: 'dd.MM.yyyy',
displayFormats: {
day: 'dd.MM.yyyy'
}
}
},
},
parsing: {
xAxisKey: 'period',
yAxisKey: 'count'
}
}
});
fetch('/api/shortcuts/<%= $shortcut->id %>/stat/?begin=' + begin + '&end=' + end , {
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
})
.then((response) => response.json())
.then((data) => {
chart.data.datasets[0].data = data;
chart.update();
}
);
$("#log").jsGrid({
width: "100%",
height: "auto",
inserting: false,
editing: false,
sorting: true,
paging: true,
selecting: false,
autoload: true,
controller: {
loadData: function(filter) {
return $.ajax({
url: "/api/shortcuts/<%= $shortcut->id %>/log/",
dataType: "json",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
data: filter,
});
},
},
fields: [
{ type: "text", name: "time", title: "Datum a čas", width: '11em' },
{ type: "text", name: "ip", title: "IP", width: '8em' },
{ type: "text", title: "Referrer", name: "referrer", width: '100%' },
]
});
// form
% if (0) {
const BASE_URL = "/api/shortcuts";
const API_HEADERS = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Auth-Token": "<%= current_user->token %>",
};
var form = new Vue({
el: '#Edit',
data: {
shortcut: [],
delete_confirm_visible: false,
},
methods: {
fetchData: function() {
fetch(BASE_URL + '/<%= $shortcut->id %>', {
headers: API_HEADERS,
})
.then((res) => res.json())
.then(res => {
this.shortcut = res;
})
},
}
});
form.fetchData();
% }
</script>
</script>
% layout 'default';
<h1 class="head-alt-md md:head-alt-lg max-w-5xl mb-8">Moje zkratky</h1>
<table id="Shortcuts" class="table table-fixed table--striped" v-cloak>
<thead>
<tr>
<th>Zkratka</th>
<th>URL</th>
<th>Přesměrování</th>
</tr>
</thead>
<tbody>
<tr v-for="shortcut in shortcuts" class="cursor-pointer" @click="window.location.href ='/shortcuts/' + shortcut.id">
<td class="head-xs">{{shortcut.shortcut}}</td>
<td class="w-96">{{ shortcut.url }}</td>
<td class="">{{ shortcut.code == 301 ? '301 trvalé' : '302 dočasné'}}</td>
</tr>
</tbody>
</table>
<script type="module">
const BASE_URL = "/api/shortcuts";
const API_HEADERS = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Auth-Token": "<%= current_user->token %>",
};
var list = new Vue({
el: '#Shortcuts',
data: {
shortcuts: [],
},
methods: {
fetchData: function() {
fetch(BASE_URL, {
headers: API_HEADERS,
})
.then((res) => res.json())
.then(res => {
this.shortcuts = res;
})
},
}
});
list.fetchData();
</script>
......@@ -86,7 +86,13 @@ $(function() {
valueField: "code",
textField: "name"
},
{ type: "control", editButton: false, width: 60 }
{ type: "text", title: "Statistika", name: "id", editing: false,
itemTemplate: function(value) {
return '<a href="/shortcuts/' + value + '">' + value + '</a>';
},
},
{ type: "control", editButton: false, width: 60 },
]
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment