RealEstate: routeRealEstate, mainRealEstate, routeFeatureRealEstate, routeSystem, requirements
This commit is contained in:
parent
32f7251b95
commit
5ddb857c4b
5 changed files with 2104 additions and 5 deletions
|
|
@ -15,6 +15,11 @@ FEATURE_ICON = "mdi-home-city"
|
||||||
|
|
||||||
# UI Objects for RBAC catalog
|
# UI Objects for RBAC catalog
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
|
{
|
||||||
|
"objectKey": "ui.feature.realestate.dashboard",
|
||||||
|
"label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
|
||||||
|
"meta": {"area": "dashboard"}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.realestate.projects",
|
"objectKey": "ui.feature.realestate.projects",
|
||||||
"label": {"en": "Projects", "de": "Projekte", "fr": "Projets"},
|
"label": {"en": "Projects", "de": "Projekte", "fr": "Projets"},
|
||||||
|
|
@ -70,6 +75,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
# UI access to main views - vollqualifizierte ObjectKeys
|
# UI access to main views - vollqualifizierte ObjectKeys
|
||||||
|
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.realestate.projects", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.projects", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.realestate.parcels", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.parcels", "view": True},
|
||||||
# Group-level DATA access
|
# Group-level DATA access
|
||||||
|
|
@ -87,6 +93,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
# UI access to view-only views - vollqualifizierte ObjectKeys
|
# UI access to view-only views - vollqualifizierte ObjectKeys
|
||||||
|
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.realestate.projects", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.projects", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.realestate.parcels", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.parcels", "view": True},
|
||||||
# Read-only DATA access (my records)
|
# Read-only DATA access (my records)
|
||||||
|
|
@ -139,10 +146,139 @@ def registerFeature(catalogService) -> bool:
|
||||||
meta=resObj.get("meta")
|
meta=resObj.get("meta")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Sync template roles to database (with AccessRules)
|
||||||
|
_syncTemplateRolesToDb()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.getLogger(__name__).error(f"Failed to register feature '{FEATURE_CODE}': {e}")
|
logging.getLogger(__name__).error(f"Failed to register feature '{FEATURE_CODE}': {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _syncTemplateRolesToDb() -> int:
|
||||||
|
"""
|
||||||
|
Sync template roles and their AccessRules to the database.
|
||||||
|
Creates global template roles (mandateId=None) if they don't exist.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
|
||||||
|
rootInterface = getRootInterface()
|
||||||
|
db = rootInterface.db
|
||||||
|
|
||||||
|
existingRoles = db.getRecordset(
|
||||||
|
Role,
|
||||||
|
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
|
||||||
|
)
|
||||||
|
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
|
||||||
|
|
||||||
|
createdCount = 0
|
||||||
|
for roleTemplate in TEMPLATE_ROLES:
|
||||||
|
roleLabel = roleTemplate["roleLabel"]
|
||||||
|
|
||||||
|
if roleLabel in existingRoleLabels:
|
||||||
|
roleId = existingRoleLabels[roleLabel]
|
||||||
|
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
|
||||||
|
else:
|
||||||
|
newRole = Role(
|
||||||
|
roleLabel=roleLabel,
|
||||||
|
description=roleTemplate.get("description", {}),
|
||||||
|
featureCode=FEATURE_CODE,
|
||||||
|
mandateId=None,
|
||||||
|
featureInstanceId=None,
|
||||||
|
isSystemRole=False
|
||||||
|
)
|
||||||
|
createdRole = db.recordCreate(Role, newRole.model_dump())
|
||||||
|
roleId = createdRole.get("id")
|
||||||
|
existingRoleLabels[roleLabel] = roleId
|
||||||
|
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
|
||||||
|
logging.getLogger(__name__).info(f"Created template role '{roleLabel}' with ID {roleId}")
|
||||||
|
createdCount += 1
|
||||||
|
|
||||||
|
if createdCount > 0:
|
||||||
|
logging.getLogger(__name__).info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
|
||||||
|
|
||||||
|
_repairInstanceRolesAccessRules(db, existingRoleLabels)
|
||||||
|
return createdCount
|
||||||
|
except Exception as e:
|
||||||
|
logging.getLogger(__name__).error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _repairInstanceRolesAccessRules(db, templateRoleLabels: dict) -> int:
|
||||||
|
"""Repair instance-specific roles by copying AccessRules from their template roles."""
|
||||||
|
from modules.datamodels.datamodelRbac import Role, AccessRule
|
||||||
|
|
||||||
|
repairedCount = 0
|
||||||
|
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
|
||||||
|
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
|
||||||
|
|
||||||
|
for instanceRole in instanceRoles:
|
||||||
|
roleLabel = instanceRole.get("roleLabel")
|
||||||
|
instanceRoleId = instanceRole.get("id")
|
||||||
|
templateRoleId = templateRoleLabels.get(roleLabel)
|
||||||
|
if not templateRoleId:
|
||||||
|
continue
|
||||||
|
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
|
||||||
|
if existingRules:
|
||||||
|
continue
|
||||||
|
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
|
||||||
|
if not templateRules:
|
||||||
|
continue
|
||||||
|
for rule in templateRules:
|
||||||
|
newRule = AccessRule(
|
||||||
|
roleId=instanceRoleId,
|
||||||
|
context=rule.get("context"),
|
||||||
|
item=rule.get("item"),
|
||||||
|
view=rule.get("view", False),
|
||||||
|
read=rule.get("read"),
|
||||||
|
create=rule.get("create"),
|
||||||
|
update=rule.get("update"),
|
||||||
|
delete=rule.get("delete"),
|
||||||
|
)
|
||||||
|
db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
|
repairedCount += 1
|
||||||
|
return repairedCount
|
||||||
|
|
||||||
|
|
||||||
|
def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: list) -> int:
|
||||||
|
"""Ensure AccessRules exist for a role based on templates."""
|
||||||
|
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||||
|
|
||||||
|
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
||||||
|
existingSignatures = {(r.get("context"), r.get("item")) for r in existingRules}
|
||||||
|
createdCount = 0
|
||||||
|
|
||||||
|
for template in ruleTemplates or []:
|
||||||
|
context = template.get("context", "UI")
|
||||||
|
item = template.get("item")
|
||||||
|
if (context, item) in existingSignatures:
|
||||||
|
continue
|
||||||
|
if context == "UI":
|
||||||
|
contextEnum = AccessRuleContext.UI
|
||||||
|
elif context == "DATA":
|
||||||
|
contextEnum = AccessRuleContext.DATA
|
||||||
|
elif context == "RESOURCE":
|
||||||
|
contextEnum = AccessRuleContext.RESOURCE
|
||||||
|
else:
|
||||||
|
contextEnum = context
|
||||||
|
newRule = AccessRule(
|
||||||
|
roleId=roleId,
|
||||||
|
context=contextEnum,
|
||||||
|
item=item,
|
||||||
|
view=template.get("view", False),
|
||||||
|
read=template.get("read"),
|
||||||
|
create=template.get("create"),
|
||||||
|
update=template.get("update"),
|
||||||
|
delete=template.get("delete"),
|
||||||
|
)
|
||||||
|
db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
|
createdCount += 1
|
||||||
|
existingSignatures.add((context, item))
|
||||||
|
return createdCount
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,14 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Request, Query, Pat
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext
|
from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
|
|
||||||
# Import models
|
# Import models
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
from modules.datamodels.datamodelPagination import (
|
||||||
|
PaginationParams,
|
||||||
|
PaginatedResponse,
|
||||||
|
PaginationMetadata,
|
||||||
|
normalize_pagination_dict,
|
||||||
|
)
|
||||||
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
|
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||||
from .datamodelFeatureRealEstate import (
|
from .datamodelFeatureRealEstate import (
|
||||||
Projekt,
|
Projekt,
|
||||||
Parzelle,
|
Parzelle,
|
||||||
|
|
@ -57,6 +64,403 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Helper Functions (instanceId-based routes, backend-driven like Trustee) =====
|
||||||
|
|
||||||
|
def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
|
||||||
|
"""Parse pagination parameter from JSON string."""
|
||||||
|
if not pagination:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
paginationDict = json.loads(pagination)
|
||||||
|
if paginationDict:
|
||||||
|
paginationDict = normalize_pagination_dict(paginationDict)
|
||||||
|
return PaginationParams(**paginationDict)
|
||||||
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid pagination parameter: {str(e)}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
||||||
|
"""
|
||||||
|
Validate that the user has access to the feature instance.
|
||||||
|
Returns the mandateId for the instance.
|
||||||
|
"""
|
||||||
|
rootInterface = getRootInterface()
|
||||||
|
featureInterface = getFeatureInterface(rootInterface.db)
|
||||||
|
instance = featureInterface.getFeatureInstance(instanceId)
|
||||||
|
if not instance:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Feature instance '{instanceId}' not found"
|
||||||
|
)
|
||||||
|
if instance.featureCode != "realestate":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Instance '{instanceId}' is not a realestate instance"
|
||||||
|
)
|
||||||
|
if not context.isSysAdmin:
|
||||||
|
featureAccesses = rootInterface.getFeatureAccessesForUser(str(context.user.id))
|
||||||
|
hasAccess = any(
|
||||||
|
str(fa.featureInstanceId) == instanceId and fa.enabled
|
||||||
|
for fa in featureAccesses
|
||||||
|
)
|
||||||
|
if not hasAccess:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail=f"Access denied to feature instance '{instanceId}'"
|
||||||
|
)
|
||||||
|
return str(instance.mandateId)
|
||||||
|
|
||||||
|
|
||||||
|
# Mapping of entity names to Pydantic model classes (for attributes endpoint)
|
||||||
|
_REALESTATE_ENTITY_MODELS = {
|
||||||
|
"Projekt": Projekt,
|
||||||
|
"Parzelle": Parzelle,
|
||||||
|
"Dokument": Dokument,
|
||||||
|
"Gemeinde": Gemeinde,
|
||||||
|
"Kanton": Kanton,
|
||||||
|
"Land": Land,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# INSTANCE-ID ROUTES (backend-driven, analog to Trustee)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/attributes/{entityType}", response_model=Dict[str, Any])
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def get_entity_attributes(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
entityType: str = Path(..., description="Entity type (e.g., Projekt, Parzelle)"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Get attribute definitions for a Real Estate entity. Used by FormGeneratorTable."""
|
||||||
|
await _validateInstanceAccess(instanceId, context)
|
||||||
|
if entityType not in _REALESTATE_ENTITY_MODELS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Unknown entity type: {entityType}. Valid types: {list(_REALESTATE_ENTITY_MODELS.keys())}"
|
||||||
|
)
|
||||||
|
modelClass = _REALESTATE_ENTITY_MODELS[entityType]
|
||||||
|
try:
|
||||||
|
attrDefs = getModelAttributeDefinitions(modelClass)
|
||||||
|
visibleAttrs = [
|
||||||
|
attr for attr in attrDefs.get("attributes", [])
|
||||||
|
if isinstance(attr, dict) and attr.get("visible", True)
|
||||||
|
]
|
||||||
|
return {"attributes": visibleAttrs}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting attributes for {entityType}: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Error getting attributes for {entityType}: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/projects/options", response_model=List[Dict[str, Any]])
|
||||||
|
@limiter.limit("60/minute")
|
||||||
|
async def get_project_options(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Get project options for select dropdowns. Returns: [{ value, label }]"""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
items = interface.getProjekte(recordFilter={"featureInstanceId": instanceId})
|
||||||
|
return [{"value": p.id, "label": getattr(p, "label", None) or p.id} for p in items]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/parcels/options", response_model=List[Dict[str, Any]])
|
||||||
|
@limiter.limit("60/minute")
|
||||||
|
async def get_parcel_options(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Get parcel options for select dropdowns. Returns: [{ value, label }]"""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
items = interface.getParzellen(recordFilter={"featureInstanceId": instanceId})
|
||||||
|
return [{"value": p.id, "label": getattr(p, "label", None) or p.id} for p in items]
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Projects CRUD -----
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/projects", response_model=PaginatedResponse[Projekt])
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def get_projects(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> PaginatedResponse[Projekt]:
|
||||||
|
"""Get all projects for a feature instance with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
recordFilter = {"featureInstanceId": instanceId}
|
||||||
|
items = interface.getProjekte(recordFilter=recordFilter)
|
||||||
|
paginationParams = _parsePagination(pagination)
|
||||||
|
if paginationParams:
|
||||||
|
if paginationParams.sort:
|
||||||
|
for sort_field in reversed(paginationParams.sort):
|
||||||
|
field_name = sort_field.field
|
||||||
|
direction = sort_field.direction.lower()
|
||||||
|
items.sort(
|
||||||
|
key=lambda x: getattr(x, field_name, None),
|
||||||
|
reverse=(direction == "desc")
|
||||||
|
)
|
||||||
|
total_items = len(items)
|
||||||
|
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize
|
||||||
|
start_idx = (paginationParams.page - 1) * paginationParams.pageSize
|
||||||
|
end_idx = start_idx + paginationParams.pageSize
|
||||||
|
paginated_items = items[start_idx:end_idx]
|
||||||
|
return PaginatedResponse(
|
||||||
|
items=paginated_items,
|
||||||
|
pagination=PaginationMetadata(
|
||||||
|
currentPage=paginationParams.page,
|
||||||
|
pageSize=paginationParams.pageSize,
|
||||||
|
totalItems=total_items,
|
||||||
|
totalPages=total_pages,
|
||||||
|
sort=paginationParams.sort or [],
|
||||||
|
filters=paginationParams.filters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return PaginatedResponse(items=items, pagination=None)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/projects/{projectId}", response_model=Projekt)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def get_project_by_id(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
projectId: str = Path(..., description="Project ID"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Projekt:
|
||||||
|
"""Get a single project by ID."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
projekt = interface.getProjekt(projectId)
|
||||||
|
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
|
return projekt
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{instanceId}/projects", response_model=Projekt)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def create_project(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
data: Dict[str, Any] = Body(...),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Projekt:
|
||||||
|
"""Create a new project."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
if "mandateId" not in data:
|
||||||
|
data["mandateId"] = mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = instanceId
|
||||||
|
try:
|
||||||
|
projekt = Projekt(**data)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid data: {str(e)}")
|
||||||
|
return interface.createProjekt(projekt)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{instanceId}/projects/{projectId}", response_model=Projekt)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def update_project(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
projectId: str = Path(..., description="Project ID"),
|
||||||
|
data: Dict[str, Any] = Body(...),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Projekt:
|
||||||
|
"""Update a project."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
projekt = interface.getProjekt(projectId)
|
||||||
|
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
|
updated = interface.updateProjekt(projectId, data)
|
||||||
|
if not updated:
|
||||||
|
raise HTTPException(status_code=500, detail="Update failed")
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{instanceId}/projects/{projectId}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def delete_project(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
projectId: str = Path(..., description="Project ID"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> None:
|
||||||
|
"""Delete a project."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
projekt = interface.getProjekt(projectId)
|
||||||
|
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
|
if not interface.deleteProjekt(projectId):
|
||||||
|
raise HTTPException(status_code=500, detail="Delete failed")
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Parcels CRUD -----
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/parcels", response_model=PaginatedResponse[Parzelle])
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def get_parcels(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> PaginatedResponse[Parzelle]:
|
||||||
|
"""Get all parcels for a feature instance with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
recordFilter = {"featureInstanceId": instanceId}
|
||||||
|
items = interface.getParzellen(recordFilter=recordFilter)
|
||||||
|
paginationParams = _parsePagination(pagination)
|
||||||
|
if paginationParams:
|
||||||
|
if paginationParams.sort:
|
||||||
|
for sort_field in reversed(paginationParams.sort):
|
||||||
|
field_name = sort_field.field
|
||||||
|
direction = sort_field.direction.lower()
|
||||||
|
items.sort(
|
||||||
|
key=lambda x: getattr(x, field_name, None),
|
||||||
|
reverse=(direction == "desc")
|
||||||
|
)
|
||||||
|
total_items = len(items)
|
||||||
|
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize
|
||||||
|
start_idx = (paginationParams.page - 1) * paginationParams.pageSize
|
||||||
|
end_idx = start_idx + paginationParams.pageSize
|
||||||
|
paginated_items = items[start_idx:end_idx]
|
||||||
|
return PaginatedResponse(
|
||||||
|
items=paginated_items,
|
||||||
|
pagination=PaginationMetadata(
|
||||||
|
currentPage=paginationParams.page,
|
||||||
|
pageSize=paginationParams.pageSize,
|
||||||
|
totalItems=total_items,
|
||||||
|
totalPages=total_pages,
|
||||||
|
sort=paginationParams.sort or [],
|
||||||
|
filters=paginationParams.filters
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return PaginatedResponse(items=items, pagination=None)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def get_parcel_by_id(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
parcelId: str = Path(..., description="Parcel ID"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Parzelle:
|
||||||
|
"""Get a single parcel by ID."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
parzelle = interface.getParzelle(parcelId)
|
||||||
|
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
|
return parzelle
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{instanceId}/parcels", response_model=Parzelle)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def create_parcel(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
data: Dict[str, Any] = Body(...),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Parzelle:
|
||||||
|
"""Create a new parcel."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
if "mandateId" not in data:
|
||||||
|
data["mandateId"] = mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = instanceId
|
||||||
|
try:
|
||||||
|
parzelle = Parzelle(**data)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid data: {str(e)}")
|
||||||
|
return interface.createParzelle(parzelle)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def update_parcel(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
parcelId: str = Path(..., description="Parcel ID"),
|
||||||
|
data: Dict[str, Any] = Body(...),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> Parzelle:
|
||||||
|
"""Update a parcel."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
parzelle = interface.getParzelle(parcelId)
|
||||||
|
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
|
updated = interface.updateParzelle(parcelId, data)
|
||||||
|
if not updated:
|
||||||
|
raise HTTPException(status_code=500, detail="Update failed")
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{instanceId}/parcels/{parcelId}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
@limiter.limit("30/minute")
|
||||||
|
async def delete_parcel(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
parcelId: str = Path(..., description="Parcel ID"),
|
||||||
|
context: RequestContext = Depends(getRequestContext)
|
||||||
|
) -> None:
|
||||||
|
"""Delete a parcel."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
interface = getRealEstateInterface(
|
||||||
|
context.user, mandateId=mandateId, featureInstanceId=instanceId
|
||||||
|
)
|
||||||
|
parzelle = interface.getParzelle(parcelId)
|
||||||
|
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
|
if not interface.deleteParzelle(parcelId):
|
||||||
|
raise HTTPException(status_code=500, detail="Delete failed")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LEGACY / STATELESS ROUTES (unchanged)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@router.post("/command", response_model=Dict[str, Any])
|
@router.post("/command", response_model=Dict[str, Any])
|
||||||
@limiter.limit("120/minute")
|
@limiter.limit("120/minute")
|
||||||
async def process_command(
|
async def process_command(
|
||||||
|
|
|
||||||
1562
modules/routes/routeRealEstate.py
Normal file
1562
modules/routes/routeRealEstate.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -106,7 +106,7 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
|
||||||
from modules.features.trustee.mainTrustee import UI_OBJECTS
|
from modules.features.trustee.mainTrustee import UI_OBJECTS
|
||||||
return UI_OBJECTS
|
return UI_OBJECTS
|
||||||
elif featureCode == "realestate":
|
elif featureCode == "realestate":
|
||||||
from modules.features.realestate.mainRealEstate import UI_OBJECTS
|
from modules.features.realEstate.mainRealEstate import UI_OBJECTS
|
||||||
return UI_OBJECTS
|
return UI_OBJECTS
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unknown feature code: {featureCode}")
|
logger.warning(f"Unknown feature code: {featureCode}")
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,6 @@ google-cloud-texttospeech==2.16.3
|
||||||
## MSFT Integration
|
## MSFT Integration
|
||||||
msal==1.24.1
|
msal==1.24.1
|
||||||
|
|
||||||
## Azure Integration
|
|
||||||
azure-communication-email>=1.0.0 # Azure Communication Services Email
|
|
||||||
|
|
||||||
## Testing Dependencies
|
## Testing Dependencies
|
||||||
pytest>=8.0.0
|
pytest>=8.0.0
|
||||||
pytest-asyncio>=0.21.0
|
pytest-asyncio>=0.21.0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue