Skip to content

Commit

Permalink
Add link to docs for missing metrics metric.
Browse files Browse the repository at this point in the history
When measuring missing metrics, make the subject type and the metric type of the missing metrics link to the reference documentation.

Closes #10528.
  • Loading branch information
fniessink committed Dec 24, 2024
1 parent 7f52974 commit 2d603c4
Show file tree
Hide file tree
Showing 21 changed files with 104 additions and 63 deletions.
3 changes: 1 addition & 2 deletions components/api_server/src/routes/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand All @@ -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)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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):
Expand Down
5 changes: 2 additions & 3 deletions components/frontend/src/dashboard/ExportCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,9 +44,7 @@ export function ExportCard({ lastUpdate, report, reportDate, isOverview = false
</List.Item>,
<List.Item key={"version"} data-testid={"version"}>
<List.Content verticalAlign={"middle"}>
<ExportCardItem
url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/changelog.html`}
>
<ExportCardItem url={`${DOCUMENTATION_URL}/changelog.html`}>
<em>Quality-time</em> v{process.env.REACT_APP_VERSION}
</ExportCardItem>
</List.Content>
Expand Down
24 changes: 8 additions & 16 deletions components/frontend/src/header_footer/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -69,16 +70,13 @@ function AboutAppColumn() {
<FooterItem icon={<ScienceIcon />} url="https://www.ictu.nl/about-us">
Created by ICTU
</FooterItem>
<FooterItem icon={<CopyrightIcon />} url="https://github.com/ICTU/quality-time/blob/master/LICENSE">
<FooterItem icon={<CopyrightIcon />} url={`${REPOSITORY_URL}/blob/master/LICENSE`}>
License
</FooterItem>
<FooterItem
icon={<HistoryIcon />}
url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/changelog.html`}
>
<FooterItem icon={<HistoryIcon />} url={`${DOCUMENTATION_URL}/changelog.html`}>
Changelog
</FooterItem>
<FooterItem icon={<GitHubIcon />} url="https://github.com/ICTU/quality-time">
<FooterItem icon={<GitHubIcon />} url={REPOSITORY_URL}>
Source code
</FooterItem>
</FooterColumn>
Expand All @@ -88,22 +86,16 @@ function AboutAppColumn() {
function SupportColumn() {
return (
<FooterColumn header="Support">
<FooterItem
icon={<MenuBookIcon />}
url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/`}
>
<FooterItem icon={<MenuBookIcon />} url={DOCUMENTATION_URL}>
Documentation
</FooterItem>
<FooterItem
icon={<PersonIcon />}
url={`https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/usage.html`}
>
<FooterItem icon={<PersonIcon />} url={`${DOCUMENTATION_URL}/usage.html`}>
User manual
</FooterItem>
<FooterItem icon={<BugReportIcon />} url="https://github.com/ICTU/quality-time/issues">
<FooterItem icon={<BugReportIcon />} url={`${REPOSITORY_URL}/issues`}>
Known issues
</FooterItem>
<FooterItem icon={<FeedbackIcon />} url="https://github.com/ICTU/quality-time/issues/new">
<FooterItem icon={<FeedbackIcon />} url={`${REPOSITORY_URL}/issues/new`}>
Report an issue
</FooterItem>
</FooterColumn>
Expand Down
5 changes: 2 additions & 3 deletions components/frontend/src/metric/MetricTypeHeader.js
Original file line number Diff line number Diff line change
@@ -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."
: ""
Expand All @@ -13,7 +12,7 @@ export function MetricTypeHeader({ metricType }) {
<Header.Content>
{metricType.name}
<Header.Subheader>
{metricType.description} <ReadTheDocsLink url={url} />
{metricType.description} <ReadTheDocsLink url={referenceDocumentationURL(metricType.name)} />
{howToConfigure}
</Header.Subheader>
</Header.Content>
Expand Down
7 changes: 3 additions & 4 deletions components/frontend/src/source/Source.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = (
<>
<p>
Expand All @@ -152,11 +151,11 @@ export function Source({
<ul>
<li>
Change the type of the metric (back) to a type that is supported by{" "}
<HyperLink url={`${referenceManualURL}#${source.type}`}>{sourceName}</HyperLink>.
<HyperLink url={referenceDocumentationURL(sourceName)}>{sourceName}</HyperLink>.
</li>
<li>
Change the type of this source to a type that supports{" "}
<HyperLink url={`${referenceManualURL}#${metric.type}`}>{metricName}</HyperLink>.
<HyperLink url={referenceDocumentationURL(metricName)}>{metricName}</HyperLink>.
</li>
<li>Move this source to another metric.</li>
<li>Remove this source altogether.</li>
Expand Down
5 changes: 2 additions & 3 deletions components/frontend/src/source/SourceTypeHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { string } from "prop-types"

import { Header } 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"
Expand All @@ -13,7 +13,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 (
<Header>
<Header.Content>
Expand All @@ -22,7 +21,7 @@ export function SourceTypeHeader({ metricTypeId, sourceTypeId, sourceType }) {
{sourceType.deprecated && <Chip color="warning" label="Deprecated" sx={{ marginLeft: "8px" }} />}
<Header.Subheader>
{`${sourceTypeDescription(sourceType)} `}
<ReadTheDocsLink url={url} />
<ReadTheDocsLink url={referenceDocumentationURL(sourceType.name)} />
{howToConfigure}
</Header.Subheader>
</Header.Content>
Expand Down
5 changes: 2 additions & 3 deletions components/frontend/src/subject/SubjectTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 (
<Header>
<Header.Content>
{subjectType.name}
<Header.Subheader>
{subjectType.description} <ReadTheDocsLink url={url} />
{subjectType.description} <ReadTheDocsLink url={referenceDocumentationURL(subjectType.name)} />
</Header.Subheader>
</Header.Content>
</Header>
Expand Down
9 changes: 6 additions & 3 deletions components/frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -421,9 +423,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) {
Expand Down
21 changes: 12 additions & 9 deletions components/shared_code/.vulture_ignore_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
8 changes: 8 additions & 0 deletions components/shared_code/src/shared/utils/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions components/shared_code/src/shared/utils/version.py
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions components/shared_code/src/shared_data_model/meta/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions components/shared_code/src/shared_data_model/meta/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions components/shared_code/src/shared_data_model/meta/subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pydantic import BaseModel, Field

from .base import DescribedModel
from .base import DocumentedModel


class SubjectContainer(BaseModel):
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
],
),
},
Expand Down
18 changes: 17 additions & 1 deletion components/shared_code/tests/shared/utils/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()"))
Loading

0 comments on commit 2d603c4

Please sign in to comment.