Skip to content

Commit

Permalink
feat!: Add support for global configuration
Browse files Browse the repository at this point in the history
The global configuration is now defined in the yaml format in the frontend.
We use the `monaco` editor.

This is a breaking change. The following configuration options
have to be configured manually after the update:
- privacy policy URL
- imprint URL
- provider
- authentication provider (Only the display name in the frontend)
- environment (Only the display name in the frontend)
  • Loading branch information
MoritzWeber0 committed Jan 30, 2024
1 parent c8fe2e4 commit 2dbdade
Show file tree
Hide file tree
Showing 34 changed files with 9,250 additions and 8,681 deletions.
1 change: 0 additions & 1 deletion backend/capellacollab/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# SPDX-License-Identifier: Apache-2.0

import os
from logging.config import fileConfig

os.environ["ALEMBIC_CONTEXT"] = "1"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

"""Add configuration table
Revision ID: 86ab7d4d1684
Revises: f55b41e32223
Create Date: 2023-10-27 14:54:40.452599
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "86ab7d4d1684"
down_revision = "f55b41e32223"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"configuration",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column(
"configuration",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_configuration_id"), "configuration", ["id"], unique=True
)
op.create_index(
op.f("ix_configuration_name"), "configuration", ["name"], unique=True
)
13 changes: 0 additions & 13 deletions backend/capellacollab/config/config_schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,6 @@ properties:
type: string
wildcardHost:
type: boolean
metadata:
type: object
properties:
privacyPolicyURL:
type: string
imprintURL:
type: string
provider:
type: string
authenticationProvider:
type: string
environment:
type: string
extensions:
type: object
additionalProperties: false
Expand Down
5 changes: 4 additions & 1 deletion backend/capellacollab/core/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@


class Base(orm.DeclarativeBase):
type_annotation_map = {dict[str, str]: postgresql.JSONB}
type_annotation_map = {
dict[str, str]: postgresql.JSONB,
dict[str, t.Any]: postgresql.JSONB,
}


### SQL MODELS ARE IMPORTED HERE ###
Expand Down
1 change: 1 addition & 0 deletions backend/capellacollab/core/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import capellacollab.projects.toolmodels.restrictions.models
import capellacollab.projects.users.models
import capellacollab.sessions.models
import capellacollab.settings.configuration.models
import capellacollab.settings.integrations.purevariants.models
import capellacollab.settings.modelsources.git.models
import capellacollab.settings.modelsources.t4c.models
Expand Down
28 changes: 16 additions & 12 deletions backend/capellacollab/core/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@

import fastapi
import pydantic
from sqlalchemy import orm

import capellacollab
from capellacollab.config import config
from capellacollab.core import database
from capellacollab.settings.configuration import core as config_core
from capellacollab.settings.configuration import models as config_models


class Metadata(pydantic.BaseModel):
Expand All @@ -28,22 +32,22 @@ class Metadata(pydantic.BaseModel):
router = fastapi.APIRouter()

general_cfg: dict[str, t.Any] = config["general"]
metadata_cfg: dict[str, str | None] = general_cfg.get("metadata", {})


@router.get(
"/metadata",
response_model=Metadata,
)
def get_metadata():
return Metadata(
version=capellacollab.__version__,
privacy_policy_url=metadata_cfg.get("privacyPolicyURL"),
imprint_url=metadata_cfg.get("imprintURL"),
provider=metadata_cfg.get("provider"),
authentication_provider=metadata_cfg.get("authenticationProvider"),
environment=metadata_cfg.get("environment"),
host=general_cfg.get("host"),
port=str(general_cfg.get("port")),
protocol=general_cfg.get("scheme"),
def get_metadata(db: orm.Session = fastapi.Depends(database.get_db)):
cfg = config_core.get_config(db, "global")
assert isinstance(cfg, config_models.GlobalConfiguration)

return Metadata.model_validate(
cfg.metadata.model_dump()
| {
"version": capellacollab.__version__,
"host": general_cfg.get("host"),
"port": str(general_cfg.get("port")),
"protocol": general_cfg.get("scheme"),
}
)
1 change: 0 additions & 1 deletion backend/capellacollab/projects/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

from . import crud, models

logger = logging.getLogger(__name__)
router = fastapi.APIRouter(
dependencies=[
fastapi.Depends(
Expand Down
4 changes: 4 additions & 0 deletions backend/capellacollab/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
prefix="/settings",
responses=auth_responses.AUTHENTICATION_RESPONSES,
)
router.include_router(
settings_routes.router_without_authentication,
prefix="/settings",
)

# Load authentication routes
ep = authentication.get_authentication_entrypoint()
Expand Down
2 changes: 2 additions & 0 deletions backend/capellacollab/settings/configuration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
15 changes: 15 additions & 0 deletions backend/capellacollab/settings/configuration/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

from sqlalchemy import orm

from . import crud, models


def get_config(db: orm.Session, name: str) -> models.ConfigurationBase:
"""Get a configuration by name."""
configuration = crud.get_configuration_by_name(db, name)
model_type = models.NAME_TO_MODEL_TYPE_MAPPING[name]
if configuration:
return model_type().model_validate(configuration.configuration)
return model_type().model_validate({})
57 changes: 57 additions & 0 deletions backend/capellacollab/settings/configuration/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import typing as t

import sqlalchemy as sa
from sqlalchemy import orm

from . import models


def get_configuration_by_name(
db: orm.Session, name: str
) -> None | models.DatabaseConfiguration:
return db.execute(
sa.select(models.DatabaseConfiguration).where(
models.DatabaseConfiguration.name == name
)
).scalar_one_or_none()


def get_pydantic_configuration_by_name(
db: orm.Session, pydantic_model: models.ConfigurationBase
):
return pydantic_model.model_validate(

Check warning on line 25 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L25

Added line #L25 was not covered by tests
get_configuration_by_name(db, pydantic_model._name)
)


def create_configuration(
db: orm.Session,
name: str,
configuration: dict[str, t.Any],
) -> models.DatabaseConfiguration:
db_configuration = models.DatabaseConfiguration(
name=name, configuration=configuration
)
db.add(db_configuration)
db.commit()
return db_configuration


def update_configuration(
db: orm.Session,
db_configuration: models.DatabaseConfiguration,
configuration: dict[str, t.Any],
) -> models.DatabaseConfiguration:
db_configuration.configuration = configuration
db.commit()
return db_configuration


def delete_configuration(
db: orm.Session, db_configuration: models.DatabaseConfiguration
):
db.delete(db_configuration)
db.commit()

Check warning on line 57 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L56-L57

Added lines #L56 - L57 were not covered by tests
65 changes: 65 additions & 0 deletions backend/capellacollab/settings/configuration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import abc
import typing as t

import pydantic
from sqlalchemy import orm

from capellacollab.core import database


class DatabaseConfiguration(database.Base):
__tablename__ = "configuration"

id: orm.Mapped[int] = orm.mapped_column(
unique=True, primary_key=True, index=True
)

name: orm.Mapped[str] = orm.mapped_column(unique=True, index=True)
configuration: orm.Mapped[dict[str, t.Any]]


class MetadataConfiguration(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid")

privacy_policy_url: str = pydantic.Field(
default="https://example.com/privacy"
)
imprint_url: str = pydantic.Field(default="https://example.com/imprint")
provider: str = pydantic.Field(
default="Systems Engineering Toolchain team"
)
authentication_provider: str = pydantic.Field(
default="OAuth2",
description="Authentication provides which is displayed in the frontend.",
)
environment: str = pydantic.Field(default="-", description="general")


class ConfigurationBase(pydantic.BaseModel, abc.ABC):
"""
Base class for configuration models. Can be used to define new configurations
in the future.
"""

model_config = pydantic.ConfigDict(extra="forbid")

_name: t.ClassVar[str]


class GlobalConfiguration(ConfigurationBase):
"""Global application configuration."""

_name: t.ClassVar[t.Literal["global"]] = "global"

metadata: MetadataConfiguration = pydantic.Field(
default_factory=MetadataConfiguration
)


# All subclasses of ConfigurationBase are automatically registered using this dict.
NAME_TO_MODEL_TYPE_MAPPING: dict[str, t.Type[ConfigurationBase]] = {
model()._name: model for model in ConfigurationBase.__subclasses__()
}
65 changes: 65 additions & 0 deletions backend/capellacollab/settings/configuration/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import typing as t

import fastapi
from sqlalchemy import orm

from capellacollab.core import database
from capellacollab.core.authentication import injectables as auth_injectables
from capellacollab.users import models as users_models

from . import core, crud, models

router = fastapi.APIRouter(
dependencies=[
fastapi.Depends(
auth_injectables.RoleVerification(
required_role=users_models.Role.ADMIN
)
)
]
)

schema_router = fastapi.APIRouter(dependencies=[])


@router.get(
f"/{models.GlobalConfiguration._name}",
response_model=models.GlobalConfiguration,
)
async def get_configuration(
db: orm.Session = fastapi.Depends(database.get_db),
):
return core.get_config(db, models.GlobalConfiguration._name)


@router.put(
f"/{models.GlobalConfiguration._name}",
response_model=models.GlobalConfiguration,
)
async def update_configuration(
body: models.GlobalConfiguration,
db: orm.Session = fastapi.Depends(database.get_db),
):
configuration = crud.get_configuration_by_name(
db, models.GlobalConfiguration._name
)

if configuration:
return crud.update_configuration(
db, configuration, body.model_dump()
).configuration
return crud.create_configuration(
db,
name=models.GlobalConfiguration._name,
configuration=body.model_dump(),
).configuration


@schema_router.get(
f"/{models.GlobalConfiguration._name}/schema", response_model=t.Any
)
async def get_json_schema():
return models.GlobalConfiguration.model_json_schema()
7 changes: 7 additions & 0 deletions backend/capellacollab/settings/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fastapi

from capellacollab.core.authentication import injectables as auth_injectables
from capellacollab.settings.configuration import routes as configuration_routes
from capellacollab.settings.integrations.purevariants import (
routes as purevariants_routes,
)
Expand All @@ -19,10 +20,16 @@
)
]
)
router_without_authentication = fastapi.APIRouter()

router.include_router(
modelsources_routes.router,
prefix="/modelsources",
)
router.include_router(
purevariants_routes.router, prefix="/integrations/pure-variants"
)
router.include_router(configuration_routes.router, prefix="/configurations")
router_without_authentication.include_router(
configuration_routes.schema_router, prefix="/configurations"
)
Loading

0 comments on commit 2dbdade

Please sign in to comment.