Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validateur NeTEx : validation "à la demande" #4158

Merged
merged 27 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1cbf180
Extract helpers for enRoute Chouette Valid
ptitfred Aug 20, 2024
dbb123f
On demand NeTEx validation with enRoute
ptitfred Jul 25, 2024
1980198
Validation NeTEx : meilleur affichage des erreurs
ptitfred Sep 4, 2024
8b53c2f
Affichage des metadata de la validation NeTEx
ptitfred Sep 4, 2024
0d8e331
Validateur NeTEx : meilleur affichage de la durée
ptitfred Sep 5, 2024
ffd4b22
NeTEx errors grouped by nature
ptitfred Sep 9, 2024
f81f146
OnDemand: validateur NeTEx désormais disponible
ptitfred Sep 3, 2024
1cacb61
Meilleur message
ptitfred Sep 9, 2024
8b10adb
Adjust metadata display
ptitfred Sep 10, 2024
21a0ef9
Display warning about errors levels
ptitfred Sep 10, 2024
8403b88
Simplify NeTEx issue template dispatcher
ptitfred Sep 12, 2024
ba62d66
Meilleur message pour les validations rapides
ptitfred Sep 12, 2024
c0c86cb
Merge remote-tracking branch 'origin/master' into netex-validator/on-…
ptitfred Sep 12, 2024
ec3370c
Please the linter
ptitfred Sep 12, 2024
45946ef
Merge remote-tracking branch 'origin/master' into netex-validator/on-…
ptitfred Sep 12, 2024
238b8e0
No inline stylesheet for CSP implementation
ptitfred Sep 16, 2024
85c16c8
Fix misusage of heex templates
ptitfred Sep 16, 2024
dac5d5d
Fix typo in i18n
ptitfred Sep 16, 2024
bf1c301
i18n: better French version
ptitfred Sep 16, 2024
d12bab2
Merge remote-tracking branch 'origin/master' into netex-validator/on-…
ptitfred Sep 16, 2024
dcd3b22
Formatage de durée : utilisons Cldr.Calendar
ptitfred Sep 16, 2024
0f3733a
Please dialyzer
ptitfred Sep 17, 2024
7fee009
Merge remote-tracking branch 'origin/master' into netex-validator/on-…
ptitfred Sep 17, 2024
097521c
Please dialyzer
ptitfred Sep 17, 2024
e6018ca
Dialyzer: keep track of the exclusions
ptitfred Sep 17, 2024
bf94ba4
Better NeTEx validation introduction
ptitfred Sep 17, 2024
add29cf
Simplify file generation for on demand validation
ptitfred Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions apps/shared/lib/date_time_display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,71 @@ defmodule Shared.DateTimeDisplay do

def format_datetime_to_paris(nil, _, _), do: ""

@doc """
Formats a duration in seconds to display, according to a locale.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si tu souhaites, on a déjà :cldr_utils en dépendance et il devrait y avoir moyen d'utiliser https://hexdocs.pm/ex_cldr_calendars/Cldr.Calendar.Duration.html

Il faudrait mettre à jour la liste de providers

use Cldr, locales: ["en", "fr"], providers: [Cldr.Number], default_locale: "fr"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah merci, ça me fendait le coeur de le faire moi-même...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fait.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai dû exclure lib/cldr.ex de dialyzer faute d'avoir une bonne correction. L'erreur :

lib/cldr.ex:1:overlapping_contract
Overloaded contract for Transport.Cldr.Calendar.localize/3 has
overlapping domains; such contracts are currently unsupported and
are simply ignored.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tu peux ouvrir une issue là-bas ? J'étais tombé sur un bug similaire récemment elixir-cldr/cldr#236

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Supported locales: "fr" and "en".

iex> format_duration(0, "en")
"0 second"
iex> format_duration(1, "en")
"1 second"
iex> format_duration(3, "en")
"3 seconds"
iex> format_duration(60, "en")
"1 minute"
iex> format_duration(61, "en")
"1 minute 1 second"
iex> format_duration(65, "en")
"1 minute 5 seconds"
iex> format_duration(120, "en")
"2 minutes"
iex> format_duration(125, "en")
"2 minutes 5 seconds"

iex> format_duration(0, "fr")
"0 seconde"
iex> format_duration(1, "fr")
"1 seconde"
iex> format_duration(3, "fr")
"3 secondes"
iex> format_duration(60, "fr")
"1 minute"
iex> format_duration(61, "fr")
"1 minute 1 seconde"
iex> format_duration(65, "fr")
"1 minute 5 secondes"
iex> format_duration(120, "fr")
"2 minutes"
iex> format_duration(125, "fr")
"2 minutes 5 secondes"
"""
@spec format_duration(non_neg_integer(), binary()) :: binary()
def format_duration(duration_in_seconds, locale) do
minutes = div(duration_in_seconds, 60)
seconds = rem(duration_in_seconds, 60)

