From 8f5141198c0ebc5cec24aa7284e138a3ec8b9a59 Mon Sep 17 00:00:00 2001 From: Jan Sander <63044278+JanhSander@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:59:35 +0200 Subject: [PATCH] Update dataset tab with ssb components (#260) * Fix/get old commit (#232) * new class * Changed properties in display_dataset --------- Co-authored-by: Jan Sander <63044278+JanhSander@users.noreply.github.com> * Feat/dpmeta 128 add unique key (#228) * Added dataset input field that uses ssb input * Updated builder for dataset input filed * Add DatasetDropdownField - change datatype DISPLAY_DATASET - temp commented out code * Change datatypes for Datasetidentifiers - som dropdowns ok - test red * DatasetDisplayTypes list * Dropdown options ok dataset * remove comment * Possible Const id for dataset * Changed tests to be compatible with new display dataset functions * Add unique dict id for Form * Add section_id to make Form ID unique * Ruff temporary commented out code --------- Co-authored-by: rlj * Display and save metadata in dataset tab (#235) * Reading metadata dataset * Change className - update comment - values are displayed too late * add language id * Trigger dataset tab on button click and dynamic id on edit section * Revert changes in callback * Add Input from button click and return no_update when no click. Is working, but is not tested in every aspect * Update names dataset * Update and write docstrings * Remove commented out code * Refactor and comment * Remove comments as notes * Refactor build dataset edit section * fix merge * fix cirkular import * Update and add styling to dataset tab (#241) * workspace title * class names for workspace - update css * refactor variables tab semantics and type content in Tab * Add tests for build dataset edit section (#240) * Test assert return html.Section * Remove unnecessary code * Add tests * details test * Tests * datasetInputField * Update tests minus atypical options-getters * Organize and check tests - all ok * Rename * correct id * Keyword should not be excluded from the tests - not same issue as dropdowns from external sources (#242) * Added validation to the date picker in the dataset tab (#243) * Added validation to the date picker in the dataset tab * Updated dataset date tests * Fixed types for dataset tab dates --------- Co-authored-by: rlj * Add method for get date metadata and stringify (#245) * Refactor Input and Dropdown * min should not be deleted * Changing metadata type * Changing type back to BaseModel * fix datetime format variables for display date (#254) * Refactor period field for dataset and variable (#255) * fix datetime format variables for display date * Add id_type to component * One periodField * update test * Added comments for noqas * Chore/dpmeta 137 remove unused methods and classes (#257) * Remove unused VariablesInputField and VariablesDropdownField * Remove input_kwargs * Move unused classes to bottom of file * Rename checkboxField * Change name from VariablesFieldTypes to MetadataFieldTypes * Correct name (#259) * Remove fontsize header - add margin-top buttons and rename css file --------- Co-authored-by: Jorgen-5 Co-authored-by: Cecilie Seim <68303562+tilen1976@users.noreply.github.com> Co-authored-by: rlj Co-authored-by: rlj --- ...rol_bar_style.css => controlbar_style.css} | 2 +- src/datadoc/assets/header.css | 1 - ...ariables_style.css => workspace_style.css} | 65 ++-- src/datadoc/frontend/callbacks/dataset.py | 79 ++-- .../frontend/callbacks/register_callbacks.py | 146 +++++--- src/datadoc/frontend/components/builders.py | 50 ++- .../frontend/components/dataset_tab.py | 92 ++--- .../frontend/components/variables_tab.py | 19 +- src/datadoc/frontend/fields/display_base.py | 202 ++++++----- .../frontend/fields/display_dataset.py | 110 +++--- .../frontend/fields/display_variables.py | 47 +-- .../callbacks/test_dataset_callbacks.py | 13 +- .../test_build_dataset_edit_section.py | 337 ++++++++++++++++++ tests/frontend/fields/test_display_dataset.py | 28 +- 14 files changed, 808 insertions(+), 383 deletions(-) rename src/datadoc/assets/{control_bar_style.css => controlbar_style.css} (93%) rename src/datadoc/assets/{variables_style.css => workspace_style.css} (69%) create mode 100644 tests/frontend/components/test_build_dataset_edit_section.py diff --git a/src/datadoc/assets/control_bar_style.css b/src/datadoc/assets/controlbar_style.css similarity index 93% rename from src/datadoc/assets/control_bar_style.css rename to src/datadoc/assets/controlbar_style.css index ba9e16e8..223ce659 100644 --- a/src/datadoc/assets/control_bar_style.css +++ b/src/datadoc/assets/controlbar_style.css @@ -12,7 +12,7 @@ } .ssb-btn.secondary-btn.file-open-button,.ssb-btn.secondary-btn.file-save-button{ - margin-top: 0.5rem; + margin-top: 1.5rem; } .progress-bar-wrapper{ diff --git a/src/datadoc/assets/header.css b/src/datadoc/assets/header.css index 0e378f9e..57397342 100644 --- a/src/datadoc/assets/header.css +++ b/src/datadoc/assets/header.css @@ -9,6 +9,5 @@ .ssb-title.main-title { color: #274247; padding: 0; - font-size: 2rem; margin-left: 1rem; } diff --git a/src/datadoc/assets/variables_style.css b/src/datadoc/assets/workspace_style.css similarity index 69% rename from src/datadoc/assets/variables_style.css rename to src/datadoc/assets/workspace_style.css index 31cf4465..d28500d5 100644 --- a/src/datadoc/assets/variables_style.css +++ b/src/datadoc/assets/workspace_style.css @@ -1,68 +1,53 @@ -.page-wrapper{ +.workspace-page-wrapper{ display: flex; flex-wrap: wrap; flex-direction: row; padding: 0; } -.content-wrapper{ - display: flex; - width: 100%; - gap: 1rem; -} - -.main-content{ - width: 100%; - display: flex; - flex-direction: column; - gap: 1rem; -} - - -.variables-header{ +.workspace-header{ width: 100%; display:flex; flex-wrap: wrap; justify-content: space-between; align-items: center; + margin-bottom: 1rem; } -.ssb-title.variables-title{ +.ssb-title.workspace-title{ margin-top: 1rem; width: 100%; } -.ssb-input{ - margin-bottom: 1rem; -} -.accordion-wrapper{ +.workspace-content{ + width: 100%; display: flex; flex-direction: column; gap: 1rem; } -.ssb-accordion.variable-accordion{ - border: 2px solid #62919A; - padding: 0.5rem; +.ssb-accordion.variable-accordion > .accordion-body { + padding: 1rem; } -.ssb-accordion.variable-accordion > .accordion-body { +.edit-section{ + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; +} + +.edit-section.dataset-edit-section{ padding: 1rem; } -.variables-input-group{ +.edit-section-form{ display: flex; flex-wrap: wrap; - justify-content: flex-start; - align-items: flex-start; - gap: 1rem; + gap: 2rem; padding-top: 1rem; } -/*.input-section{ - margin: 1rem; -}*/ - .alert-section{ display: flex; flex-wrap: wrap; @@ -71,13 +56,19 @@ padding-top: 1rem; } -.ssb-title.input-section-title{ +.ssb-title.edit-section-title{ border-bottom: 2px solid #62919A; margin-bottom: 1rem; + padding-bottom: 1rem; + +} +.ssb-input.input-component{ + max-width: 500px; + width: 100%; } -.ssb-input.variable-input{ +.ssb-dropdown.dropdown-component { max-width: 500px; width: 100%; } @@ -86,6 +77,10 @@ align-self: center; } +.form-check{ + padding-left: 0; +} + /*Target child selector (label) of variable-dropdown*/ .ssb-dropdown.variable-dropdown > .dropdown-label{ margin-bottom: 0.6rem; diff --git a/src/datadoc/frontend/callbacks/dataset.py b/src/datadoc/frontend/callbacks/dataset.py index fe6eec3a..2f81183a 100644 --- a/src/datadoc/frontend/callbacks/dataset.py +++ b/src/datadoc/frontend/callbacks/dataset.py @@ -105,26 +105,6 @@ def process_special_cases( str, ): updated_value = process_keyword(value) - elif metadata_identifier == DatasetIdentifiers.CONTAINS_DATA_FROM.value: - updated_value, _ = parse_and_validate_dates( - str(value), - getattr( - state.metadata.dataset, - DatasetIdentifiers.CONTAINS_DATA_UNTIL.value, - ), - ) - if updated_value: - updated_value = updated_value.isoformat() - elif metadata_identifier == DatasetIdentifiers.CONTAINS_DATA_UNTIL.value: - _, updated_value = parse_and_validate_dates( - getattr( - state.metadata.dataset, - DatasetIdentifiers.CONTAINS_DATA_FROM.value, - ), - str(value), - ) - if updated_value: - updated_value = updated_value.isoformat() elif metadata_identifier == DatasetIdentifiers.VERSION.value: updated_value = str(value) elif metadata_identifier in MULTIPLE_LANGUAGE_DATASET_METADATA and isinstance( @@ -203,3 +183,62 @@ def change_language_dataset_metadata( *(e.options_getter(language) for e in DISPLAYED_DROPDOWN_DATASET_METADATA), update_dataset_metadata_language(), ) + + +def accept_dataset_metadata_date_input( + dataset_identifier: DatasetIdentifiers, + contains_data_from: str | None, + contains_data_until: str | None, +) -> tuple[bool, str, bool, str]: + """Validate and save date range inputs.""" + try: + ( + parsed_contains_data_from, + parsed_contains_data_until, + ) = parse_and_validate_dates( + str(contains_data_from), + str(contains_data_until), + ) + + if parsed_contains_data_from: + state.metadata.dataset.contains_data_from = ( + parsed_contains_data_from.isoformat() + ) + + if parsed_contains_data_until: + state.metadata.dataset.contains_data_until = ( + parsed_contains_data_until.isoformat() + ) + + except (ValidationError, ValueError) as e: + logger.exception( + "Validation failed for %s, %s, %s: %s, %s", + dataset_identifier, + "contains_data_from", + contains_data_from, + "contains_data_until", + contains_data_until, + ) + message: str | None = str(e) + else: + logger.debug( + "Successfully updated %s, %s, %s: %s, %s", + dataset_identifier, + "contains_data_from", + contains_data_from, + "contains_data_until", + contains_data_until, + ) + message = None + + no_error = (False, "") + if not message: + # No error to display. + return no_error + no_error + + error = (True, message) + return ( + error + no_error + if dataset_identifier == DatasetIdentifiers.CONTAINS_DATA_FROM + else no_error + error + ) diff --git a/src/datadoc/frontend/callbacks/register_callbacks.py b/src/datadoc/frontend/callbacks/register_callbacks.py index f13aae85..377190c9 100644 --- a/src/datadoc/frontend/callbacks/register_callbacks.py +++ b/src/datadoc/frontend/callbacks/register_callbacks.py @@ -19,21 +19,26 @@ from datadoc import state from datadoc.enums import SupportedLanguages +from datadoc.frontend.callbacks.dataset import accept_dataset_metadata_date_input from datadoc.frontend.callbacks.dataset import accept_dataset_metadata_input -from datadoc.frontend.callbacks.dataset import change_language_dataset_metadata from datadoc.frontend.callbacks.dataset import open_dataset_handling from datadoc.frontend.callbacks.utils import update_global_language_state from datadoc.frontend.callbacks.variables import accept_variable_metadata_date_input from datadoc.frontend.callbacks.variables import accept_variable_metadata_input +from datadoc.frontend.components.builders import build_dataset_edit_section from datadoc.frontend.components.builders import build_edit_section from datadoc.frontend.components.builders import build_ssb_accordion -from datadoc.frontend.components.dataset_tab import DATASET_METADATA_INPUT -from datadoc.frontend.components.dataset_tab import build_dataset_metadata_accordion +from datadoc.frontend.components.dataset_tab import SECTION_WRAPPER_ID from datadoc.frontend.components.variables_tab import ACCORDION_WRAPPER_ID from datadoc.frontend.components.variables_tab import VARIABLES_INFORMATION_ID +from datadoc.frontend.fields.display_base import DATASET_METADATA_DATE_INPUT +from datadoc.frontend.fields.display_base import DATASET_METADATA_INPUT from datadoc.frontend.fields.display_base import VARIABLES_METADATA_DATE_INPUT from datadoc.frontend.fields.display_base import VARIABLES_METADATA_INPUT -from datadoc.frontend.fields.display_dataset import DISPLAYED_DROPDOWN_DATASET_METADATA +from datadoc.frontend.fields.display_dataset import NON_EDITABLE_DATASET_METADATA +from datadoc.frontend.fields.display_dataset import OBLIGATORY_EDITABLE_DATASET_METADATA +from datadoc.frontend.fields.display_dataset import OPTIONAL_DATASET_METADATA +from datadoc.frontend.fields.display_dataset import DatasetIdentifiers from datadoc.frontend.fields.display_variables import OBLIGATORY_VARIABLES_METADATA from datadoc.frontend.fields.display_variables import OPTIONAL_VARIABLES_METADATA from datadoc.frontend.fields.display_variables import VariableIdentifiers @@ -83,29 +88,6 @@ def callback_save_metadata_file(n_clicks: int) -> bool: return False - @app.callback( - *[ - Output( - { - "type": DATASET_METADATA_INPUT, - "id": m.identifier, - }, - "options", - ) - for m in DISPLAYED_DROPDOWN_DATASET_METADATA - ], - Output( - {"type": DATASET_METADATA_INPUT, "id": ALL}, - "value", - ), - Input("language-dropdown", "value"), - ) - def callback_change_language_dataset_metadata( - language: str, - ) -> tuple[object, ...]: - """Update dataset metadata values upon change of language.""" - return change_language_dataset_metadata(SupportedLanguages(language)) - @app.callback( Output("dataset-validation-error", "is_open"), Output("dataset-validation-explanation", "children"), @@ -147,21 +129,6 @@ def callback_open_dataset( """ return open_dataset_handling(n_clicks, dataset_path) - @app.callback( - Output("dataset-accordion", "children"), - Input("open-button", "n_clicks"), - prevent_initial_call=True, - ) - def callback_clear_accordion_values(n_clicks: int) -> list[dbc.AccordionItem]: - """Recreate accordion items with unique IDs. - - The purpose is to avoid browser caching and clear the values of all - components inside the dataset accordion when new file is opened - """ - if n_clicks and n_clicks > 0: - return build_dataset_metadata_accordion(n_clicks) - return no_update - @app.callback( Output(VARIABLES_INFORMATION_ID, "children"), Input("language-dropdown", "value"), @@ -209,6 +176,45 @@ def callback_populate_variables_workspace( for variable in list(state.metadata.variables) ] + @app.callback( + Output(SECTION_WRAPPER_ID, "children"), + Input("language-dropdown", "value"), + Input("open-button", "n_clicks"), + prevent_initial_call=True, + ) + def callback_populate_dataset_workspace( + language: str, + n_clicks: int, + ) -> list: + """Create dataset workspace with sections.""" + update_global_language_state(SupportedLanguages(language)) + logger.info("Populating new dataset workspace") + if n_clicks: + return [ + build_dataset_edit_section( + "Obligatorisk", + OBLIGATORY_EDITABLE_DATASET_METADATA, + state.current_metadata_language, + state.metadata.dataset, + {"type": "dataset-edit-section", "id": f"obligatory-{language}"}, + ), + build_dataset_edit_section( + "Anbefalt", + OPTIONAL_DATASET_METADATA, + state.current_metadata_language, + state.metadata.dataset, + {"type": "dataset-edit-section", "id": f"recommended-{language}"}, + ), + build_dataset_edit_section( + "Maskingenerert", + NON_EDITABLE_DATASET_METADATA, + state.current_metadata_language, + state.metadata.dataset, + {"type": "dataset-edit-section", "id": f"machine-{language}"}, + ), + ] + return no_update + @app.callback( Output( { @@ -313,3 +319,59 @@ def callback_accept_variable_metadata_date_input( contains_data_from, contains_data_until, ) + + @app.callback( + Output( + { + "type": DATASET_METADATA_DATE_INPUT, + "id": DatasetIdentifiers.CONTAINS_DATA_FROM.value, + }, + "error", + ), + Output( + { + "type": DATASET_METADATA_DATE_INPUT, + "id": DatasetIdentifiers.CONTAINS_DATA_FROM.value, + }, + "errorMessage", + ), + Output( + { + "type": DATASET_METADATA_DATE_INPUT, + "id": DatasetIdentifiers.CONTAINS_DATA_UNTIL.value, + }, + "error", + ), + Output( + { + "type": DATASET_METADATA_DATE_INPUT, + "id": DatasetIdentifiers.CONTAINS_DATA_UNTIL.value, + }, + "errorMessage", + ), + Input( + { + "type": DATASET_METADATA_DATE_INPUT, + "id": DatasetIdentifiers.CONTAINS_DATA_FROM.value, + }, + "value", + ), + Input( + { + "type": DATASET_METADATA_DATE_INPUT, + "id": DatasetIdentifiers.CONTAINS_DATA_UNTIL.value, + }, + "value", + ), + prevent_initial_call=True, + ) + def callback_accept_dataset_metadata_date_input( + contains_data_from: str, + contains_data_until: str, + ) -> dbc.Alert: + """Special case handling for date fields which have a relationship to one another.""" + return accept_dataset_metadata_date_input( + DatasetIdentifiers(ctx.triggered_id["id"]), + contains_data_from, + contains_data_until, + ) diff --git a/src/datadoc/frontend/components/builders.py b/src/datadoc/frontend/components/builders.py index 2a5d5d2b..457e48c6 100644 --- a/src/datadoc/frontend/components/builders.py +++ b/src/datadoc/frontend/components/builders.py @@ -12,7 +12,9 @@ import ssb_dash_components as ssb from dash import html +from datadoc.frontend.fields.display_base import DATASET_METADATA_INPUT from datadoc.frontend.fields.display_base import VARIABLES_METADATA_INPUT +from datadoc.frontend.fields.display_base import DatasetFieldTypes from datadoc.frontend.fields.display_base import VariablesFieldTypes if TYPE_CHECKING: @@ -51,7 +53,7 @@ def get_type(alert_type: AlertTypes) -> AlertType: } -def build_ssb_styled_tab(label: str, content: dbc.Container) -> dbc.Tab: +def build_ssb_styled_tab(label: str, content: html.Article) -> dbc.Tab: """Make a Dash Tab according to SSBs Design System.""" return dbc.Tab( label=label, @@ -99,7 +101,7 @@ def build_input_field_section( variable: model.Variable, language: str, ) -> dbc.Form: - """Create input fields.""" + """Create form with input fields for variable workspace.""" return dbc.Form( [ i.render( @@ -114,7 +116,7 @@ def build_input_field_section( for i in metadata_fields ], id=VARIABLES_METADATA_INPUT, - className="variables-input-group", + className="edit-section-form", ) @@ -124,14 +126,14 @@ def build_edit_section( variable: model.Variable, language: str, ) -> html.Section: - """Create input section.""" + """Create input section for variable workspace.""" return html.Section( id={"type": "edit-section", "title": title}, children=[ - ssb.Title(title, size=3, className="input-section-title"), + ssb.Title(title, size=3, className="edit-section-title"), build_input_field_section(metadata_inputs, variable, language), ], - className="input-section", + className="edit-section", ) @@ -141,7 +143,7 @@ def build_ssb_accordion( variable_short_name: str, children: list, ) -> ssb.Accordion: - """Build Accordion for one variable.""" + """Build Accordion for one variable in variable workspace.""" return ssb.Accordion( header=header, id=key, @@ -161,5 +163,37 @@ def build_ssb_accordion( children=children, ), ], - className="variable", + className="variable-accordion", + ) + + +def build_dataset_edit_section( + title: str, + metadata_inputs: list[DatasetFieldTypes], + language: str, + dataset: model.Dataset, + key: dict, +) -> html.Section: + """Create edit section for dataset workspace.""" + return html.Section( + id=key, + children=[ + ssb.Title(title, size=3, className="edit-section-title"), + dbc.Form( + [ + i.render( + { + "type": DATASET_METADATA_INPUT, + "id": i.identifier, + }, + language, + dataset, + ) + for i in metadata_inputs + ], + id=f"{DATASET_METADATA_INPUT}-{title}", + className="edit-section-form", + ), + ], + className="edit-section dataset-edit-section", ) diff --git a/src/datadoc/frontend/components/dataset_tab.py b/src/datadoc/frontend/components/dataset_tab.py index fbbe41b8..393d9910 100644 --- a/src/datadoc/frontend/components/dataset_tab.py +++ b/src/datadoc/frontend/components/dataset_tab.py @@ -2,90 +2,40 @@ from __future__ import annotations -import dash_bootstrap_components as dbc +from typing import TYPE_CHECKING + +import ssb_dash_components as ssb from dash import html from datadoc.frontend.components.builders import build_ssb_styled_tab -from datadoc.frontend.fields.display_dataset import NON_EDITABLE_DATASET_METADATA -from datadoc.frontend.fields.display_dataset import OBLIGATORY_EDITABLE_DATASET_METADATA -from datadoc.frontend.fields.display_dataset import OPTIONAL_DATASET_METADATA -from datadoc.frontend.fields.display_dataset import DisplayDatasetMetadata - -DATASET_METADATA_INPUT = "dataset-metadata-input" +if TYPE_CHECKING: + import dash_bootstrap_components as dbc -def build_dataset_metadata_accordion_item( - title: str, - metadata_inputs: list[DisplayDatasetMetadata], - accordion_item_id: str, -) -> dbc.AccordionItem: - """Build a Dash AccordionItem for the given Metadata inputs with a unique ID. - - Typically used to categorize metadata fields. - """ - return dbc.AccordionItem( - title=title, - id=accordion_item_id, - children=[ - dbc.Row( - [ - dbc.Col(html.Label(i.display_name)), - dbc.Col( - i.component( - placeholder=i.description, - disabled=not i.editable, - id={ - "type": DATASET_METADATA_INPUT, - "id": i.identifier, - }, - **i.extra_kwargs, - ), - width=5, - ), - dbc.Col(width=4), - ], - ) - for i in metadata_inputs - ], - ) - - -def build_dataset_metadata_accordion(n_clicks: int = 0) -> list[dbc.AccordionItem]: - """Build the accordion on Dataset metadata tab. - - n_clicks parameter is appended to the accordions' items id - to avoid browser caching and refresh the values - """ - obligatory = build_dataset_metadata_accordion_item( - "Obligatorisk", - OBLIGATORY_EDITABLE_DATASET_METADATA, - accordion_item_id=f"obligatory-metadata-accordion-item-{n_clicks}", - ) - optional = build_dataset_metadata_accordion_item( - "Anbefalt", - OPTIONAL_DATASET_METADATA, - accordion_item_id=f"optional-metadata-accordion-item-{n_clicks}", - ) - non_editable = build_dataset_metadata_accordion_item( - "Maskingenerert", - NON_EDITABLE_DATASET_METADATA, - accordion_item_id=f"non-editable-metadata-accordion-item-{n_clicks}", - ) - return [obligatory, optional, non_editable] +SECTION_WRAPPER_ID = "section-wrapper-id" def build_dataset_tab() -> dbc.Tab: """Build the Dataset metadata tab.""" return build_ssb_styled_tab( "Datasett", - dbc.Container( + html.Article( [ - dbc.Row(html.H2("Datasett detaljer", className="ssb-title")), - dbc.Accordion( - id="dataset-accordion", - always_open=True, - children=build_dataset_metadata_accordion(), + html.Header( + [ + ssb.Title( + "Datasett detaljer", + size=2, + className="workspace-title", + ), + ], + className="workspace-header", + ), + html.Article( + id=SECTION_WRAPPER_ID, + className="workspace-content", ), ], + className="workspace-page-wrapper", ), ) diff --git a/src/datadoc/frontend/components/variables_tab.py b/src/datadoc/frontend/components/variables_tab.py index 11e738ba..85644d51 100644 --- a/src/datadoc/frontend/components/variables_tab.py +++ b/src/datadoc/frontend/components/variables_tab.py @@ -2,12 +2,16 @@ from __future__ import annotations -import dash_bootstrap_components as dbc +from typing import TYPE_CHECKING + import ssb_dash_components as ssb from dash import html from datadoc.frontend.components.builders import build_ssb_styled_tab +if TYPE_CHECKING: + import dash_bootstrap_components as dbc + VARIABLES_INFORMATION_ID = "variables-information" ACCORDION_WRAPPER_ID = "accordion-wrapper" @@ -16,17 +20,18 @@ def build_variables_tab() -> dbc.Tab: """Build the framework for the variables tab.""" return build_ssb_styled_tab( "Variabler", - dbc.Container( + html.Article( [ html.Header( [ ssb.Title( "Variabel detaljer", size=2, - className="variables-title", + className="workspace-title", ), ssb.Paragraph( id=VARIABLES_INFORMATION_ID, + className="workspace-info-paragraph", ), ssb.Input( label="Søk i variabler", @@ -38,13 +43,13 @@ def build_variables_tab() -> dbc.Tab: value="", ), ], - className="variables-header", + className="workspace-header", ), - html.Main( + html.Article( id=ACCORDION_WRAPPER_ID, - className="main-content", + className="workspace-content", ), ], - class_name="page-wrapper", + className="workspace-page-wrapper", ), ) diff --git a/src/datadoc/frontend/fields/display_base.py b/src/datadoc/frontend/fields/display_base.py index e9844b6b..d545b277 100644 --- a/src/datadoc/frontend/fields/display_base.py +++ b/src/datadoc/frontend/fields/display_base.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import re import typing as t from dataclasses import dataclass from dataclasses import field @@ -30,16 +31,11 @@ logger = logging.getLogger(__name__) +DATASET_METADATA_INPUT = "dataset-metadata-input" VARIABLES_METADATA_INPUT = "variables-metadata-input" VARIABLES_METADATA_DATE_INPUT = "variables-metadata-date-input" - -# Must be changed if new design -INPUT_KWARGS = { - "debounce": True, - "style": {"width": "100%"}, - "className": "ssb-input", -} +DATASET_METADATA_DATE_INPUT = "dataset-metadata-date-input" def get_enum_options_for_language( @@ -58,15 +54,6 @@ def get_enum_options_for_language( return dropdown_options -def input_kwargs_factory() -> dict[str, t.Any]: - """Initialize the field extra_kwargs. - - We aren't allowed to directly assign a mutable type like a dict to - a dataclass field. - """ - return INPUT_KWARGS - - def empty_kwargs_factory() -> dict[str, t.Any]: """Initialize the field extra_kwargs. @@ -89,6 +76,29 @@ def get_metadata_and_stringify(metadata: BaseModel, identifier: str) -> str | No return str(value) +def get_date_metadata_and_stringify(metadata: BaseModel, identifier: str) -> str | None: + """Get a metadata date value from the model. + + Handle converting datetime format to date format string. + """ + value = get_standard_metadata(metadata, identifier) + if value is None: + return "" + logger.info("Date registered: %s", value) + date = str(value) + # Pattern for datetime without T, with space - used for variables + pattern = r"\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\+\d{2}:\d{2}" + if re.match(pattern, date): + convert_date_to_iso = date.replace(" ", "T") + logger.info("Date converted to iso format: %s", convert_date_to_iso) + convert_date_format = convert_date_to_iso[:10] + logger.info("Display date: %s", convert_date_format) + return convert_date_format + convert_value = date[:10] + logger.info("Display date: %s", convert_value) + return convert_value + + def get_multi_language_metadata(metadata: BaseModel, identifier: str) -> str | None: """Get a metadata value supporting multiple languages from the model.""" value: LanguageStringType | None = getattr(metadata, identifier) @@ -121,91 +131,34 @@ class DisplayMetadata: @dataclass -class DisplayDatasetMetadata(DisplayMetadata): - """Controls for how a given metadata field should be displayed. - - Specific to dataset fields. - """ - - extra_kwargs: dict[str, Any] = field(default_factory=input_kwargs_factory) - component: type[Component] = dcc.Input - value_getter: Callable[[BaseModel, str], Any] = get_metadata_and_stringify - +class MetadataInputField(DisplayMetadata): + """Controls how a input field should be displayed.""" -@dataclass -class DisplayDatasetMetadataDropdown(DisplayDatasetMetadata): - """Include the possible options which a user may choose from.""" - - # fmt: off - options_getter: Callable[[SupportedLanguages], list[dict[str, str]]] = lambda _: [] # noqa: E731 - # fmt: on - extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) - component: type[Component] = dcc.Dropdown - - -@dataclass -class VariablesInputField(DisplayMetadata): - """Controls for how a given metadata field should be displayed. - - Specific to variable fields. - """ - - extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) - value_getter: Callable[[BaseModel, str], Any] = get_metadata_and_stringify type: str = "text" - - def render( - self, - variable_id: dict, - language: str, # noqa: ARG002 - variable: model.Variable, - ) -> ssb.Input: - """Build Input component.""" - value = self.value_getter(variable, self.identifier) - return ssb.Input( - label=self.display_name, - id=variable_id, - debounce=True, - type=self.type, - disabled=not self.editable, - value=value, - className="variable-input", - ) - - -@dataclass -class VariablesPeriodField(DisplayMetadata): - """Control how fields which define a time period are displayed. - - These are a special case since two fields have a relationship to one another.> - """ - extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) value_getter: Callable[[BaseModel, str], Any] = get_metadata_and_stringify - type: str = "date" def render( self, - variable_id: dict, - language: str, # noqa: ARG002 - variable: model.Variable, + component_id: dict, + language: str, # noqa: ARG002 Required by Dash + metadata: BaseModel, ) -> ssb.Input: - """Build Input date component.""" - value = self.value_getter(variable, self.identifier) - variable_id["type"] = VARIABLES_METADATA_DATE_INPUT + """Build component.""" + value = self.value_getter(metadata, self.identifier) return ssb.Input( label=self.display_name, - id=variable_id, - debounce=False, + id=component_id, + debounce=True, type=self.type, disabled=not self.editable, value=value, - className="variable-input", + className="input-component", ) @dataclass -class VariablesDropdownField(DisplayMetadata): +class MetadataDropdownField(DisplayMetadata): """Control how a Dropdown should be displayed.""" extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) @@ -216,32 +169,64 @@ class VariablesDropdownField(DisplayMetadata): def render( self, - variable_id: dict, + component_id: dict, language: str, - variable: model.Variable, + dataset: BaseModel, ) -> ssb.Dropdown: """Build Dropdown component.""" - value = self.value_getter(variable, self.identifier) + value = self.value_getter(dataset, self.identifier) return ssb.Dropdown( header=self.display_name, - id=variable_id, + id=component_id, items=self.options_getter(SupportedLanguages(language)), value=value, - className="variable-dropdown", + className="dropdown-component", ) @dataclass -class VariablesCheckboxField(DisplayMetadata): +class MetadataPeriodField(DisplayMetadata): + """Control how fields which define a time period are displayed for Dataset. + + These are a special case since two fields have a relationship to one another.> + """ + + id_type: str = "" + extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) + value_getter: Callable[[BaseModel, str], Any] = get_date_metadata_and_stringify + type: str = "date" + + def render( + self, + component_id: dict, + language: str, # noqa: ARG002 Required by Dash + dataset: BaseModel, + ) -> ssb.Input: + """Build Input date component.""" + value = self.value_getter(dataset, self.identifier) + component_id["type"] = self.id_type + return ssb.Input( + label=self.display_name, + id=component_id, + debounce=False, + type=self.type, + disabled=not self.editable, + value=value, + className="input-component", + ) + + +@dataclass +class MetadataCheckboxField(DisplayMetadata): """Controls for how a checkbox metadata field should be displayed.""" - extra_kwargs: dict[str, Any] = field(default_factory=input_kwargs_factory) + extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) value_getter: Callable[[BaseModel, str], Any] = get_standard_metadata def render( self, variable_id: dict, - language: str, # noqa: ARG002 + language: str, # noqa: ARG002 Required by Dash variable: model.Variable, ) -> dbc.Checkbox: """Build Checkbox component.""" @@ -257,8 +242,33 @@ def render( VariablesFieldTypes = ( - VariablesInputField - | VariablesDropdownField - | VariablesCheckboxField - | VariablesPeriodField + MetadataInputField + | MetadataDropdownField + | MetadataCheckboxField + | MetadataPeriodField ) + +DatasetFieldTypes = MetadataInputField | MetadataDropdownField | MetadataPeriodField + + +@dataclass +class DisplayDatasetMetadata(DisplayMetadata): + """Controls for how a given metadata field should be displayed. + + Specific to dataset fields. + """ + + extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) + component: type[Component] = dcc.Input + value_getter: Callable[[BaseModel, str], Any] = get_metadata_and_stringify + + +@dataclass +class DisplayDatasetMetadataDropdown(DisplayDatasetMetadata): + """Include the possible options which a user may choose from.""" + + # fmt: off + options_getter: Callable[[SupportedLanguages], list[dict[str, str]]] = lambda _: [] # noqa: E731 + # fmt: on + extra_kwargs: dict[str, Any] = field(default_factory=empty_kwargs_factory) + component: type[Component] = dcc.Dropdown diff --git a/src/datadoc/frontend/fields/display_dataset.py b/src/datadoc/frontend/fields/display_dataset.py index c7d26b98..860ecfb0 100644 --- a/src/datadoc/frontend/fields/display_dataset.py +++ b/src/datadoc/frontend/fields/display_dataset.py @@ -7,15 +7,16 @@ from enum import Enum from typing import TYPE_CHECKING -from dash import dcc - from datadoc import enums from datadoc import state -from datadoc.frontend.callbacks.utils import get_language_strings_enum -from datadoc.frontend.fields.display_base import INPUT_KWARGS -from datadoc.frontend.fields.display_base import DisplayDatasetMetadata +from datadoc.frontend.fields.display_base import DATASET_METADATA_DATE_INPUT +from datadoc.frontend.fields.display_base import DatasetFieldTypes from datadoc.frontend.fields.display_base import DisplayDatasetMetadataDropdown +from datadoc.frontend.fields.display_base import MetadataDropdownField +from datadoc.frontend.fields.display_base import MetadataInputField +from datadoc.frontend.fields.display_base import MetadataPeriodField from datadoc.frontend.fields.display_base import get_comma_separated_string +from datadoc.frontend.fields.display_base import get_enum_options_for_language from datadoc.frontend.fields.display_base import get_metadata_and_stringify from datadoc.frontend.fields.display_base import get_multi_language_metadata @@ -25,35 +26,19 @@ logger = logging.getLogger(__name__) -def get_enum_options_for_language( - enum: Enum, - language: SupportedLanguages, -) -> list[dict[str, str]]: - """Generate the list of options based on the currently chosen language.""" - dropdown_options = [ - { - "label": i.get_value_for_language(language), - "value": i.name, - } - for i in get_language_strings_enum(enum) # type: ignore [attr-defined] - ] - dropdown_options.insert(0, {"label": "", "value": ""}) - return dropdown_options - - def get_statistical_subject_options( language: SupportedLanguages, ) -> list[dict[str, str]]: """Generate the list of options for statistical subject.""" dropdown_options = [ { - "label": f"{primary.get_title(language)} - {secondary.get_title(language)}", - "value": secondary.subject_code, + "title": f"{primary.get_title(language)} - {secondary.get_title(language)}", + "id": secondary.subject_code, } for primary in state.statistic_subject_mapping.primary_subjects for secondary in primary.secondary_subjects ] - dropdown_options.insert(0, {"label": "", "value": ""}) + dropdown_options.insert(0, {"title": "", "id": ""}) return dropdown_options @@ -63,12 +48,12 @@ def get_unit_type_options( """Collect the unit type options for the given language.""" dropdown_options = [ { - "label": unit_type.get_title(language), - "value": unit_type.code, + "title": unit_type.get_title(language), + "id": unit_type.code, } for unit_type in state.unit_types.classifications ] - dropdown_options.insert(0, {"label": "", "value": ""}) + dropdown_options.insert(0, {"title": "", "id": ""}) return dropdown_options @@ -78,12 +63,12 @@ def get_owner_options( """Collect the owner options for the given language.""" dropdown_options = [ { - "label": f"{option.code} - {option.get_title(language)}", - "value": option.code, + "title": f"{option.code} - {option.get_title(language)}", + "id": option.code, } for option in state.organisational_units.classifications ] - dropdown_options.insert(0, {"label": "", "value": ""}) + dropdown_options.insert(0, {"title": "", "id": ""}) return dropdown_options @@ -117,16 +102,18 @@ class DatasetIdentifiers(str, Enum): CONTAINS_DATA_UNTIL = "contains_data_until" -DISPLAY_DATASET: dict[DatasetIdentifiers, DisplayDatasetMetadata] = { - DatasetIdentifiers.SHORT_NAME: DisplayDatasetMetadata( +DISPLAY_DATASET: dict[ + DatasetIdentifiers, + DatasetFieldTypes, +] = { + DatasetIdentifiers.SHORT_NAME: MetadataInputField( identifier=DatasetIdentifiers.SHORT_NAME.value, display_name="Kortnavn", description="Navn på (fysisk) datafil, datatabell eller datasett", - component=dcc.Input, obligatory=True, editable=False, ), - DatasetIdentifiers.ASSESSMENT: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.ASSESSMENT: MetadataDropdownField( identifier=DatasetIdentifiers.ASSESSMENT.value, display_name="Verdivurdering", description="Verdivurdering (sensitivitetsklassifisering) for datasettet.", @@ -135,7 +122,7 @@ class DatasetIdentifiers(str, Enum): enums.Assessment, ), ), - DatasetIdentifiers.DATASET_STATUS: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.DATASET_STATUS: MetadataDropdownField( identifier=DatasetIdentifiers.DATASET_STATUS.value, display_name="Status", description="Livssyklus for datasettet", @@ -145,7 +132,7 @@ class DatasetIdentifiers(str, Enum): ), obligatory=True, ), - DatasetIdentifiers.DATASET_STATE: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.DATASET_STATE: MetadataDropdownField( identifier=DatasetIdentifiers.DATASET_STATE.value, display_name="Datatilstand", description="Datatilstand. Se Intern dokument 2021- 17 Datatilstander i SSB", @@ -155,48 +142,51 @@ class DatasetIdentifiers(str, Enum): enums.DataSetState, ), ), - DatasetIdentifiers.NAME: DisplayDatasetMetadata( + DatasetIdentifiers.NAME: MetadataInputField( identifier=DatasetIdentifiers.NAME.value, display_name="Navn", description="Datasettnavn", obligatory=True, multiple_language_support=True, + value_getter=get_metadata_and_stringify, ), - DatasetIdentifiers.DATA_SOURCE: DisplayDatasetMetadata( + DatasetIdentifiers.DATA_SOURCE: MetadataInputField( identifier=DatasetIdentifiers.DATA_SOURCE.value, display_name="Datakilde", description="Datakilde. Settes enten for datasettet eller variabelforekomst.", obligatory=True, multiple_language_support=True, ), - DatasetIdentifiers.REGISTER_URI: DisplayDatasetMetadata( + DatasetIdentifiers.REGISTER_URI: MetadataInputField( identifier=DatasetIdentifiers.REGISTER_URI.value, display_name="Register URI", description="Lenke (URI) til register i registeroversikt (oversikt over alle registre meldt Datatilsynet (oppdatering foretas av sikkerhetsrådgiver))", multiple_language_support=True, + url=True, + type="url", ), - DatasetIdentifiers.POPULATION_DESCRIPTION: DisplayDatasetMetadata( + DatasetIdentifiers.POPULATION_DESCRIPTION: MetadataInputField( identifier=DatasetIdentifiers.POPULATION_DESCRIPTION.value, display_name="Populasjon", description="Populasjonen datasettet dekker. Populasjonsbeskrivelsen inkluderer enhetstype, geografisk dekningsområde og tidsperiode.", obligatory=True, multiple_language_support=True, ), - DatasetIdentifiers.VERSION: DisplayDatasetMetadata( + DatasetIdentifiers.VERSION: MetadataInputField( identifier=DatasetIdentifiers.VERSION.value, display_name="Versjon", description="Versjon", - extra_kwargs=dict(type="number", min=1, **INPUT_KWARGS), + extra_kwargs={"type": "number", "min": 1}, obligatory=True, ), - DatasetIdentifiers.VERSION_DESCRIPTION: DisplayDatasetMetadata( + DatasetIdentifiers.VERSION_DESCRIPTION: MetadataInputField( identifier=DatasetIdentifiers.VERSION_DESCRIPTION.value, display_name="Versjonsbeskrivelse", description="Årsak/grunnlag for denne versjonen av datasettet i form av beskrivende tekst.", multiple_language_support=True, obligatory=True, ), - DatasetIdentifiers.UNIT_TYPE: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.UNIT_TYPE: MetadataDropdownField( identifier=DatasetIdentifiers.UNIT_TYPE.value, display_name="Enhetstype", description="Primær enhetstype for datafil, datatabell eller datasett. Se Vi jobber med en avklaring av behov for flere enhetstyper her.", @@ -204,7 +194,7 @@ class DatasetIdentifiers(str, Enum): options_getter=get_unit_type_options, obligatory=True, ), - DatasetIdentifiers.TEMPORALITY_TYPE: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.TEMPORALITY_TYPE: MetadataDropdownField( identifier=DatasetIdentifiers.TEMPORALITY_TYPE.value, display_name="Temporalitetstype", description="Temporalitetstype. Settes enten for variabelforekomst eller datasett. Se Temporalitet, hendelser og forløp.", @@ -214,14 +204,14 @@ class DatasetIdentifiers(str, Enum): ), obligatory=True, ), - DatasetIdentifiers.DESCRIPTION: DisplayDatasetMetadata( + DatasetIdentifiers.DESCRIPTION: MetadataInputField( identifier=DatasetIdentifiers.DESCRIPTION.value, display_name="Beskrivelse", description="Beskrivelse av datasettet", multiple_language_support=True, obligatory=True, ), - DatasetIdentifiers.SUBJECT_FIELD: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.SUBJECT_FIELD: MetadataDropdownField( identifier=DatasetIdentifiers.SUBJECT_FIELD.value, display_name="Statistikkområde", description="Primær statistikkområdet som datasettet inngår i", @@ -231,19 +221,19 @@ class DatasetIdentifiers(str, Enum): multiple_language_support=True, options_getter=get_statistical_subject_options, ), - DatasetIdentifiers.KEYWORD: DisplayDatasetMetadata( + DatasetIdentifiers.KEYWORD: MetadataInputField( identifier=DatasetIdentifiers.KEYWORD.value, display_name="Nøkkelord", description="En kommaseparert liste med søkbare nøkkelord som kan bidra til utvikling av effektive filtrerings- og søketjeneste.", value_getter=get_comma_separated_string, ), - DatasetIdentifiers.SPATIAL_COVERAGE_DESCRIPTION: DisplayDatasetMetadata( + DatasetIdentifiers.SPATIAL_COVERAGE_DESCRIPTION: MetadataInputField( identifier=DatasetIdentifiers.SPATIAL_COVERAGE_DESCRIPTION.value, display_name="Geografisk dekningsområde", description="Beskrivelse av datasettets geografiske dekningsområde. Målet er på sikt at dette skal hentes fra Klass, men fritekst vil også kunne brukes.", multiple_language_support=True, ), - DatasetIdentifiers.ID: DisplayDatasetMetadata( + DatasetIdentifiers.ID: MetadataInputField( identifier=DatasetIdentifiers.ID.value, display_name="ID", description="Unik SSB-identifikator for datasettet (løpenummer)", @@ -251,7 +241,7 @@ class DatasetIdentifiers(str, Enum): editable=False, value_getter=get_metadata_and_stringify, ), - DatasetIdentifiers.OWNER: DisplayDatasetMetadataDropdown( + DatasetIdentifiers.OWNER: MetadataDropdownField( identifier=DatasetIdentifiers.OWNER.value, display_name="Eier", description="Maskingenerert seksjonstilhørighet til den som oppretter metadata om datasettet, men kan korrigeres manuelt", @@ -260,54 +250,56 @@ class DatasetIdentifiers(str, Enum): multiple_language_support=False, options_getter=get_owner_options, ), - DatasetIdentifiers.FILE_PATH: DisplayDatasetMetadata( + DatasetIdentifiers.FILE_PATH: MetadataInputField( identifier=DatasetIdentifiers.FILE_PATH.value, display_name="Filsti", description="Filstien inneholder datasettets navn og stien til hvor det er lagret.", obligatory=True, editable=False, ), - DatasetIdentifiers.METADATA_CREATED_DATE: DisplayDatasetMetadata( + DatasetIdentifiers.METADATA_CREATED_DATE: MetadataInputField( identifier=DatasetIdentifiers.METADATA_CREATED_DATE.value, display_name="Dato opprettet", description="Opprettet dato for metadata om datasettet", obligatory=True, editable=False, ), - DatasetIdentifiers.METADATA_CREATED_BY: DisplayDatasetMetadata( + DatasetIdentifiers.METADATA_CREATED_BY: MetadataInputField( identifier=DatasetIdentifiers.METADATA_CREATED_BY.value, display_name="Opprettet av", description="Opprettet av person. Kun til bruk i SSB.", obligatory=True, editable=False, ), - DatasetIdentifiers.METADATA_LAST_UPDATED_DATE: DisplayDatasetMetadata( + DatasetIdentifiers.METADATA_LAST_UPDATED_DATE: MetadataInputField( identifier=DatasetIdentifiers.METADATA_LAST_UPDATED_DATE.value, display_name="Dato oppdatert", description="Sist oppdatert dato for metadata om datasettet", obligatory=True, editable=False, ), - DatasetIdentifiers.METADATA_LAST_UPDATED_BY: DisplayDatasetMetadata( + DatasetIdentifiers.METADATA_LAST_UPDATED_BY: MetadataInputField( identifier=DatasetIdentifiers.METADATA_LAST_UPDATED_BY.value, display_name="Oppdatert av", description="Siste endring utført av person. Kun til bruk i SSB.", obligatory=True, editable=False, ), - DatasetIdentifiers.CONTAINS_DATA_FROM: DisplayDatasetMetadata( + DatasetIdentifiers.CONTAINS_DATA_FROM: MetadataPeriodField( identifier=DatasetIdentifiers.CONTAINS_DATA_FROM.value, display_name="Inneholder data f.o.m.", description="ÅÅÅÅ-MM-DD", obligatory=True, editable=True, + id_type=DATASET_METADATA_DATE_INPUT, ), - DatasetIdentifiers.CONTAINS_DATA_UNTIL: DisplayDatasetMetadata( + DatasetIdentifiers.CONTAINS_DATA_UNTIL: MetadataPeriodField( identifier=DatasetIdentifiers.CONTAINS_DATA_UNTIL.value, display_name="Inneholder data t.o.m.", description="ÅÅÅÅ-MM-DD", obligatory=True, editable=True, + id_type=DATASET_METADATA_DATE_INPUT, ), } @@ -331,7 +323,7 @@ class DatasetIdentifiers(str, Enum): # The order of this list MUST match the order of display components, as defined in DatasetTab.py -DISPLAYED_DATASET_METADATA: list[DisplayDatasetMetadata] = ( +DISPLAYED_DATASET_METADATA: list[DatasetFieldTypes] = ( OBLIGATORY_EDITABLE_DATASET_METADATA + OPTIONAL_DATASET_METADATA + NON_EDITABLE_DATASET_METADATA diff --git a/src/datadoc/frontend/fields/display_variables.py b/src/datadoc/frontend/fields/display_variables.py index 60ace25d..16470218 100644 --- a/src/datadoc/frontend/fields/display_variables.py +++ b/src/datadoc/frontend/fields/display_variables.py @@ -6,11 +6,12 @@ from enum import Enum from datadoc import enums -from datadoc.frontend.fields.display_base import VariablesCheckboxField -from datadoc.frontend.fields.display_base import VariablesDropdownField +from datadoc.frontend.fields.display_base import VARIABLES_METADATA_DATE_INPUT +from datadoc.frontend.fields.display_base import MetadataCheckboxField +from datadoc.frontend.fields.display_base import MetadataDropdownField +from datadoc.frontend.fields.display_base import MetadataInputField +from datadoc.frontend.fields.display_base import MetadataPeriodField from datadoc.frontend.fields.display_base import VariablesFieldTypes -from datadoc.frontend.fields.display_base import VariablesInputField -from datadoc.frontend.fields.display_base import VariablesPeriodField from datadoc.frontend.fields.display_base import get_enum_options_for_language from datadoc.frontend.fields.display_base import get_multi_language_metadata @@ -42,14 +43,14 @@ class VariableIdentifiers(str, Enum): VariableIdentifiers, VariablesFieldTypes, ] = { - VariableIdentifiers.SHORT_NAME: VariablesInputField( + VariableIdentifiers.SHORT_NAME: MetadataInputField( identifier=VariableIdentifiers.SHORT_NAME.value, display_name="Kortnavn", description="Fysisk navn på variabelen i datasettet. Bør tilsvare anbefalt kortnavn.", obligatory=True, editable=False, ), - VariableIdentifiers.NAME: VariablesInputField( + VariableIdentifiers.NAME: MetadataInputField( identifier=VariableIdentifiers.NAME.value, display_name="Navn", description="Variabelnavn kan arves fra VarDef, men kan også dokumenteres/endres her.", @@ -57,7 +58,7 @@ class VariableIdentifiers(str, Enum): multiple_language_support=True, type="text", ), - VariableIdentifiers.DATA_TYPE: VariablesDropdownField( + VariableIdentifiers.DATA_TYPE: MetadataDropdownField( identifier=VariableIdentifiers.DATA_TYPE.value, display_name="Datatype", description="Datatype", @@ -67,7 +68,7 @@ class VariableIdentifiers(str, Enum): enums.DataType, ), ), - VariableIdentifiers.VARIABLE_ROLE: VariablesDropdownField( + VariableIdentifiers.VARIABLE_ROLE: MetadataDropdownField( identifier=VariableIdentifiers.VARIABLE_ROLE.value, display_name="Variabelens rolle", description="Variabelens rolle i datasett", @@ -77,7 +78,7 @@ class VariableIdentifiers(str, Enum): enums.VariableRole, ), ), - VariableIdentifiers.DEFINITION_URI: VariablesInputField( + VariableIdentifiers.DEFINITION_URI: MetadataInputField( identifier=VariableIdentifiers.DEFINITION_URI.value, display_name="Definition URI", description="En lenke (URI) til variabelens definisjon i SSB (Vardok/VarDef)", @@ -85,34 +86,34 @@ class VariableIdentifiers(str, Enum): obligatory=True, type="url", ), - VariableIdentifiers.DIRECT_PERSON_IDENTIFYING: VariablesCheckboxField( + VariableIdentifiers.DIRECT_PERSON_IDENTIFYING: MetadataCheckboxField( identifier=VariableIdentifiers.DIRECT_PERSON_IDENTIFYING.value, display_name="Direkte personidentifiserende informasjon", description="Direkte personidentifiserende informasjon (DPI)", obligatory=True, ), - VariableIdentifiers.DATA_SOURCE: VariablesInputField( + VariableIdentifiers.DATA_SOURCE: MetadataInputField( identifier=VariableIdentifiers.DATA_SOURCE.value, display_name="Datakilde", description="Datakilde. Settes på datasettnivå, men kan overstyres på variabelforekomstnivå.", multiple_language_support=True, type="text", ), - VariableIdentifiers.POPULATION_DESCRIPTION: VariablesInputField( + VariableIdentifiers.POPULATION_DESCRIPTION: MetadataInputField( identifier=VariableIdentifiers.POPULATION_DESCRIPTION.value, display_name="Populasjonen", description="Populasjonen variabelen beskriver kan spesifiseres nærmere her. Settes på datasettnivå, men kan overstyres på variabelforekomstnivå.", multiple_language_support=True, type="text", ), - VariableIdentifiers.COMMENT: VariablesInputField( + VariableIdentifiers.COMMENT: MetadataInputField( identifier=VariableIdentifiers.COMMENT.value, display_name="Kommentar", description="Ytterligere presiseringer av variabeldefinisjon", multiple_language_support=True, type="text", ), - VariableIdentifiers.TEMPORALITY_TYPE: VariablesDropdownField( + VariableIdentifiers.TEMPORALITY_TYPE: MetadataDropdownField( identifier=VariableIdentifiers.TEMPORALITY_TYPE.value, display_name="Temporalitetstype", description="Temporalitetstype. Settes enten for variabelforekomst eller datasett. Se Temporalitet, hendelser og forløp.", @@ -121,52 +122,54 @@ class VariableIdentifiers(str, Enum): enums.TemporalityTypeType, ), ), - VariableIdentifiers.MEASUREMENT_UNIT: VariablesInputField( + VariableIdentifiers.MEASUREMENT_UNIT: MetadataInputField( identifier=VariableIdentifiers.MEASUREMENT_UNIT.value, display_name="Måleenhet", description="Måleenhet. Eksempel: NOK eller USD for valuta, KG eller TONN for vekt. Se også forslag til SSBs måletyper/måleenheter.", type="text", ), - VariableIdentifiers.FORMAT: VariablesInputField( + VariableIdentifiers.FORMAT: MetadataInputField( identifier=VariableIdentifiers.FORMAT.value, display_name="Format", description="Verdienes format (fysisk format eller regulært uttrykk) i maskinlesbar form ifm validering. Dette kan benyttes som en ytterligere presisering av datatypen (dataType) i de tilfellene hvor dette er relevant. ", ), - VariableIdentifiers.CLASSIFICATION_URI: VariablesInputField( + VariableIdentifiers.CLASSIFICATION_URI: MetadataInputField( identifier=VariableIdentifiers.CLASSIFICATION_URI.value, display_name="Kodeverkets URI", description="Lenke (URI) til gyldige kodeverk (klassifikasjon eller kodeliste) i KLASS", url=True, type="url", ), - VariableIdentifiers.SENTINEL_VALUE_URI: VariablesInputField( + VariableIdentifiers.SENTINEL_VALUE_URI: MetadataInputField( identifier=VariableIdentifiers.SENTINEL_VALUE_URI.value, display_name="Spesialverdienes URI", description="En lenke (URI) til en oversikt over 'spesialverdier' som inngår i variabelen.", url=True, type="url", ), - VariableIdentifiers.INVALID_VALUE_DESCRIPTION: VariablesInputField( + VariableIdentifiers.INVALID_VALUE_DESCRIPTION: MetadataInputField( identifier=VariableIdentifiers.INVALID_VALUE_DESCRIPTION.value, display_name="Ugyldige verdier", description="En beskrivelse av ugyldige verdier som inngår i variabelen dersom spesialverdiene ikke er tilstrekkelige eller ikke kan benyttes.", multiple_language_support=True, ), - VariableIdentifiers.IDENTIFIER: VariablesInputField( + VariableIdentifiers.IDENTIFIER: MetadataInputField( identifier=VariableIdentifiers.IDENTIFIER.value, display_name="ID", description="Unik SSB identifikator for variabelforekomsten i datasettet", editable=False, ), - VariableIdentifiers.CONTAINS_DATA_FROM: VariablesPeriodField( + VariableIdentifiers.CONTAINS_DATA_FROM: MetadataPeriodField( identifier=VariableIdentifiers.CONTAINS_DATA_FROM.value, display_name="Inneholder data f.o.m.", description="Variabelforekomsten i datasettet inneholder data fra og med denne dato.", + id_type=VARIABLES_METADATA_DATE_INPUT, ), - VariableIdentifiers.CONTAINS_DATA_UNTIL: VariablesPeriodField( + VariableIdentifiers.CONTAINS_DATA_UNTIL: MetadataPeriodField( identifier=VariableIdentifiers.CONTAINS_DATA_UNTIL.value, display_name="Inneholder data t.o.m.", description="Variabelforekomsten i datasettet inneholder data til og med denne dato.", + id_type=VARIABLES_METADATA_DATE_INPUT, ), } diff --git a/tests/frontend/callbacks/test_dataset_callbacks.py b/tests/frontend/callbacks/test_dataset_callbacks.py index 88387d2e..5fa81274 100644 --- a/tests/frontend/callbacks/test_dataset_callbacks.py +++ b/tests/frontend/callbacks/test_dataset_callbacks.py @@ -17,6 +17,7 @@ from datadoc.enums import DataSetState from datadoc.enums import LanguageStringsEnum from datadoc.enums import SupportedLanguages +from datadoc.frontend.callbacks.dataset import accept_dataset_metadata_date_input from datadoc.frontend.callbacks.dataset import accept_dataset_metadata_input from datadoc.frontend.callbacks.dataset import change_language_dataset_metadata from datadoc.frontend.callbacks.dataset import open_dataset_handling @@ -185,19 +186,17 @@ def test_accept_dataset_metadata_input_date_validation( expect_error: bool, # noqa: FBT001 ): state.metadata = metadata - accept_dataset_metadata_input( + output = accept_dataset_metadata_date_input( + DatasetIdentifiers.CONTAINS_DATA_UNTIL, start_date, - DatasetIdentifiers.CONTAINS_DATA_FROM.value, - ) - output = accept_dataset_metadata_input( end_date, - DatasetIdentifiers.CONTAINS_DATA_UNTIL.value, ) - assert output[0] is expect_error + assert output[2] is expect_error if expect_error: - assert "Validation error" in output[1] + assert "Validation error" in output[3] else: assert output[1] == "" + assert output[3] == "" def test_update_dataset_metadata_language_strings( diff --git a/tests/frontend/components/test_build_dataset_edit_section.py b/tests/frontend/components/test_build_dataset_edit_section.py new file mode 100644 index 00000000..7f2d3cbb --- /dev/null +++ b/tests/frontend/components/test_build_dataset_edit_section.py @@ -0,0 +1,337 @@ +"""Test function build_dataset_edit_section.""" + +import dash_bootstrap_components as dbc +import pytest +import ssb_dash_components as ssb # type: ignore[import-untyped] +from dash import html +from datadoc_model import model + +from datadoc.enums import SupportedLanguages +from datadoc.frontend.components.builders import build_dataset_edit_section +from datadoc.frontend.fields.display_base import DatasetFieldTypes +from datadoc.frontend.fields.display_base import MetadataDropdownField +from datadoc.frontend.fields.display_base import MetadataInputField +from datadoc.frontend.fields.display_base import MetadataPeriodField +from datadoc.frontend.fields.display_dataset import DISPLAY_DATASET +from datadoc.frontend.fields.display_dataset import NON_EDITABLE_DATASET_METADATA +from datadoc.frontend.fields.display_dataset import OPTIONAL_DATASET_METADATA +from datadoc.frontend.fields.display_dataset import DatasetIdentifiers + +# List of obligatory and editable fields minus dropdowns with atypical options-getters +OBLIGATORY_MINUS_ATYPICAL_DROPDOWNS = [ + m + for m in DISPLAY_DATASET.values() + if m.obligatory + and m.editable + and m.identifier != DatasetIdentifiers.UNIT_TYPE.value + and m.identifier != DatasetIdentifiers.SUBJECT_FIELD.value + and m.identifier != DatasetIdentifiers.OWNER.value +] + +INPUT_DATA_BUILD_DATASET_SECTION = [ + ( + "Obligatorisk", + NON_EDITABLE_DATASET_METADATA, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="fantastic_dataset"), + {"type": "dataset-edit-section", "id": "obligatory-nb"}, + ), + ( + "", + [], + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="fantastic_dataset"), + {"type": "dataset-edit-section", "id": "obligatory-nb"}, + ), + ( + "", + [], + "", + None, + {}, + ), + ( + "Amazing title", + [], + SupportedLanguages.ENGLISH, + None, + {"type": "dataset-edit-section", "id": "obligatory-en"}, + ), + ( + "New title", + OPTIONAL_DATASET_METADATA, + SupportedLanguages.NORSK_NYNORSK, + model.Dataset(), + {}, + ), + ( + "New title", + {}, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="last_dataset"), + {}, + ), + ( + "Lazy title", + OBLIGATORY_MINUS_ATYPICAL_DROPDOWNS, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="nosy_dataset"), + {"type": "dataset-edit-section", "id": "obligatory-nb"}, + ), +] + + +@pytest.mark.parametrize( + ("title", "field_types", "language", "dataset", "key"), + INPUT_DATA_BUILD_DATASET_SECTION, +) +def test_build_dataset_section_is_html_section( + title, + field_types, + language, + dataset, + key, +): + edit_section = build_dataset_edit_section( + title, + field_types, + language, + dataset, + key, + ) + assert isinstance(edit_section, html.Section) + assert edit_section._namespace == "dash_html_components" # noqa: SLF001 + assert edit_section.id == key + + +@pytest.mark.parametrize( + ("title", "field_types", "language", "dataset", "key"), + INPUT_DATA_BUILD_DATASET_SECTION, +) +def test_build_dataset_has_title_component(title, field_types, language, dataset, key): + edit_section = build_dataset_edit_section( + title, + field_types, + language, + dataset, + key, + ) + section_title = edit_section.children[0] + assert isinstance(section_title, ssb.Title) + section_title_content = section_title.children + assert section_title_content == title + + +@pytest.mark.parametrize( + ("title", "field_types", "language", "dataset", "key"), + INPUT_DATA_BUILD_DATASET_SECTION, +) +def test_build_dataset_is_form_component(title, field_types, language, dataset, key): + edit_section = build_dataset_edit_section( + title, + field_types, + language, + dataset, + key, + ) + dataset_form = edit_section.children[1] + assert dataset_form.id == f"dataset-metadata-input-{title}" + assert isinstance(dataset_form, dbc.Form) + + +@pytest.mark.parametrize( + ("edit_section", "expected_inputs", "expected_dropdowns"), + [ + ( + build_dataset_edit_section( + "", + NON_EDITABLE_DATASET_METADATA, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="fantastic_dataset"), + {"type": "dataset-edit-section", "id": "obligatory-nb"}, + ), + 7, + 0, + ), + ( + build_dataset_edit_section( + "New title", + OPTIONAL_DATASET_METADATA, + SupportedLanguages.NORSK_NYNORSK, + model.Dataset(), + {}, + ), + 3, + 1, + ), + ( + build_dataset_edit_section( + "", + OBLIGATORY_MINUS_ATYPICAL_DROPDOWNS, + SupportedLanguages.ENGLISH, + model.Dataset(short_name="super_dataset"), + {"type": "dataset-edit-section", "id": "obligatory-en"}, + ), + 8, + 3, + ), + ], +) +def test_build_dataset_edit_section_renders_ssb_components( + edit_section, + expected_inputs, + expected_dropdowns, +): + fields = edit_section.children[1].children + + input_components = [element for element in fields if isinstance(element, ssb.Input)] + dropdown_components = [ + element for element in fields if isinstance(element, ssb.Dropdown) + ] + assert len(input_components) == expected_inputs + assert len(dropdown_components) == expected_dropdowns + + +@pytest.mark.parametrize( + ("title", "field_types", "language", "dataset", "key"), + INPUT_DATA_BUILD_DATASET_SECTION, +) +def test_build_dataset_edit_section_input_component_props( + title, + field_types, + language, + dataset, + key, +): + edit_section = build_dataset_edit_section( + title, + field_types, + language, + dataset, + key, + ) + fields = edit_section.children[1].children + input_components = [element for element in fields if isinstance(element, ssb.Input)] + for item in input_components: + assert item.type in ("text", "url", "date") + for item in input_components: + if item.type in ("text", "url"): + assert item.debounce is True + for item in input_components: + if item.type == "date": + assert item.debounce is False + for item in input_components: + assert item.label != "" + + +@pytest.mark.parametrize( + ("title", "field_types", "language", "dataset", "key"), + INPUT_DATA_BUILD_DATASET_SECTION, +) +def test_build_dataset_edit_section_dropdown_component_props( + title, + field_types, + language, + dataset, + key, +): + edit_section = build_dataset_edit_section( + title, + field_types, + language, + dataset, + key, + ) + fields = edit_section.children[1].children + dropdown_components = [ + element for element in fields if isinstance(element, ssb.Dropdown) + ] + for item in dropdown_components: + assert item.items != [] + for item in dropdown_components: + assert item.header != "" + + +# Test data sorted by DatasetFieldTypes +DATASET_INPUT_FIELD_LIST: list[DatasetFieldTypes] = [ + m for m in DISPLAY_DATASET.values() if isinstance(m, MetadataInputField) +] + +DATASET_INPUT_URL_FIELD_LIST: list[DatasetFieldTypes] = [ + m + for m in DISPLAY_DATASET.values() + if isinstance(m, MetadataInputField) and m.type == "url" +] + +DATASET_DATE_FIELD_LIST: list[DatasetFieldTypes] = [ + m for m in DISPLAY_DATASET.values() if isinstance(m, MetadataPeriodField) +] + +DATASET_DROPDOWN_FIELD_LIST_MINUS_ATYPICAL: list[DatasetFieldTypes] = [ + m + for m in DISPLAY_DATASET.values() + if isinstance(m, MetadataDropdownField) + and m.identifier != DatasetIdentifiers.UNIT_TYPE.value + and m.identifier != DatasetIdentifiers.SUBJECT_FIELD.value + and m.identifier != DatasetIdentifiers.OWNER.value +] + + +@pytest.mark.parametrize( + ("edit_section", "expected_num", "expected_component"), + [ + ( + build_dataset_edit_section( + "", + DATASET_INPUT_FIELD_LIST, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="input_dataset"), + {"type": "dataset-edit-section", "id": "recommended-nb"}, + ), + 16, + ssb.Input, + ), + ( + build_dataset_edit_section( + "", + DATASET_INPUT_URL_FIELD_LIST, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="url_dataset"), + {"type": "dataset-edit-section", "id": "obligatory-nb"}, + ), + 1, + ssb.Input, + ), + ( + build_dataset_edit_section( + "title", + DATASET_DATE_FIELD_LIST, + SupportedLanguages.NORSK_BOKMÅL, + model.Dataset(short_name="date_dataset"), + {"type": "dataset-edit-section", "id": "title-nb"}, + ), + 2, + ssb.Input, + ), + ( + build_dataset_edit_section( + "dropdown", + DATASET_DROPDOWN_FIELD_LIST_MINUS_ATYPICAL, + SupportedLanguages.ENGLISH, + model.Dataset(short_name="dropdown_dataset"), + {"type": "dataset-edit-section", "id": "dropdown-en"}, + ), + 4, + ssb.Dropdown, + ), + ], +) +def test_build_dataset_input_fields_from_datasetidentifiers( + edit_section, + expected_num, + expected_component, +): + fields = edit_section.children[1].children + assert len(fields) == expected_num + for item in fields: + assert isinstance(item, expected_component) diff --git a/tests/frontend/fields/test_display_dataset.py b/tests/frontend/fields/test_display_dataset.py index 01817e9d..0f18ce15 100644 --- a/tests/frontend/fields/test_display_dataset.py +++ b/tests/frontend/fields/test_display_dataset.py @@ -19,11 +19,11 @@ / STATISTICAL_SUBJECT_STRUCTURE_DIR / "extract_secondary_subject.xml", [ - {"label": "", "value": ""}, - {"label": "aa norwegian - aa00 norwegian", "value": "aa00"}, - {"label": "aa norwegian - aa01 norwegian", "value": "aa01"}, - {"label": "ab norwegian - ab00 norwegian", "value": "ab00"}, - {"label": "ab norwegian - ab01 norwegian", "value": "ab01"}, + {"title": "", "id": ""}, + {"title": "aa norwegian - aa00 norwegian", "id": "aa00"}, + {"title": "aa norwegian - aa01 norwegian", "id": "aa01"}, + {"title": "ab norwegian - ab00 norwegian", "id": "ab00"}, + {"title": "ab norwegian - ab01 norwegian", "id": "ab01"}, ], ), ( @@ -31,11 +31,11 @@ / STATISTICAL_SUBJECT_STRUCTURE_DIR / "missing_language.xml", [ - {"label": "", "value": ""}, - {"label": " - aa00 norwegian", "value": "aa00"}, - {"label": " - aa01 norwegian", "value": "aa01"}, - {"label": " - ab00 norwegian", "value": "ab00"}, - {"label": " - ", "value": "ab01"}, + {"title": "", "id": ""}, + {"title": " - aa00 norwegian", "id": "aa00"}, + {"title": " - aa01 norwegian", "id": "aa01"}, + {"title": " - ab00 norwegian", "id": "ab00"}, + {"title": " - ", "id": "ab01"}, ], ), ], @@ -55,10 +55,10 @@ def test_get_statistical_subject_options( ( TEST_RESOURCES_DIRECTORY / CODE_LIST_DIR / "code_list_nb.csv", [ - {"label": "", "value": ""}, - {"label": "Adresse", "value": "01"}, - {"label": "Arbeidsulykke", "value": "02"}, - {"label": "Bolig", "value": "03"}, + {"title": "", "id": ""}, + {"title": "Adresse", "id": "01"}, + {"title": "Arbeidsulykke", "id": "02"}, + {"title": "Bolig", "id": "03"}, ], ), ],