Feature/authentication (#213)

* basic crud NOT SECURE

* refactor/database init on startup

* added scratch.py

* tests/user CRUD routes

* password hashing

* change app_config location

* bump python version

* formatting

* login ui starter

* change import from url design

* move components

* remove old snackbar

* refactor/Componenet folder structure rework

* refactor/remove old code

* refactor/rename componenets/js files

* remove console.logs

* refactor/ models to schema and sql to models

* new header styling for imports

* token request

* fix url scrapper

* refactor/rename schema files

* split routes file

* redesigned admin page

* enable relative imports for vue components

* refactor/switch to pages view

* add CamelCase package

* majors settings rework

* user management second pass

* super user CRUD

* refactor/consistent models names

* refactor/consistent model names

* password reset

* store refactor

* dependency update

* abstract button props

* profile page refactor

* basic password validation

* login form refactor/split v-container

* remo unused code

* hide editor buttons when not logged in

* mkdocs dev dependency

* v0.4.0 docs update

* profile image upload

* additional token routes

* Smaller recipe cards for smaller viewports

* fix admin sidebar

* add users

* change to outlined

* theme card starter

* code cleanup

* signups

* signup pages

* fix #194

* fix #193

* clarify mealie_port

* fix #184

* fixes #178

* fix blank card error on meal-plan creator

* admin signup

* formatting

* improved search bar

* improved search bar

* refresh token on page refresh

* allow mealplan with no categories

* fix card layout

* remove cdn dependencies

* start on groups

* Fixes #196

* recipe databse refactor

* changelog draft

* database refactoring

* refactor recipe schema/model

* site settings refactor

* continued model refactor

* merge docs changes from master

* site-settings work

* cleanup + tag models

* notes

* typo

* user table

* sign up data validation

* package updates

* group store init

* Fix home page settings

* group admin init

* group dashboard init

* update deps

* formatting

* bug / added libffi-dev

* pages refactor

* fix mealplan

* docs update

* multi group supporot for job scheduler

* formatting

* formatting

* home-page redesign

* set background for docs darkmode

* code cleanup

* docs refactor

* v0.4.0 image

* mkdocs port change

* formatting

* Fix Meal-Plan Today

* fix webhook bug

* fix meal plan this week

* export users

* 📦 Proper Package + Black Config

* formatting

* delete old files

* fix ci

* fix failing builds

* package/makefile docs update

* add docs server to tasks

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-03-20 13:44:10 -08:00 committed by GitHub
commit 25ec3a66f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
190 changed files with 17972 additions and 3679 deletions

View file

@ -31,7 +31,7 @@ jobs:
virtualenvs-create: true
virtualenvs-in-project: true
# #----------------------------------------------
# # load cached venv if cache exists
# # load cached venv if cache exists #! This Breaks Stuff
# #----------------------------------------------
# - name: Load cached venv
# id: cached-poetry-dependencies
@ -51,4 +51,4 @@ jobs:
- name: Run tests
run: |
source .venv/bin/activate
pytest mealie/tests/
pytest

View file

@ -5,13 +5,12 @@
"python.linting.enabled": true,
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],
"python.analysis.extraPaths": ["mealie", "mealie/mealie"],
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.autoTestDiscoverOnSaveEnabled": false,
"python.testing.pytestArgs": ["tests"],
"cSpell.enableFiletypes": ["!javascript", "!python"],
"python.testing.pytestArgs": ["mealie"],
"i18n-ally.localesPaths": "frontend/src/locales/messages",
"i18n-ally.sourceLanguage": "en",
"i18n-ally.enabledFrameworks": ["vue"],

37
.vscode/tasks.json vendored
View file

