Skip to content

Commit

Permalink
Add more subject types.
Browse files Browse the repository at this point in the history
Closes #3130.
  • Loading branch information
fniessink committed Jun 3, 2024
1 parent 90e33ae commit cc9b42e
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 32 deletions.
2 changes: 1 addition & 1 deletion components/api_server/src/model/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ def default_report_attributes() -> dict[str, str | dict]:

def default_subject_attributes(subject_type: str) -> dict[str, Any]:
"""Return the default attributes with their default values for the specified subject type."""
subject = DATA_MODEL.subjects[subject_type]
subject = DATA_MODEL.all_subjects[subject_type]
return {"type": subject_type, "name": None, "description": subject.description, "metrics": {}}
5 changes: 4 additions & 1 deletion components/frontend/src/metric/MetricType.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DataModel } from "../context/DataModel"
import { EDIT_REPORT_PERMISSION } from "../context/Permissions"
import { SingleChoiceInput } from "../fields/SingleChoiceInput"
import { Header } from "../semantic_ui_react_wrappers"
import { getSubjectTypeMetrics } from "../utils"

export function metricTypeOption(key, metricType) {
return {
Expand All @@ -18,7 +19,9 @@ export function metricTypeOption(key, metricType) {

export function metricTypeOptions(dataModel, subjectType) {
// Return menu options for all metric that support the subject type
return dataModel.subjects[subjectType].metrics.map((key) => metricTypeOption(key, dataModel.metrics[key]))
return getSubjectTypeMetrics(subjectType, dataModel.subjects).map((key) =>
metricTypeOption(key, dataModel.metrics[key]),
)
}

export function allMetricTypeOptions(dataModel) {
Expand Down
6 changes: 6 additions & 0 deletions components/frontend/src/sharedPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,15 @@ export const metricTypePropType = shape({
name: string,
})

export const subjectTypePropType = shape({
description: string,
name: string,
})

export const dataModelPropType = shape({
metrics: objectOf(metricTypePropType),
sources: objectOf(sourceTypePropType),
subjects: objectOf(subjectTypePropType),
})

export const destinationPropType = shape({
Expand Down
4 changes: 2 additions & 2 deletions components/frontend/src/subject/SubjectTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissio
import { Header, Tab } from "../semantic_ui_react_wrappers"
import { Share } from "../share/Share"
import { reportPropType, settingsPropType } from "../sharedPropTypes"
import { slugify } from "../utils"
import { getSubjectType, slugify } from "../utils"
import { DeleteButton, ReorderButtonGroup } from "../widgets/Button"
import { FocusableTab } from "../widgets/FocusableTab"
import { HeaderWithDetails } from "../widgets/HeaderWithDetails"
Expand Down Expand Up @@ -76,7 +76,7 @@ export function SubjectTitle({
}) {
const dataModel = useContext(DataModel)
const tabIndex = activeTabIndex(settings.expandedItems, subject_uuid)
const subjectType = dataModel.subjects[subject.type] || { name: "Unknown subject type" }
const subjectType = getSubjectType(subject.type, dataModel.subjects) || { name: "Unknown subject type" }
const subjectName = subject.name || subjectType.name
const subjectTitle = (atReportsOverview ? report.title + " ❯ " : "") + subjectName
const subjectUrl = `${window.location}#${subject_uuid}`
Expand Down
21 changes: 17 additions & 4 deletions components/frontend/src/subject/SubjectType.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import { func, objectOf, string } from "prop-types"
import { func, number, objectOf, string } from "prop-types"
import { useContext } from "react"
import { HeaderContent, HeaderSubheader } from "semantic-ui-react"

import { DataModel } from "../context/DataModel"
import { EDIT_REPORT_PERMISSION } from "../context/Permissions"
import { SingleChoiceInput } from "../fields/SingleChoiceInput"
import { Header } from "../semantic_ui_react_wrappers"
import { Header, Icon } from "../semantic_ui_react_wrappers"
import { subjectPropType } from "../sharedPropTypes"

export function subjectTypes(subjectTypesMapping) {
export function subjectTypes(subjectTypesMapping, level = 0) {
const options = []
const headingLevel = `h${Math.min(level, 2) + 4}`
const bullet = level === 0 ? null : <Icon name="circle" size="tiny" style={{ paddingLeft: `${level}em` }} />
Object.entries(subjectTypesMapping).forEach(([key, subjectType]) => {
options.push({
key: key,
text: subjectType.name,
value: key,
content: <Header as="h4" content={subjectType.name} subheader={subjectType.description} />,
content: (
<Header as={headingLevel}>
{bullet}
<HeaderContent>
{subjectType.name}
<HeaderSubheader>{subjectType.description}</HeaderSubheader>
</HeaderContent>
</Header>
),
})
options.push(...subjectTypes(subjectType?.subjects ?? [], level + 1))
})
return options
}
subjectTypes.propTypes = {
subjectTypesMapping: objectOf(subjectPropType),
level: number,
}

export function SubjectType({ subjectType, setValue }) {
Expand Down
39 changes: 38 additions & 1 deletion components/frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
reportsPropType,
scalePropType,
stringsPropType,
subjectTypePropType,
} from "./sharedPropTypes"
import { HyperLink } from "./widgets/HyperLink"

Expand Down Expand Up @@ -75,8 +76,44 @@ export function getSourceName(source, dataModel) {
return source.name || dataModel.sources[source.type].name
}

export function getSubjectTypeMetrics(subjectTypeKey, subjects) {
// Return the metric types supported by the specified subject type, and all parents of the subject type, recursively
if (Object.keys(subjects).includes(subjectTypeKey)) {
return subjects[subjectTypeKey].metrics
}
const childSubjects = Object.values(subjects).filter((subject) => !!subject.subjects)
for (const childSubject of childSubjects) {
const metrics = getSubjectTypeMetrics(subjectTypeKey, childSubject.subjects)
if (metrics) {
return [...childSubject.metrics, ...metrics]
}
}
}
getSubjectTypeMetrics.propTypes = {
subjectTypeKey: string,
subjects: objectOf(subjectTypePropType),
}

export function getSubjectType(subjectTypeKey, subjects) {
// Return the subject type object
if (Object.keys(subjects).includes(subjectTypeKey)) {
return subjects[subjectTypeKey]
}
const childSubjects = Object.values(subjects).filter((subject) => !!subject.subjects)
for (const childSubject of childSubjects) {
const result = getSubjectType(subjectTypeKey, childSubject.subjects)
if (result) {
return result
}
}
}
getSubjectType.propTypes = {
subjectTypeKey: string,
subjects: objectOf(subjectTypePropType),
}

export function getSubjectName(subject, dataModel) {
return subject.name || dataModel.subjects[subject.type].name
return subject.name || getSubjectType(subject.type, dataModel.subjects).name
}

export function getMetricTarget(metric) {
Expand Down
43 changes: 43 additions & 0 deletions components/frontend/src/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
getSourceName,
getStatusName,
getSubjectName,
getSubjectType,
getSubjectTypeMetrics,
getUserPermissions,
niceNumber,
nrMetricsInReport,
Expand Down Expand Up @@ -139,6 +141,47 @@ it("gets the source name from the data model if the source has no name", () => {
)
})

it("gets the subject type", () => {
const subject = { name: "Subject" }
expect(getSubjectType("subject", { subject: subject })).toStrictEqual({ name: "Subject" })
})

it("gets the subject type recursively", () => {
const subject = { name: "Subject" }
expect(getSubjectType("subject", { parent: { subjects: { subject: subject } } })).toStrictEqual({
name: "Subject",
})
})

it("gets the subject type recursively from the second subject type", () => {
const subject = { name: "Subject" }
expect(
getSubjectType("subject", {
first: { subjects: { foo: { name: "Foo" } } },
second: { subjects: { subject: subject } },
}),
).toStrictEqual(subject)
})

it("gets the subject type metrics", () => {
expect(getSubjectTypeMetrics("subject", { subject: { metrics: ["metric"] } })).toStrictEqual(["metric"])
})

it("gets the subject type metrics recursively", () => {
const metrics = getSubjectTypeMetrics("child", {
parent: { metrics: ["parent metric"], subjects: { child: { metrics: ["child metric"] } } },
})
expect(metrics).toStrictEqual(["parent metric", "child metric"])
})

it("gets the subject type metrics recursively from the second subject type", () => {
const metrics = getSubjectTypeMetrics("child", {
first: { subjects: { foo: { metrics: ["foo metric"] } } },
second: { metrics: ["second metric"], subjects: { child: { metrics: ["child metric"] } } },
})
expect(metrics).toStrictEqual(["second metric", "child metric"])
})

it("gets the subject name", () => {
expect(getSubjectName({ name: "subject" }, {})).toStrictEqual("subject")
})
Expand Down
4 changes: 2 additions & 2 deletions components/frontend/src/widgets/menu_options.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getMetricName, getSourceName, getSubjectName } from "../utils"
import { getMetricName, getSourceName, getSubjectName, getSubjectType } from "../utils"
import { ItemBreadcrumb } from "./ItemBreadcrumb"

export function metric_options(reports, dataModel, current_subject_type, current_subject_uuid) {
const subject_metrics = dataModel.subjects[current_subject_type].metrics
const subject_metrics = getSubjectType(current_subject_type, dataModel.subjects).metrics
let options = []
reports.forEach((report) => {
Object.entries(report.subjects).forEach(([subject_uuid, subject]) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@
import pathlib
from typing import Self

from pydantic import BaseModel, model_validator
from pydantic import model_validator

from .metric import Metric
from .scale import Scale
from .source import Source
from .subject import Subject
from .subject import SubjectContainer


class DataModel(BaseModel):
class DataModel(SubjectContainer):
"""Data model model."""

scales: dict[str, Scale]
metrics: dict[str, Metric]
sources: dict[str, Source]
subjects: dict[str, Subject]

@model_validator(mode="after")
def check_scales(self) -> Self:
Expand Down Expand Up @@ -137,7 +136,7 @@ def check_quality_time_source_types(self) -> None:
def check_subjects(self) -> Self:
"""Check that each metric belongs to at least one subject."""
for metric_key in self.metrics:
for subject in self.subjects.values():
for subject in self.all_subjects.values():
if metric_key in subject.metrics:
break
else:
Expand Down
23 changes: 21 additions & 2 deletions components/shared_code/src/shared_data_model/meta/subject.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
"""Data model subject."""

from pydantic import Field
from __future__ import annotations

from pydantic import BaseModel, Field

from .base import DescribedModel


class Subject(DescribedModel):
class SubjectContainer(BaseModel):
"""Base model for subject containers."""

subjects: dict[str, Subject] = Field(default_factory=dict)

@property
def all_subjects(self) -> dict[str, Subject]:
"""Return all subjects, recursively."""
all_subjects = {**self.subjects}
for subject in self.subjects.values():
all_subjects.update(**subject.all_subjects)
return all_subjects


class Subject(SubjectContainer, DescribedModel):
"""Base model for subjects."""

metrics: list[str] = Field(..., min_length=1)


SubjectContainer.model_rebuild()
34 changes: 24 additions & 10 deletions components/shared_code/src/shared_data_model/subjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,40 +46,54 @@
"software": Subject(
name="Software",
description="A custom software application or component.",
subjects={
"software_application": Subject(
name="Software application",
description="A custom software application.",
metrics=[
"scalability",
"slow_transactions",
"test_cases",
],
),
"software_component": Subject(
name="Software component",
description="A custom software component.",
metrics=[
"commented_out_code",
"complex_units",
"dependencies",
"duplicated_lines",
"long_units",
"many_parameters",
"violations",
],
),
},
metrics=[
"average_issue_lead_time",
"change_failure_rate",
"commented_out_code",
"complex_units",
"dependencies",
"duplicated_lines",
"issues",
"job_runs_within_time_period",
"loc",
"long_units",
"manual_test_duration",
"manual_test_execution",
"many_parameters",
"merge_requests",
"performancetest_duration",
"performancetest_stability",
"pipeline_duration",
"remediation_effort",
"scalability",
"security_warnings",
"slow_transactions",
"software_version",
"source_up_to_dateness",
"source_version",
"suppressed_violations",
"test_cases",
"tests",
"todo_and_fixme_comments",
"uncovered_branches",
"uncovered_lines",
"unmerged_branches",
"user_story_points",
"violations",
],
),
}
9 changes: 5 additions & 4 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ If your currently installed *Quality-time* version is v4.10.0 or older, please r

### Added

- Add more subject types. Closes [#3130](https://github.com/ICTU/quality-time/issues/3130).
- Group digits in numbers. Closes [#8076](https://github.com/ICTU/quality-time/issues/8076).
- In the measurement entity status menu, the description of the menu items would say "undefined days" if the desired response time for the status had not been changed from its default value. Fixes [#8284](https://github.com/ICTU/quality-time/issues/8284).
- Allow for specifying supported source versions in the data model. Show the supported source version in the UI and the reference documentation. Closes [#8786](https://github.com/ICTU/quality-time/issues/8786).

### Changed

- Rename the "CI-environment" subject type to "Development environment". Prepares for [#3130](https://github.com/ICTU/quality-time/issues/3130).

## v5.13.0 - 2024-05-23

### Deployment notes
Expand All @@ -41,10 +46,6 @@ If your currently installed *Quality-time* version is v4.10.0 or older, please r

- When using Dependency-Track as source for dependencies, security warnings, or source-up-to-dateness, allow for filtering by project name and version. Closes [#8686](https://github.com/ICTU/quality-time/issues/8686).

### Changed

- Rename the "CI-environment" subject type to "Development environment". Prepares for [#3130](https://github.com/ICTU/quality-time/issues/3130).

## v5.12.0 - 2024-05-17

### Deployment notes
Expand Down

0 comments on commit cc9b42e

Please sign in to comment.