Skip to content

Commit

Permalink
Merge pull request #25 from mlibrary/DWI-40-delete-an-item
Browse files Browse the repository at this point in the history
DWI-40 Deletion endpoint
  • Loading branch information
niquerio authored Nov 20, 2024
2 parents 3864c7b + c943366 commit a3893cf
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 35 deletions.
65 changes: 49 additions & 16 deletions aim/digifeeds/database/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
Operations that act on the digifeeds database
"""

from sqlalchemy.orm import Session
from sqlalchemy import select, func
from sqlalchemy.orm import Session, joinedload
from aim.digifeeds.database import schemas
from aim.digifeeds.database import models


class NotFoundError(Exception):
pass


def get_item(db: Session, barcode: str):
"""
Get item from the database
Expand All @@ -21,12 +26,25 @@ def get_item(db: Session, barcode: str):
aim.digifeeds.database.models.Item: Item object
"""
return db.query(models.Item).filter(models.Item.barcode == barcode).first()
stmnt = (
select(models.Item)
.filter_by(barcode=barcode)
.options(
# this is here so when we delete the barcode the statuses show up in the output
joinedload(models.Item.statuses)
)
)

item = db.scalars(stmnt).first()
if item is None:
raise NotFoundError()
else:
return item


def get_items_total(db: Session, filter: schemas.ItemFilters = None):
query = get_items_query(db=db, filter=filter)
return query.count()
stmnt = get_items_statement(filter=filter)
return db.execute(select(func.count()).select_from(stmnt.subquery())).scalar_one()


def get_items(
Expand All @@ -45,40 +63,40 @@ def get_items(
Returns:
aim.digifeeds.database.models.Item: Item object
"""
query = get_items_query(db=db, filter=filter)
return query.offset(offset).limit(limit).all()
stmnt = get_items_statement(filter=filter).offset(offset).limit(limit)
return db.scalars(stmnt).all()


def get_items_query(db: Session, filter: schemas.ItemFilters = None):
query = db.query(models.Item)
def get_items_statement(filter: schemas.ItemFilters = None):
stmnt = select(models.Item)

if filter == "in_zephir":
query = query.filter(
stmnt = stmnt.where(
models.Item.statuses.any(models.ItemStatus.status_name == "in_zephir")
)
elif filter == "not_in_zephir":
query = query.filter(
stmnt = stmnt.where(
~models.Item.statuses.any(models.ItemStatus.status_name == "in_zephir")
)
elif filter == "pending_deletion":
query = query.filter(
stmnt = stmnt.where(
models.Item.statuses.any(
models.ItemStatus.status_name == "pending_deletion"
)
)
elif filter == "not_pending_deletion":
query = query.filter(
stmnt = stmnt.where(
~models.Item.statuses.any(
models.ItemStatus.status_name == "pending_deletion"
)
)
elif filter == "not_found_in_alma":
query = query.filter(
stmnt = stmnt.where(
models.Item.statuses.any(
models.ItemStatus.status_name == "not_found_in_alma"
)
)
return query
return stmnt


def add_item(db: Session, item: schemas.ItemCreate):
Expand Down Expand Up @@ -108,7 +126,13 @@ def get_status(db: Session, name: str):
Returns:
aim.digifeeds.database.models.Status: Status object
"""
return db.query(models.Status).filter(models.Status.name == name).first()
stmnt = select(models.Status).filter_by(name=name)

status = db.scalars(stmnt).first()
if status is None:
raise NotFoundError()
else:
return status


def get_statuses(db: Session):
Expand All @@ -120,7 +144,7 @@ def get_statuses(db: Session):
Returns:
aim.digifeeds.database.models.Status: Status object
"""
return db.query(models.Status).all()
return db.scalars(select(models.Status)).all()


def add_item_status(db: Session, item: models.Item, status: models.Status):
Expand All @@ -139,3 +163,12 @@ def add_item_status(db: Session, item: models.Item, status: models.Status):
db.commit()
db.refresh(item)
return item


def delete_item(db: Session, barcode: str):
db_item = get_item(db=db, barcode=barcode)
# need to load this now so the statuses show up in the return
item = schemas.Item(**db_item.__dict__)
db.delete(db_item)
db.commit()
return item
48 changes: 38 additions & 10 deletions aim/digifeeds/database/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from aim.digifeeds.database import crud, schemas
from aim.digifeeds.database.crud import NotFoundError
from aim.services import S

# This is here so SessionLocal won't have a problem in tests in github
Expand Down Expand Up @@ -86,8 +87,9 @@ def get_item(
The item can be fetched by the barcode of the item.
"""

db_item = crud.get_item(barcode=barcode, db=db)
if db_item is None:
try:
db_item = crud.get_item(barcode=barcode, db=db)
except NotFoundError:
raise HTTPException(status_code=404, detail="Item not found")
return db_item

Expand All @@ -114,11 +116,13 @@ def create_item(
"""

item = schemas.ItemCreate(barcode=barcode)
db_item = crud.get_item(barcode=item.barcode, db=db)
if db_item:
try:
crud.get_item(barcode=item.barcode, db=db)
except NotFoundError:
db_item = crud.add_item(item=item, db=db)
return db_item
else:
raise HTTPException(status_code=400, detail="Item already exists")
db_item = crud.add_item(item=item, db=db)
return db_item


desc_put_404 = """
Expand Down Expand Up @@ -151,15 +155,39 @@ def update_item(
This is how to add a status to an existing item.
"""

db_status = crud.get_status(name=status_name, db=db)
if db_status is None:
try:
db_status = crud.get_status(name=status_name, db=db)
except NotFoundError:
raise HTTPException(status_code=404, detail="Status not found")
db_item = crud.get_item(barcode=barcode, db=db)
if db_item is None:

try:
db_item = crud.get_item(barcode=barcode, db=db)
except NotFoundError:
raise HTTPException(status_code=404, detail="Item not found")

return crud.add_item_status(db=db, item=db_item, status=db_status)


@app.delete(
"/items/{barcode}",
response_model_by_alias=False,
responses={
404: {
"description": "Bad request: The item doesn't exist",
"model": schemas.Response404,
}
},
tags=["Digifeeds Database"],
)
def delete_item(barcode: str, db: Session = Depends(get_db)) -> schemas.Item:
try:
db_item = crud.delete_item(db=db, barcode=barcode)
except NotFoundError:
raise HTTPException(status_code=404, detail="Item not found")
S.logger.info(db_item)
return db_item


@app.get("/statuses", tags=["Digifeeds Database"])
def get_statuses(db: Session = Depends(get_db)) -> list[schemas.Status]:
"""
Expand Down
7 changes: 4 additions & 3 deletions aim/digifeeds/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

from sqlalchemy import String, ForeignKey, DateTime
from sqlalchemy.sql import func
from sqlalchemy.sql import func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
from sqlalchemy.ext.associationproxy import association_proxy
import datetime
Expand All @@ -21,7 +21,7 @@ class Item(Base):
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
statuses: Mapped[list["ItemStatus"]] = relationship()
statuses: Mapped[list["ItemStatus"]] = relationship(cascade="all, delete")


class Status(Base):
Expand Down Expand Up @@ -78,7 +78,8 @@ def load_statuses(session: Session):
]
objects = []
for status in statuses:
sts = session.query(Status).filter_by(name=status["name"]).first()
stmnt = select(Status).filter_by(name=status["name"])
sts = session.scalars(stmnt).first()
if sts is None:
objects.append(Status(**status))

Expand Down
32 changes: 28 additions & 4 deletions tests/digifeeds/database/test_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
get_statuses,
add_item_status,
get_items_total,
delete_item,
NotFoundError,
)
from aim.digifeeds.database import models
from sqlalchemy import select
from aim.digifeeds.database.schemas import ItemCreate
import pytest


class TestCrud:
Expand All @@ -18,8 +23,9 @@ def test_get_item(self, db_session):
assert (item_in_db.barcode) == "valid_barcode"

def test_get_item_that_does_not_exist(self, db_session):
item_in_db = get_item(barcode="does not exist", db=db_session)
assert (item_in_db) is None
with pytest.raises(Exception) as exc_info:
get_item(barcode="does not exist", db=db_session)
assert exc_info.type is NotFoundError

def test_get_items_and_total_any(self, db_session):
item1 = add_item(db=db_session, item=ItemCreate(barcode="valid_barcode"))
Expand Down Expand Up @@ -106,10 +112,28 @@ def test_get_status_that_exists(self, db_session):
assert (status.name) == "in_zephir"

def test_get_status_that_does_not_exist(self, db_session):
status = get_status(db=db_session, name="does_not_exist")
assert (status) is None
with pytest.raises(Exception) as exc_info:
get_status(name="does not exist", db=db_session)
assert exc_info.type is NotFoundError

def test_get_statuses(self, db_session):
statuses = get_statuses(db=db_session)
assert (len(statuses)) > 1
assert (statuses[0].name) == "in_zephir"

def test_delete_item(self, db_session):
item = add_item(db=db_session, item=ItemCreate(barcode="valid_barcode"))
status = get_status(db=db_session, name="in_zephir")
add_item_status(db=db_session, item=item, status=status)
delete_item(db=db_session, barcode=item.barcode)
item_result = db_session.scalars(
select(models.Item).filter_by(barcode=item.barcode)
).all()
item_statuses = db_session.scalars(select(models.ItemStatus)).all()
assert item_result == []
assert item_statuses == []

def test_delete_non_existent_item(self, db_session):
with pytest.raises(Exception) as exc_info:
delete_item(barcode="does not exist", db=db_session)
assert exc_info.type is NotFoundError
11 changes: 11 additions & 0 deletions tests/digifeeds/database/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,14 @@ def test_update_existing_item_with_nonexistent_status(client, valid_item):
response = client.put(f"/items/{valid_item.barcode}/status/non_existent_status")
assert response.status_code == 404
assert response.json() == {"detail": "Status not found"}


def test_delete_item(client, valid_item):
response = client.delete(f"/items/{valid_item.barcode}")
assert response.status_code == 200, response.text


def test_delete_not_existent_item(client):
response = client.delete("/items/barcode_does_not_exist")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
7 changes: 5 additions & 2 deletions tests/digifeeds/database/test_models.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
from aim.digifeeds.database.models import Item, Status, ItemStatus
from sqlalchemy import select


class TestItem:
def test_item_valid(self, db_session):
valid_item = Item(barcode="valid_barcode")
db_session.add(valid_item)
db_session.commit()
item = db_session.query(Item).filter_by(barcode="valid_barcode").first()
stmnt = select(Item).filter_by(barcode="valid_barcode")
item = db_session.scalars(stmnt).first()
assert item.barcode == "valid_barcode"
assert item.created_at

def test_item_statuses(self, db_session):
item = Item(barcode="valid_barcode")
db_session.add(item)
db_session.commit()
status = db_session.query(Status).filter_by(name="in_zephir").first()
stmnt = select(Status).filter_by(name="in_zephir")
status = db_session.scalars(stmnt).first()
db_session.refresh(item)
assert (len(item.statuses)) == 0

Expand Down

0 comments on commit a3893cf

Please sign in to comment.