Add base records routes

This commit is contained in:
Samuel Ortion 2022-08-16 19:28:19 +02:00
parent 4e6d4f396b
commit b90a0cd63a
18 changed files with 208 additions and 36 deletions

33
www/.env Normal file
View File

@ -0,0 +1,33 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=8bd3643031a08d0cd34e6fd3f680fb22
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8"
###< doctrine/doctrine-bundle ###
### records folder
RECORDS_DIR=%kernel.project_dir%/../var/chunks # adapt to your needs
###

1
www/.gitignore vendored
View File

@ -1,5 +1,6 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
!.env
/.env.local /.env.local
/.env.local.php /.env.local.php
/.env.*.local /.env.*.local

View File

@ -12,9 +12,13 @@ import './styles/menu.css';
// start the Stimulus application // start the Stimulus application
import './bootstrap'; import './bootstrap';
import './utils/spectro'; import feather from 'feather-icons';
import './utils/date'; feather.replace();
document.getElementsByClassName('prevent').map( try {
document.getElementsByClassName('prevent').map(
(e) => e.addEventListener('click', (e) => e.preventDefault()) (e) => e.addEventListener('click', (e) => e.preventDefault())
); );
} catch {
console.debug('no prevent class found');
}

View File

@ -0,0 +1,7 @@
(function() {
try {
let delete_buttons = document.getElementsByClassName("delete-button");
} catch {
console.debug("no delete buttons found");
}
})();

View File

@ -164,6 +164,12 @@ footer a:hover {
font-style: italic; font-style: italic;
} }
li, td {
align-items: center;
}
td
/* .dropdown-button:hover { /* .dropdown-button:hover {
background-color: #900; background-color: #900;
color: white color: white

View File

@ -1,7 +1,17 @@
let date_input;
let endpoint;
let date_input = document.querySelector("input[type='date']"); try {
let next_date_button = document.getElementsByClassName("next-date-button")[0]; date_input = document.querySelector(".date-selector input[type='date']");
let previous_date_button = document.getElementsByClassName("previous-date-button")[0]; let next_date_button = document.getElementsByClassName("next-date-button")[0];
let previous_date_button = document.getElementsByClassName("previous-date-button")[0];
endpoint = document.querySelector(".date-selector a").href.split("/")[3];
next_date_button.addEventListener("click", next_date);
previous_date_button.addEventListener("click", previous_date);
} catch {
console.debug("no date input found");
}
function next_date() { function next_date() {
let date = new Date(date_input.value); let date = new Date(date_input.value);
@ -20,8 +30,5 @@ function previous_date() {
function update_date_link() { function update_date_link() {
let date = new Date(date_input.value); let date = new Date(date_input.value);
let date_link = document.querySelector(".date-selector a"); let date_link = document.querySelector(".date-selector a");
date_link.href = `/today/${date.toISOString().split('T')[0]}/species`; date_link.href = `/${endpoint}/${date.toISOString().split('T')[0]}/`;
} }
next_date_button.addEventListener("click", next_date);
previous_date_button.addEventListener("click", previous_date);

View File

@ -15,7 +15,7 @@ try {
initialize(); initialize();
}) })
} catch { } catch {
console.log("spectro not found"); console.debug("spectro not found");
} }
function initialize() { function initialize() {

View File

@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed # Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: parameters:
app.records_dir: '%env(resolve:RECORDS_DIR)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file

View File

@ -14,5 +14,8 @@
"dev": "encore dev", "dev": "encore dev",
"watch": "encore dev --watch", "watch": "encore dev --watch",
"build": "encore production --progress" "build": "encore production --progress"
},
"dependencies": {
"feather-icons": "^4.29.0"
} }
} }

View File

@ -0,0 +1,74 @@
<?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 RecordsController extends AbstractController
{
private Connection $connection;
/**
* @Route("/records/{date}", name="records_index")
*/
public function records_index($date="now")
{
if ($date == "now") {
$date = date("Y-m-d");
}
$records = $this->list_records();
$records = $this->only_on($date, $records);
return $this->render('records/index.html.twig', [
'records' => $records,
'date' => $date,
]);
}
/**
* @Route("/records/remove/{basename}", name="record_remove")
*/
public function remove_record($basename)
{
$this->remove_record_by_basename($basename);
return $this->redirectToRoute('records_index');
}
private function list_records()
{
$records_path = $this->getParameter('app.records_dir')."/out/*.wav";
$records = glob($records_path);
$records = array_map(function($record) {
$record = basename($record);
return $record;
}, $records);
return $records;
}
private function get_record_date($record_path)
{
$record_basename = basename($record_path);
$record_date = explode("_", explode(".", $record_basename)[0])[1];
$year = substr($record_date, 0, 4);
$month = substr($record_date, 4, 2);
$day = substr($record_date, 6, 2);
$date = "$year-$month-$day";
return $date;
}
private function only_on($date, $records) {
$filtered_records = array_filter($records, function($record) use ($date) {
return $this->get_record_date($record) == $date;
});
return $filtered_records;
}
private function remove_record_by_basename($basename) {
$record_path = $this->getParameter('app.records_dir')."/out/$basename";
unlink($record_path);
unlink($record_path.".d/model.out.csv");
rmdir($this->getParameter('app.records_dir')."/out/$basename.d");
}
}

