Skip to content

Commit 910dc3c

Browse files
committed
implemented asset model using filedepot. refactor. added ondelete to foreignkeys
1 parent 6e4c0c5 commit 910dc3c

24 files changed

Lines changed: 338 additions & 114 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ wheels/
1818
# local development files
1919
development.db
2020
.env
21-
reset_db.sh
21+
reset_db.sh
22+
tests/uploads

api/asset.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# ===============================================================================
2+
# Copyright 2025 ross
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# ===============================================================================
16+
from fastapi import APIRouter, Depends, UploadFile
17+
from sqlalchemy import select
18+
from sqlalchemy.orm import Session
19+
20+
from db import get_db_session
21+
from db.asset import Asset
22+
23+
router = APIRouter(prefix="/asset", tags=["asset"])
24+
25+
@router.get("/{asset_id}")
26+
async def get_asset(asset_id: int,
27+
database_session: Session = Depends(get_db_session)
28+
):
29+
"""
30+
Retrieve an asset by its ID.
31+
"""
32+
sql = select(Asset).where(Asset.id == asset_id)
33+
return database_session.scalars(sql).one_or_none()
34+
35+
36+
@router.post("/", status_code=201)
37+
async def add_asset(file: UploadFile,
38+
database_session: Session = Depends(get_db_session)
39+
):
40+
"""
41+
Add a new asset.
42+
"""
43+
asset = Asset()
44+
asset.name = file.filename
45+
asset.file_type = file.content_type
46+
47+
content = file.file.read()
48+
asset.content = content
49+
if file.content_type.startswith('image/'):
50+
asset.photo = content
51+
52+
database_session.add(asset)
53+
database_session.commit()
54+
return asset
55+
# ============= EOF =============================================