cond do
minutes > 0 and seconds > 0 -> "#{format_minutes(minutes, locale)} #{format_seconds(seconds, locale)}"
minutes > 0 -> format_minutes(minutes, locale)
true -> format_seconds(seconds, locale)
end
end

defp format_seconds(seconds, "en"), do: format_duration_unit(seconds, "second")
defp format_seconds(seconds, _locale), do: format_duration_unit(seconds, "seconde")

defp format_minutes(seconds, "en"), do: format_duration_unit(seconds, "minute")
defp format_minutes(seconds, _locale), do: format_duration_unit(seconds, "minute")

defp format_duration_unit(duration, unit) do
if duration > 1 do
"#{duration} #{unit}s"
else
"#{duration} #{unit}"
end
end

@spec convert_to_paris_time(DateTime.t() | NaiveDateTime.t()) :: DateTime.t()
defp convert_to_paris_time(%DateTime{} = dt) do
case Timex.Timezone.convert(dt, "Europe/Paris") do
Expand Down
23 changes: 23 additions & 0 deletions apps/transport/lib/jobs/on_demand_validation_job.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Transport.Jobs.OnDemandValidationJob do
alias Transport.DataVisualization
alias Transport.Validators.GTFSRT
alias Transport.Validators.GTFSTransport
alias Transport.Validators.NeTEx
@download_timeout_ms 10_000

@impl Oban.Worker
Expand Down Expand Up @@ -71,6 +72,28 @@ defmodule Transport.Jobs.OnDemandValidationJob do
end
end

defp perform_validation(%{"type" => "netex", "permanent_url" => url}) do
validator = NeTEx.validator_name()

case NeTEx.validate(url, []) do
{:error, msg} ->
%{oban_args: %{"state" => "error", "error_reason" => msg}, validator: validator}

{:ok, %{"validations" => validation, "metadata" => metadata}} ->
%{
result: validation,
metadata: metadata,
data_vis: nil,
validator: validator,
validated_data_name: url,
max_error: NeTEx.get_max_severity_error(validation),
oban_args: %{
"state" => "completed"
}
}
end
end

