Skip to content

Commit dedc349

Browse files
committed
Renaming
1 parent 10b9daa commit dedc349

File tree

6 files changed

+216
-236
lines changed

6 files changed

+216
-236
lines changed

api/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class Settings(BaseSettings):
55
"""Application settings."""
66

77
PROJECT_NAME: str = "Workspaces API"
8-
DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost:5432/dbname"
8+
DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost:5432/tasking_manager"
99
DEBUG: bool = False
1010

1111
WS_LONGFORM_SCHEMA_URL: str = (

api/src/workspaces/models.py

Lines changed: 121 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,151 @@
1-
from enum import Enum
1+
import json
2+
from datetime import datetime
3+
from typing import Any, Optional
4+
from uuid import UUID
25

3-
from geoalchemy2 import Geometry
4-
from sqlalchemy import (
5-
JSON,
6-
UUID,
7-
Column,
8-
DateTime,
9-
ForeignKey,
10-
Integer,
11-
SmallInteger,
12-
Unicode,
13-
)
14-
from sqlalchemy.orm import Mapped, relationship
15-
from sqlalchemy.sql import func
6+
import requests
7+
from geoalchemy2 import WKBElement
8+
from jsonschema import ValidationError, validate
9+
from pydantic import BaseModel, ConfigDict, Field, Json, field_validator
10+
from typing_extensions import Annotated
1611

17-
from api.core.database import Base
12+
from api.core.config import Settings
13+
from api.src.workspaces.schemas import ExternalAppsDefinitionType, QuestDefinitionType
1814

1915

20-
class ExternalAppsDefinitionType(Enum):
21-
NONE = 0
22-
PUBLIC = 1
23-
PROJECT_GROUP = 2
16+
class WorkspaceLongQuestBase(BaseModel):
2417

18+
workspace_id: int
2519

26-
class Workspace(Base):
27-
"""Workspaces"""
20+
definition: Optional[str]
21+
type: QuestDefinitionType
22+
url: Optional[str]
2823

29-
__tablename__ = "workspaces"
24+
modifiedAt: datetime
25+
modifiedBy: UUID
26+
modifiedByName: str
3027

31-
id = Column(Integer, primary_key=True)
32-
type = Column(Unicode, nullable=False)
28+
model_config = ConfigDict(from_attributes=True)
3329

34-
title = Column(Unicode, nullable=False)
35-
description = Column(Unicode)
30+
def validate_definition(self, data, value):
31+
if QuestDefinitionType[data["type"]] == QuestDefinitionType.NONE:
32+
if not value:
33+
return None
34+
raise ValidationError("'definition' field not allowed.")
3635

37-
tdeiProjectGroupId = Column(UUID(as_uuid=True), nullable=False)
38-
tdeiRecordId = Column(UUID(as_uuid=True))
39-
tdeiServiceId = Column(UUID(as_uuid=True))
36+
if QuestDefinitionType[data["type"]] != QuestDefinitionType.JSON:
37+
return value
4038

41-
tdeiMetadata = Column(JSON)
39+
if not value:
40+
raise ValidationError("This field is required.")
41+
if data["url"]:
42+
raise ValidationError("'url' field not allowed.")
4243

43-
createdAt = Column(DateTime, nullable=False, default=func.now())
44-
createdBy = Column(UUID(as_uuid=True), nullable=False)
45-
createdByName = Column(Unicode)
44+
try:
45+
parsed = json.loads(value)
46+
if not parsed or not isinstance(parsed, dict):
47+
raise ValidationError("must be a JSON object.")
48+
validate_json_against_schema(parsed, Settings.WS_LONGFORM_SCHEMA_URL)
49+
except json.JSONDecodeError as e:
50+
return ValidationError(f"{e}")
51+
except ValidationError as e:
52+
raise ValidationError(f"{e}")
4653

47-
geometry = Column(Geometry("MULTIPOLYGON", srid=4326))
54+
return value
4855

49-
externalAppAccess = Column(
50-
SmallInteger, nullable=False, default=ExternalAppsDefinitionType.NONE.value
51-
)
56+
def validate_url(self, data, value):
57+
if QuestDefinitionType[data["type"]] == QuestDefinitionType.NONE:
58+
if not value:
59+
return None
60+
raise ValidationError("'url' field not allowed.")
5261

53-
kartaViewToken = Column(Unicode)
62+
if QuestDefinitionType[data["type"]] != QuestDefinitionType.URL:
63+
return value
5464

55-
longFormQuestDef: Mapped[list["WorkspaceLongQuest"]] = relationship(
56-
"WorkspaceLongQuest", uselist=False, lazy="joined", cascade="all, delete"
57-
)
65+
if not value:
66+
raise ValidationError("This field is required.")
67+
if data["definition"]:
68+
raise ValidationError("'definition' field not allowed.")
5869

59-
imageryListDef: Mapped[list["WorkspaceImagery"]] = relationship(
60-
"WorkspaceImagery", uselist=False, lazy="joined", cascade="all, delete"
61-
)
70+
return value
6271

6372

64-
class QuestDefinitionType(Enum):
65-
NONE = 0
66-
JSON = 1
67-
URL = 2
73+
class WorkspaceImageryBase(BaseModel):
6874

75+
workspace_id: int
6976

70-
class WorkspaceLongQuest(Base):
71-
"""Stores mobile app quest definitions for a workspace"""
77+
# Note the below column is of the JSON *database* type vs string type, so we're not
78+
# using pydantic's JSON mapping, hence this is not defined as Optional[Json[Any]]
79+
definition: Optional[list[Any]]
7280

73-
__tablename__ = "workspaces_long_quests"
81+
modifiedAt: datetime
82+
modifiedBy: UUID
83+
modifiedByName: str
7484

75-
workspace_id = Column(Integer, ForeignKey(Workspace.id), primary_key=True)
85+
model_config = ConfigDict(from_attributes=True)
7686

77-
definition = Column(Unicode, nullable=True, default=None)
78-
type = Column(Integer, nullable=False, default=QuestDefinitionType.NONE.value)
79-
url = Column(Unicode, nullable=True, default=None)
8087

81-
modifiedAt = Column(
82-
DateTime, nullable=False, default=func.now(), onupdate=func.now()
83-
)
84-
modifiedBy = Column(UUID(as_uuid=True), nullable=False)
85-
modifiedByName = Column(Unicode, nullable=False)
88+
class WorkspaceBase(BaseModel):
8689

90+
id: int
91+
type: str = Field(...)
8792

88-
class WorkspaceImagery(Base):
89-
"""Stores imagery list for a workspace"""
93+
title: str = Field(...)
94+
description: Optional[str]
9095

91-
__tablename__ = "workspaces_imagery"
96+
tdeiProjectGroupId: UUID
97+
tdeiRecordId: Optional[UUID]
98+
tdeiServiceId: Optional[UUID]
9299

93-
workspace_id = Column(Integer, ForeignKey(Workspace.id), primary_key=True)
94-
definition = Column(JSON, nullable=True, default=None)
100+
tdeiMetadata: Optional[Json[Any]]
95101

96-
modifiedAt = Column(
97-
DateTime, nullable=False, default=func.now(), onupdate=func.now()
98-
)
99-
modifiedBy = Column(UUID(as_uuid=True), nullable=False)
100-
modifiedByName = Column(Unicode, nullable=False)
102+
createdAt: datetime
103+
createdBy: UUID
104+
createdByName: str
105+
106+
geometry: Optional[Annotated[str, WKBElement]]
107+
108+
externalAppAccess: ExternalAppsDefinitionType
109+
110+
kartaViewToken: Optional[str]
111+
112+
longFormQuestDef: Optional[WorkspaceLongQuestBase]
113+
114+
imageryListDef: Optional[WorkspaceImageryBase]
115+
116+
model_config = ConfigDict(from_attributes=True)
117+
118+
# there are some legacy records with '', which is not valid JSON, so map those to None
119+
@field_validator("*", mode="before")
120+
@classmethod
121+
def empty_str_to_none(cls, v):
122+
if v == "":
123+
return None
124+
return v
125+
126+
127+
class WorkspaceCreate(WorkspaceBase):
128+
pass
129+
130+
131+
class WorkspaceUpdate(WorkspaceBase):
132+
pass
133+
134+
135+
class WorkspaceResponse(WorkspaceBase):
136+
pass
137+
138+
139+
def validate_json_against_schema(json, schema_url) -> bool:
140+
"""
141+
Validate a JSON string against a JSON schema from a URL.
142+
Returns True if valid, raises ValidationError if not.
143+
"""
144+
# Fetch the schema
145+
response = requests.get(schema_url)
146+
response.raise_for_status()
147+
schema = response.json()
148+
149+
# Validate
150+
validate(instance=json, schema=schema)
151+
return True

api/src/workspaces/repository.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from sqlalchemy.ext.asyncio import AsyncSession
44

55
from api.core.exceptions import AlreadyExistsException, NotFoundException
6-
from api.src.workspaces.models import Workspace
7-
from api.src.workspaces.schemas import WorkspaceCreate, WorkspaceUpdate
6+
from api.src.workspaces.schemas import Workspace
7+
from api.src.workspaces.models import WorkspaceCreate, WorkspaceUpdate
88

99

1010
class WorkspaceRepository:

api/src/workspaces/routes.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from api.core.logging import get_logger
66
from api.core.security import UserInfo, validate_token
77
from api.src.workspaces.repository import WorkspaceRepository
8-
from api.src.workspaces.schemas import (
8+
from api.src.workspaces.models import (
99
WorkspaceCreate,
1010
WorkspaceLongQuestBase,
1111
WorkspaceResponse,
@@ -18,14 +18,12 @@
1818

1919
router = APIRouter(prefix="/api/v1/workspaces", tags=["workspaces"])
2020

21-
2221
def get_workspace_service(
2322
session: AsyncSession = Depends(get_session),
2423
) -> WorkspaceService:
2524
repository = WorkspaceRepository(session)
2625
return WorkspaceService(repository)
2726

28-
2927
@router.get("/mine", response_model=list[WorkspaceResponse])
3028
async def get_my_workspaces(
3129
service: WorkspaceService = Depends(get_workspace_service),
@@ -38,7 +36,6 @@ async def get_my_workspaces(
3836
logger.error(f"Failed to fetch workspaces: {str(e)}")
3937
raise
4038

41-
4239
@router.get("/{workspace_id}", response_model=WorkspaceResponse)
4340
async def get_workspace(
4441
workspace_id: int,
@@ -54,7 +51,6 @@ async def get_workspace(
5451
logger.error(f"Failed to fetch workspace {workspace_id}: {str(e)}")
5552
raise
5653

57-
5854
@router.post("/", response_model=WorkspaceResponse, status_code=status.HTTP_201_CREATED)
5955
async def create_workspace(
6056
workspace_data: WorkspaceCreate,
@@ -70,7 +66,6 @@ async def create_workspace(
7066
logger.error(f"Failed to create workspace: {str(e)}")
7167
raise
7268

73-
7469
@router.patch("/{workspace_id}", response_model=WorkspaceResponse)
7570
async def update_workspace(
7671
workspace_id: int,
@@ -87,7 +82,6 @@ async def update_workspace(
8782
logger.error(f"Failed to update workspace {workspace_id}: {str(e)}")
8883
raise
8984

90-
9185
@router.delete("/{workspace_id}", status_code=status.HTTP_204_NO_CONTENT)
9286
async def delete_workspace(
9387
workspace_id: int,
@@ -100,7 +94,6 @@ async def delete_workspace(
10094
logger.error(f"Failed to delete workspace {workspace_id}: {str(e)}")
10195
raise
10296

103-
10497
@router.get("/{workspace_id}/quests/long", response_model=WorkspaceResponse)
10598
async def get_long_quest(
10699
workspace_id: int,
@@ -116,7 +109,6 @@ async def get_long_quest(
116109
logger.error(f"Failed to fetch workspace {workspace_id}: {str(e)}")
117110
raise
118111

119-
120112
@router.get("/{workspace_id}/quests/long/settings", response_model=WorkspaceLongQuestBase)
121113
async def get_long_quest_settings(
122114
workspace_id: int,

0 commit comments

Comments
 (0)