api/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
Well,
3737
SampleLocation,
3838
Group,
39-
GroupLocation,
39+
GroupLocationAssociation,
4040
Owner,
4141
Contact,
4242
WellScreen,
@@ -143,7 +143,7 @@ def create_group_location(
143143
"""
144144
Create a new group location association in the database.
145145
"""
146-
return adder(session, GroupLocation, group_location_data)
146+
return adder(session, GroupLocationAssociation, group_location_data)
147147

148148

149149
@router.post(
@@ -384,7 +384,7 @@ async def get_group_locations(session: Session = Depends(get_db_session)):
384384
"""
385385
Retrieve all group locations from the database.
386386
"""
387-
return simple_all_getter(session, GroupLocation)
387+
return simple_all_getter(session, GroupLocationAssociation)
388388

389389

390390
@router.get(
@@ -528,7 +528,7 @@ async def get_group_location_by_id(
528528
"""
529529
Retrieve a group location by ID from the database.
530530
"""
531-
group_location = simple_get_by_id(session, GroupLocation, group_location_id)
531+
group_location = simple_get_by_id(session, GroupLocationAssociation, group_location_id)
532532
if not group_location:
533533
return {"message": "Group location not found"}
534534
return group_location

api/form.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@
1515
# ===============================================================================
1616

1717
from fastapi import APIRouter, Depends, status
18-
from fastapi.responses import JSONResponse
1918

2019
from db import get_db_session
21-
from db.base import SampleLocation, Owner, Contact, Well, Group, GroupLocation
22-
from services.query_helper import simple_get_by_name, simple_get_by_id
20+
from db.base import SampleLocation, Owner, Well, Group
2321
from schemas.form import (
2422
WellForm,
2523
WellFormResponse,
2624
GroundwaterLevelFormResponse,
2725
GroundwaterLevelForm,
2826
)
2927
from services.people_helper import add_contact
28+
from services.query_helper import simple_get_by_name
3029

3130
router = APIRouter(prefix="/form")
3231

core/app.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from fastapi import FastAPI
2020

21-
from db import database_sessionmaker, engine, Base
21+
from db import database_sessionmaker, engine, Base, get_db_session
2222
from db.lexicon import Lexicon, Category, TermCategoryAssociation
2323
from services.lexicon import add_lexicon_term
2424
from .settings import settings
@@ -40,26 +40,28 @@ def init_lexicon():
4040
default_lexicon = json.load(f)
4141

4242
# populate lexicon
43-
with database_sessionmaker() as s:
44-
for term_dict in default_lexicon:
45-
add_lexicon_term(
46-
s, term_dict["term"], term_dict["definition"], term_dict["category"]
47-
)
48-
# s.add(Lexicon(**term_dict))
49-
s.commit()
43+
44+
session = next(get_db_session())
45+
46+
for term_dict in default_lexicon:
47+
add_lexicon_term(
48+
session, term_dict["term"], term_dict["definition"], term_dict["category"]
49+
)
50+
# s.add(Lexicon(**term_dict))
51+
session.commit()
5052

5153

5254
def create_superuser():
5355
from admin.user import User
5456

55-
with database_sessionmaker() as s:
56-
user = User(
57-
username="admin",
58-
password="admin",
59-
is_superuser=True,
60-
)
61-
s.add(user)
62-
s.commit()
57+
session = next(get_db_session())
58+
user = User(
59+
username="admin",
60+
password="admin",
61+
is_superuser=True,
62+
)
63+
session.add(user)
64+
session.commit()
6365

6466

6567
@asynccontextmanager

db/__init__.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515
# ===============================================================================
1616
import os
17-
17+
import re
1818
from geoalchemy2 import load_spatialite
1919
from sqlalchemy import create_engine, Column, Integer, DateTime, func, JSON
2020
from sqlalchemy.event import listen
@@ -75,10 +75,12 @@ def on_connect(dbapi_connection, connection_record):
7575
database_sessionmaker = sessionmaker(engine, expire_on_commit=False)
7676

7777

78-
async def get_db_session():
78+
def get_db_session():
7979
session = database_sessionmaker()
80-
yield session
81-
session.close()
80+
try:
81+
yield session
82+
finally:
83+
session.close()
8284

8385

8486
Base = declarative_base()
@@ -105,20 +107,16 @@ class AuditMixin:
105107
def created_at(self):
106108
return Column(DateTime, nullable=False, server_default=func.now())
107109

108-
@declared_attr
109-
def updated_at(self):
110-
return Column(
111-
DateTime,
112-
nullable=False,
113-
server_default=func.now(),
114-
server_onupdate=func.now(),
115-
)
110+
111+
def pascal_to_snake(name):
112+
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
116113

117114

118115
class AutoBaseMixin(AuditMixin):
119116
@declared_attr
120117
def __tablename__(self):
121-
return self.__name__.lower()
118+
return pascal_to_snake(self.__name__)
119+
122120

123121
@declared_attr
124122
def id(self):

db/asset.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# ===============================================================================
2+
# Copyright 2025 ross
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# ===============================================================================
16+
from sqlalchemy import Column, String, Integer, ForeignKey
17+
from sqlalchemy.orm import relationship
18+
19+
from db import Base, AutoBaseMixin
20+
21+
from depot.fields.sqlalchemy import UploadedFileField
22+
from depot.fields.specialized.image import UploadedImageWithThumb
23+
24+
25+
class Asset(Base, AutoBaseMixin):
26+
name = Column(String(100), nullable=False, unique=True)
27+
file_type = Column(String(50), nullable=False)
28+
29+
content = Column(UploadedFileField)
30+
photo = Column(UploadedFileField(upload_type=UploadedImageWithThumb))
31+
32+
33+
class AssetLocationAssociation(Base, AutoBaseMixin):
34+
35+
36+
asset_id = Column(Integer, ForeignKey("asset.id", ondelete='CASCADE'), nullable=False)
37+
location_id = Column(Integer, ForeignKey("sample_location.id", ondelete="CASCADE"), nullable=False)
38+
39+
location = relationship("SampleLocation", back_populates="asset_associations")
40+
41+
# publication = relationship("Publication", back_populates="author_associations")
42+
# author = relationship("Author", back_populates="publication_associations")
43+
44+
# ============= EOF =============================================

db/base.py

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,16 @@
1919
Integer,
2020
String,
2121
ForeignKey,
22-
UUID,
2322
Float,
2423
Boolean,
2524
Text,
2625
DateTime,
27-
func,
2826
)
29-
from sqlalchemy.orm import relationship, declared_attr, Mapped, mapped_column
27+
from sqlalchemy.ext.associationproxy import association_proxy
28+
from sqlalchemy.orm import relationship, Mapped, mapped_column
3029
from sqlalchemy_utils import TSVectorType
3130

3231
from db import Base, AutoBaseMixin
33-
from db.lexicon import Lexicon
3432

3533

3634
class SampleLocation(Base, AutoBaseMixin):
@@ -42,21 +40,13 @@ class SampleLocation(Base, AutoBaseMixin):
4240
Geometry(geometry_type="POINT", srid=4326, spatial_index=True)
4341
)
4442

45-
owner_id = Column(Integer, ForeignKey("owner.id"), nullable=True)
43+
owner_id = Column(Integer, ForeignKey("owner.id", ondelete='CASCADE'), nullable=True)
4644

47-
48-
class Asset(Base, AutoBaseMixin):
49-
fs_path = Column(String(255), nullable=False, unique=True)
50-
name = Column(String(100), nullable=False, unique=True)
51-
file_type = Column(String(50), nullable=False)
52-
53-
54-
class AssetLocation(Base, AutoBaseMixin):
55-
asset_id = Column(Integer, ForeignKey("asset.id"), nullable=False)
56-
location_id = Column(Integer, ForeignKey("samplelocation.id"), nullable=False)
57-
58-
asset = relationship("Asset")
59-
location = relationship("SampleLocation")
45+
asset_associations = relationship("AssetLocationAssociation",
46+
back_populates="location",
47+
cascade="all, delete-orphan"
48+
)
49+
assets = association_proxy("asset_associations", "asset")
6050

6151

6252
class Owner(Base, AutoBaseMixin):
@@ -95,7 +85,7 @@ class Contact(Base, AutoBaseMixin):
9585

9686

9787
class Well(Base, AutoBaseMixin):
98-
location_id = Column(Integer, ForeignKey("samplelocation.id"), nullable=False)
88+
location_id = Column(Integer, ForeignKey("sample_location.id", ondelete="CASCADE"), nullable=False)
9989

10090
ose_pod_id = Column(String(50), nullable=True)
10191
api_id = Column(String(50), nullable=True, default="") # API well number
@@ -136,7 +126,7 @@ class Well(Base, AutoBaseMixin):
136126

137127

138128
class WellScreen(Base, AutoBaseMixin):
139-
well_id = Column(Integer, ForeignKey("well.id"), nullable=False)
129+
well_id = Column(Integer, ForeignKey("well.id", ondelete='CASCADE'), nullable=False)
140130
screen_depth_top = Column(
141131
Float, nullable=False, info={"unit": "feet below ground surface"}
142132
)
@@ -148,7 +138,7 @@ class WellScreen(Base, AutoBaseMixin):
148138
) # e.g., "PVC", "Steel", etc.
149139

150140
# Define a relationship to well if needed
151-
well = relationship("Well")
141+
# well = relationship("Well")
152142

153143

154144
class Equipment(Base, AutoBaseMixin):
@@ -159,14 +149,14 @@ class Equipment(Base, AutoBaseMixin):
159149
date_removed = Column(DateTime)
160150
recording_interval = Column(Integer)
161151
equipment_notes = Column(String(50))
162-
location_id = Column(Integer, ForeignKey("samplelocation.id"), nullable=False)
152+
location_id = Column(Integer, ForeignKey("sample_location.id", ondelete="CASCADE"), nullable=False)
163153

164154
location = relationship("SampleLocation")
165155

166156

167157
class Spring(Base, AutoBaseMixin):
168158
description = Column(String(255), nullable=True)
169-
location_id = Column(Integer, ForeignKey("samplelocation.id"), nullable=False)
159+
location_id = Column(Integer, ForeignKey("sample_location.id", ondelete="CASCADE"), nullable=False)
170160

171161
# Define a relationship to samplelocations if needed
172162
location = relationship("SampleLocation")
@@ -177,12 +167,12 @@ class Group(Base, AutoBaseMixin):
177167
description = Column(String(255), nullable=True)
178168

179169
# Define a relationship to samplelocations if needed
180-
locations = relationship("SampleLocation", secondary="grouplocation")
170+
locations = relationship("SampleLocation", secondary="group_location_association")
181171

182172

183-
class GroupLocation(Base, AutoBaseMixin):
184-
group_id = Column(Integer, ForeignKey("group.id"), nullable=False)
185-
location_id = Column(Integer, ForeignKey("samplelocation.id"), nullable=False)
173+
class GroupLocationAssociation(Base, AutoBaseMixin):
174+
group_id = Column(Integer, ForeignKey("group.id", ondelete='CASCADE'), nullable=False)
175+
location_id = Column(Integer, ForeignKey("sample_location.id", ondelete="CASCADE"), nullable=False)
186176

187177
# group = relationship("Group")
188178
# location = relationship("SampleLocation")

db/chemistry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class WaterChemistryAnalysisSet(Base, AutoBaseMixin):
5252

5353
__tablename__ = "water_chemistry_analysis_set"
5454

55-
well_id = mapped_column(Integer, ForeignKey("well.id"))
55+
well_id = mapped_column(Integer, ForeignKey("well.id" , ondelete='CASCADE'))
5656
note = mapped_column(String(255), nullable=True)
5757

5858
collection_timestamp = mapped_column(DateTime, nullable=False)

db/collabnet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class CollaborativeNetworkWell(Base, AutoBaseMixin):
2323
""" """
2424

2525
actively_monitored = mapped_column(Boolean, default=False, nullable=False)
26-
well_id = mapped_column(Integer, ForeignKey("well.id"), nullable=False)
26+
well_id = mapped_column(Integer, ForeignKey("well.id", ondelete='CASCADE'), nullable=False)
2727

2828
well = relationship("Well")
2929

0 commit comments

Comments
 (0)