|
1 | | -import json |
2 | | -from datetime import datetime |
3 | | -from typing import Any, Optional |
4 | | -from uuid import UUID |
| 1 | +from enum import Enum |
5 | 2 |
|
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 |
| 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 |
11 | 16 |
|
12 | | -from api.core.config import Settings |
13 | | -from api.src.workspaces.schemas import ExternalAppsDefinitionType, QuestDefinitionType |
| 17 | +from api.core.database import Base |
14 | 18 |
|
15 | | -class WorkspaceLongQuestBase(BaseModel): |
16 | 19 |
|
17 | | - workspace_id: int |
| 20 | +class ExternalAppsDefinitionType(Enum): |
| 21 | + NONE = 0 |
| 22 | + PUBLIC = 1 |
| 23 | + PROJECT_GROUP = 2 |
18 | 24 |
|
19 | | - definition: Optional[str] |
20 | | - type: QuestDefinitionType |
21 | | - url: Optional[str] |
22 | 25 |
|
23 | | - modifiedAt: datetime |
24 | | - modifiedBy: UUID |
25 | | - modifiedByName: str |
| 26 | +class Workspace(Base): |
| 27 | + """Workspaces""" |
26 | 28 |
|
27 | | - model_config = ConfigDict(from_attributes=True) |
| 29 | + __tablename__ = "workspaces" |
28 | 30 |
|
29 | | - def validate_definition(self, data, value): |
30 | | - if QuestDefinitionType[data["type"]] == QuestDefinitionType.NONE: |
31 | | - if not value: |
32 | | - return None |
33 | | - raise ValidationError("'definition' field not allowed.") |
| 31 | + id = Column(Integer, primary_key=True) |
| 32 | + type = Column(Unicode, nullable=False) |
34 | 33 |
|
35 | | - if QuestDefinitionType[data["type"]] != QuestDefinitionType.JSON: |
36 | | - return value |
| 34 | + title = Column(Unicode, nullable=False) |
| 35 | + description = Column(Unicode) |
37 | 36 |
|
38 | | - if not value: |
39 | | - raise ValidationError("This field is required.") |
40 | | - if data["url"]: |
41 | | - raise ValidationError("'url' field not allowed.") |
| 37 | + tdeiProjectGroupId = Column(UUID(as_uuid=True), nullable=False) |
| 38 | + tdeiRecordId = Column(UUID(as_uuid=True)) |
| 39 | + tdeiServiceId = Column(UUID(as_uuid=True)) |
42 | 40 |
|
43 | | - try: |
44 | | - parsed = json.loads(value) |
45 | | - if not parsed or not isinstance(parsed, dict): |
46 | | - raise ValidationError("must be a JSON object.") |
47 | | - validate_json_against_schema(parsed, Settings.WS_LONGFORM_SCHEMA_URL) |
48 | | - except json.JSONDecodeError as e: |
49 | | - return ValidationError(f"{e}") |
50 | | - except ValidationError as e: |
51 | | - raise ValidationError(f"{e}") |
| 41 | + tdeiMetadata = Column(JSON) |
52 | 42 |
|
53 | | - return value |
| 43 | + createdAt = Column(DateTime, nullable=False, default=func.now()) |
| 44 | + createdBy = Column(UUID(as_uuid=True), nullable=False) |
| 45 | + createdByName = Column(Unicode) |
54 | 46 |
|
55 | | - def validate_url(self, data, value): |
56 | | - if QuestDefinitionType[data["type"]] == QuestDefinitionType.NONE: |
57 | | - if not value: |
58 | | - return None |
59 | | - raise ValidationError("'url' field not allowed.") |
| 47 | + geometry = Column(Geometry("MULTIPOLYGON", srid=4326)) |
60 | 48 |
|
61 | | - if QuestDefinitionType[data["type"]] != QuestDefinitionType.URL: |
62 | | - return value |
| 49 | + externalAppAccess = Column( |
| 50 | + SmallInteger, nullable=False, default=ExternalAppsDefinitionType.NONE.value |
| 51 | + ) |
63 | 52 |
|
64 | | - if not value: |
65 | | - raise ValidationError("This field is required.") |
66 | | - if data["definition"]: |
67 | | - raise ValidationError("'definition' field not allowed.") |
| 53 | + kartaViewToken = Column(Unicode) |
68 | 54 |
|
69 | | - return value |
| 55 | + longFormQuestDef: Mapped[list["WorkspaceLongQuest"]] = relationship( |
| 56 | + "WorkspaceLongQuest", uselist=False, lazy="joined", cascade="all, delete" |
| 57 | + ) |
70 | 58 |
|
71 | | -class WorkspaceLongQuestUpdate(BaseModel): |
72 | | - definition: Optional[str] |
73 | | - url: Optional[str] |
| 59 | + imageryListDef: Mapped[list["WorkspaceImagery"]] = relationship( |
| 60 | + "WorkspaceImagery", uselist=False, lazy="joined", cascade="all, delete" |
| 61 | + ) |
74 | 62 |
|
75 | | -class WorkspaceImageryBase(BaseModel): |
76 | 63 |
|
77 | | - workspace_id: int |
| 64 | +class QuestDefinitionType(Enum): |
| 65 | + NONE = 0 |
| 66 | + JSON = 1 |
| 67 | + URL = 2 |
78 | 68 |
|
79 | | - # Note the below column is of the JSON *database* type vs string type, so we're not |
80 | | - # using pydantic's JSON mapping, hence this is not defined as Optional[Json[Any]] |
81 | | - definition: Optional[list[Any]] |
82 | 69 |
|
83 | | - modifiedAt: datetime |
84 | | - modifiedBy: UUID |
85 | | - modifiedByName: str |
| 70 | +class WorkspaceLongQuest(Base): |
| 71 | + """Stores mobile app quest definitions for a workspace""" |
86 | 72 |
|
87 | | - model_config = ConfigDict(from_attributes=True) |
| 73 | + __tablename__ = "workspaces_long_quests" |
88 | 74 |
|
89 | | -class WorkspaceImageryUpdate(BaseModel): |
90 | | - definition: Optional[str] |
| 75 | + workspace_id = Column(Integer, ForeignKey(Workspace.id), primary_key=True) |
91 | 76 |
|
92 | | -class WorkspaceBase(BaseModel): |
| 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) |
93 | 80 |
|
94 | | - id: int |
95 | | - type: str = Field(...) |
| 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) |
96 | 86 |
|
97 | | - title: str = Field(...) |
98 | | - description: Optional[str] |
99 | 87 |
|
100 | | - tdeiProjectGroupId: UUID |
101 | | - tdeiRecordId: Optional[UUID] |
102 | | - tdeiServiceId: Optional[UUID] |
| 88 | +class WorkspaceImagery(Base): |
| 89 | + """Stores imagery list for a workspace""" |
103 | 90 |
|
104 | | - tdeiMetadata: Optional[Json[Any]] |
| 91 | + __tablename__ = "workspaces_imagery" |
105 | 92 |
|
106 | | - createdAt: datetime |
107 | | - createdBy: UUID |
108 | | - createdByName: str |
| 93 | + workspace_id = Column(Integer, ForeignKey(Workspace.id), primary_key=True) |
| 94 | + definition = Column(JSON, nullable=True, default=None) |
109 | 95 |
|
110 | | - geometry: Optional[Annotated[str, WKBElement]] |
111 | | - |
112 | | - externalAppAccess: ExternalAppsDefinitionType |
113 | | - |
114 | | - kartaViewToken: Optional[str] |
115 | | - |
116 | | - longFormQuestDef: Optional[WorkspaceLongQuestBase] |
117 | | - |
118 | | - imageryListDef: Optional[WorkspaceImageryBase] |
119 | | - |
120 | | - model_config = ConfigDict(from_attributes=True) |
121 | | - |
122 | | - # there are some legacy records with '', which is not valid JSON, so map those to None |
123 | | - @field_validator("*", mode="before") |
124 | | - @classmethod |
125 | | - def empty_str_to_none(cls, v): |
126 | | - if v == "": |
127 | | - return None |
128 | | - return v |
129 | | - |
130 | | - |
131 | | -class WorkspaceCreate(WorkspaceBase): |
132 | | - pass |
133 | | - |
134 | | -class WorkspaceUpdate(BaseModel): |
135 | | - title: Optional[str] = None |
136 | | - description: Optional[str] = None |
137 | | - externalAppAccess: Optional[ExternalAppsDefinitionType] = None |
138 | | - longFormQuestDef: Optional[WorkspaceLongQuestBase] = None |
139 | | - imageryListDef: Optional[WorkspaceImageryBase] = None |
140 | | - |
141 | | -class WorkspaceResponse(WorkspaceBase): |
142 | | - pass |
143 | | - |
144 | | - |
145 | | -def validate_json_against_schema(json, schema_url) -> bool: |
146 | | - """ |
147 | | - Validate a JSON string against a JSON schema from a URL. |
148 | | - Returns True if valid, raises ValidationError if not. |
149 | | - """ |
150 | | - # Fetch the schema |
151 | | - response = requests.get(schema_url) |
152 | | - response.raise_for_status() |
153 | | - schema = response.json() |
154 | | - |
155 | | - # Validate |
156 | | - validate(instance=json, schema=schema) |
157 | | - return True |
| 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) |
0 commit comments