feat: Implement user profile preferences for start page, Git identity, and task drawer auto-open, alongside Git server default branch configuration.

This commit is contained in:
2026-03-08 10:19:38 +03:00
parent 12d17ec35e
commit e864a9e08b
30 changed files with 2041 additions and 211 deletions

View File

@@ -15,11 +15,13 @@ from sqlalchemy.orm import Session
from typing import List, Optional
import typing
import os
from src.dependencies import get_config_manager, has_permission
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.core.database import get_db
from src.models.auth import User
from src.models.git import GitServerConfig, GitRepository, GitProvider
from src.models.profile import UserDashboardPreference
from src.api.routes.git_schemas import (
GitServerConfigSchema, GitServerConfigCreate,
GitServerConfigSchema, GitServerConfigCreate, GitServerConfigUpdate,
BranchSchema, BranchCreate,
BranchCheckout, CommitSchema, CommitCreate,
DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest,
@@ -208,6 +210,84 @@ def _resolve_repo_key_from_ref(
return f"dashboard-{dashboard_id}"
# [/DEF:_resolve_repo_key_from_ref:Function]
# [DEF:_sanitize_optional_identity_value:Function]
# @PURPOSE: Normalize optional identity value into trimmed string or None.
# @PRE: value may be None or blank.
# @POST: Returns sanitized value suitable for git identity configuration.
# @RETURN: Optional[str]
def _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]:
normalized = str(value or "").strip()
if not normalized:
return None
return normalized
# [/DEF:_sanitize_optional_identity_value:Function]
# [DEF:_resolve_current_user_git_identity:Function]
# @PURPOSE: Resolve configured Git username/email from current user's profile preferences.
# @PRE: `db` may be stubbed in tests; `current_user` may be absent for direct handler invocations.
# @POST: Returns tuple(username, email) only when both values are configured.
# @RETURN: Optional[tuple[str, str]]
def _resolve_current_user_git_identity(
db: Session,
current_user: Optional[User],
) -> Optional[tuple[str, str]]:
if db is None or not hasattr(db, "query"):
return None
user_id = _sanitize_optional_identity_value(getattr(current_user, "id", None))
if not user_id:
return None
try:
preference = (
db.query(UserDashboardPreference)
.filter(UserDashboardPreference.user_id == user_id)
.first()
)
except Exception as resolve_error:
logger.warning(
"[_resolve_current_user_git_identity][Action] Failed to load profile preference for user %s: %s",
user_id,
resolve_error,
)
return None
if not preference:
return None
git_username = _sanitize_optional_identity_value(getattr(preference, "git_username", None))
git_email = _sanitize_optional_identity_value(getattr(preference, "git_email", None))
if not git_username or not git_email:
return None
return git_username, git_email
# [/DEF:_resolve_current_user_git_identity:Function]
# [DEF:_apply_git_identity_from_profile:Function]
# @PURPOSE: Apply user-scoped Git identity to repository-local config before write/pull operations.
# @PRE: dashboard_id is resolved; db/current_user may be missing in direct test invocation context.
# @POST: git_service.configure_identity is called only when identity and method are available.
# @RETURN: None
def _apply_git_identity_from_profile(
dashboard_id: int,
db: Session,
current_user: Optional[User],
) -> None:
identity = _resolve_current_user_git_identity(db, current_user)
if not identity:
return
configure_identity = getattr(git_service, "configure_identity", None)
if not callable(configure_identity):
return
git_username, git_email = identity
configure_identity(dashboard_id, git_username, git_email)
# [/DEF:_apply_git_identity_from_profile:Function]
# [DEF:get_git_configs:Function]
# @PURPOSE: List all configured Git servers.
# @PRE: Database session `db` is available.
@@ -219,7 +299,13 @@ async def get_git_configs(
_ = Depends(has_permission("admin:settings", "READ"))
):
with belief_scope("get_git_configs"):
return db.query(GitServerConfig).all()
configs = db.query(GitServerConfig).all()
result = []
for config in configs:
schema = GitServerConfigSchema.from_orm(config)
schema.pat = "********"
result.append(schema)
return result
# [/DEF:get_git_configs:Function]
# [DEF:create_git_config:Function]
@@ -242,6 +328,40 @@ async def create_git_config(
return db_config
# [/DEF:create_git_config:Function]
# [DEF:update_git_config:Function]
# @PURPOSE: Update an existing Git server configuration.
# @PRE: `config_id` corresponds to an existing configuration.
# @POST: The configuration record is updated in the database.
# @PARAM: config_id (str)
# @PARAM: config_update (GitServerConfigUpdate)
# @RETURN: GitServerConfigSchema
@router.put("/config/{config_id}", response_model=GitServerConfigSchema)
async def update_git_config(
config_id: str,
config_update: GitServerConfigUpdate,
db: Session = Depends(get_db),
_ = Depends(has_permission("admin:settings", "WRITE"))
):
with belief_scope("update_git_config"):
db_config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()
if not db_config:
raise HTTPException(status_code=404, detail="Configuration not found")
update_data = config_update.dict(exclude_unset=True)
if update_data.get("pat") == "********":
update_data.pop("pat")
for key, value in update_data.items():
setattr(db_config, key, value)
db.commit()
db.refresh(db_config)
result_schema = GitServerConfigSchema.from_orm(db_config)
result_schema.pat = "********"
return result_schema
# [/DEF:update_git_config:Function]
# [DEF:delete_git_config:Function]
# @PURPOSE: Remove a Git server configuration.
# @PRE: `config_id` corresponds to an existing configuration.
@@ -271,10 +391,22 @@ async def delete_git_config(
@router.post("/config/test")
async def test_git_config(
config: GitServerConfigCreate,
db: Session = Depends(get_db),
_ = Depends(has_permission("admin:settings", "READ"))
):
with belief_scope("test_git_config"):
success = await git_service.test_connection(config.provider, config.url, config.pat)
pat_to_use = config.pat
if pat_to_use == "********":
if config.config_id:
db_config = db.query(GitServerConfig).filter(GitServerConfig.id == config.config_id).first()
if db_config:
pat_to_use = db_config.pat
else:
db_config = db.query(GitServerConfig).filter(GitServerConfig.url == config.url).first()
if db_config:
pat_to_use = db_config.pat
success = await git_service.test_connection(config.provider, config.url, pat_to_use)
if success:
return {"status": "success", "message": "Connection successful"}
else:
@@ -459,7 +591,7 @@ async def init_repository(
try:
# 2. Perform Git clone/init
logger.info(f"[init_repository][Action] Initializing repo for dashboard {dashboard_id}")
git_service.init_repo(dashboard_id, init_data.remote_url, config.pat, repo_key=repo_key)
git_service.init_repo(dashboard_id, init_data.remote_url, config.pat, repo_key=repo_key, default_branch=config.default_branch)
# 3. Save to DB
repo_path = git_service._get_repo_path(dashboard_id, repo_key=repo_key)
@@ -583,11 +715,14 @@ async def create_branch(
branch_data: BranchCreate,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("create_branch"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
_apply_git_identity_from_profile(dashboard_id, db, current_user)
git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch)
return {"status": "success"}
except HTTPException:
@@ -633,11 +768,14 @@ async def commit_changes(
commit_data: CommitCreate,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("commit_changes"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
_apply_git_identity_from_profile(dashboard_id, db, current_user)
git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files)
return {"status": "success"}
except HTTPException:
@@ -679,11 +817,14 @@ async def pull_changes(
dashboard_ref: str,
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("pull_changes"):
try:
dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, config_manager, env_id)
_apply_git_identity_from_profile(dashboard_id, db, current_user)
git_service.pull_changes(dashboard_id)
return {"status": "success"}
except HTTPException:
@@ -734,6 +875,7 @@ async def promote_dashboard(
env_id: Optional[str] = None,
config_manager=Depends(get_config_manager),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("promote_dashboard"):
@@ -762,6 +904,7 @@ async def promote_dashboard(
to_branch,
reason,
)
_apply_git_identity_from_profile(dashboard_id, db, current_user)
result = git_service.promote_direct_merge(
dashboard_id=dashboard_id,
from_branch=from_branch,