Skip to content

Commit 22cc937

Browse files
authored
Merge branch 'main' into weblate-kitchenowl-kitchenowl
2 parents 9b498c5 + 8d55606 commit 22cc937

File tree

104 files changed

+977
-360
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+977
-360
lines changed

backend/app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
STORAGE_PATH = os.getenv("STORAGE_PATH", PROJECT_DIR)
4242
UPLOAD_FOLDER = STORAGE_PATH + "/upload"
43-
ALLOWED_FILE_EXTENSIONS = {"txt", "pdf", "png", "jpg", "jpeg", "gif"}
43+
ALLOWED_FILE_EXTENSIONS = {"txt", "pdf", "png", "jpg", "jpeg", "gif", "webp", "jxl"}
4444

4545
FRONT_URL = os.getenv("FRONT_URL")
4646

backend/app/controller/recipe/recipe_controller.py

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import re
2-
31
from app.errors import NotFoundRequest, InvalidUsage
42
from app.models import Household, RecipeItems, RecipeTags
53
from flask import jsonify, Blueprint
64
from flask_jwt_extended import jwt_required
75
from app.helpers import validate_args, authorize_household
86
from app.models import Recipe, Item, Tag
9-
from recipe_scrapers import scrape_html
10-
from recipe_scrapers._exceptions import SchemaOrgException, NoSchemaFoundInWildMode
117
from app.service.file_has_access_or_download import file_has_access_or_download
12-
from app.service.ingredient_parsing import parseIngredients
8+
from app.service.recipe_scraping import scrape
139
from .schemas import (
1410
SearchByNameRequest,
1511
AddRecipe,
@@ -32,12 +28,13 @@ def getAllRecipes(household_id):
3228

3329

3430
@recipe.route("/<int:id>", methods=["GET"])
35-
@jwt_required()
31+
@jwt_required(optional=True)
3632
def getRecipeById(id):
3733
recipe = Recipe.find_by_id(id)
3834
if not recipe:
3935
raise NotFoundRequest()
40-
recipe.checkAuthorized()
36+
if not recipe.public:
37+
recipe.checkAuthorized()
4138
return jsonify(recipe.obj_to_full_dict())
4239

4340

@@ -60,6 +57,8 @@ def addRecipe(args, household_id):
6057
recipe.yields = args["yields"]
6158
if "source" in args:
6259
recipe.source = args["source"]
60+
if "public" in args:
61+
recipe.public = args["public"]
6362
if "photo" in args and args["photo"] != recipe.photo:
6463
recipe.photo = file_has_access_or_download(args["photo"], recipe.photo)
6564
recipe.save()
@@ -109,6 +108,8 @@ def updateRecipe(args, id): # noqa: C901
109108
recipe.yields = args["yields"]
110109
if "source" in args:
111110
recipe.source = args["source"]
111+
if "public" in args:
112+
recipe.public = args["public"]
112113
if "photo" in args and args["photo"] != recipe.photo:
113114
recipe.photo = file_has_access_or_download(args["photo"], recipe.photo)
114115
recipe.save()
@@ -197,56 +198,7 @@ def scrapeRecipe(args, household_id):
197198
if not household:
198199
raise NotFoundRequest()
199200

200-
try:
201-
scraper = scrape_html(args["url"], wild_mode=True)
202-
except:
203-
return "Unsupported website", 400
204-
recipe = Recipe()
205-
recipe.name = scraper.title()
206-
try:
207-
recipe.time = int(scraper.total_time())
208-
except (NotImplementedError, ValueError, TypeError, AttributeError, SchemaOrgException):
209-
pass
210-
try:
211-
recipe.cook_time = int(scraper.cook_time())
212-
except (NotImplementedError, ValueError, TypeError, AttributeError, SchemaOrgException):
213-
pass
214-
try:
215-
recipe.prep_time = int(scraper.prep_time())
216-
except (NotImplementedError, ValueError, TypeError, AttributeError, SchemaOrgException):
217-
pass
218-
try:
219-
yields = re.search(r"\d*", scraper.yields())
220-
if yields:
221-
recipe.yields = int(yields.group())
222-
except (NotImplementedError, ValueError, TypeError, AttributeError, SchemaOrgException):
223-
pass
224-
description = ""
225-
try:
226-
description = scraper.description() + "\n\n"
227-
except (NotImplementedError, ValueError, TypeError, AttributeError, SchemaOrgException):
228-
pass
229-
try:
230-
description = description + scraper.instructions()
231-
except (NotImplementedError, ValueError, TypeError, AttributeError, SchemaOrgException):
232-
pass
233-
recipe.description = description
234-
recipe.photo = scraper.image()
235-
recipe.source = args["url"]
236-
items = {}
237-
for ingredient in parseIngredients(scraper.ingredients(), household.language):
238-
name = ingredient.name if ingredient.name else ingredient.originalText
239-
item = Item.find_name_starts_with(household_id, name)
240-
if item:
241-
items[ingredient.originalText] = item.obj_to_dict() | {
242-
"description": ingredient.description,
243-
"optional": False,
244-
}
245-
else:
246-
items[ingredient.originalText] = None
247-
return jsonify(
248-
{
249-
"recipe": recipe.obj_to_dict(),
250-
"items": items,
251-
}
252-
)
201+
res = scrape(args["url"], household)
202+
if res:
203+
return jsonify(res)
204+
return "Unsupported website", 400

backend/app/controller/recipe/schemas.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class RecipeItem(Schema):
1515
yields = fields.Integer(validate=lambda a: a >= 0)
1616
source = fields.String()
1717
photo = fields.String()
18+
public = fields.Bool()
1819
items = fields.List(fields.Nested(RecipeItem()))
1920
tags = fields.List(fields.String())
2021

@@ -33,6 +34,7 @@ class RecipeItem(Schema):
3334
yields = fields.Integer(validate=lambda a: a >= 0)
3435
source = fields.String()
3536
photo = fields.String()
37+
public = fields.Bool()
3638
items = fields.List(fields.Nested(RecipeItem()))
3739
tags = fields.List(fields.String())
3840

backend/app/controller/shoppinglist/shoppinglist_controller.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ def addRecipeItems(args, id):
389389
try:
390390
for recipeItem in args["items"]:
391391
item = Item.find_by_id(recipeItem["id"])
392+
item.checkAuthorized()
392393
if item:
393394
description = recipeItem["description"]
394395
con = ShoppinglistItems.find_by_ids(shoppinglist.id, item.id)
@@ -423,4 +424,4 @@ def addRecipeItems(args, id):
423424
db.session.rollback()
424425
raise e
425426

426-
return jsonify(item.obj_to_dict())
427+
return jsonify({"msg": "DONE"})

backend/app/controller/upload/upload_controller.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,28 +48,14 @@ def upload_file():
4848

4949

5050
@upload.route("<filename>", methods=["GET"])
51-
@jwt_required()
51+
@jwt_required(optional=True)
5252
def download_file(filename):
5353
filename = secure_filename(filename)
5454
f: File = File.query.filter(File.filename == filename).first()
5555

5656
if not f:
5757
raise NotFoundRequest()
5858

59-
if f.household or f.recipe:
60-
household_id = None
61-
if f.household:
62-
household_id = f.household.id
63-
if f.recipe:
64-
household_id = f.recipe.household_id
65-
if f.expense:
66-
household_id = f.expense.household_id
67-
f.checkAuthorized(household_id=household_id)
68-
elif f.created_by and current_user and f.created_by == current_user.id:
69-
pass # created by user can access his pictures
70-
elif f.profile_picture:
71-
pass # profile pictures are public
72-
else:
73-
raise ForbiddenRequest()
59+
f.checkAuthorized()
7460

7561
return send_from_directory(UPLOAD_FOLDER, filename)

backend/app/helpers/db_model_authorize_mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
class DbModelAuthorizeMixin(object):
7-
def checkAuthorized(self, requires_admin=False, household_id: int = None):
7+
def checkAuthorized(self, requires_admin=False, household_id: int | None = None):
88
"""
99
Checks if current user ist authorized to access this model. Throws and unauthorized exception if not
1010
IMPORTANT: requires household_id

backend/app/models/file.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import annotations
22
from typing import Self
3+
4+
from flask_jwt_extended import current_user
35
from app import db
46
from app.config import UPLOAD_FOLDER
7+
from app.errors import ForbiddenRequest
58
from app.helpers import DbModelMixin, TimestampMixin, DbModelAuthorizeMixin
69
from app.models.user import User
710
import os
@@ -37,6 +40,21 @@ def isUnused(self) -> bool:
3740
and not self.profile_picture
3841
)
3942

43+
def checkAuthorized(self, requires_admin=False, household_id: int | None = None):
44+
if self.created_by and current_user and self.created_by == current_user.id:
45+
pass # created by user can access his pictures
46+
elif self.profile_picture:
47+
pass # profile pictures are public
48+
elif self.recipe:
49+
if not self.recipe.public:
50+
super().checkAuthorized(household_id=self.recipe.household_id, requires_admin=requires_admin)
51+
elif self.household:
52+
super().checkAuthorized(household_id=self.household.id, requires_admin=requires_admin)
53+
elif self.expense:
54+
super().checkAuthorized(household_id=self.expense.household_id, requires_admin=requires_admin)
55+
else:
56+
raise ForbiddenRequest()
57+
4058
@classmethod
4159
def find(cls, filename: str) -> Self:
4260
"""

backend/app/models/history.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,16 @@ def find_all(cls) -> list[Self]:
8383
def get_recent(cls, shoppinglist_id: int, limit: int = 9) -> list[Self]:
8484
sq = db.session.query(ShoppinglistItems.item_id).subquery().select()
8585
sq2 = (
86-
db.session.query(func.max(cls.id))
87-
.filter(cls.status == Status.DROPPED)
88-
.filter(cls.item_id.notin_(sq))
89-
.group_by(cls.item_id)
90-
.join(cls.item)
91-
.subquery()
92-
.select()
93-
)
94-
return (
95-
cls.query.filter(cls.shoppinglist_id == shoppinglist_id)
96-
.filter(cls.id.in_(sq2))
97-
.order_by(cls.created_at.desc(), cls.item_id)
86+
cls.query.filter(
87+
cls.shoppinglist_id == shoppinglist_id,
88+
cls.status == Status.DROPPED,
89+
cls.item_id.notin_(sq),
90+
)
91+
.distinct(cls.item_id)
92+
.order_by(cls.item_id, cls.created_at.desc())
9893
.limit(limit)
94+
.subquery()
9995
)
96+
alias = db.aliased(cls, sq2)
97+
q = db.session.query(alias).order_by(alias.created_at.desc())
98+
return q.all()

backend/app/models/household.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def obj_to_dict(self) -> dict:
4949
if self.photo_file:
5050
res["photo_hash"] = self.photo_file.blur_hash
5151
return res
52+
53+
def obj_to_public_dict(self) -> dict:
54+
res = super().obj_to_dict(include_columns=["id", "name", "photo", "language"])
55+
if self.photo_file:
56+
res["photo_hash"] = self.photo_file.blur_hash
57+
return res
5258

5359
def obj_to_export_dict(self) -> dict:
5460
return {

backend/app/models/recipe.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Recipe(db.Model, DbModelMixin, TimestampMixin, DbModelAuthorizeMixin):
2020
prep_time = db.Column(db.Integer)
2121
yields = db.Column(db.Integer)
2222
source = db.Column(db.String())
23+
public = db.Column(db.Boolean(), nullable=False, default=False)
2324
suggestion_score = db.Column(db.Integer, server_default="0")
2425
suggestion_rank = db.Column(db.Integer, server_default="0")
2526
household_id = db.Column(
@@ -53,6 +54,7 @@ def obj_to_full_dict(self) -> dict:
5354
res = self.obj_to_dict()
5455
res["items"] = [e.obj_to_item_dict() for e in self.items]
5556
res["tags"] = [e.obj_to_item_dict() for e in self.tags]
57+
res["household"] = self.household.obj_to_public_dict()
5658
return res
5759

5860
def obj_to_export_dict(self) -> dict:

0 commit comments

Comments
 (0)