defp perform_validation(%{
"type" => "tableschema",
"permanent_url" => url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule TransportWeb.ResourceController do
alias Transport.DataVisualization
import Ecto.Query

import TransportWeb.ResourceView, only: [issue_type: 1, latest_validations_nb_days: 0]
import TransportWeb.ResourceView, only: [latest_validations_nb_days: 0]
import TransportWeb.DatasetView, only: [availability_number_days: 0]

@enabled_validators MapSet.new([
Expand Down Expand Up @@ -144,7 +144,7 @@ defmodule TransportWeb.ResourceController do

issue_type =
case params["issue_type"] do
nil -> issue_type(issues)
nil -> Transport.Validators.GTFSTransport.issue_type(issues)
issue_type -> issue_type
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ defmodule TransportWeb.ValidationController do
use TransportWeb, :controller
alias DB.{MultiValidation, Repo}
alias Transport.DataVisualization
import TransportWeb.ResourceView, only: [issue_type: 1]
import Ecto.Query

def validate(%Plug.Conn{} = conn, %{"upload" => %{"url" => url, "type" => "gbfs"} = params}) do
Expand Down Expand Up @@ -98,44 +97,57 @@ defmodule TransportWeb.ValidationController do
unauthorized(conn)

%MultiValidation{oban_args: %{"state" => "completed", "type" => "gtfs"}} = validation ->
current_issues = Transport.Validators.GTFSTransport.get_issues(validation.result, params)
validator = Transport.Validators.GTFSTransport
current_issues = validator.get_issues(validation.result, params)

issue_type =
case params["issue_type"] do
nil -> issue_type(current_issues)
nil -> validator.issue_type(current_issues)
issue_type -> issue_type
end

conn
|> assign(:validation_id, params["id"])
|> assign(:other_resources, [])
|> assign(:issues, Scrivener.paginate(current_issues, make_pagination_config(params)))
|> assign(
:validation_summary,
Transport.Validators.GTFSTransport.summary(validation.result)
)
|> assign(
:severities_count,
Transport.Validators.GTFSTransport.count_by_severity(validation.result)
)
|> assign_base_validation_details(validator, validation, params, current_issues)
|> assign(:metadata, validation.metadata.metadata)
|> assign(:modes, validation.metadata.modes)
|> assign(:data_vis, data_vis(validation, issue_type))
|> assign(:token, token)
|> render("show.html")
|> render("show_gtfs.html")

%MultiValidation{oban_args: %{"state" => "completed", "type" => "netex"}} = validation ->
validator = Transport.Validators.NeTEx
current_issues = validator.get_issues(validation.result, params)

conn
|> assign_base_validation_details(validator, validation, params, current_issues)
|> assign(:metadata, validation.metadata.metadata)
|> assign(:modes, [])
|> assign(:data_vis, nil)
|> render("show_netex.html")

# Handles waiting for validation to complete, errors and
# validation for schemas
_ ->
live_render(conn, TransportWeb.Live.OnDemandValidationLive,
session: %{
"validation_id" => params["id"],
"issue_type" => params["issue_type"],
"current_url" => validation_path(conn, :show, params["id"], token: token)
}
)
end
end

defp assign_base_validation_details(conn, validator, validation, params, current_issues) do
conn
|> assign(:validator, validator)
|> assign(:validation_id, params["id"])
|> assign(:other_resources, [])
|> assign(:issues, Scrivener.paginate(current_issues, make_pagination_config(params)))
|> assign(:validation_summary, validator.summary(validation.result))
|> assign(:severities_count, validator.count_by_severity(validation.result))
|> assign(:token, params["token"])
end

defp data_vis(%MultiValidation{} = validation, issue_type) do
data_vis = validation.data_vis[issue_type]
has_features = DataVisualization.has_features(data_vis["geojson"])
Expand All @@ -150,7 +162,7 @@ defmodule TransportWeb.ValidationController do
defp filepath(type) do
cond do
type == "tableschema" -> Ecto.UUID.generate() <> ".csv"
type in ["jsonschema", "gtfs"] -> Ecto.UUID.generate()
type in ["jsonschema", "gtfs", "netex"] -> Ecto.UUID.generate()
ptitfred marked this conversation as resolved.
Show resolved Hide resolved
end
end

Expand Down Expand Up @@ -215,6 +227,7 @@ defmodule TransportWeb.ValidationController do
args =
case type do
"gtfs" -> %{"type" => "gtfs"}
"netex" -> %{"type" => "netex"}
schema_name -> %{"schema_name" => schema_name, "type" => schema_type(schema_name)}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ defmodule TransportWeb.Live.OnDemandValidationLive do
schedule_next_update_data()
end

if gtfs_validation_completed?(socket) do
if gtfs_or_netex_validation_completed?(socket) do
redirect(socket, to: socket_value(socket, :current_url))
else
socket
Expand All @@ -57,10 +57,10 @@ defmodule TransportWeb.Live.OnDemandValidationLive do
end
end

defp gtfs_validation_completed?(socket) do
defp gtfs_or_netex_validation_completed?(socket) do
case socket_value(socket, :validation) do
%DB.MultiValidation{oban_args: oban_args} ->
oban_args["type"] == "gtfs" and oban_args["state"] == "completed"
oban_args["type"] in ["gtfs", "netex"] and oban_args["state"] == "completed"

_ ->
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ defmodule TransportWeb.Live.OnDemandValidationSelectLive do

def determine_input_type(type) when type in ["gbfs"], do: "link"
def determine_input_type(type) when type in ["gtfs-rt"], do: "gtfs-rt"
def determine_input_type(type) when type in ["netex"], do: nil
def determine_input_type(_), do: "file"

def handle_event("form_changed", %{"upload" => params, "_target" => target}, socket) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,13 @@
type: "url"
) %>
<% end %>
<%= unless @input_type == "file" or @type == "netex" do %>
<%= unless @input_type == "file" do %>
<%= submit(dgettext("validations", "Validate"), nodiv: true) %>
<% end %>
</.form>
<p :if={@trigger_submit} class="small">
<%= TransportWeb.Gettext.dgettext("validations", "Upload in progress") %>
</p>
<div :if={@type == "netex"} class="container section section-grey" id="netex-explanations">
<p>
<%= raw(
TransportWeb.Gettext.dgettext("validations", "netex-siri-validator", link: "https://greenlight.itxpt.eu")
) %>
</p>

<p>
<%= raw(
TransportWeb.Gettext.dgettext("validations", "doc-eu-formats",
link: "https://doc.transport.data.gouv.fr/documentation/normes-europeennes"
)
) %>
</p>
</div>
</div>
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<style>
ptitfred marked this conversation as resolved.
Show resolved Hide resolved
table.generic_issue tr.message td {
vertical-align: top;
}

table.generic_issue th:nth-child(1) {
width: 60%;
}

table.generic_issue tr.debug:hover {
background: revert;
}

table.generic_issue tr.debug td {
border-top: none;
padding-top: 0;
}

table.generic_issue tr.debug td summary {
cursor: pointer;
}

table.generic_issue tr.debug td pre {
margin-block: 0;
}

table.generic_issue tr.debug td code {
width: 100%;
}
</style>

<table class="table generic_issue">
<tr>
<th><%= dgettext("validations-explanations", "Message") %></th>
<th><%= dgettext("validations-explanations", "Location") %></th>
</tr>

<%= for issue <- @issues do %>
<tr class="message">
<td><%= issue["message"] %></td>
<td>
<%= if is_nil(issue["resource"]) or is_nil(issue["resource"]["filename"]) or is_nil(issue["resource"]["line"]) do %>
<%= dgettext("validations-explanations", "Unknown location") %>
<% else %>
<%= issue["resource"]["filename"] %>:<%= issue["resource"]["line"] %>
<% end %>
</td>
</tr>
<tr class="debug">
<td colspan="2">
<details>
<summary><%= dgettext("validations-explanations", "Details for debugging purposes") %></summary>
<pre><code><%= to_string(Jason.encode!(issue, pretty: true)) %></code></pre>
</details>
ptitfred marked this conversation as resolved.
Show resolved Hide resolved
</td>
</tr>
<% end %>
</table>
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ stats = @metadata["stats"] %>
<li if={networks != []}>
ptitfred marked this conversation as resolved.
Show resolved Hide resolved
<div>
<div class="networks-list">
<%= dngettext("validations", "network", "networks", length(@metadata["networks"])) %> :
<strong><%= Enum.join(@metadata["networks"], ", ") %></strong>
<%= dngettext("validations", "network", "networks", length(networks)) %> :
<strong><%= Enum.join(networks, ", ") %></strong>
ptitfred marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<% locale = get_session(@conn, :locale) %>
<% duration =
if @metadata["elapsed_seconds"] < 1 do
dgettext("validations", "less than 1 second")
else
format_duration(@metadata["elapsed_seconds"], locale)
end %>
<div>
<%= dgettext("validations", "Elapsed time: %{duration}.", duration: duration) %>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<%= for {severity, issues} <- @validation_summary do %>
<%= if Map.get(@severities_count, severity, 0) > 0 do %>
<div class="validation-issue">
<h4><%= @severities_count[severity] %> <%= severity(severity).text %></h4>
<h4><%= @severities_count[severity] %> <%= @validator.severity(severity).text %></h4>
<ul>
<%= for {key, issue} <- issues do %>
<li>
Expand All @@ -12,7 +12,7 @@
"#{issue.title} (#{issue.count})",
to:
"#{current_url(@conn, %{"issue_type" => key, "token" => @token} |> Map.reject(fn {_, v} -> is_nil(v) end))}#issues",
class: if(key == issue_type(@issues.entries), do: "active")
class: if(key == @validator.issue_type(@issues.entries), do: "active")
) %>
<% end %>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ associated_netex = get_associated_netex(@related_files) %>
</nav>
<div class="main-pane">
<%= pagination_links(@conn, @issues, [@resource.id],
issue_type: issue_type(@issues.entries),
issue_type: Transport.Validators.GTFSTransport.issue_type(@issues.entries),
path: &resource_path/4,
action: :details
) %>
Expand Down
Loading