Add today/by date or taxon_id stats and record access
This commit is contained in:
parent
e4488e1918
commit
128e3f33bb
|
@ -24,7 +24,7 @@ header img#logo {
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
min-height: 80vh;
|
min-height: 100vh;
|
||||||
padding: 5em;
|
padding: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,14 +51,14 @@ nav ul {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
background-color: lightgreen;
|
background-color: #333;
|
||||||
margin: 0;
|
margin: auto;
|
||||||
padding: 0;
|
min-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul li a,
|
nav ul li a,
|
||||||
.dropdown-button {
|
.dropdown-button {
|
||||||
color: green;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1em 0.5em;
|
padding: 1em 0.5em;
|
||||||
|
@ -66,7 +66,13 @@ nav ul li a,
|
||||||
|
|
||||||
nav ul li a.active,
|
nav ul li a.active,
|
||||||
nav ul li a:hover {
|
nav ul li a:hover {
|
||||||
background-color: aquamarine;
|
background-color: #999;
|
||||||
|
color: #101010
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-button:hover {
|
||||||
|
background-color: #900;
|
||||||
|
color: white
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown:hover .dropdown-content {
|
.dropdown:hover .dropdown-content {
|
||||||
|
@ -76,7 +82,6 @@ nav ul li a:hover {
|
||||||
.dropdown-content {
|
.dropdown-content {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #f9f9f9;
|
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -86,4 +91,8 @@ canvas {
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scientific-name {
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
|
@ -7,17 +7,22 @@
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
|
"doctrine/doctrine-bundle": "^2.7",
|
||||||
|
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||||
|
"doctrine/orm": "^2.13",
|
||||||
"sensio/framework-extra-bundle": "^6.2",
|
"sensio/framework-extra-bundle": "^6.2",
|
||||||
"symfony/console": "6.1.*",
|
"symfony/console": "6.1.*",
|
||||||
"symfony/dotenv": "6.1.*",
|
"symfony/dotenv": "6.1.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "6.1.*",
|
"symfony/framework-bundle": "6.1.*",
|
||||||
|
"symfony/proxy-manager-bridge": "6.1.*",
|
||||||
"symfony/runtime": "6.1.*",
|
"symfony/runtime": "6.1.*",
|
||||||
"symfony/translation": "6.1.*",
|
"symfony/translation": "6.1.*",
|
||||||
"symfony/twig-bundle": "6.1.*",
|
"symfony/twig-bundle": "6.1.*",
|
||||||
"symfony/webpack-encore-bundle": "^1.15",
|
"symfony/webpack-encore-bundle": "^1.15",
|
||||||
"symfony/yaml": "6.1.*",
|
"symfony/yaml": "6.1.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
|
"twig/intl-extra": "^3.4",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
@ -71,5 +76,8 @@
|
||||||
"allow-contrib": false,
|
"allow-contrib": false,
|
||||||
"require": "6.1.*"
|
"require": "6.1.*"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/maker-bundle": "^1.45"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,4 +6,7 @@ return [
|
||||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version,
|
||||||
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
#server_version: '13'
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App:
|
||||||
|
is_bundle: false
|
||||||
|
dir: '%kernel.project_dir%/src/Entity'
|
||||||
|
prefix: 'App\Entity'
|
||||||
|
alias: App
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
# "TEST_TOKEN" is typically set by ParaTest
|
||||||
|
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: false
|
||||||
|
query_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
result_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.result_cache_pool
|
||||||
|
|
||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
pools:
|
||||||
|
doctrine.result_cache_pool:
|
||||||
|
adapter: cache.app
|
||||||
|
doctrine.system_cache_pool:
|
||||||
|
adapter: cache.system
|
|
@ -0,0 +1,6 @@
|
||||||
|
doctrine_migrations:
|
||||||
|
migrations_paths:
|
||||||
|
# namespace is arbitrary but should be different from App\Migrations
|
||||||
|
# as migrations classes should NOT be autoloaded
|
||||||
|
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||||
|
enable_profiler: '%kernel.debug%'
|
|
@ -0,0 +1,8 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
database:
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
|
@ -0,0 +1,21 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
database:
|
||||||
|
image: postgres:${POSTGRES_VERSION:-14}-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||||
|
# You should definitely change the password in production
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data:rw
|
||||||
|
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||||
|
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
db-data:
|
||||||
|
###< doctrine/doctrine-bundle ###
|
|
@ -0,0 +1,146 @@
|
||||||
|
<?php
|
||||||
|
// src/Controller/TodayController.php
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
|
||||||
|
class TodayController extends AbstractController
|
||||||
|
{
|
||||||
|
|
||||||
|
private Connection $connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/today", name="today")
|
||||||
|
*/
|
||||||
|
public function today(Connection $connection)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
return $this->render('today/index.html.twig', [
|
||||||
|
"species" => $this->recorded_species_by_date($date),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/today/species", name="today_species")
|
||||||
|
*/
|
||||||
|
public function today_species_page(Connection $connection)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
return $this->render('today/index.html.twig', [
|
||||||
|
"species" => $this->recorded_species_by_date($date)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/today/species/{id}", name="today_species_id")
|
||||||
|
*/
|
||||||
|
public function today_species_by_id(Connection $connection, $id)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
return $this->render('today/species.html.twig', [
|
||||||
|
"results" => $this->recorded_species_by_id_and_date($id, $date)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/today/{date}", name="today_date")
|
||||||
|
*/
|
||||||
|
public function today_date(Connection $connection, $date)
|
||||||
|
{
|
||||||
|
return $this->redirectToRoute('today_species_date', array('date' => $date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/today/{date}/species", name="today_species_date")
|
||||||
|
*/
|
||||||
|
public function today_species_by_date(Connection $connection, $date)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
return $this->render('today/index.html.twig', [
|
||||||
|
"date" => $date,
|
||||||
|
"results" => $this->recorded_species_by_date($date)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/today/{date}/species/{id}", name="today_species_id_and_date")
|
||||||
|
*/
|
||||||
|
public function today_species_by_id_and_date(Connection $connection, $date, $id)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
return $this->render('today/species.html.twig', [
|
||||||
|
"date" => $date,
|
||||||
|
"results" => $this->recorded_species_by_id_and_date($id, $date)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function recorded_species_by_date($date)
|
||||||
|
{
|
||||||
|
$sql = "SELECT `taxon`.`taxon_id`, `scientific_name`, `common_name`, COUNT(*) AS `contact_count`, MAX(`confidence`) AS max_confidence
|
||||||
|
FROM observation
|
||||||
|
INNER JOIN taxon
|
||||||
|
ON observation.taxon_id = taxon.taxon_id
|
||||||
|
WHERE strftime('%Y-%m-%d', `observation`.`date`) =:date
|
||||||
|
GROUP BY observation.taxon_id";
|
||||||
|
$stmt = $this->connection->prepare($sql);
|
||||||
|
$stmt->bindValue(':date', $date);
|
||||||
|
$result = $stmt->executeQuery();
|
||||||
|
return $result->fetchAllAssociative();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function recorded_species_by_id_and_date($id, $date)
|
||||||
|
{
|
||||||
|
/* Get taxon even if there is no record this date */
|
||||||
|
$sql = "SELECT * FROM `taxon` WHERE `taxon_id` = :id";
|
||||||
|
$stmt = $this->connection->prepare($sql);
|
||||||
|
$stmt->bindValue(':id', $id);
|
||||||
|
$result = $stmt->executeQuery();
|
||||||
|
$taxon = $result->fetchAssociative();
|
||||||
|
if (!$taxon) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
/* Get daily stats */
|
||||||
|
$sql = "SELECT COUNT(*) AS `contact_count`, MAX(`confidence`) AS `max_confidence`
|
||||||
|
FROM `taxon`
|
||||||
|
INNER JOIN `observation`
|
||||||
|
ON `taxon`.`taxon_id` = `observation`.`taxon_id`
|
||||||
|
WHERE strftime('%Y-%m-%d', `observation`.`date`) = :date
|
||||||
|
AND `observation`.`taxon_id` = :id";
|
||||||
|
$stmt = $this->connection->prepare($sql);
|
||||||
|
$stmt->bindValue(':id', $id);
|
||||||
|
$stmt->bindValue(':date', $date);
|
||||||
|
$result = $stmt->executeQuery();
|
||||||
|
$stat = $result->fetchAllAssociative();
|
||||||
|
$sql = "SELECT * FROM `observation`
|
||||||
|
WHERE `taxon_id` = :id
|
||||||
|
AND strftime('%Y-%m-%d', `observation`.`date`) = :date";
|
||||||
|
$stmt = $this->connection->prepare($sql);
|
||||||
|
$stmt->bindValue(':id', $id);
|
||||||
|
$stmt->bindValue(':date', $date);
|
||||||
|
$result = $stmt->executeQuery();
|
||||||
|
$records = $result->fetchAllAssociative();
|
||||||
|
return array("taxon" => $taxon, "stat" => $stat, "records" => $records);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function best_confidence_today($id, $date)
|
||||||
|
{
|
||||||
|
$sql = "SELECT MAX(`confidence`) AS confidence
|
||||||
|
FROM `observation`
|
||||||
|
WHERE strftime('%Y-%m-%d', `observation`.`date`) = :date
|
||||||
|
AND `taxon_id` = :id";
|
||||||
|
$stmt = $this->connection->prepare($sql);
|
||||||
|
$stmt->bindValue(':id', $id);
|
||||||
|
$stmt->bindValue(':date', $date);
|
||||||
|
$result = $stmt->executeQuery();
|
||||||
|
return $result->fetchAllAssociative();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,33 @@
|
||||||
"ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05"
|
"ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"doctrine/doctrine-bundle": {
|
||||||
|
"version": "2.7",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "da713d006953b90d1c085c1be480ecdd6c4a95e0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine.yaml",
|
||||||
|
"src/Entity/.gitignore",
|
||||||
|
"src/Repository/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-migrations-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.1",
|
||||||
|
"ref": "ee609429c9ee23e22d6fa5728211768f51ed2818"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine_migrations.yaml",
|
||||||
|
"migrations/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
"sensio/framework-extra-bundle": {
|
"sensio/framework-extra-bundle": {
|
||||||
"version": "6.2",
|
"version": "6.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
@ -63,6 +90,15 @@
|
||||||
"src/Kernel.php"
|
"src/Kernel.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/maker-bundle": {
|
||||||
|
"version": "1.45",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||||
|
}
|
||||||
|
},
|
||||||
"symfony/routing": {
|
"symfony/routing": {
|
||||||
"version": "6.1",
|
"version": "6.1",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ "Today's Detected Species" | trans }}</h2>
|
||||||
|
{# Display a list of records if any, else, print message #}
|
||||||
|
{% if results[0] is defined and results[0] | length > 0 %}
|
||||||
|
<ul>
|
||||||
|
{% for sp in results %}
|
||||||
|
<li class="species">
|
||||||
|
<a href="./species/{{ sp['taxon_id'] }}">
|
||||||
|
<span class="scientific-name">{{ sp["scientific_name"] }} (</span><span class="common-name">{{ sp["common_name"] }}</span>)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,72 @@
|
||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if results is defined and results | length > 0 %}
|
||||||
|
{% set taxon = results["taxon"] %}
|
||||||
|
<h2>
|
||||||
|
{% set today = "now" | date("Y-m-d") %}
|
||||||
|
{% if today == date %}
|
||||||
|
{{ "Today's contact for" | trans }}
|
||||||
|
{% else %}
|
||||||
|
{{ "Contact on " | trans }}
|
||||||
|
{{ date | format_datetime("full", "none") }}
|
||||||
|
{{ " for " | trans }}
|
||||||
|
{% endif %}
|
||||||
|
<span class="scientific-name">
|
||||||
|
{{ taxon["scientific_name"] }}
|
||||||
|
</span>
|
||||||
|
(<span class="common-name">
|
||||||
|
{{ taxon["common_name"] }}
|
||||||
|
</span>)
|
||||||
|
</h2>
|
||||||
|
<div class="stats">
|
||||||
|
{% set stat = results["stat"][0] %}
|
||||||
|
<h3>{{ "Stats" | trans }}</h3>
|
||||||
|
<div class="contact-count">
|
||||||
|
{{ "Contact count:" | trans }}
|
||||||
|
<span class="counter">{{ stat["contact_count"] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-confidence">
|
||||||
|
{{ "Max confidence" | trans }}
|
||||||
|
<span class="value">{{ stat["max_confidence"] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% set records = results["records"] %}
|
||||||
|
<div class="records">
|
||||||
|
<h3>{{ "Contact records" | trans }}</h3>
|
||||||
|
{% if records is defined and records | length > 0 %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ "Filename" | trans }}</th>
|
||||||
|
<th>{{ "Time" | trans }}</th>
|
||||||
|
<th>{{ "Confidence" | trans }}</th>
|
||||||
|
<th>{{ "Audio" | trans }}</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for record in records %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a title="{{ "Download audio file" | trans }}" href="/media/records/{{ record['audio_file'] }}">
|
||||||
|
{{ record["audio_file"] }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ record["date"] | date("H:m") }}</td>
|
||||||
|
<td>{{ record["confidence"] }}</td>
|
||||||
|
<td>
|
||||||
|
<audio controls>
|
||||||
|
<source src="/media/records/{{ record['audio_file'] }}" type="audio/wav">
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ "No records this day for this species" | trans }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue