Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/app/api/register_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
api.shoppinglistHousehold, url_prefix="/<int:household_id>/shoppinglist"
)
api.household.register_blueprint(api.tagHousehold, url_prefix="/<int:household_id>/tag")
api.household.register_blueprint(
api.storeHousehold, url_prefix="/<int:household_id>/store"
)

apiv1.register_blueprint(
api.health, url_prefix="/health/8M4F88S8ooi4sMbLBfkkV7ctWwgibW6V"
Expand All @@ -41,6 +44,7 @@
apiv1.register_blueprint(api.settings, url_prefix="/settings")
apiv1.register_blueprint(api.shoppinglist, url_prefix="/shoppinglist")
apiv1.register_blueprint(api.tag, url_prefix="/tag")
apiv1.register_blueprint(api.store, url_prefix="/store")
apiv1.register_blueprint(api.user, url_prefix="/user")
apiv1.register_blueprint(api.upload, url_prefix="/upload")
apiv1.register_blueprint(api.analytics, url_prefix="/analytics")
Expand Down
1 change: 1 addition & 0 deletions backend/app/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .upload import *
from .household import *
from .category import *
from .store import *
from .health_controller import health
from .analytics import *
from .report import *
Expand Down
1 change: 1 addition & 0 deletions backend/app/controller/exportimport/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Item(Schema):
name = fields.String(required=True, validate=lambda a: a and not a.isspace())
category = fields.String(validate=lambda a: a and not a.isspace())
icon = fields.String()
stores = fields.List(fields.String())

class Recipe(Schema):
class Meta:
Expand Down
21 changes: 20 additions & 1 deletion backend/app/controller/item/item_controller.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
from app import db
from app.helpers import validate_args, authorize_household
from flask import jsonify, Blueprint
from app.errors import InvalidUsage, NotFoundRequest
import app.util.description_splitter as description_splitter
from flask_jwt_extended import jwt_required
from app.models import Item, RecipeItems, Recipe, Category
from app.models import Item, RecipeItems, Recipe, Category, Store, ItemStores
from .schemas import SearchByNameRequest, UpdateItem, AddItem

item = Blueprint("item", __name__)
itemHousehold = Blueprint("item", __name__)


def _setItemStores(item: Item, stores: list[dict] | None) -> None:
new_ids = {s["id"] for s in stores or []}
existing_ids = {s.store_id for s in item.stores}
for store_id in existing_ids - new_ids:
ItemStores.query.filter_by(item_id=item.id, store_id=store_id).delete()
for store_id in new_ids - existing_ids:
store = Store.find_by_id(store_id)
if store and store.household_id == item.household_id:
db.session.add(ItemStores(item_id=item.id, store_id=store_id))
db.session.commit()


@itemHousehold.route("", methods=["GET"])
@jwt_required()
@authorize_household()
Expand Down Expand Up @@ -91,6 +104,9 @@ def addItem(args, household_id):
item.icon = args["icon"]
item.save()

if "stores" in args:
_setItemStores(item, args["stores"])

return jsonify(item.obj_to_dict())


Expand Down Expand Up @@ -118,6 +134,9 @@ def updateItem(args, id):
item.name = newName
item.save()

if "stores" in args:
_setItemStores(item, args["stores"])

if "merge_item_id" in args and args["merge_item_id"] != id:
mergeItem = Item.find_by_id(args["merge_item_id"])
if mergeItem:
Expand Down
22 changes: 22 additions & 0 deletions backend/app/controller/item/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ class Meta:
id = fields.Integer(required=True, validate=lambda a: a > 0)
name = fields.String(validate=lambda a: not a or a and not a.isspace())

class Store(Schema):
class Meta:
unknown = EXCLUDE

id = fields.Integer(required=True, validate=lambda a: a > 0)
name = fields.String(validate=lambda a: not a or a and not a.isspace())

category = fields.Nested(Category(), allow_none=True)
icon = fields.String(
validate=lambda a: not a or not a.isspace(),
Expand All @@ -25,6 +32,10 @@ class Meta:
validate=lambda a: not a or not a.isspace(),
allow_none=True,
)
stores = fields.List(
fields.Nested(Store()),
allow_none=True,
)

# if set this merges the specified item into this item thus combining them to one
merge_item_id = fields.Integer(
Expand All @@ -44,6 +55,13 @@ class Meta:
id = fields.Integer(required=True, validate=lambda a: a > 0)
name = fields.String(validate=lambda a: not a or a and not a.isspace())

class Store(Schema):
class Meta:
unknown = EXCLUDE

id = fields.Integer(required=True, validate=lambda a: a > 0)
name = fields.String(validate=lambda a: not a or a and not a.isspace())

category = fields.Nested(Category(), allow_none=True)
icon = fields.String(
validate=lambda a: not a or not a.isspace(),
Expand All @@ -54,3 +72,7 @@ class Meta:
allow_none=False,
required=True,
)
stores = fields.List(
fields.Nested(Store()),
allow_none=True,
)
1 change: 1 addition & 0 deletions backend/app/controller/store/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .store_controller import store, storeHousehold
15 changes: 15 additions & 0 deletions backend/app/controller/store/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from marshmallow import fields, Schema


class AddStore(Schema):
name = fields.String(required=True, validate=lambda a: a and not a.isspace())


class UpdateStore(Schema):
name = fields.String(validate=lambda a: a and not a.isspace())

# if set this merges the specified store into this store thus combining them to one
merge_store_id = fields.Integer(
validate=lambda a: a > 0,
allow_none=True,
)
73 changes: 73 additions & 0 deletions backend/app/controller/store/store_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from app.helpers import validate_args, authorize_household
from flask import jsonify, Blueprint
from app.errors import NotFoundRequest
from flask_jwt_extended import jwt_required
from app.models import Store
from .schemas import AddStore, UpdateStore

store = Blueprint("store", __name__)
storeHousehold = Blueprint("store", __name__)


@storeHousehold.route("", methods=["GET"])
@jwt_required()
@authorize_household()
def getAllStores(household_id):
return jsonify(
[e.obj_to_dict() for e in Store.all_from_household_by_name(household_id)]
)


@store.route("/<int:id>", methods=["GET"])
@jwt_required()
def getStore(id):
store = Store.find_by_id(id)
if not store:
raise NotFoundRequest()
store.checkAuthorized()
return jsonify(store.obj_to_dict())


@storeHousehold.route("", methods=["POST"])
@jwt_required()
@authorize_household()
@validate_args(AddStore)
def addStore(args, household_id):
store = Store()
store.name = args["name"]
store.household_id = household_id
store.save()
return jsonify(store.obj_to_dict())


@store.route("/<int:id>", methods=["POST"])
@jwt_required()
@validate_args(UpdateStore)
def updateStore(args, id):
store = Store.find_by_id(id)
if not store:
raise NotFoundRequest()
store.checkAuthorized()

if "name" in args:
store.name = args["name"]

store.save()

if "merge_store_id" in args and args["merge_store_id"] != id:
mergeStore = Store.find_by_id(args["merge_store_id"])
if mergeStore:
store.merge(mergeStore)

return jsonify(store.obj_to_dict())


@store.route("/<int:id>", methods=["DELETE"])
@jwt_required()
def deleteStoreById(id):
store = Store.find_by_id(id)
if not store:
raise NotFoundRequest()
store.checkAuthorized()
store.delete()
return jsonify({"msg": "DONE"})
3 changes: 2 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .user import User
from .item import Item
from .item import Item, ItemStores
from .association import Association
from .expense import Expense, ExpensePaidFor
from .settings import Settings
Expand All @@ -11,6 +11,7 @@
from .recipe_history import RecipeHistory
from .expense_category import ExpenseCategory
from .category import Category
from .store import Store
from .token import Token
from .household import Household, HouseholdMember
from .file import File
Expand Down
68 changes: 62 additions & 6 deletions backend/app/models/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Model = db.Model
if TYPE_CHECKING:
from app.models import Household, RecipeItems, ShoppinglistItems
from app.models import Household, RecipeItems, ShoppinglistItems, Store
from app.helpers.db_model_base import DbModelBase

Model = DbModelBase
Expand Down Expand Up @@ -61,6 +61,14 @@ class Item(Model, DbModelAuthorizeMixin):
cascade="all, delete-orphan",
),
)
stores: Mapped[List["ItemStores"]] = cast(
Mapped[List["ItemStores"]],
db.relationship(
"ItemStores",
back_populates="item",
cascade="all, delete-orphan",
),
)

# determines order of items in the shoppinglist
ordering = db.Column(db.Integer, server_default="0")
Expand Down Expand Up @@ -92,6 +100,8 @@ def obj_to_dict(
if self.category_id:
category = cast(Category, Category.find_by_id(self.category_id))
res["category"] = category.obj_to_dict()
if self.stores:
res["stores"] = [s.store.obj_to_dict() for s in self.stores]
return res

def obj_to_export_dict(self) -> dict[str, Any]:
Expand All @@ -102,6 +112,8 @@ def obj_to_export_dict(self) -> dict[str, Any]:
res["icon"] = self.icon
if self.category:
res["category"] = self.category.name
if self.stores:
res["stores"] = [s.store.name for s in self.stores]
return res

def save(self, keepDefault: bool = False) -> Self:
Expand All @@ -116,6 +128,7 @@ def merge(self, other: Self) -> None:
from app.models import RecipeItems
from app.models import History
from app.models import ShoppinglistItems
from app.models import ItemStores

if not self.default_key and other.default_key:
self.default_key = other.default_key
Expand Down Expand Up @@ -154,6 +167,14 @@ def merge(self, other: Self) -> None:
db.session.delete(si)
db.session.add(existingSi)

for istore in ItemStores.query.filter(ItemStores.item_id == other.id).all():
istore: ItemStores
if not ItemStores.find_by_ids(self.id, istore.store_id):
istore.item_id = self.id
db.session.add(istore)
else:
db.session.delete(istore)

for history in History.query.filter(History.item_id == other.id).all():
history.item_id = self.id
db.session.add(history)
Expand Down Expand Up @@ -192,12 +213,14 @@ def find_by_default_key(cls, household_id: int, default_key: str) -> Self | None
@classmethod
def find_name_starts_with(cls, household_id: int, starts_with: str) -> Self | None:
starts_with = starts_with.strip()
return (cls.query.filter(
cls.household_id == household_id,
func.lower(cls.name).like(func.lower(starts_with) + "%"),
return (
cls.query.filter(
cls.household_id == household_id,
func.lower(cls.name).like(func.lower(starts_with) + "%"),
)
.order_by(cls.name)
.first()
)
.order_by(cls.name)
.first())

@classmethod
def search_name(cls, name: str, household_id: int) -> list[Self]:
Expand Down Expand Up @@ -261,3 +284,36 @@ def search_name(cls, name: str, household_id: int) -> list[Self]:
if item_count <= 0:
return found
return found


class ItemStores(Model):
__tablename__ = "item_stores"

item_id: Mapped[int] = db.Column(
db.Integer, db.ForeignKey("item.id"), primary_key=True
)
store_id: Mapped[int] = db.Column(
db.Integer, db.ForeignKey("store.id"), primary_key=True
)

item: Mapped["Item"] = cast(
Mapped["Item"],
db.relationship(
"Item",
back_populates="stores",
),
)
store: Mapped["Store"] = cast(
Mapped["Store"],
db.relationship(
"Store",
back_populates="items",
lazy="joined",
),
)

@classmethod
def find_by_ids(cls, item_id: int, store_id: int) -> Self | None:
return cls.query.filter(
cls.item_id == item_id, cls.store_id == store_id
).first()
Loading