View File

@ -119,7 +119,8 @@ class TodayController extends AbstractController
$stat = $result->fetchAllAssociative(); $stat = $result->fetchAllAssociative();
$sql = "SELECT * FROM `observation` $sql = "SELECT * FROM `observation`
WHERE `taxon_id` = :id WHERE `taxon_id` = :id
AND strftime('%Y-%m-%d', `observation`.`date`) = :date"; AND strftime('%Y-%m-%d', `observation`.`date`) = :date
ORDER BY `observation`.`date` ASC";
$stmt = $this->connection->prepare($sql); $stmt = $this->connection->prepare($sql);
$stmt->bindValue(':id', $id); $stmt->bindValue(':id', $id);
$stmt->bindValue(':date', $date); $stmt->bindValue(':date', $date);

View File

@ -0,0 +1,15 @@
{% extends "base.html.twig" %}
{% block content %}
<h2>{{ "Records" }}</h2>
{% set endpoint = "records" %}
{% include "utils/calendar.html.twig" %}
{% if records is defined and records | length > 0 %}
<ul>
{% for record in records %}
<li>{{ record }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -10,7 +10,8 @@
{{ date | format_datetime("full", "none") }} {{ date | format_datetime("full", "none") }}
{% endif %} {% endif %}
</h2> </h2>
{% include "today/calendar.html.twig" %} {% set endpoint = "today" %}
{% include "utils/calendar.html.twig" %}
{# Display a list of records if any, else, print message #} {# Display a list of records if any, else, print message #}
{% if results is defined and results | length > 0 %} {% if results is defined and results | length > 0 %}
<ul> <ul>

View File

@ -2,36 +2,36 @@
{% block content %} {% block content %}
{% if results is defined and results | length > 0 %} {% if results is defined and results | length > 0 %}
{% set taxon = results["taxon"] %} {% set taxon = results.taxon %}
<h2> <h2>
{% set today = "now" | date("Y-m-d") %} {% set today = "now" | date("Y-m-d") %}
{% if today == date %} {% if today == date %}
{{ "Today's contact for" | trans }} {{ "Today's contacts for" | trans }}
{% else %} {% else %}
{{ "Contact on " | trans }} {{ "Contacts on " | trans }}
{{ date | format_datetime("full", "none") }} {{ date | format_datetime("full", "none") }}
{{ " for " | trans }} {{ " for " | trans }}
{% endif %} {% endif %}
<span class="scientific-name"> <span class="scientific-name">
{{ taxon["scientific_name"] }} {{ taxon.scientific_name }}
</span> </span>
(<span class="common-name"> (<span class="common-name">
{{ taxon["common_name"] }} {{ taxon.common_name }}
</span>) </span>)
</h2> </h2>
<div class="stats"> <div class="stats">
{% set stat = results["stat"][0] %} {% set stat = results.stat[0] %}
<h3>{{ "Stats" | trans }}</h3> <h3>{{ "Stats" | trans }}</h3>
<div class="contact-count"> <div class="contact-count">
{{ "Contact count:" | trans }} {{ "Contact count:" | trans }}
<span class="counter">{{ stat["contact_count"] }}</span> <span class="counter">{{ stat.contact_count }}</span>
</div> </div>
<div class="contact-confidence"> <div class="contact-confidence">
{{ "Max confidence" | trans }} {{ "Max confidence" | trans }}
<span class="value">{{ stat["max_confidence"] }}</span> <span class="value">{{ stat.max_confidence }}</span>
</div> </div>
</div> </div>
{% set records = results["records"] %} {% set records = results.records %}
<div class="records"> <div class="records">
<h3>{{ "Contact records" | trans }}</h3> <h3>{{ "Contact records" | trans }}</h3>
{% if records is defined and records | length > 0 %} {% if records is defined and records | length > 0 %}
@ -47,17 +47,15 @@
{% for record in records %} {% for record in records %}
<tr> <tr>
<td> <td>
<a title="{{ "Download audio file" | trans }}" href="/media/records/{{ record['audio_file'] }}"> <a title="{{ "Download audio file" | trans }}" href="/media/records/{{ record.audio_file }}">
{{ record["audio_file"] }} {{ record.audio_file }}
</a> </a>
</td> </td>
<td>{{ record["date"] | date("H:m") }}</td> <td>{{ record.date | date("H:m") }}</td>
<td>{{ record["confidence"] }}</td> <td>{{ record.confidence }}</td>
<td> <td>
<audio controls> {% include "utils/player.html.twig" with { "file": record.audio_file } only %}
<source src="/media/records/{{ record['audio_file'] }}" type="audio/wav"> <button class="delete" value="{{ record.audio_file }}"><i data-feather="trash-2"></i></button>
Your browser does not support the audio element.
</audio>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -1,7 +1,9 @@
<div class="date-selector container row"> <div class="date-selector container row">
<label for="date ">{{ "Date" | trans }}</label> <label for="date">{{ "Date" | trans }}</label>
<button class="previous-date-button button">&lt;</button> <button class="previous-date-button button">&lt;</button>
<input type="date" id="date" name="date" value="{{ date ? date : "now" | date("Y-m-d") }}" placeholder="YYYY-MM-DD" required pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}" title="{{ "Enter a date in this format YYYY-MM-DD" | trans }}"/> <input type="date" id="date" name="date" value="{{ date ? date : "now" | date("Y-m-d") }}" placeholder="YYYY-MM-DD" required pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}" title="{{ "Enter a date in this format YYYY-MM-DD" | trans }}"/>
<button class="next-date-button button prevent">&gt;</button> <button class="next-date-button button prevent">&gt;</button>
<a href="/today/{{ date ? date : " now" | date(" y-m-d") }}/species" class="button main">{{ "Go" | trans }}</a> <a href="/{{ endpoint }}/{{ date ? date : " now" | date("Y-m-d") }}/" class="button main">{{ "Go" | trans }}</a>
</div> </div>
{{ encore_entry_script_tags('date') }}

View File

@ -0,0 +1,3 @@
<audio preload="none" controls>
<source src="/media/records/{{ file }}">
</audio>

View File

@ -22,6 +22,9 @@ Encore
*/ */
.addEntry('app', './assets/app.js') .addEntry('app', './assets/app.js')
.addEntry('spectro', './assets/utils/spectro.js')
.addEntry('date', './assets/utils/date.js')
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge('./assets/controllers.json') .enableStimulusBridge('./assets/controllers.json')

View File

@ -1660,6 +1660,11 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
classnames@^2.2.5:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-webpack-plugin@^4.0.0: clean-webpack-plugin@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729"
@ -1797,7 +1802,7 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1:
browserslist "^4.21.3" browserslist "^4.21.3"
semver "7.0.0" semver "7.0.0"
core-js@^3.23.0: core-js@^3.1.3, core-js@^3.23.0:
version "3.24.1" version "3.24.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.1.tgz#cf7724d41724154010a6576b7b57d94c5d66e64f" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.1.tgz#cf7724d41724154010a6576b7b57d94c5d66e64f"
integrity sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg== integrity sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==
@ -2249,6 +2254,14 @@ faye-websocket@^0.11.3:
dependencies: dependencies:
websocket-driver ">=0.5.1" websocket-driver ">=0.5.1"
feather-icons@^4.29.0:
version "4.29.0"
resolved "https://registry.yarnpkg.com/feather-icons/-/feather-icons-4.29.0.tgz#4e40e3cbb7bf359ffbbf700edbebdde4e4a74ab6"
integrity sha512-Y7VqN9FYb8KdaSF0qD1081HCkm0v4Eq/fpfQYQnubpqi0hXx14k+gF9iqtRys1SIcTEi97xDi/fmsPFZ8xo0GQ==
dependencies:
classnames "^2.2.5"
core-js "^3.1.3"
fill-range@^7.0.1: fill-range@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"