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 = [
|
||||
{
|
||||
"objectKey": "ui.feature.realestate.dashboard",
|
||||
"label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
|
||||
"meta": {"area": "dashboard"}
|
||||
},
|
||||
{
|
||||
"objectKey": "ui.feature.realestate.projects",
|
||||
"label": {"en": "Projects", "de": "Projekte", "fr": "Projets"},
|
||||
|
|
@ -70,6 +75,7 @@ TEMPLATE_ROLES = [
|
|||
},
|
||||
"accessRules": [
|
||||
# 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.parcels", "view": True},
|
||||
# Group-level DATA access
|
||||
|
|
@ -87,6 +93,7 @@ TEMPLATE_ROLES = [
|
|||
},
|
||||
"accessRules": [
|
||||
# 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.parcels", "view": True},
|
||||
# Read-only DATA access (my records)
|
||||
|
|
@ -139,10 +146,139 @@ def registerFeature(catalogService) -> bool:
|
|||
meta=resObj.get("meta")
|
||||
)
|
||||
|
||||
# Sync template roles to database (with AccessRules)
|
||||
_syncTemplateRolesToDb()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).error(f"Failed to register feature '{FEATURE_CODE}': {e}")
|
||||
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
|
||||
from typing import Optional, Dict, Any, List
|
||||
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
|
||||
|
||||
# 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 (
|
||||
Projekt,
|
||||
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])
|
||||
@limiter.limit("120/minute")
|
||||
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
|
||||
return UI_OBJECTS
|
||||
elif featureCode == "realestate":
|
||||
from modules.features.realestate.mainRealEstate import UI_OBJECTS
|
||||
from modules.features.realEstate.mainRealEstate import UI_OBJECTS
|
||||
return UI_OBJECTS
|
||||
else:
|
||||
logger.warning(f"Unknown feature code: {featureCode}")
|
||||
|
|
|
|||
|
|
@ -71,9 +71,6 @@ google-cloud-texttospeech==2.16.3
|
|||
## MSFT Integration
|
||||
msal==1.24.1
|
||||
|
||||
## Azure Integration
|
||||
azure-communication-email>=1.0.0 # Azure Communication Services Email
|
||||
|
||||
## Testing Dependencies
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue