From 58d7c9d331950c4a619cfedd7411da74730f8daf Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Mon, 23 Dec 2024 12:15:18 +0100 Subject: [PATCH] Add link to docs for missing metrics metric. When measuring missing metrics, make the subject type and the metric type of the missing metrics link to the reference documentation. Closes #10528. --- components/api_server/src/routes/server.py | 3 +-- .../quality_time/missing_metrics.py | 5 +++- .../quality_time/test_missing_metrics.py | 4 ++++ .../frontend/src/dashboard/ExportCard.js | 5 ++-- .../frontend/src/header_footer/Footer.js | 24 +++++++------------ .../frontend/src/metric/MetricTypeHeader.js | 5 ++-- components/frontend/src/source/Source.js | 7 +++--- .../frontend/src/source/SourceTypeHeader.js | 5 ++-- .../frontend/src/subject/SubjectTitle.js | 5 ++-- components/frontend/src/utils.js | 9 ++++--- .../shared_code/.vulture_ignore_list.py | 21 +++++++++------- .../shared_code/src/shared/utils/functions.py | 8 +++++++ .../shared_code/src/shared/utils/version.py | 4 ++++ .../src/shared_data_model/meta/base.py | 15 ++++++++++++ .../src/shared_data_model/meta/metric.py | 4 ++-- .../src/shared_data_model/meta/subject.py | 4 ++-- .../shared_data_model/sources/quality_time.py | 4 ++-- .../tests/shared/utils/test_functions.py | 18 +++++++++++++- docs/src/changelog.md | 6 +++++ docs/src/create_reference_md.py | 9 +------ release/pyproject.toml | 2 +- 21 files changed, 104 insertions(+), 63 deletions(-) create mode 100644 components/shared_code/src/shared/utils/version.py diff --git a/components/api_server/src/routes/server.py b/components/api_server/src/routes/server.py index ce2967dcbd..7bfdff97b7 100644 --- a/components/api_server/src/routes/server.py +++ b/components/api_server/src/routes/server.py @@ -2,8 +2,7 @@ import bottle - -QUALITY_TIME_VERSION = "5.21.0" +from shared.utils.version import QUALITY_TIME_VERSION @bottle.get("/api/v3/server", authentication_required=False) diff --git a/components/collector/src/source_collectors/quality_time/missing_metrics.py b/components/collector/src/source_collectors/quality_time/missing_metrics.py index 4e25fb15c3..b26f46a796 100644 --- a/components/collector/src/source_collectors/quality_time/missing_metrics.py +++ b/components/collector/src/source_collectors/quality_time/missing_metrics.py @@ -77,7 +77,8 @@ def __subject_missing_metric_type_entities( report_uuid = report["report_uuid"] report_url = f"{landing_url}/{report_uuid}" subject = report["subjects"][subject_uuid] - subject_type_name = self.subject_type(data_model["subjects"], subject["type"])["name"] + subject_type = self.subject_type(data_model["subjects"], subject["type"]) + subject_type_name = subject_type["name"] return Entities( Entity( key=f"{report_uuid}:{subject_uuid}:{metric_type}", @@ -87,7 +88,9 @@ def __subject_missing_metric_type_entities( subject_url=f"{report_url}#{subject_uuid}", subject_uuid=f"{subject_uuid}", subject_type=subject_type_name, + subject_type_url=subject_type["reference_documentation_url"], metric_type=data_model["metrics"][metric_type]["name"], + metric_type_url=data_model["metrics"][metric_type]["reference_documentation_url"], ) for metric_type in self.__missing_metric_types(data_model, subject) ) diff --git a/components/collector/tests/source_collectors/quality_time/test_missing_metrics.py b/components/collector/tests/source_collectors/quality_time/test_missing_metrics.py index a1a1711201..9fb356ccc0 100644 --- a/components/collector/tests/source_collectors/quality_time/test_missing_metrics.py +++ b/components/collector/tests/source_collectors/quality_time/test_missing_metrics.py @@ -90,6 +90,8 @@ def create_entity( self, report, subject_uuid: str, expected_subject_type: str, expected_subject_name: str, metric_type: str ) -> dict[str, str]: """Create an expected missing metric entity.""" + subject_type_name = report["subjects"][subject_uuid]["type"] + subject_type = QualityTimeMissingMetrics.subject_type(self.data_model["subjects"], subject_type_name) return { "key": f"{report['report_uuid']}:{subject_uuid}:{metric_type}", "report": report["title"], @@ -98,7 +100,9 @@ def create_entity( "subject_url": f"https://quality_time/{report['report_uuid']}#{subject_uuid}", "subject_uuid": f"{subject_uuid}", "subject_type": expected_subject_type, + "subject_type_url": subject_type["reference_documentation_url"], "metric_type": self.data_model["metrics"][metric_type]["name"], + "metric_type_url": self.data_model["metrics"][metric_type]["reference_documentation_url"], } async def test_nr_of_metrics(self): diff --git a/components/frontend/src/dashboard/ExportCard.js b/components/frontend/src/dashboard/ExportCard.js index 3ff8f30416..b6eec93f08 100644 --- a/components/frontend/src/dashboard/ExportCard.js +++ b/components/frontend/src/dashboard/ExportCard.js @@ -4,6 +4,7 @@ import { bool, string } from "prop-types" import { Card, List } from "semantic-ui-react" import { childrenPropType, datePropType, reportPropType } from "../sharedPropTypes" +import { DOCUMENTATION_URL } from "../utils" function ExportCardItem({ children, url }) { const item = children @@ -43,9 +44,7 @@ export function ExportCard({ lastUpdate, report, reportDate, isOverview = false , - + Quality-time v{process.env.REACT_APP_VERSION} diff --git a/components/frontend/src/header_footer/Footer.js b/components/frontend/src/header_footer/Footer.js index a4a97eec6f..1cce21d25c 100644 --- a/components/frontend/src/header_footer/Footer.js +++ b/components/frontend/src/header_footer/Footer.js @@ -22,6 +22,7 @@ import { import { element, object, oneOfType, string } from "prop-types" import { alignmentPropType, childrenPropType, datePropType, reportPropType } from "../sharedPropTypes" +import { DOCUMENTATION_URL, REPOSITORY_URL } from "../utils" function FooterItem({ children, icon, url }) { const color = "silver" @@ -69,16 +70,13 @@ function AboutAppColumn() { } url="https://www.ictu.nl/about-us"> Created by ICTU - } url="https://github.com/ICTU/quality-time/blob/master/LICENSE"> + } url={`${REPOSITORY_URL}/blob/master/LICENSE`}> License - } - url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/changelog.html`} - > + } url={`${DOCUMENTATION_URL}/changelog.html`}> Changelog - } url="https://github.com/ICTU/quality-time"> + } url={REPOSITORY_URL}> Source code @@ -88,22 +86,16 @@ function AboutAppColumn() { function SupportColumn() { return ( - } - url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/`} - > + } url={DOCUMENTATION_URL}> Documentation - } - url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/usage.html`} - > + } url={`${DOCUMENTATION_URL}/usage.html`}> User manual - } url="https://github.com/ICTU/quality-time/issues"> + } url={`${REPOSITORY_URL}/issues`}> Known issues - } url="https://github.com/ICTU/quality-time/issues/new"> + } url={`${REPOSITORY_URL}/issues/new`}> Report an issue diff --git a/components/frontend/src/metric/MetricTypeHeader.js b/components/frontend/src/metric/MetricTypeHeader.js index bc91a5c021..a03bc13273 100644 --- a/components/frontend/src/metric/MetricTypeHeader.js +++ b/components/frontend/src/metric/MetricTypeHeader.js @@ -1,10 +1,9 @@ import { Header } from "../semantic_ui_react_wrappers" import { metricTypePropType } from "../sharedPropTypes" -import { slugify } from "../utils" +import { referenceDocumentationURL } from "../utils" import { ReadTheDocsLink } from "../widgets/ReadTheDocsLink" export function MetricTypeHeader({ metricType }) { - const url = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/reference.html${slugify(metricType.name)}` const howToConfigure = metricType.documentation ? " for specific information on how to configure this metric type." : "" @@ -13,7 +12,7 @@ export function MetricTypeHeader({ metricType }) { {metricType.name} - {metricType.description} + {metricType.description} {howToConfigure} diff --git a/components/frontend/src/source/Source.js b/components/frontend/src/source/Source.js index 9bee36716a..b94f115789 100644 --- a/components/frontend/src/source/Source.js +++ b/components/frontend/src/source/Source.js @@ -16,7 +16,7 @@ import { sourcePropType, stringsPropType, } from "../sharedPropTypes" -import { getMetricName, getSourceName } from "../utils" +import { getMetricName, getSourceName, referenceDocumentationURL } from "../utils" import { ButtonRow } from "../widgets/ButtonRow" import { DeleteButton } from "../widgets/buttons/DeleteButton" import { ReorderButtonGroup } from "../widgets/buttons/ReorderButtonGroup" @@ -141,7 +141,6 @@ export function Source({ const metricName = getMetricName(metric, dataModel) const connectionError = measurement_source?.connection_error || "" const parseError = measurement_source?.parse_error || "" - const referenceManualURL = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/reference.html` const configErrorMessage = ( <>

@@ -152,11 +151,11 @@ export function Source({

  • Change the type of the metric (back) to a type that is supported by{" "} - {sourceName}. + {sourceName}.
  • Change the type of this source to a type that supports{" "} - {metricName}. + {metricName}.
  • Move this source to another metric.
  • Remove this source altogether.
  • diff --git a/components/frontend/src/source/SourceTypeHeader.js b/components/frontend/src/source/SourceTypeHeader.js index a874a54087..9387333b40 100644 --- a/components/frontend/src/source/SourceTypeHeader.js +++ b/components/frontend/src/source/SourceTypeHeader.js @@ -2,7 +2,7 @@ import { string } from "prop-types" import { Header, Label } from "../semantic_ui_react_wrappers" import { sourceTypePropType } from "../sharedPropTypes" -import { slugify } from "../utils" +import { referenceDocumentationURL } from "../utils" import { ReadTheDocsLink } from "../widgets/ReadTheDocsLink" import { Logo } from "./Logo" import { sourceTypeDescription } from "./SourceType" @@ -12,7 +12,6 @@ export function SourceTypeHeader({ metricTypeId, sourceTypeId, sourceType }) { if (sourceType?.documentation?.generic || sourceType?.documentation?.[metricTypeId]) { howToConfigure = " for specific information on how to configure this source type." } - const url = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/reference.html${slugify(sourceType.name)}` return (
    @@ -21,7 +20,7 @@ export function SourceTypeHeader({ metricTypeId, sourceTypeId, sourceType }) { {sourceType.deprecated && } {`${sourceTypeDescription(sourceType)} `} - + {howToConfigure} diff --git a/components/frontend/src/subject/SubjectTitle.js b/components/frontend/src/subject/SubjectTitle.js index a5370644cc..23bc9fcc2d 100644 --- a/components/frontend/src/subject/SubjectTitle.js +++ b/components/frontend/src/subject/SubjectTitle.js @@ -8,7 +8,7 @@ import { DataModel } from "../context/DataModel" import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissions" import { Header, Tab } from "../semantic_ui_react_wrappers" import { reportPropType, settingsPropType } from "../sharedPropTypes" -import { getSubjectType, slugify } from "../utils" +import { getSubjectType, referenceDocumentationURL } from "../utils" import { ButtonRow } from "../widgets/ButtonRow" import { DeleteButton } from "../widgets/buttons/DeleteButton" import { PermLinkButton } from "../widgets/buttons/PermLinkButton" @@ -19,13 +19,12 @@ import { changelogTabPane, configurationTabPane } from "../widgets/TabPane" import { SubjectParameters } from "./SubjectParameters" function SubjectHeader({ subjectType }) { - const url = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/reference.html${slugify(subjectType.name)}` return (
    {subjectType.name} - {subjectType.description} + {subjectType.description}
    diff --git a/components/frontend/src/utils.js b/components/frontend/src/utils.js index ef1cb76eb3..2970a3b521 100644 --- a/components/frontend/src/utils.js +++ b/components/frontend/src/utils.js @@ -15,6 +15,8 @@ import { subjectTypePropType, } from "./sharedPropTypes" +export const DOCUMENTATION_URL = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}` +export const REPOSITORY_URL = "https://github.com/ICTU/quality-time" export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000 const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR @@ -420,9 +422,10 @@ export function dropdownOptions(options) { return options.map((option) => ({ key: option, text: option, value: option })) } -export function slugify(name) { - // The hash isn't really part of the slug, but to prevent duplication it is included anyway - return `#${name?.toLowerCase().replaceAll(" ", "-").replaceAll("(", "").replaceAll(")", "").replaceAll("/", "")}` +export function referenceDocumentationURL(name) { + // Return a URL to the documentation for the metric/subject/source name + const slug = `${name?.toLowerCase().replaceAll(" ", "-").replaceAll(/[()/]/g, "")}` + return `${DOCUMENTATION_URL}/reference.html#${slug}` } export function addCounts(object1, object2) { diff --git a/components/shared_code/.vulture_ignore_list.py b/components/shared_code/.vulture_ignore_list.py index 06587b20ae..7d3b3ead74 100644 --- a/components/shared_code/.vulture_ignore_list.py +++ b/components/shared_code/.vulture_ignore_list.py @@ -12,14 +12,17 @@ _.set_key # unused method (src/shared_data_model/meta/entity.py:56) _.set_name_plural # unused method (src/shared_data_model/meta/entity.py:74) _.check_measured_attribute # unused method (src/shared_data_model/meta/entity.py:81) -model_config # unused variable (src/shared_data_model/meta/metric.py:41) -tags # unused variable (src/shared_data_model/meta/metric.py:51) -rationale # unused variable (src/shared_data_model/meta/metric.py:52) -rationale_urls # unused variable (src/shared_data_model/meta/metric.py:53) -explanation # unused variable (src/shared_data_model/meta/metric.py:54) -explanation_urls # unused variable (src/shared_data_model/meta/metric.py:55) -documentation # unused variable (src/shared_data_model/meta/metric.py:56) -_.set_default_scale # unused method (src/shared_data_model/meta/metric.py:58) +model_config # unused variable (src/shared_data_model/meta/metric.py:44) +tags # unused variable (src/shared_data_model/meta/metric.py:54) +rationale # unused variable (src/shared_data_model/meta/metric.py:55) +rationale_urls # unused variable (src/shared_data_model/meta/metric.py:56) +explanation # unused variable (src/shared_data_model/meta/metric.py:57) +explanation_urls # unused variable (src/shared_data_model/meta/metric.py:58) +documentation # unused variable (src/shared_data_model/meta/metric.py:59) +reference_documentation_url # unused variable (src/shared_data_model/meta/metric.py:60) +_.set_default_scale # unused method (src/shared_data_model/meta/metric.py:62) +_.set_reference_documentation_url # unused method (src/shared_data_model/meta/metric.py:68) +_.reference_documentation_url # unused attribute (src/shared_data_model/meta/metric.py:71) model_config # unused variable (src/shared_data_model/meta/parameter.py:26) mandatory # unused variable (src/shared_data_model/meta/parameter.py:33) _.set_short_name # unused method (src/shared_data_model/meta/parameter.py:41) @@ -40,4 +43,4 @@ mandatory # unused variable (src/shared_data_model/parameters.py:44) validation_path # unused variable (src/shared_data_model/parameters.py:98) min_value # unused variable (src/shared_data_model/parameters.py:105) -_.set_help # unused method (src/shared_data_model/parameters.py:115) +_.set_help # unused method (src/shared_data_model/parameters.py:125) diff --git a/components/shared_code/src/shared/utils/functions.py b/components/shared_code/src/shared/utils/functions.py index 8433b2fde5..d38d72d5d9 100644 --- a/components/shared_code/src/shared/utils/functions.py +++ b/components/shared_code/src/shared/utils/functions.py @@ -29,3 +29,11 @@ def md5_hash(string: str) -> str: """Return a md5 hash of the string.""" md5 = hashlib.md5(string.encode("utf-8"), usedforsecurity=False) return md5.hexdigest() + + +def slugify(name: str) -> str: + """Return a slugified version of the name.""" + # Add type to prevent mypy complaining that 'Argument 1 to "maketrans" of "str" has incompatible type...' + char_mapping: dict[str, str | int | None] = {" ": "-", "(": "", ")": "", "/": ""} + slug = name.lower().translate(str.maketrans(char_mapping)) + return f"#{slug}" # The hash isn't really part of the slug, but to prevent duplication it is included anyway diff --git a/components/shared_code/src/shared/utils/version.py b/components/shared_code/src/shared/utils/version.py new file mode 100644 index 0000000000..5db288aa43 --- /dev/null +++ b/components/shared_code/src/shared/utils/version.py @@ -0,0 +1,4 @@ +"""Quality-time version information.""" + +QUALITY_TIME_VERSION = "5.21.0" +REFERENCE_DOCUMENTATION_URL = f"https://quality-time.readthedocs.io/en/v{QUALITY_TIME_VERSION}/reference.html" diff --git a/components/shared_code/src/shared_data_model/meta/base.py b/components/shared_code/src/shared_data_model/meta/base.py index 1a0ea5c3d7..b64127f87e 100644 --- a/components/shared_code/src/shared_data_model/meta/base.py +++ b/components/shared_code/src/shared_data_model/meta/base.py @@ -6,6 +6,9 @@ from pydantic import BaseModel, Field, model_validator +from shared.utils.functions import slugify +from shared.utils.version import REFERENCE_DOCUMENTATION_URL + class StrEnum(str, Enum): """Enums that use strings as values.""" @@ -39,3 +42,15 @@ def check_description(self) -> Self: """Check the description.""" self.check_punctuation("description", self.description) return self + + +class DocumentedModel(DescribedModel): + """Extend the described model with a reference documentation URL.""" + + reference_documentation_url: str | None = None # Set auomatically by set_reference_documentation_url() below + + @model_validator(mode="after") + def set_reference_documentation_url(self) -> Self: + """Set the reference documentation URL based on the metric name.""" + self.reference_documentation_url = f"{REFERENCE_DOCUMENTATION_URL}{slugify(self.name)}" + return self diff --git a/components/shared_code/src/shared_data_model/meta/metric.py b/components/shared_code/src/shared_data_model/meta/metric.py index c412d23d5e..8edc594d77 100644 --- a/components/shared_code/src/shared_data_model/meta/metric.py +++ b/components/shared_code/src/shared_data_model/meta/metric.py @@ -4,7 +4,7 @@ from pydantic import ConfigDict, Field, model_validator -from .base import DescribedModel, StrEnum +from .base import DocumentedModel, StrEnum from .unit import Unit @@ -35,7 +35,7 @@ class Tag(StrEnum): TESTABILITY = "testability" -class Metric(DescribedModel): +class Metric(DocumentedModel): """Base model for metrics.""" model_config = ConfigDict(validate_default=True) diff --git a/components/shared_code/src/shared_data_model/meta/subject.py b/components/shared_code/src/shared_data_model/meta/subject.py index b271220304..6139ceb67a 100644 --- a/components/shared_code/src/shared_data_model/meta/subject.py +++ b/components/shared_code/src/shared_data_model/meta/subject.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field -from .base import DescribedModel +from .base import DocumentedModel class SubjectContainer(BaseModel): @@ -21,7 +21,7 @@ def all_subjects(self) -> dict[str, Subject]: return all_subjects -class Subject(SubjectContainer, DescribedModel): +class Subject(SubjectContainer, DocumentedModel): """Base model for subjects.""" metrics: list[str] = Field(default_factory=list) diff --git a/components/shared_code/src/shared_data_model/sources/quality_time.py b/components/shared_code/src/shared_data_model/sources/quality_time.py index b392258fd9..6474d5c0d9 100644 --- a/components/shared_code/src/shared_data_model/sources/quality_time.py +++ b/components/shared_code/src/shared_data_model/sources/quality_time.py @@ -310,8 +310,8 @@ attributes=[ EntityAttribute(name="Report", url="report_url"), EntityAttribute(name="Subject", url="subject_url"), - EntityAttribute(name="Subject type"), - EntityAttribute(name="Metric type"), + EntityAttribute(name="Subject type", url="subject_type_url"), + EntityAttribute(name="Metric type", url="metric_type_url"), ], ), }, diff --git a/components/shared_code/tests/shared/utils/test_functions.py b/components/shared_code/tests/shared/utils/test_functions.py index b553c643b2..125e6a5da6 100644 --- a/components/shared_code/tests/shared/utils/test_functions.py +++ b/components/shared_code/tests/shared/utils/test_functions.py @@ -4,7 +4,7 @@ from datetime import UTC, datetime from unittest.mock import patch -from shared.utils.functions import first, iso_timestamp, md5_hash +from shared.utils.functions import first, iso_timestamp, md5_hash, slugify class IsoTimestampTest(unittest.TestCase): @@ -40,3 +40,19 @@ class MD5HashTest(unittest.TestCase): def test_hash(self): """Test that the md5 hash is returned.""" self.assertEqual("acbd18db4cc2f85cedef654fccc4a4d8", md5_hash("foo")) + + +class SlugifyTest(unittest.TestCase): + """Unit tests for the slugify function.""" + + def test_simple_string(self): + """Test that a simple string is returned unchanged.""" + self.assertEqual("#name", slugify("name")) + + def test_mixed_case(self): + """Test that a upper case characters are made lower case.""" + self.assertEqual("#name", slugify("Name")) + + def test_forbidden_characters(self): + """Test that forbidden characters are removed.""" + self.assertEqual("#name-part-two", slugify("/name part two()")) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index d81432857d..a33eac5eae 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -12,6 +12,12 @@ If your currently installed *Quality-time* version is not the latest version, pl +## [Unreleased] + +### Added + +- When measuring missing metrics, make the subject type and the metric type of the missing metrics link to the reference documentation. Closes [#10528](https://github.com/ICTU/quality-time/issues/10528). + ## v5.21.0 - 2024-12-12 ### Fixed diff --git a/docs/src/create_reference_md.py b/docs/src/create_reference_md.py index 9631c865be..18036e0908 100644 --- a/docs/src/create_reference_md.py +++ b/docs/src/create_reference_md.py @@ -6,6 +6,7 @@ from pydantic import HttpUrl +from shared.utils.functions import slugify from shared_data_model import DATA_MODEL from shared_data_model.meta import Metric, NamedModel, Parameter, Source, Subject @@ -197,14 +198,6 @@ def admonition(text: str, title: str = "", admonition: AdmonitionType = "admonit return f"{indent}```{{{admonition_type}}}{admonition_title}\n{admonition_class}{indent}{text}\n{indent}```\n\n" -def slugify(name: str) -> str: - """Return a slugified version of the name.""" - # Add type to prevent mypy complaining that 'Argument 1 to "maketrans" of "str" has incompatible type...' - char_mapping: dict[str, str | int | None] = {" ": "-", "(": "", ")": "", "/": ""} - slug = name.lower().translate(str.maketrans(char_mapping)) - return f"#{slug}" # The hash isn't really part of the slug, but to prevent duplication it is included anyway - - def decapitalize(name: str) -> str: """Return the name starting with a lower case letter.""" return name[0].lower() + name[1:] diff --git a/release/pyproject.toml b/release/pyproject.toml index 33512d4214..26b917e186 100644 --- a/release/pyproject.toml +++ b/release/pyproject.toml @@ -103,7 +103,7 @@ search = "v{current_version}" replace = "v{new_version}" [[tool.bumpversion.files]] -filename = "../components/api_server/src/routes/server.py" +filename = "../components/shared_code/src/shared/utils/version.py" search = 'QUALITY_TIME_VERSION = "{current_version}"' replace = 'QUALITY_TIME_VERSION = "{new_version}"'