@ -3,12 +3,10 @@
"tasks": [
{
"label": "DEV: Build and Start Docker Compose",
"command": "./dev/scripts/docker-compose.dev.sh",
"command": "make docker-dev",
"type": "shell",
"args": [],
"problemMatcher": [
"$tsc"
],
"problemMatcher": ["$tsc"],
"presentation": {
"reveal": "always"
},
@ -16,26 +14,18 @@
},
{
"label": "Production: Build and Start Docker Compose",
"command": "./dev/scripts/docker-compose.sh",
"command": "make docker-prod",
"type": "shell",
"args": [],
"problemMatcher": [
"$tsc"
],
"problemMatcher": ["$tsc"],
"presentation": {
"reveal": "always"
},
"group": "test"
},
{
"label": "Dev: Start local Backend",
"command": "../${config:python.pythonPath}",
"args": [
"app.py"
],
"options": {
"cwd": "${workspaceFolder}/mealie/"
},
"label": "Dev: Start Backend",
"command": "make backend",
"type": "shell",
"presentation": {
"reveal": "always",
@ -44,12 +34,19 @@
"problemMatcher": []
},
{
"label": "Dev: Start local Frontend",
"command": "npm run serve",
"label": "Dev: Start Frontend",
"command": "make vue",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}/frontend/"
"presentation": {
"reveal": "always",
"group": "groupA"
},
"problemMatcher": []
},
{
"label": "Dev: Start Docs Server",
"command": "make mdocs",
"type": "shell",
"presentation": {
"reveal": "always",
"group": "groupA"

View file

@ -10,10 +10,11 @@ FROM python:3.9-alpine
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2 caddy libffi-dev
ENV ENV prod
EXPOSE 80
WORKDIR /app
WORKDIR /app/
COPY ./pyproject.toml /app/
RUN apk add --update --no-cache --virtual .build-deps \
curl \
g++ \
@ -29,13 +30,12 @@ RUN apk add --update --no-cache --virtual .build-deps \
apk --purge del .build-deps
COPY ./mealie /app
COPY ./mealie /app/mealie
COPY ./Caddyfile /app
COPY ./app_data/templates /app/data/templates
RUN rm -rf /app/tests /app/.temp
COPY --from=build-stage /app/dist /app/dist
VOLUME [ "/app/data/" ]
RUN chmod +x /app/run.sh
CMD /app/run.sh
RUN chmod +x /app/mealie/run.sh
CMD /app/mealie/run.sh

View file

@ -1,21 +1,21 @@
FROM python:3
WORKDIR /app/
RUN apt-get update -y && \
apt-get install -y python-pip python-dev
# Install Poetry
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
cd /usr/local/bin && \
ln -s /opt/poetry/bin/poetry && \
poetry config virtualenvs.create false
# Copy poetry.lock* in case it doesn't exist in the repo
COPY ./pyproject.toml ./poetry.lock* /app/
WORKDIR /app
RUN poetry install
RUN poetry install --no-root
COPY ./mealie /app/mealie
COPY ./mealie /app
ENTRYPOINT [ "python" ]
CMD [ "app.py" ]
CMD ["uvicorn", "mealie.app:app", "--host", "0.0.0.0", "--port", "9000"]

View file

@ -1,19 +1,17 @@
![Recipe Image](../images/{{ recipe.image }})
![Recipe Image](../../images/{{ recipe.image }})
# {{ recipe.name }}
{{ recipe.description }}
## Ingredients
{% for ingredient in recipe.recipeIngredient %}
- [ ] {{ ingredient }}
{% endfor %}
{% for ingredient in recipe.recipeIngredient %}
- [ ] {{ ingredient }} {% endfor %}
## Instructions
{% for step in recipe.recipeInstructions %}
- [ ] {{ step.text }}
{% endfor %}
{% for step in recipe.recipeInstructions %}
- [ ] {{ step.text }} {% endfor %}
{% for note in recipe.notes %}
**{{ note.title }}:** {{ note.text }}

View file

@ -1,48 +0,0 @@
# Getting A Developer Instance Started
For the best experience developing I recommend using docker. I've used both WSL2 and Ubuntu to develop mealie and have had no issues with cross compatibility with docker. 2 Scripts are available along ith docker-compose files to make development instances easier. After cloning the repo you can set the scripts in /dev/scripts/ as executable and then use VSCode tasks to execute the scripts or execute them from the CLI.
`docker-compose.dev.sh` Will spin up a development stack with hot-reloading enabled.
`docker-compose.sh` Will spin up a production version of the stack.
After the stack is running navigate to the [admin page localhost:9090/settings/site](http://localhost:9090/settings/site). On the Backups and Exports section import the backup labeled dev_sample_data_{DATE}.zip. This will give you some recipe data to work with.
Once you're up and running you should be able to make changes and see them reflected on the frontend/backend. If you're not sure what to work on you can check:
- The Todo's below.
- The [Development Road Map](https://hay-kot.github.io/mealie/2.0%20-%20roadmap/)
- The [Current Open Issues](https://github.com/hay-kot/mealie/issues)
Don't forget to [join the Discord](https://discord.gg/R6QDyJgbD2)!
# Todo's
Test
- [ ] Image Upload Test
- [ ] Rename and Upload Image Test
- [x] Chowdown Migration End Point Test
Frontend
- [ ] No Meal Today Page instead of Null
- [ ] Recipe Print Page
- [ ] Recipe Editor Data Validation Client Side
- [ ] Organize Home Page my Category, ideally user selectable.
- [ ] Advanced Search Page, draft started
- [ ] Filter by Category
- [ ] Filter by Tags
- [ ] Search Bar Results Redesign
Backend
- [ ] Database Import
- [x] Recipes
- [x] Images
- [ ] Meal Plans
- [x] Settings
- [x] Themes
- [ ] Remove Print / Debug Code
- [ ] Support how to sections and how to steps
- [ ] Recipe request by category/tags
SQL
- [ ] Setup Database Migrations
# Draft Changelog

View file

@ -1 +0,0 @@
http://www.cookingforkeeps.com/2013/02/05/blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2/

View file

@ -1,32 +0,0 @@
import requests
import json
POST_URL = "http://localhost:9921/api/recipe/create-url"
URL_LIST = [
"https://www.bonappetit.com/recipe/hallacas"
"https://www.bonappetit.com/recipe/oat-and-pecan-brittle-cookies",
"https://www.bonappetit.com/recipe/tequila-beer-and-citrus-cocktail",
"https://www.bonappetit.com/recipe/corn-and-crab-beignets-with-yaji-aioli",
"https://www.bonappetit.com/recipe/nan-e-berenji",
"https://www.bonappetit.com/recipe/ginger-citrus-cookies",
"https://www.bonappetit.com/recipe/chocolate-pizzettes-cookies",
"https://www.bonappetit.com/recipe/swedish-glogg",
"https://www.bonappetit.com/recipe/roasted-beets-with-dukkah-and-sage",
"https://www.bonappetit.com/recipe/collard-greens-salad-with-pickled-fennel-and-coconut"
"https://www.bonappetit.com/recipe/sparkling-wine-cocktail",
"https://www.bonappetit.com/recipe/pretzel-and-potato-chip-moon-pies",
"https://www.bonappetit.com/recipe/coffee-hazlenut-biscotti",
"https://www.bonappetit.com/recipe/curry-cauliflower-rice",
"https://www.bonappetit.com/recipe/life-of-the-party-layer-cake",
"https://www.bonappetit.com/recipe/marranitos-enfiestados",
]
if __name__ == "__main__":
for url in URL_LIST:
data = {"url": url}
data = json.dumps(data)
try:
response = requests.post(POST_URL, data)
except:
continue

View file

@ -1,39 +0,0 @@
import json
import requests
POST_URL = "http://localhost:9921/api/site-settings/themes/create/"
GET_URL = "http://localhost:9921/api/site-settings/themes/"
SITE_SETTINGS = [
{
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#FFFD99",
"warning": "#FF4081",
"error": "#EF5350",
},
},
{
"name": "purple",
"colors": {
"accent": "#4527A0",
"primary": "#FF4081",
"secondary": "#26C6DA",
"success": "#4CAF50",
"info": "#2196F3",
"warning": "#FB8C00",
"error": "#FF5252",
},
},
]
if __name__ == "__main__":
for theme in SITE_SETTINGS:
data = json.dumps(theme)
response = requests.post(POST_URL, data)
response = requests.get(GET_URL)

View file

@ -1 +0,0 @@
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build

View file

@ -1 +0,0 @@
docker-compose -p mealie up --build

View file

@ -2,19 +2,19 @@
version: "3.1"
services:
# Vue Frontend
mealie-frontend:
image: mealie-frontend:dev
build:
context: ./frontend
dockerfile: frontend.Dockerfile
restart: always
ports:
- 9920:8080
environment:
VUE_APP_API_BASE_URL: "http://mealie-api:9000"
volumes:
- ./frontend/:/app
- /app/node_modules
# mealie-frontend:
# image: mealie-frontend:dev
# build:
# context: ./frontend
# dockerfile: frontend.Dockerfile
# restart: always
# ports:
# - 9920:8080
# environment:
# VUE_APP_API_BASE_URL: "http://mealie-api:9000"
# volumes:
# - ./frontend/:/app
# - /app/node_modules
# Fast API
mealie-api:
@ -32,11 +32,11 @@ services:
- ./app_data:/app_data
- ./mealie:/app
mealie-docs:
image: squidfunk/mkdocs-material
restart: always
ports:
- 9922:8000
volumes:
- ./docs:/docs
# mealie-docs:
# image: squidfunk/mkdocs-material
# restart: always
# ports:
# - 9922:8000
# volumes:
# - ./docs:/docs

View file

@ -73,3 +73,7 @@
- Removed CDN dependencies
- Database Model Refactoring
- File/Folder Name Refactoring
- Development is now easier with a makefile!
- Mealie is now a proper package using poetry
- Test refactoring
- Test Coverage 75%

View file

@ -3,27 +3,36 @@
After reading through the [Code Contributions Guide](https://hay-kot.github.io/mealie/contributors/developers-guide/code-contributions/) and forking the repo you can start working. This project is developed with :whale: docker and as such you will be greatly aided by using docker for development. It's not necessary but it is helpful.
## With Docker
`cd` into frontend directory and run `npm install` to install the node modules.
Prerequisites
There are 2 scripts to help set up the docker containers in dev/scripts/.
`docker-compose.dev.sh` - Will spin up a docker development server
`docker-compose.sh` - Will spin up a docker production server
There are VSCode tasks created in the .vscode folder. You can use these to quickly execute the scripts above using the command palette.
- Docker
- docker-compose
You can easily start the development stack by running `make docker-dev` in the root of the project directory. This will run and build the docker-compose.dev.yml file.
## Without Docker
Prerequisites
- Python 3.8+
- Python 3.9+
- Poetry
- Nodejs
- npm
change directories into the mealie directory and run poetry install. cd into the frontend directory and run npm install. After installing dependencies, you can use vscode tasks to run the front and backend server. Use the command pallette to access the tasks.
Once the prerequisites are installed you can cd into the project base directory and run `make setup` to install the python and node dependencies. Once that is complete you can run `make backend` and `make vue` to start the backend and frontend servers.
## Make File Reference
`make setup` installs python and node dependencies
`make backend` Starts the backend server on port `9000`
`make vue` Starts the frontend server on port `8080`
`make mdocs` Starts the documentation server on port `8000`
`make docker-dev` Builds docker-compose.dev.yml
`make docker-prod` Builds docker-compose.yml to test for production
Alternatively you can run `npm run serve` in the frontend directory and `python app.py` in the mealie directory to get everything up and running for development.
## Trouble Shooting

View file

@ -12,15 +12,15 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
## Key Features
- 🔍 Powerful fuzzy search
- 🔍 Fuzzy search
- 🏷️ Tag recipes with categories or tags to flexible sorting
- ⬇️ Import recipes from around the web by URL
- 🕸 Import recipes from around the web by URL
- 📱 Beautiful Mobile Views
- 📆 Create Meal Plans
- 🛒 Generate shopping lists from Meal Plans
- 🛒 Generate shopping lists
- 🐳 Easy setup with Docker
- 🎨 Customize your interface with color themes layouts
- ✉️ Export all your data in any format with Jinja2 Templates, with easy data restoration from the UI.
- 💾 Export all your data in any format with Jinja2 Templates, with easy data restoration from the user interface.
- 🌍 localized in many languages
- Plus tons more!
- Flexible API

View file

@ -1,4 +1,4 @@
FROM node:lts-alpine
FROM node:latest
# # install simple http server for serving static content
# RUN npm install -g http-server

20316
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
},
"dependencies": {
"@adapttive/vue-markdown": "^3.0.3",
"@adapttive/vue-markdown": "^4.0.1",
"@smartweb/vue-flash-message": "^0.6.10",
"axios": "^0.21.1",
"core-js": "^3.9.1",

24
makefile Normal file
View file

@ -0,0 +1,24 @@
setup:
poetry install && \
cd frontend && \
npm install && \
cd ..
backend:
source ./.venv/bin/activate && python mealie/app.py
vue:
cd frontend && npm run serve
mdocs:
source ./.venv/bin/activate && \
cd docs && \
mkdocs serve
docker-dev:
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build
docker-prod:
docker-compose -p mealie up --build -d

View file

@ -3,25 +3,25 @@ from fastapi import FastAPI
from fastapi.logger import logger
# import utils.startup as startup
from core.config import APP_VERSION, PORT, docs_url, redoc_url
from db.db_setup import sql_exists
from db.init_db import init_db
from routes import (
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
from mealie.db.db_setup import sql_exists
from mealie.db.init_db import init_db
from mealie.routes import (
backup_routes,
debug_routes,
migration_routes,
setting_routes,
theme_routes,
)
from routes.groups import groups
from routes.mealplans import mealplans
from routes.recipe import (
from mealie.routes.groups import groups
from mealie.routes.mealplans import mealplans
from mealie.routes.recipe import (
all_recipe_routes,
category_routes,
recipe_crud_routes,
tag_routes,
)
from routes.users import users
from mealie.routes.users import users
app = FastAPI(
title="Mealie",
@ -37,7 +37,7 @@ def data_base_first_run():
def start_scheduler():
import services.scheduler.scheduled_jobs
import mealie.services.scheduler.scheduled_jobs
def api_routers():
@ -68,8 +68,8 @@ if not sql_exists:
api_routers()
start_scheduler()
if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----")
def main():
uvicorn.run(
"app:app",
@ -81,3 +81,8 @@ if __name__ == "__main__":
workers=1,
forwarded_allow_ips="*",
)
if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----")
main()

View file

@ -78,9 +78,7 @@ if DATABASE_TYPE == "sqlite":
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
else:
raise Exception(
"Unable to determine database type. Acceptible options are 'sqlite' "
)
raise Exception("Unable to determine database type. Acceptible options are 'sqlite' ")
# Mongo Database
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")

View file

@ -1,21 +1,21 @@
from schema.category import RecipeCategoryResponse, RecipeTagResponse
from schema.meal import MealPlanInDB
from schema.recipe import Recipe
from schema.settings import SiteSettings as SiteSettingsSchema
from schema.sign_up import SignUpOut
from schema.theme import SiteTheme
from schema.user import GroupInDB, UserInDB
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.meal import MealPlanInDB
from mealie.schema.recipe import Recipe
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session
from db.db_base import BaseDocument
from db.models.group import Group
from db.models.mealplan import MealPlanModel
from db.models.recipe.recipe import Category, RecipeModel, Tag
from db.models.settings import SiteSettings
from db.models.sign_up import SignUp
from db.models.theme import SiteThemeModel
from db.models.users import User
from mealie.db.db_base import BaseDocument
from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlanModel
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.settings import SiteSettings
from mealie.db.models.sign_up import SignUp
from mealie.db.models.theme import SiteThemeModel
from mealie.db.models.users import User
class _Recipes(BaseDocument):
@ -95,9 +95,7 @@ class _Groups(BaseDocument):
self.orm_mode = True
self.schema = GroupInDB
def get_meals(
self, session: Session, match_value: str, match_key: str = "name"
) -> list[MealPlanInDB]:
def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanInDB]:
"""A Helper function to get the group from the database and return a sorted list of
Args:
@ -108,13 +106,11 @@ class _Groups(BaseDocument):
Returns:
list[MealPlanInDB]: [description]
"""
group: GroupInDB = (
session.query(self.sql_model)
.filter_by(**{match_key: match_value})
.one_or_none()
)
group: GroupInDB = session.query(self.sql_model).filter_by(**{match_key: match_value}).one_or_none()
return sorted(group.mealplans, key=lambda mealplan: mealplan.startDate)
# Potentially not needed? column is sorted by SqlAlchemy based on startDate
# return sorted(group.mealplans, key=lambda mealplan: mealplan.startDate)
return group.mealplans
class _SignUps(BaseDocument):

View file

@ -4,7 +4,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class BaseDocument:
@ -16,15 +16,10 @@ class BaseDocument:
self.schema: BaseModel
# TODO: Improve Get All Query Functionality
def get_all(
self, session: Session, limit: int = None, order_by: str = None
) -> List[dict]:
def get_all(self, session: Session, limit: int = None, order_by: str = None) -> List[dict]:
if self.orm_mode:
return [
self.schema.from_orm(x)
for x in session.query(self.sql_model).limit(limit).all()
]
return [self.schema.from_orm(x) for x in session.query(self.sql_model).limit(limit).all()]
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
@ -33,9 +28,7 @@ class BaseDocument:
return list
def get_all_limit_columns(
self, session: Session, fields: List[str], limit: int = None
) -> List[SqlAlchemyBase]:
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
"""Queries the database for the selected model. Restricts return responses to the
keys specified under "fields"
@ -47,9 +40,7 @@ class BaseDocument:
Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects
"""
results = (
session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
)
results = session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
return results
@ -63,15 +54,11 @@ class BaseDocument:
Returns:
list[str]:
"""
results = session.query(self.sql_model).options(
load_only(str(self.primary_key))
)
results = session.query(self.sql_model).options(load_only(str(self.primary_key)))
results_as_dict = [x.dict() for x in results]
return [x.get(self.primary_key) for x in results_as_dict]
def _query_one(
self, session: Session, match_value: str, match_key: str = None
) -> SqlAlchemyBase:
def _query_one(self, session: Session, match_value: str, match_key: str = None) -> SqlAlchemyBase:
"""Query the sql database for one item an return the sql alchemy model
object. If no match key is provided the primary_key attribute will be used.
@ -85,15 +72,11 @@ class BaseDocument:
if match_key == None:
match_key = self.primary_key
result = (
session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
)
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
return result
def get(
self, session: Session, match_value: str, match_key: str = None, limit=1
) -> dict or List[dict]:
def get(self, session: Session, match_value: str, match_key: str = None, limit=1) -> dict or List[dict]:
"""Retrieves an entry from the database by matching a key/value pair. If no
key is provided the class objects primary key will be used to match against.
@ -109,12 +92,7 @@ class BaseDocument:
if match_key == None:
match_key = self.primary_key
result = (
session.query(self.sql_model)
.filter_by(**{match_key: match_value})
.limit(limit)
.all()
)
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
if limit == 1:
try:
@ -167,11 +145,7 @@ class BaseDocument:
return return_data
def delete(self, session: Session, primary_key_value) -> dict:
result = (
session.query(self.sql_model)
.filter_by(**{self.primary_key: primary_key_value})
.one()
)
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()
session.delete(result)
session.commit()

View file

@ -1,7 +1,7 @@
from core.config import SQLITE_FILE, USE_SQL
from mealie.core.config import SQLITE_FILE, USE_SQL
from sqlalchemy.orm.session import Session
from db.models.db_session import sql_global_init
from mealie.db.models.db_session import sql_global_init
sql_exists = True

View file

@ -1,13 +1,13 @@
from core.config import DEFAULT_GROUP
from core.security import get_password_hash
from mealie.core.config import DEFAULT_GROUP
from mealie.core.security import get_password_hash
from fastapi.logger import logger
from schema.settings import SiteSettings
from schema.theme import SiteTheme
from mealie.schema.settings import SiteSettings
from mealie.schema.theme import SiteTheme
from sqlalchemy.orm import Session
from sqlalchemy.orm.session import Session
from db.database import db
from db.db_setup import create_session
from mealie.db.database import db
from mealie.db.db_setup import create_session
def init_db(db: Session = None) -> None:

View file

@ -1,7 +1,7 @@
from db.models.mealplan import *
from db.models.recipe.recipe import *
from db.models.settings import *
from db.models.theme import *
from db.models.users import *
from db.models.sign_up import *
from db.models.group import *
from mealie.db.models.mealplan import *
from mealie.db.models.recipe.recipe import *
from mealie.db.models.settings import *
from mealie.db.models.theme import *
from mealie.db.models.users import *
from mealie.db.models.sign_up import *
from mealie.db.models.group import *

View file

@ -1,7 +1,7 @@
from pathlib import Path
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
from sqlalchemy.orm import sessionmaker
@ -18,7 +18,7 @@ def sql_global_init(db_file: Path, check_thread=False):
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
import db.models._all_models
import mealie.db.models._all_models
SqlAlchemyBase.metadata.create_all(engine)

View file

@ -1,8 +1,8 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from core.config import DEFAULT_GROUP
from db.models.model_base import BaseMixins, SqlAlchemyBase
from db.models.recipe.category import Category, group2categories
from mealie.core.config import DEFAULT_GROUP
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.category import Category, group2categories
from fastapi.logger import logger
from sqlalchemy.orm.session import Session
@ -20,18 +20,17 @@ class Group(SqlAlchemyBase, BaseMixins):
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
mealplans = orm.relationship(
"MealPlanModel", back_populates="group", single_parent=True
)
categories = orm.relationship(
"Category", secondary=group2categories, single_parent=True
"MealPlanModel",
back_populates="group",
single_parent=True,
order_by="MealPlanModel.startDate",
)
categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
# Webhook Settings
webhook_enable = sa.Column(sa.Boolean, default=False)
webhook_time = sa.Column(sa.String, default="00:00")
webhook_urls = orm.relationship(
"WebhookURLModel", uselist=True, cascade="all, delete"
)
webhook_urls = orm.relationship("WebhookURLModel", uselist=True, cascade="all, delete")
def __init__(
self,
@ -46,10 +45,7 @@ class Group(SqlAlchemyBase, BaseMixins):
webhook_urls=[],
) -> None:
self.name = name
self.categories = [
Category.get_ref(session=session, slug=cat.get("slug"))
for cat in categories
]
self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
self.webhook_enable = webhook_enable
self.webhook_time = webhook_time

View file

@ -2,8 +2,8 @@ from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.group import Group
from db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
class Meal(SqlAlchemyBase):
@ -33,9 +33,7 @@ class MealPlanModel(SqlAlchemyBase, BaseMixins):
group_id = sa.Column(sa.String, sa.ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="mealplans")
def __init__(
self, startDate, endDate, meals, group: str, uid=None, session=None
) -> None:
def __init__(self, startDate, endDate, meals, group: str, uid=None, session=None) -> None:
self.startDate = startDate
self.endDate = endDate
self.group = Group.get_ref(session, group)

View file

@ -1,7 +1,7 @@
from datetime import date
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class ApiExtras(SqlAlchemyBase):

View file

@ -1,6 +1,6 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
from fastapi.logger import logger
from slugify import slugify
from sqlalchemy.orm import validates
@ -32,9 +32,7 @@ class Category(SqlAlchemyBase):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2categories, back_populates="recipeCategory"
)
recipes = orm.relationship("RecipeModel", secondary=recipes2categories, back_populates="recipeCategory")
@validates("name")
def validate_name(self, key, name):

View file

@ -1,5 +1,5 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class RecipeIngredient(SqlAlchemyBase):

View file

@ -1,5 +1,5 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class RecipeInstruction(SqlAlchemyBase):

View file

@ -1,5 +1,5 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class Note(SqlAlchemyBase):
@ -12,4 +12,3 @@ class Note(SqlAlchemyBase):
def __init__(self, title, text) -> None:
self.title = title
self.text = text

View file

@ -1,5 +1,5 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class Nutrition(SqlAlchemyBase):
@ -28,4 +28,3 @@ class Nutrition(SqlAlchemyBase):
self.proteinContent = proteinContent
self.sodiumContent = sodiumContent
self.sugarContent = sugarContent

View file

@ -4,15 +4,15 @@ from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import BaseMixins, SqlAlchemyBase
from db.models.recipe.api_extras import ApiExtras
from db.models.recipe.category import Category, recipes2categories
from db.models.recipe.ingredient import RecipeIngredient
from db.models.recipe.instruction import RecipeInstruction
from db.models.recipe.note import Note
from db.models.recipe.nutrition import Nutrition
from db.models.recipe.tag import Tag, recipes2tags
from db.models.recipe.tool import Tool
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.api_extras import ApiExtras
from mealie.db.models.recipe.category import Category, recipes2categories
from mealie.db.models.recipe.ingredient import RecipeIngredient
from mealie.db.models.recipe.instruction import RecipeInstruction
from mealie.db.models.recipe.note import Note
from mealie.db.models.recipe.nutrition import Nutrition
from mealie.db.models.recipe.tag import Tag, recipes2tags
from mealie.db.models.recipe.tool import Tool
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import validates
@ -33,12 +33,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipeYield = sa.Column(sa.String)
recipeCuisine = sa.Column(sa.String)
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete")
nutrition: Nutrition = orm.relationship(
"Nutrition", uselist=False, cascade="all, delete"
)
recipeCategory: List = orm.relationship(
"Category", secondary=recipes2categories, back_populates="recipes"
)
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete")
recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
recipeIngredient: List[RecipeIngredient] = orm.relationship(
"RecipeIngredient",
@ -55,9 +51,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
# Mealie Specific
slug = sa.Column(sa.String, index=True, unique=True)
tags: List[Tag] = orm.relationship(
"Tag", secondary=recipes2tags, back_populates="recipes"
)
tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
dateAdded = sa.Column(sa.Date, default=date.today)
notes: List[Note] = orm.relationship("Note", cascade="all, delete")
rating = sa.Column(sa.Integer)
@ -106,9 +100,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.tool = [Tool(tool=x) for x in tool] if tool else []
self.recipeYield = recipeYield
self.recipeIngredient = [
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
]
self.recipeIngredient = [RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient]
self.recipeInstructions = [
RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None))
for instruc in recipeInstructions
@ -117,10 +109,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.prepTime = prepTime
self.performTime = performTime
self.recipeCategory = [
Category.create_if_not_exist(session=session, name=cat)
for cat in recipeCategory
]
self.recipeCategory = [Category.create_if_not_exist(session=session, name=cat) for cat in recipeCategory]
# Mealie Specific
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]

View file

@ -1,6 +1,6 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
from fastapi.logger import logger
from slugify import slugify
from sqlalchemy.orm import validates
@ -18,9 +18,7 @@ class Tag(SqlAlchemyBase):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2tags, back_populates="tags"
)
recipes = orm.relationship("RecipeModel", secondary=recipes2tags, back_populates="tags")
@validates("name")
def validate_name(self, key, name):
@ -31,7 +29,6 @@ class Tag(SqlAlchemyBase):
self.name = name.strip()
self.slug = slugify(self.name)
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)

View file

@ -1,5 +1,5 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class Tool(SqlAlchemyBase):

View file

@ -1,7 +1,7 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import BaseMixins, SqlAlchemyBase
from db.models.recipe.category import Category, site_settings2categories
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.category import Category, site_settings2categories
from sqlalchemy.orm import Session
@ -29,10 +29,7 @@ class SiteSettings(SqlAlchemyBase, BaseMixins):
self.language = language
self.cards_per_section = cards_per_section
self.show_recent = show_recent
self.categories = [
Category.get_ref(session=session, name=cat.get("slug"))
for cat in categories
]
self.categories = [Category.get_ref(session=session, name=cat.get("slug")) for cat in categories]
def update(self, *args, **kwarg):
self.__init__(*args, **kwarg)

View file

@ -1,4 +1,4 @@
from db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, Integer, String

View file

@ -1,6 +1,6 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import SqlAlchemyBase
from mealie.db.models.model_base import SqlAlchemyBase
class SiteThemeModel(SqlAlchemyBase):

View file

@ -1,6 +1,6 @@
from core.config import DEFAULT_GROUP
from db.models.group import Group
from db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.core.config import DEFAULT_GROUP
from mealie.db.models.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
# I'm not sure this is necessasry, browser based settings may be sufficient

View file

@ -1,13 +1,13 @@
import operator
import shutil
from core.config import BACKUP_DIR, TEMPLATE_DIR
from db.db_setup import generate_session
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from schema.snackbar import SnackResponse
from services.backups.exports import backup_all
from services.backups.imports import ImportDatabase
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.schema.snackbar import SnackResponse
from mealie.services.backups.exports import backup_all
from mealie.services.backups.imports import ImportDatabase
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
@ -71,17 +71,13 @@ async def upload_nextcloud_zipfile(file_name: str):
file = BACKUP_DIR.joinpath(file_name)
if file.is_file:
return FileResponse(
file, media_type="application/octet-stream", filename=file_name
)
return FileResponse(file, media_type="application/octet-stream", filename=file_name)
else:
return SnackResponse.error("No File Found")
@router.post("/{file_name}/import", status_code=200)
def import_database(
file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)
):
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
""" Import a database backup file generated from Mealie. """
import_db = ImportDatabase(

View file

@ -1,6 +1,6 @@
import json
from core.config import APP_VERSION, DEBUG_DIR, LOGGER_FILE
from mealie.core.config import APP_VERSION, DEBUG_DIR, LOGGER_FILE
from fastapi import APIRouter
router = APIRouter(prefix="/api/debug", tags=["Debug"])

View file

@ -1,10 +1,10 @@
from core.config import SECRET
from db.database import db
from db.db_setup import create_session
from mealie.core.config import SECRET
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi_login import LoginManager
from sqlalchemy.orm.session import Session
from schema.user import UserInDB
from mealie.schema.user import UserInDB
manager = LoginManager(SECRET, "/api/auth/token")
@ -21,4 +21,3 @@ def query_user(user_email: str, session: Session = None) -> UserInDB:
user = db.users.get(session, user_email, "email")
session.close()
return user

View file

@ -1,9 +1,9 @@
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from routes.deps import manager
from schema.snackbar import SnackResponse
from schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from mealie.routes.deps import manager
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/groups", tags=["Groups"])
@ -59,9 +59,7 @@ async def update_group_data(
@router.delete("/{id}")
async def delete_user_group(
id: int, current_user=Depends(manager), session: Session = Depends(generate_session)
):
async def delete_user_group(id: int, current_user=Depends(manager), session: Session = Depends(generate_session)):
""" Removes a user group from the database """
if id == 1:

View file

@ -1,7 +1,6 @@
from fastapi import APIRouter
from routes.groups import crud
from mealie.routes.groups import crud
router = APIRouter()
router.include_router(crud.router)

View file

@ -1,13 +1,13 @@
import datetime
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from routes.deps import manager
from schema.meal import MealPlanIn, MealPlanInDB
from schema.snackbar import SnackResponse
from schema.user import GroupInDB, UserInDB
from services.meal_services import get_todays_meal, process_meals
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import manager
from mealie.schema.meal import MealPlanIn, MealPlanInDB
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.meal_services import get_todays_meal, process_meals
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@ -37,9 +37,7 @@ def create_meal_plan(
@router.put("/{plan_id}")
def update_meal_plan(
plan_id: str, meal_plan: MealPlanIn, session: Session = Depends(generate_session)
):
def update_meal_plan(plan_id: str, meal_plan: MealPlanIn, session: Session = Depends(generate_session)):
""" Updates a meal plan based off ID """
processed_plan = process_meals(session, meal_plan)
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())

View file

@ -1,8 +1,8 @@
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from schema.meal import MealPlanInDB
from schema.recipe import Recipe
from mealie.schema.meal import MealPlanInDB
from mealie.schema.recipe import Recipe
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@ -16,8 +16,6 @@ def get_shopping_list(id: str, session: Session = Depends(generate_session)):
mealplan: MealPlanInDB
slugs = [x.slug for x in mealplan.meals]
recipes: list[Recipe] = [db.recipes.get(session, x) for x in slugs]
ingredients = [
{"name": x.name, "recipeIngredient": x.recipeIngredient} for x in recipes if x
]
ingredients = [{"name": x.name, "recipeIngredient": x.recipeIngredient} for x in recipes if x]
return ingredients

View file

@ -1,5 +1,5 @@
from fastapi import APIRouter
from routes.mealplans import crud, helpers
from mealie.routes.mealplans import crud, helpers
router = APIRouter()

View file

@ -2,14 +2,14 @@ import operator
import shutil
from typing import List
from core.config import MIGRATION_DIR
from db.db_setup import generate_session
from mealie.core.config import MIGRATION_DIR
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from schema.migration import MigrationFile, Migrations
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
from services.migrations.nextcloud import migrate as nextcloud_migrate
from mealie.schema.migration import MigrationFile, Migrations
from mealie.services.migrations.chowdown import chowdown_migrate as chowdow_migrate
from mealie.services.migrations.nextcloud import migrate as nextcloud_migrate
from sqlalchemy.orm.session import Session
from schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
router = APIRouter(prefix="/api/migrations", tags=["Migration"])
@ -36,9 +36,7 @@ def get_avaiable_nextcloud_imports():
@router.post("/{type}/{file_name}/import")
def import_nextcloud_directory(
type: str, file_name: str, session: Session = Depends(generate_session)
):
def import_nextcloud_directory(type: str, file_name: str, session: Session = Depends(generate_session)):
""" Imports all the recipes in a given directory """
file_path = MIGRATION_DIR.joinpath(type, file_name)
if type == "nextcloud":

View file

@ -1,9 +1,9 @@
from typing import List, Optional
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, Query
from schema.recipe import AllRecipeRequest
from mealie.schema.recipe import AllRecipeRequest
from slugify import slugify
from sqlalchemy.orm.session import Session
@ -44,9 +44,7 @@ def get_all_recipes(
@router.post("/api/recipes")
def get_all_recipes_post(
body: AllRecipeRequest, session: Session = Depends(generate_session)
):
def get_all_recipes_post(body: AllRecipeRequest, session: Session = Depends(generate_session)):
"""
Returns key data for all recipes based off the body data provided.
For example, if slug, image, and name are provided you will recieve a list of
@ -76,9 +74,7 @@ def get_all_recipes_post(
def filter_by_category(categories: list, session: Session = Depends(generate_session)):
""" pass a list of categories and get a list of recipes associated with those categories """
#! This should be refactored into a single database call, but I couldn't figure it out
in_category = [
db.categories.get(session, slugify(cat), limit=1) for cat in categories
]
in_category = [db.categories.get(session, slugify(cat), limit=1) for cat in categories]
in_category = [cat.get("recipes") for cat in in_category if cat]
in_category = [item for sublist in in_category for item in sublist]
return in_category

View file

@ -1,11 +1,11 @@
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from schema.category import RecipeCategoryResponse
from mealie.schema.category import RecipeCategoryResponse
from sqlalchemy.orm.session import Session
from schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
from schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
router = APIRouter(
prefix="/api/categories",
@ -20,17 +20,13 @@ async def get_all_recipe_categories(session: Session = Depends(generate_session)
@router.get("/{category}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(
category: str, session: Session = Depends(generate_session)
):
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)):
""" Returns a list of recipes associated with the provided category. """
return db.categories.get(session, category)
@router.delete("/{category}")
async def delete_recipe_category(
category: str, session: Session = Depends(generate_session)
):
async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
"""Removes a recipe category from the database. Deleting a
category does not impact a recipe. The category will be removed
from any recipes that contain it"""

View file

@ -1,12 +1,12 @@
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, Form, HTTPException
from fastapi.logger import logger
from fastapi.responses import FileResponse
from schema.recipe import Recipe, RecipeURLIn
from schema.snackbar import SnackResponse
from services.image_services import read_image, write_image
from services.scraper.scraper import create_from_url
from mealie.schema.recipe import Recipe, RecipeURLIn
from mealie.schema.snackbar import SnackResponse
from mealie.services.image_services import read_image, write_image
from mealie.services.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session
router = APIRouter(
@ -41,9 +41,7 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session)):
@router.put("/{recipe_slug}")
def update_recipe(
recipe_slug: str, data: Recipe, session: Session = Depends(generate_session)
):
def update_recipe(recipe_slug: str, data: Recipe, session: Session = Depends(generate_session)):
""" Updates a recipe by existing slug and data. """
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
@ -58,9 +56,7 @@ def delete_recipe(recipe_slug: str, session: Session = Depends(generate_session)
try:
db.recipes.delete(session, recipe_slug)
except:
raise HTTPException(
status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")
)
raise HTTPException(status_code=404, detail=SnackResponse.error("Unable to Delete Recipe"))
return SnackResponse.error(f"Recipe {recipe_slug} Deleted")

View file

@ -1,10 +1,10 @@
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
from schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
router = APIRouter(tags=["Recipes"])

View file

@ -1,13 +1,13 @@
from db.database import db
from db.db_setup import generate_session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from schema.settings import SiteSettings
from schema.snackbar import SnackResponse
from schema.user import GroupInDB, UserInDB
from mealie.schema.settings import SiteSettings
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupInDB, UserInDB
from sqlalchemy.orm.session import Session
from utils.post_webhooks import post_webhooks
from mealie.utils.post_webhooks import post_webhooks
from routes.deps import manager
from mealie.routes.deps import manager
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])

View file

@ -1,9 +1,9 @@
from db.db_setup import generate_session
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from schema.theme import SiteTheme
from mealie.schema.theme import SiteTheme
from sqlalchemy.orm.session import Session
from schema.snackbar import SnackResponse
from db.database import db
from mealie.schema.snackbar import SnackResponse
from mealie.db.database import db
router = APIRouter(prefix="/api", tags=["Themes"])
@ -30,9 +30,7 @@ def get_single_theme(theme_name: str, session: Session = Depends(generate_sessio
@router.put("/themes/{theme_name}")
def update_theme(
theme_name: str, data: SiteTheme, session: Session = Depends(generate_session)
):
def update_theme(theme_name: str, data: SiteTheme, session: Session = Depends(generate_session)):
""" Update a theme database entry """
db.themes.update(session, theme_name, data.dict())

View file

@ -1,13 +1,13 @@
from datetime import timedelta
from core.security import verify_password
from db.db_setup import generate_session
from mealie.core.security import verify_password
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_login.exceptions import InvalidCredentialsException
from routes.deps import manager, query_user
from schema.snackbar import SnackResponse
from schema.user import UserInDB
from mealie.routes.deps import manager, query_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
@ -27,9 +27,7 @@ def get_token(
elif not verify_password(password, user.password):
raise InvalidCredentialsException
access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(hours=2)
)
access_token = manager.create_access_token(data=dict(sub=email), expires=timedelta(hours=2))
return SnackResponse.success(
"User Successfully Logged In",
{"access_token": access_token, "token_type": "bearer"},
@ -51,9 +49,7 @@ def get_long_token(
elif not verify_password(password, user.password):
raise InvalidCredentialsException
access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(days=1)
)
access_token = manager.create_access_token(data=dict(sub=email), expires=timedelta(days=1))
return SnackResponse.success(
"User Successfully Logged In",
{"access_token": access_token, "token_type": "bearer"},
@ -63,7 +59,5 @@ def get_long_token(
@router.get("/refresh")
async def refresh_token(current_user: UserInDB = Depends(manager)):
""" Use a valid token to get another token"""
access_token = manager.create_access_token(
data=dict(sub=current_user.email), expires=timedelta(hours=1)
)
access_token = manager.create_access_token(data=dict(sub=current_user.email), expires=timedelta(hours=1))
return {"access_token": access_token, "token_type": "bearer"}

View file

@ -2,15 +2,15 @@ import shutil
from datetime import timedelta
from os import access
from core.config import USER_DIR
from core.security import get_password_hash, verify_password
from db.database import db
from db.db_setup import generate_session
from mealie.core.config import USER_DIR
from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends, File, UploadFile
from fastapi.responses import FileResponse
from routes.deps import manager
from schema.snackbar import SnackResponse
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
from mealie.routes.deps import manager
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["Users"])
@ -71,9 +71,7 @@ async def update_user(
updated_user: UserInDB = db.users.update(session, id, new_data.dict())
email = updated_user.email
if current_user.id == id:
access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(hours=2)
)
access_token = manager.create_access_token(data=dict(sub=email), expires=timedelta(hours=2))
access_token = {"access_token": access_token, "token_type": "bearer"}
return SnackResponse.success("User Updated", access_token)
@ -126,9 +124,7 @@ async def update_password(
):
""" Resets the User Password"""
match_passwords = verify_password(
password_change.current_password, current_user.password
)
match_passwords = verify_password(password_change.current_password, current_user.password)
match_id = current_user.id == id
if match_passwords and match_id:

View file

@ -1,13 +1,13 @@
import uuid
from core.security import get_password_hash
from db.database import db
from db.db_setup import generate_session
from mealie.core.security import get_password_hash
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from routes.deps import manager
from schema.sign_up import SignUpIn, SignUpOut, SignUpToken
from schema.snackbar import SnackResponse
from schema.user import UserIn, UserInDB
from mealie.routes.deps import manager
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import UserIn, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])

View file

@ -1,5 +1,5 @@
from fastapi import APIRouter
from routes.users import auth, crud, sign_up
from mealie.routes.users import auth, crud, sign_up
router = APIRouter()

View file

@ -9,4 +9,4 @@
caddy start --config ./Caddyfile
## Start API
uvicorn app:app --host 0.0.0.0 --port 9000
uvicorn mealie.app:app --host 0.0.0.0 --port 9000

View file

@ -8,6 +8,8 @@ class BackupOptions(BaseModel):
recipes: bool = True
settings: bool = True
themes: bool = True
groups: bool = True
users: bool = True
class Config:
schema_extra = {
@ -15,6 +17,8 @@ class BackupOptions(BaseModel):
"recipes": True,
"settings": True,
"themes": True,
"groups": True,
"users": True,
}
}

View file

@ -2,7 +2,7 @@ from typing import List, Optional
from fastapi_camelcase import CamelModel
from schema.recipe import Recipe
from mealie.schema.recipe import Recipe
class CategoryBase(CamelModel):

View file

@ -1,7 +1,7 @@
from datetime import date
from typing import List, Optional
from db.models.mealplan import MealPlanModel
from mealie.db.models.mealplan import MealPlanModel
from pydantic import BaseModel, validator
from pydantic.utils import GetterDict

View file

@ -1,7 +1,7 @@
import datetime
from typing import Any, List, Optional
from db.models.recipe.recipe import RecipeModel
from mealie.db.models.recipe.recipe import RecipeModel
from pydantic import BaseModel, validator
from pydantic.utils import GetterDict
from slugify import slugify

View file

@ -9,12 +9,14 @@ class RecipeImport(BaseModel):
status: bool
exception: Optional[str]
class ThemeImport(BaseModel):
name: str
status: bool
exception: Optional[str]
class SettingsImport(BaseModel):
name: str
status: bool
exception: Optional[str]
exception: Optional[str]

View file

@ -2,7 +2,7 @@ from typing import Optional
from fastapi_camelcase import CamelModel
from schema.category import CategoryBase
from mealie.schema.category import CategoryBase
class SiteSettings(CamelModel):

View file

@ -1,13 +1,13 @@
from typing import Any, Optional
from core.config import DEFAULT_GROUP
from db.models.group import Group
from db.models.users import User
from mealie.core.config import DEFAULT_GROUP
from mealie.db.models.group import Group
from mealie.db.models.users import User
from fastapi_camelcase import CamelModel
from pydantic.utils import GetterDict
from schema.category import CategoryBase
from schema.meal import MealPlanInDB
from mealie.schema.category import CategoryBase
from mealie.schema.meal import MealPlanInDB
class ChangePassword(CamelModel):

View file

View file

View file

@ -4,13 +4,12 @@ from datetime import datetime
from pathlib import Path
from typing import Union
from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from db.database import db
from db.db_setup import create_session
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi.logger import logger
from jinja2 import Template
from pydantic.main import BaseModel
from schema.recipe import Recipe
class ExportDatabase:
@ -75,14 +74,10 @@ class ExportDatabase:
out_dir.mkdir(parents=True, exist_ok=True)
if export_list:
ExportDatabase._write_json_file(
items, out_dir.joinpath(f"{folder_name}.json")
)
ExportDatabase._write_json_file(items, out_dir.joinpath(f"{folder_name}.json"))
else:
for item in items:
ExportDatabase._write_json_file(
item, out_dir.joinpath(f"{item.get('name')}.json")
)
ExportDatabase._write_json_file(item, out_dir.joinpath(f"{item.get('name')}.json"))
@staticmethod
def _write_json_file(data: Union[dict, list], out_file: Path):

View file

@ -4,13 +4,13 @@ import zipfile
from pathlib import Path
from typing import List
from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from db.database import db
from db.db_setup import create_session
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi.logger import logger
from schema.recipe import Recipe
from schema.restore import RecipeImport, SettingsImport, ThemeImport
from schema.theme import SiteTheme
from mealie.schema.recipe import Recipe
from mealie.schema.restore import RecipeImport, SettingsImport, ThemeImport
from mealie.schema.theme import SiteTheme
from sqlalchemy.orm.session import Session
@ -93,9 +93,7 @@ class ImportDatabase:
recipe_obj = Recipe(**recipe_dict)
db.recipes.create(session, recipe_obj.dict())
import_status = RecipeImport(
name=recipe_obj.name, slug=recipe_obj.slug, status=True
)
import_status = RecipeImport(name=recipe_obj.name, slug=recipe_obj.slug, status=True)
imports.append(import_status)
successful_imports.append(recipe.stem)
logger.info(f"Imported: {recipe.stem}")
@ -125,17 +123,13 @@ class ImportDatabase:
# Migration from list to Object Type Data
try:
if "" in recipe_dict["tags"]:
recipe_dict["tags"] = [
tag for tag in recipe_dict["tags"] if not tag == ""
]
recipe_dict["tags"] = [tag for tag in recipe_dict["tags"] if not tag == ""]
except:
pass
try:
if "" in recipe_dict["categories"]:
recipe_dict["categories"] = [
cat for cat in recipe_dict["categories"] if not cat == ""
]
recipe_dict["categories"] = [cat for cat in recipe_dict["categories"] if not cat == ""]
except:
pass
@ -165,9 +159,7 @@ class ImportDatabase:
theme_imports.append(ThemeImport(name=new_theme.name, status=True))
except Exception as inst:
logger.info(f"Unable Import Theme {new_theme.name}")
theme_imports.append(
ThemeImport(name=new_theme.name, status=False, exception=str(inst))
)
theme_imports.append(ThemeImport(name=new_theme.name, status=False, exception=str(inst)))
return theme_imports
@ -185,9 +177,7 @@ class ImportDatabase:
import_status = SettingsImport(name=name, status=True)
except Exception as inst:
import_status = SettingsImport(
name=name, status=False, exception=str(inst)
)
import_status = SettingsImport(name=name, status=False, exception=str(inst))
settings_imports.append(import_status)

View file

@ -2,7 +2,7 @@ import shutil
from pathlib import Path
import requests
from core.config import IMG_DIR
from mealie.core.config import IMG_DIR
from fastapi.logger import logger

View file

@ -2,12 +2,18 @@ from datetime import date, timedelta, timezone
from typing import Union
import pytz
from db.database import db
from db.db_setup import create_session
from mealie.db.database import db
from mealie.db.db_setup import create_session
from pydantic.tools import T
from schema.meal import MealIn, MealOut, MealPlanIn, MealPlanInDB, MealPlanProcessed
from schema.recipe import Recipe
from schema.user import GroupInDB
from mealie.schema.meal import (
MealIn,
MealOut,
MealPlanIn,
MealPlanInDB,
MealPlanProcessed,
)
from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB
from sqlalchemy.orm.session import Session

View file

View file

@ -2,11 +2,11 @@ import shutil
from pathlib import Path
import yaml
from core.config import IMG_DIR, TEMP_DIR
from db.database import db
from schema.recipe import Recipe
from mealie.core.config import IMG_DIR, TEMP_DIR
from mealie.db.database import db
from mealie.schema.recipe import Recipe
from sqlalchemy.orm.session import Session
from utils.unzip import unpack_zip
from mealie.utils.unzip import unpack_zip
try:
from yaml import CLoader as Loader

View file

@ -4,11 +4,11 @@ import shutil
import zipfile
from pathlib import Path
from core.config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
from schema.recipe import Recipe
from services.scraper.cleaner import Cleaner
from core.config import IMG_DIR, TEMP_DIR
from db.database import db
from mealie.core.config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
from mealie.schema.recipe import Recipe
from mealie.services.scraper.cleaner import Cleaner
from mealie.core.config import IMG_DIR, TEMP_DIR
from mealie.db.database import db
def process_selection(selection: Path) -> Path:
@ -79,7 +79,7 @@ def migrate(session, selection: str):
try:
recipe = import_recipes(dir)
db.recipes.create(session, recipe.dict())
successful_imports.append(recipe.name)
except:
logging.error(f"Failed Nextcloud Import: {dir.name}")

View file

View file

@ -1,3 +1,3 @@
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
scheduler = BackgroundScheduler()

View file

@ -1,12 +1,12 @@
from apscheduler.schedulers.background import BackgroundScheduler
from db.database import db
from db.db_setup import create_session
from mealie.db.database import db
from mealie.db.db_setup import create_session
from fastapi.logger import logger
from schema.user import GroupInDB
from services.backups.exports import auto_backup_job
from services.scheduler.global_scheduler import scheduler
from services.scheduler.scheduler_utils import Cron, cron_parser
from utils.post_webhooks import post_webhooks
from mealie.schema.user import GroupInDB
from mealie.services.backups.exports import auto_backup_job
from mealie.services.scheduler.global_scheduler import scheduler
from mealie.services.scheduler.scheduler_utils import Cron, cron_parser
from mealie.utils.post_webhooks import post_webhooks
# TODO Fix Scheduler
@ -83,9 +83,7 @@ def init_webhook_schedule(scheduler, job_store: dict):
logger.info("----INIT SCHEDULE OBJECT-----")
JOB_STORE = {
"backup_job": ScheduledFunction(
scheduler, auto_backup_job, Cron(hours=00, minutes=00), "backups"
),
"backup_job": ScheduledFunction(scheduler, auto_backup_job, Cron(hours=00, minutes=00), "backups"),
}
JOB_STORE = init_webhook_schedule(scheduler=scheduler, job_store=JOB_STORE)

View file

View file

@ -31,24 +31,15 @@ class Cleaner:
recipe_data["prepTime"] = Cleaner.time(recipe_data.get("prepTime", None))
recipe_data["performTime"] = Cleaner.time(recipe_data.get("performTime", None))
recipe_data["totalTime"] = Cleaner.time(recipe_data.get("totalTime", None))
recipe_data["recipeCategory"] = Cleaner.category(
recipe_data.get("recipeCategory", [])
)
recipe_data["recipeCategory"] = Cleaner.category(recipe_data.get("recipeCategory", []))
recipe_data["recipeYield"] = Cleaner.yield_amount(
recipe_data.get("recipeYield")
)
recipe_data["recipeIngredient"] = Cleaner.ingredient(
recipe_data.get("recipeIngredient")
)
recipe_data["recipeInstructions"] = Cleaner.instructions(
recipe_data["recipeInstructions"]
)
recipe_data["recipeYield"] = Cleaner.yield_amount(recipe_data.get("recipeYield"))
recipe_data["recipeIngredient"] = Cleaner.ingredient(recipe_data.get("recipeIngredient"))
recipe_data["recipeInstructions"] = Cleaner.instructions(recipe_data["recipeInstructions"])
recipe_data["image"] = Cleaner.image(recipe_data.get("image"))
recipe_data["slug"] = slugify(recipe_data.get("name"))
recipe_data["orgURL"] = url
return recipe_data
@staticmethod
@ -84,11 +75,7 @@ class Cleaner:
# One long string split by (possibly multiple) new lines
if isinstance(instructions, str):
return [
{"text": Cleaner._instruction(line)}
for line in instructions.splitlines()
if line
]
return [{"text": Cleaner._instruction(line)} for line in instructions.splitlines() if line]
# Plain strings in a list
elif type(instructions) == list and type(instructions[0]) == str:
@ -97,13 +84,8 @@ class Cleaner:
# Dictionaries (let's assume it's a HowToStep) in a list
elif type(instructions) == list and type(instructions[0]) == dict:
# Try List of Dictionary without "@type" or "type"
if not instructions[0].get("@type", False) and not instructions[0].get(
"type", False
):
return [
{"text": Cleaner._instruction(step["text"])}
for step in instructions
]
if not instructions[0].get("@type", False) and not instructions[0].get("type", False):
return [{"text": Cleaner._instruction(step["text"])} for step in instructions]
try:
# If HowToStep is under HowToSection

View file

@ -1,7 +1,7 @@
from typing import Tuple
import extruct
from core.config import DEBUG_DIR
from mealie.core.config import DEBUG_DIR
from slugify import slugify
from w3lib.html import get_base_url

View file

@ -3,12 +3,12 @@ from typing import List
import requests
import scrape_schema_recipe
from core.config import DEBUG_DIR
from mealie.core.config import DEBUG_DIR
from fastapi.logger import logger
from services.image_services import scrape_image
from schema.recipe import Recipe
from services.scraper import open_graph
from services.scraper.cleaner import Cleaner
from mealie.services.image_services import scrape_image
from mealie.schema.recipe import Recipe
from mealie.services.scraper import open_graph
from mealie.services.scraper.cleaner import Cleaner
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
@ -36,15 +36,11 @@ def create_from_url(url: str) -> Recipe:
def extract_recipe_from_html(html: str, url: str) -> dict:
try:
scraped_recipes: List[dict] = scrape_schema_recipe.loads(
html, python_objects=True
)
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True)
dump_last_json(scraped_recipes)
if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(
url, python_objects=True
)
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(url, python_objects=True)
except Exception as e:
# trying without python_objects
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html)

View file

@ -1,4 +0,0 @@
[pytest]
python_files = test_*
python_classes = *Tests
python_functions = test_*

0
mealie/utils/__init__.py Normal file
View file

View file

@ -1,6 +1,6 @@
import json
from core.config import DATA_DIR
from mealie.core.config import DATA_DIR
"""Script to export the ReDoc documentation page into a standalone HTML file."""

View file

@ -1,8 +1,8 @@
import requests
from db.database import db
from db.db_setup import create_session
from schema.user import GroupInDB
from services.meal_services import get_todays_meal
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.user import GroupInDB
from mealie.services.meal_services import get_todays_meal
from sqlalchemy.orm.session import Session
@ -11,7 +11,7 @@ def post_webhooks(group: int, session: Session = None):
group_settings: GroupInDB = db.groups.get(session, group)
if not group_settings.webhook_enable:
return
return
todays_recipe = get_todays_meal(session, group)

View file

@ -2,7 +2,7 @@ import tempfile
import zipfile
from pathlib import Path
from core.config import TEMP_DIR
from mealie.core.config import TEMP_DIR
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:

Some files were not shown because too many files have changed in this diff Show more