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 Dec 18, 2023
1 parent eb64a9d commit 2cb2ab5
Show file tree
Hide file tree
Showing 32 changed files with 15,358 additions and 13,924 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 Netz AG and the capella-collab-manager 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)

Check warning on line 43 in backend/capellacollab/core/metadata.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/core/metadata.py#L42-L43

Added lines #L42 - L43 were not covered by tests

return Metadata.model_validate(

Check warning on line 45 in backend/capellacollab/core/metadata.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/core/metadata.py#L45

Added line #L45 was not covered by tests
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
3 changes: 3 additions & 0 deletions backend/capellacollab/settings/configuration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager 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 Netz AG and the capella-collab-manager 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]

Check warning on line 12 in backend/capellacollab/settings/configuration/core.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/core.py#L11-L12

Added lines #L11 - L12 were not covered by tests
if configuration:
return model_type().model_validate(configuration.configuration)
return model_type().model_validate({})

Check warning on line 15 in backend/capellacollab/settings/configuration/core.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/core.py#L14-L15

Added lines #L14 - L15 were not covered by tests
55 changes: 55 additions & 0 deletions backend/capellacollab/settings/configuration/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

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(

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L13 was not covered by tests
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 23 in backend/capellacollab/settings/configuration/crud.py

View check run for this annotation

Codecov / codecov/patch

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

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


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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L33 was not covered by tests
name=name, configuration=configuration
)
db.add(db_configuration)
db.commit()
return db_configuration

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

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L36-L38

Added lines #L36 - L38 were not covered by tests


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

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

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L46-L48

Added lines #L46 - L48 were not covered by tests


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

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

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/crud.py#L54-L55

Added lines #L54 - L55 were not covered by tests
64 changes: 64 additions & 0 deletions backend/capellacollab/settings/configuration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager 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):
"""
All you need to do to add a new configuration is to create a new class
which inherits from this class.
"""

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
)


NAME_TO_MODEL_TYPE_MAPPING: dict[str, t.Type[ConfigurationBase]] = {
model()._name: model for model in ConfigurationBase.__subclasses__()
}
77 changes: 77 additions & 0 deletions backend/capellacollab/settings/configuration/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager 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=[])


def get_endpoints(model_instance: models.ConfigurationBase):
async def get_configuration(
db: orm.Session = fastapi.Depends(database.get_db),
):
return core.get_config(db, model._name)

Check warning on line 32 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L32

Added line #L32 was not covered by tests

async def update_configuration(
body: models.GlobalConfiguration,
db: orm.Session = fastapi.Depends(database.get_db),
):
configuration = crud.get_configuration_by_name(db, model._name)

Check warning on line 38 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L38

Added line #L38 was not covered by tests

if configuration:
return crud.update_configuration(

Check warning on line 41 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L41

Added line #L41 was not covered by tests
db, configuration, body.model_dump()
)
return crud.create_configuration(

Check warning on line 44 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L44

Added line #L44 was not covered by tests
db, name=model._name, configuration=body.model_dump()
)

async def get_json_schema():
return model.model_json_schema()

Check warning on line 49 in backend/capellacollab/settings/configuration/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/settings/configuration/routes.py#L49

Added line #L49 was not covered by tests

return get_configuration, update_configuration, get_json_schema


for model in models.ConfigurationBase.__subclasses__():
model_instance = model()
get_config, update_config, get_schema = get_endpoints(model_instance)

router.add_api_route(
path=f"/{model_instance._name}",
endpoint=get_config,
methods=["GET"],
response_model=model,
)

router.add_api_route(
path=f"/{model_instance._name}",
endpoint=update_config,
methods=["PUT"],
response_model=models.ConfigurationBase,
)

schema_router.add_api_route(
path=f"/{model_instance._name}/schema",
endpoint=get_schema,
methods=["GET"],
response_model=t.Any,
)
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 2cb2ab5

Please sign in to comment.