diff --git a/.vscode/settings.json b/.vscode/settings.json
index 21ff09b98881..b0dfef42d8b3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,28 +1,19 @@
{
"python.formatting.provider": "black",
"python.pythonPath": ".venv/bin/python3.9",
- "python.linting.pylintEnabled": true,
+ "python.linting.pylintEnabled": false,
"python.linting.enabled": true,
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.autoTestDiscoverOnSaveEnabled": false,
"python.testing.pytestArgs": ["tests"],
- "cSpell.enableFiletypes": [
- "!javascript",
- "!python",
- "!yaml"
- ],
+ "cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
"i18n-ally.localesPaths": "frontend/src/locales/messages",
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.keystyle": "nested",
- "cSpell.words": [
- "compression",
- "hkotel",
- "performant",
- "postgres",
- "webp"
- ],
- "search.mode": "reuseEditor"
+ "cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"],
+ "search.mode": "reuseEditor",
+ "python.linting.flake8Enabled": true
}
diff --git a/Caddyfile b/Caddyfile
index 0faf9fe7d519..73fb21ab345b 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -6,11 +6,11 @@
:80 {
@proxied path /api/* /docs /openapi.json
- root * /app/dist
- encode gzip
+ encode gzip zstd
uri strip_suffix /
- handle_path /api/recipes/media/* {
+ # Handles Recipe Images / Assets
+ handle_path /api/media/recipes/* {
root * /app/data/recipes/
file_server
}
@@ -20,8 +20,8 @@
}
handle {
- try_files {path}.html {path} /
+ root * /app/dist
+ try_files {path}.html {path} /index.html
file_server
}
-
}
\ No newline at end of file
diff --git a/Caddyfile.dev b/Caddyfile.dev
new file mode 100644
index 000000000000..f8d99bb39756
--- /dev/null
+++ b/Caddyfile.dev
@@ -0,0 +1,9 @@
+{
+ admin off
+}
+
+localhost {
+ handle /mealie/* {
+ reverse_proxy http://127.0.0.1:9090
+ }
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 07593c5670d3..09452c92cf5a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,6 +6,8 @@ services:
dockerfile: Dockerfile
container_name: mealie
restart: always
+ depends_on:
+ - "postgres"
ports:
- 9090:80
environment:
diff --git a/frontend/src/api/recipe.js b/frontend/src/api/recipe.js
index 4410b23a1e11..948f766d5062 100644
--- a/frontend/src/api/recipe.js
+++ b/frontend/src/api/recipe.js
@@ -135,14 +135,18 @@ export const recipeAPI = {
},
recipeImage(recipeSlug) {
- return `/api/recipes/media/${recipeSlug}/images/original.webp`;
+ return `/api/media/recipes/${recipeSlug}/images/original.webp`;
},
recipeSmallImage(recipeSlug) {
- return `/api/recipes/media/${recipeSlug}/images/min-original.webp`;
+ return `/api/media/recipes/${recipeSlug}/images/min-original.webp`;
},
recipeTinyImage(recipeSlug) {
- return `/api/recipes/media/${recipeSlug}/images/tiny-original.webp`;
+ return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp`;
+ },
+
+ recipeAssetPath(recipeSlug, assetName) {
+ return `api/media/recipes/${recipeSlug}/assets/${assetName}`;
},
};
diff --git a/frontend/src/api/themes.js b/frontend/src/api/themes.js
index cfd433594398..0917afb31a4d 100644
--- a/frontend/src/api/themes.js
+++ b/frontend/src/api/themes.js
@@ -6,10 +6,10 @@ const prefix = baseURL + "themes";
const settingsURLs = {
allThemes: `${baseURL}themes`,
- specificTheme: themeName => `${prefix}/${themeName}`,
+ specificTheme: id => `${prefix}/${id}`,
createTheme: `${prefix}/create`,
- updateTheme: themeName => `${prefix}/${themeName}`,
- deleteTheme: themeName => `${prefix}/${themeName}`,
+ updateTheme: id => `${prefix}/${id}`,
+ deleteTheme: id => `${prefix}/${id}`,
};
export const themeAPI = {
@@ -32,22 +32,18 @@ export const themeAPI = {
);
},
- update(themeName, colors) {
- const body = {
- name: themeName,
- colors: colors,
- };
+ update(data) {
return apiReq.put(
- settingsURLs.updateTheme(themeName),
- body,
+ settingsURLs.updateTheme(data.id),
+ data,
() => i18n.t("settings.theme.error-updating-theme"),
() => i18n.t("settings.theme.theme-updated")
);
},
- delete(themeName) {
+ delete(id) {
return apiReq.delete(
- settingsURLs.deleteTheme(themeName),
+ settingsURLs.deleteTheme(id),
null,
() => i18n.t("settings.theme.error-deleting-theme"),
() => i18n.t("settings.theme.theme-deleted")
diff --git a/frontend/src/components/FormHelpers/ColorPickerDialog.vue b/frontend/src/components/FormHelpers/ColorPickerDialog.vue
index 3c0fc13be4b3..13ba003ec499 100644
--- a/frontend/src/components/FormHelpers/ColorPickerDialog.vue
+++ b/frontend/src/components/FormHelpers/ColorPickerDialog.vue
@@ -3,7 +3,7 @@
{{ buttonText }}
-
+
@@ -17,15 +17,7 @@
-
-
-
-
- {{ color }}
-
-
-
-
+
diff --git a/frontend/src/pages/Admin/Backup/ImportOptions.vue b/frontend/src/components/FormHelpers/ImportOptions.vue
similarity index 92%
rename from frontend/src/pages/Admin/Backup/ImportOptions.vue
rename to frontend/src/components/FormHelpers/ImportOptions.vue
index 35e783c06450..74f133edbbe6 100644
--- a/frontend/src/pages/Admin/Backup/ImportOptions.vue
+++ b/frontend/src/components/FormHelpers/ImportOptions.vue
@@ -1,8 +1,8 @@
-
-
+
\ No newline at end of file
diff --git a/frontend/src/components/Recipe/Parts/Assets.vue b/frontend/src/components/Recipe/Parts/Assets.vue
index 62119ba21ad0..1abada0b0d6c 100644
--- a/frontend/src/components/Recipe/Parts/Assets.vue
+++ b/frontend/src/components/Recipe/Parts/Assets.vue
@@ -14,14 +14,7 @@
-
+
mdi-download
@@ -118,6 +111,9 @@ export default {
},
},
methods: {
+ assetURL(assetName) {
+ return api.recipes.recipeAssetPath(this.slug, assetName);
+ },
setFileObject(obj) {
this.fileObject = obj;
},
@@ -135,7 +131,8 @@ export default {
this.value.splice(index, 1);
},
copyLink(name, fileName) {
- const copyText = ``;
+ const assetLink = api.recipes.recipeAssetPath(this.slug, fileName);
+ const copyText = ``;
navigator.clipboard.writeText(copyText).then(
() => console.log("Copied", copyText),
() => console.log("Copied Failed", copyText)
diff --git a/frontend/src/components/Recipe/RecipeViewer/index.vue b/frontend/src/components/Recipe/RecipeViewer/index.vue
index d4a4fac9a68a..74498da71ba4 100644
--- a/frontend/src/components/Recipe/RecipeViewer/index.vue
+++ b/frontend/src/components/Recipe/RecipeViewer/index.vue
@@ -18,7 +18,7 @@
color="secondary darken-1"
class="rounded-sm static"
>
- {{ yields }}
+ {{ recipe.yields }}
diff --git a/frontend/src/components/UI/Buttons/TheUploadBtn.vue b/frontend/src/components/UI/Buttons/TheUploadBtn.vue
index 6b2180d90815..8b009daba943 100644
--- a/frontend/src/components/UI/Buttons/TheUploadBtn.vue
+++ b/frontend/src/components/UI/Buttons/TheUploadBtn.vue
@@ -2,7 +2,7 @@
-
+
{{ icon }}
{{ text ? text : defaultText }}
@@ -15,6 +15,9 @@ const UPLOAD_EVENT = "uploaded";
import { api } from "@/api";
export default {
props: {
+ small: {
+ default: false,
+ },
post: {
type: Boolean,
default: true,
@@ -27,7 +30,7 @@ export default {
default: true,
},
},
- data: () => ({
+ data: () => ({
file: null,
isSelecting: false,
}),
diff --git a/frontend/src/components/UI/CardSection.vue b/frontend/src/components/UI/CardSection.vue
index 6a5530a7090f..6bb57e4d2624 100644
--- a/frontend/src/components/UI/CardSection.vue
+++ b/frontend/src/components/UI/CardSection.vue
@@ -1,39 +1,30 @@
-
-
-
-
-
-
-
- {{ title.toUpperCase() }}
-
-
-
-
-
-
-
-
-
- {{ $t("general.sort") }}
-
-
-
-
-
- {{ $t("general.recent") }}
-
-
- {{ $t("general.sort-alphabetically") }}
-
-
-
-
-
-
-
-
+
+
+
+ {{ titleIcon }}
+
+ {{ title }}
+
+
+
+
+
+ {{ $t("general.sort") }}
+
+
+
+
+
+ {{ $t("general.recent") }}
+
+
+ {{ $t("general.sort-alphabetically") }}
+
+
+
+
+
+
+
+
+ mdi-plus Custom
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("general.options") }}
+
+
+
+ {{ $t("general.templates") }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/UI/Dialogs/BaseDialog.vue b/frontend/src/components/UI/Dialogs/BaseDialog.vue
index 8030d65c2983..e4a80296eed1 100644
--- a/frontend/src/components/UI/Dialogs/BaseDialog.vue
+++ b/frontend/src/components/UI/Dialogs/BaseDialog.vue
@@ -3,14 +3,14 @@
-
+
{{ titleIcon }}
{{ title }}
-
+
@@ -18,8 +18,12 @@
{{ $t("general.cancel") }}
+
+
+ {{ $t("general.delete") }}
+
- {{ $t("general.submit") }}
+ {{ submitText }}
@@ -31,6 +35,7 @@
diff --git a/frontend/src/pages/Admin/Backup/ImportDialog.vue b/frontend/src/components/UI/Dialogs/ImportDialog.vue
similarity index 97%
rename from frontend/src/pages/Admin/Backup/ImportDialog.vue
rename to frontend/src/components/UI/Dialogs/ImportDialog.vue
index 0aed4c0e2f46..49eef5a33082 100644
--- a/frontend/src/pages/Admin/Backup/ImportDialog.vue
+++ b/frontend/src/components/UI/Dialogs/ImportDialog.vue
@@ -48,7 +48,7 @@
-
-
diff --git a/frontend/src/pages/Admin/Backup/NewBackupCard.vue b/frontend/src/pages/Admin/Backup/NewBackupCard.vue
deleted file mode 100644
index e7d903a12b55..000000000000
--- a/frontend/src/pages/Admin/Backup/NewBackupCard.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
- {{ $t("settings.backup.create-heading") }}
-
-
-
-
-
-
-
- {{ $t("general.create") }}
-
-
-
-
-
-
-
- {{ $t("general.options") }}
-
-
-
- {{ $t("general.templates") }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/Admin/Backup/index.vue b/frontend/src/pages/Admin/Backup/index.vue
deleted file mode 100644
index 6644901b50bc..000000000000
--- a/frontend/src/pages/Admin/Backup/index.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
- {{ $t("settings.backup-and-exports") }}
-
-
-
-
-
-
-
-
-
-
- {{ $t("settings.backup-info") }}
-
-
-
-
-
- {{ $t("settings.available-backups") }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/Admin/Dashboard/BackupViewer.vue b/frontend/src/pages/Admin/Dashboard/BackupViewer.vue
index 5ef1259685c6..cda4e95df8f4 100644
--- a/frontend/src/pages/Admin/Dashboard/BackupViewer.vue
+++ b/frontend/src/pages/Admin/Dashboard/BackupViewer.vue
@@ -26,6 +26,8 @@
+
+
mdi-plus Create
@@ -36,7 +38,7 @@
- mdi-backup-restore
+ mdi-database
@@ -65,13 +67,14 @@
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
import { api } from "@/api";
-import StatCard from "./StatCard";
-import ImportDialog from "../Backup/ImportDialog";
+import StatCard from "@/components/UI/StatCard";
+import BackupDialog from "@/components/UI/Dialogs/BackupDialog";
+import ImportDialog from "@/components/UI/Dialogs/ImportDialog";
export default {
- components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog },
+ components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog, BackupDialog },
data() {
return {
- color: "secondary",
+ color: "accent",
selectedName: "",
selectedDate: "",
loading: false,
@@ -91,7 +94,6 @@ export default {
async getAvailableBackups() {
const response = await api.backups.requestAvailable();
this.availableBackups = response.imports;
- console.log(this.availableBackups);
},
async deleteBackup(name) {
@@ -106,6 +108,7 @@ export default {
this.selectedName = backup.name;
this.$refs.import_dialog.open();
},
+
async importBackup(data) {
this.loading = true;
const response = await api.backups.import(data.name, data);
diff --git a/frontend/src/pages/Admin/Dashboard/EventViewer.vue b/frontend/src/pages/Admin/Dashboard/EventViewer.vue
index 5070c677b6eb..417fe1a176a7 100644
--- a/frontend/src/pages/Admin/Dashboard/EventViewer.vue
+++ b/frontend/src/pages/Admin/Dashboard/EventViewer.vue
@@ -49,12 +49,12 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/pages/Admin/Profile/UserCard.vue b/frontend/src/pages/Admin/Profile/UserCard.vue
new file mode 100644
index 000000000000..190c0fd65142
--- /dev/null
+++ b/frontend/src/pages/Admin/Profile/UserCard.vue
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+ {{ initials }}
+
+
+
+
+
+
+
+
+ {{ $t("group.group") }}: {{ user.group }}
+
+
+
+
+
+
+
+ mdi-lock
+ Change Password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-content-save
+ {{ $t("general.update") }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Admin/Profile/index.vue b/frontend/src/pages/Admin/Profile/index.vue
index 106d4b9899b5..9aea25370871 100644
--- a/frontend/src/pages/Admin/Profile/index.vue
+++ b/frontend/src/pages/Admin/Profile/index.vue
@@ -1,206 +1,27 @@
-
-
-
-
-
-
-
- {{ $t("settings.profile") }}
-
- {{ $t("user.user-id-with-value", { id: user.id }) }}
-
-
-
-
-
-
-
-
- {{ initials }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mdi-content-save
- {{ $t("general.save") }}
-
-
-
-
-
-
-
- {{ $t("user.reset-password") }}
-
-
-
-
-
-
-
-
-
-
-
-
- mdi-eye-off
- mdi-eye
-
-
-
- mdi-lock
- {{ $t("settings.change-password") }}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Admin/Theme/NewThemeDialog.vue b/frontend/src/pages/Admin/Theme/NewThemeDialog.vue
deleted file mode 100644
index f704bada500d..000000000000
--- a/frontend/src/pages/Admin/Theme/NewThemeDialog.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
- {{ $t("settings.add-a-new-theme") }}
-
-
-
-
-
- mdi-format-color-fill
-
-
-
- {{ $t("settings.add-a-new-theme") }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("general.cancel") }}
-
-
- {{ $t("general.create") }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/Admin/Theme/ThemeCard.vue b/frontend/src/pages/Admin/Theme/ThemeCard.vue
deleted file mode 100644
index e0d67e2855f0..000000000000
--- a/frontend/src/pages/Admin/Theme/ThemeCard.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
- {{ theme.name }}
- {{ current ? $t("general.current-parenthesis") : "" }}
-
-
-
-
-
-
-
-
-
-
-
- {{ $t("general.delete") }}
-
-
-
- {{ $t("general.apply") }}
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/Admin/Theme/index.vue b/frontend/src/pages/Admin/Theme/index.vue
deleted file mode 100644
index 4c30adf7df22..000000000000
--- a/frontend/src/pages/Admin/Theme/index.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-
-
-
- {{ $t("settings.theme.theme-settings") }}
-
-
-
- {{ $t("settings.theme.dark-mode") }}
-
- {{
- $t(
- "settings.theme.choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme"
- )
- }}
-
-
-
-
-
- mdi-desktop-tower-monitor
-
- {{ $t("settings.theme.default-to-system") }}
-
-
-
-
- mdi-white-balance-sunny
-
- {{ $t("settings.theme.light") }}
-
-
-
-
- mdi-weather-night
-
- {{ $t("settings.theme.dark") }}
-
-
-
-
-
-
-
- {{ $t("settings.theme.theme") }}
-
- {{
- $t(
- "settings.theme.select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference"
- )
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mdi-content-save
- {{ $t("general.save") }}
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/HomePage.vue b/frontend/src/pages/HomePage.vue
index abc1f7d52d71..f8d0b5cd1f15 100644
--- a/frontend/src/pages/HomePage.vue
+++ b/frontend/src/pages/HomePage.vue
@@ -1,6 +1,7 @@
None:
- self.primary_key = "name"
+ self.primary_key = "id"
self.sql_model = SiteThemeModel
self.schema = SiteTheme
diff --git a/mealie/db/models/theme.py b/mealie/db/models/theme.py
index 17cb7a229ad5..5d7113399c1d 100644
--- a/mealie/db/models/theme.py
+++ b/mealie/db/models/theme.py
@@ -1,23 +1,21 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
-from mealie.db.models.model_base import SqlAlchemyBase
+from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
+from sqlalchemy.sql.sqltypes import Integer
-class SiteThemeModel(SqlAlchemyBase):
+class SiteThemeModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "site_theme"
- name = sa.Column(sa.String, primary_key=True)
+ id = sa.Column(Integer, primary_key=True, unique=True)
+ name = sa.Column(sa.String, nullable=False, unique=True)
colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete")
- def __init__(self, name: str, colors: dict, session=None) -> None:
+ def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
self.name = name
self.colors = ThemeColorsModel(**colors)
- def update(self, session=None, name: str = None, colors: dict = None) -> dict:
- self.colors.update(**colors)
- return self
-
-class ThemeColorsModel(SqlAlchemyBase):
+class ThemeColorsModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "theme_colors"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
@@ -28,21 +26,3 @@ class ThemeColorsModel(SqlAlchemyBase):
info = sa.Column(sa.String)
warning = sa.Column(sa.String)
error = sa.Column(sa.String)
-
- def update(
- self,
- primary: str = None,
- accent: str = None,
- secondary: str = None,
- success: str = None,
- info: str = None,
- warning: str = None,
- error: str = None,
- ) -> None:
- self.primary = primary
- self.accent = accent
- self.secondary = secondary
- self.success = success
- self.info = info
- self.warning = warning
- self.error = error
diff --git a/mealie/routes/about/__init__.py b/mealie/routes/about/__init__.py
index e36affaa90d1..70eb8f5c86f8 100644
--- a/mealie/routes/about/__init__.py
+++ b/mealie/routes/about/__init__.py
@@ -1,7 +1,7 @@
from fastapi import APIRouter
-from .events import router as events_router
+from . import events
about_router = APIRouter(prefix="/api/about")
-about_router.include_router(events_router)
+about_router.include_router(events.router)
diff --git a/mealie/routes/groups/__init__.py b/mealie/routes/groups/__init__.py
index e69de29bb2d1..f8935bdb64c0 100644
--- a/mealie/routes/groups/__init__.py
+++ b/mealie/routes/groups/__init__.py
@@ -0,0 +1,8 @@
+from fastapi import APIRouter
+
+from . import crud, groups
+
+groups_router = APIRouter()
+
+groups_router.include_router(crud.router)
+groups_router.include_router(groups.router)
diff --git a/mealie/routes/mealplans/__init__.py b/mealie/routes/mealplans/__init__.py
index e69de29bb2d1..c2dfae352c02 100644
--- a/mealie/routes/mealplans/__init__.py
+++ b/mealie/routes/mealplans/__init__.py
@@ -0,0 +1,9 @@
+from fastapi import APIRouter
+
+from . import crud, helpers, mealplans
+
+meal_plan_router = APIRouter()
+
+meal_plan_router.include_router(crud.router)
+meal_plan_router.include_router(helpers.router)
+meal_plan_router.include_router(mealplans.router)
diff --git a/mealie/routes/media/__init__.py b/mealie/routes/media/__init__.py
new file mode 100644
index 000000000000..e28579ee6607
--- /dev/null
+++ b/mealie/routes/media/__init__.py
@@ -0,0 +1,7 @@
+from fastapi import APIRouter
+
+from . import recipe
+
+media_router = APIRouter(prefix="/api/media", tags=["Site Media"])
+
+media_router.include_router(recipe.router)
diff --git a/mealie/routes/media/recipe.py b/mealie/routes/media/recipe.py
new file mode 100644
index 000000000000..ad8ffac4e417
--- /dev/null
+++ b/mealie/routes/media/recipe.py
@@ -0,0 +1,41 @@
+from enum import Enum
+
+from fastapi import APIRouter, HTTPException, status
+from mealie.schema.recipe import Recipe
+from starlette.responses import FileResponse
+
+"""
+These routes are for development only! These assets are served by Caddy when not
+in development mode. If you make changes, be sure to test the production container.
+"""
+
+router = APIRouter(prefix="/recipes")
+
+
+class ImageType(str, Enum):
+ original = "original.webp"
+ small = "min-original.webp"
+ tiny = "tiny-original.webp"
+
+
+@router.get("/{recipe_slug}/images/{file_name}")
+async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
+ """Takes in a recipe slug, returns the static image. This route is proxied in the docker image
+ and should not hit the API in production"""
+ recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
+
+ if recipe_image:
+ return FileResponse(recipe_image)
+ else:
+ raise HTTPException(status.HTTP_404_NOT_FOUND)
+
+
+@router.get("/{recipe_slug}/assets/{file_name}")
+async def get_recipe_asset(recipe_slug: str, file_name: str):
+ """ Returns a recipe asset """
+ file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
+
+ try:
+ return FileResponse(file)
+ except Exception:
+ raise HTTPException(status.HTTP_404_NOT_FOUND)
diff --git a/mealie/routes/media/user.py b/mealie/routes/media/user.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/mealie/routes/recipe/__init__.py b/mealie/routes/recipe/__init__.py
index 1d54034f0bf7..fb9a0d55bd32 100644
--- a/mealie/routes/recipe/__init__.py
+++ b/mealie/routes/recipe/__init__.py
@@ -1,10 +1,9 @@
from fastapi import APIRouter
-from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, recipe_media, tag_routes
+from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
-router = APIRouter()
+recipe_router = APIRouter()
-router.include_router(all_recipe_routes.router)
-router.include_router(recipe_crud_routes.router)
-router.include_router(recipe_media.router)
-router.include_router(category_routes.router)
-router.include_router(tag_routes.router)
+recipe_router.include_router(all_recipe_routes.router)
+recipe_router.include_router(recipe_crud_routes.router)
+recipe_router.include_router(category_routes.router)
+recipe_router.include_router(tag_routes.router)
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index 6604b84e664f..110393050970 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -1,13 +1,17 @@
+from shutil import copyfileobj
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
+from fastapi.datastructures import UploadFile
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
-from mealie.schema.recipe import Recipe, RecipeURLIn
+from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
from mealie.services.events import create_recipe_event
from mealie.services.image.image import scrape_image, write_image
from mealie.services.recipe.media import check_assets, delete_assets
from mealie.services.scraper.scraper import create_from_url
+from slugify import slugify
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
@@ -126,3 +130,30 @@ def scrape_image_url(
""" Removes an existing image and replaces it with the incoming file. """
scrape_image(url.url, recipe_slug)
+
+
+@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
+def upload_recipe_asset(
+ recipe_slug: str,
+ name: str = Form(...),
+ icon: str = Form(...),
+ extension: str = Form(...),
+ file: UploadFile = File(...),
+ session: Session = Depends(generate_session),
+ current_user=Depends(get_current_user),
+):
+ """ Upload a file to store as a recipe asset """
+ file_name = slugify(name) + "." + extension
+ asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
+ dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
+
+ with dest.open("wb") as buffer:
+ copyfileobj(file.file, buffer)
+
+ if not dest.is_file():
+ raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ recipe: Recipe = db.recipes.get(session, recipe_slug)
+ recipe.assets.append(asset_in)
+ db.recipes.update(session, recipe_slug, recipe.dict())
+ return asset_in
diff --git a/mealie/routes/recipe/recipe_media.py b/mealie/routes/recipe/recipe_media.py
deleted file mode 100644
index e7c3402a53f9..000000000000
--- a/mealie/routes/recipe/recipe_media.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import shutil
-from enum import Enum
-
-from fastapi import APIRouter, Depends, File, Form, HTTPException, status
-from fastapi.datastructures import UploadFile
-from mealie.db.database import db
-from mealie.db.db_setup import generate_session
-from mealie.routes.deps import get_current_user
-from mealie.schema.recipe import Recipe, RecipeAsset
-from slugify import slugify
-from sqlalchemy.orm.session import Session
-from starlette.responses import FileResponse
-
-router = APIRouter(prefix="/api/recipes/media", tags=["Recipe Media"])
-
-
-class ImageType(str, Enum):
- original = "original.webp"
- small = "min-original.webp"
- tiny = "tiny-original.webp"
-
-
-@router.get("/{recipe_slug}/images/{file_name}")
-async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
- """Takes in a recipe slug, returns the static image. This route is proxied in the docker image
- and should not hit the API in production"""
- recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
-
- if recipe_image:
- return FileResponse(recipe_image)
- else:
- raise HTTPException(status.HTTP_404_NOT_FOUND)
-
-
-@router.get("/{recipe_slug}/assets/{file_name}")
-async def get_recipe_asset(recipe_slug: str, file_name: str):
- """ Returns a recipe asset """
- file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
-
- try:
- return FileResponse(file)
- except Exception:
- raise HTTPException(status.HTTP_404_NOT_FOUND)
-
-
-@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
-def upload_recipe_asset(
- recipe_slug: str,
- name: str = Form(...),
- icon: str = Form(...),
- extension: str = Form(...),
- file: UploadFile = File(...),
- session: Session = Depends(generate_session),
- current_user=Depends(get_current_user),
-):
- """ Upload a file to store as a recipe asset """
- file_name = slugify(name) + "." + extension
- asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
- dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
-
- with dest.open("wb") as buffer:
- shutil.copyfileobj(file.file, buffer)
-
- if not dest.is_file():
- raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- recipe: Recipe = db.recipes.get(session, recipe_slug)
- recipe.assets.append(asset_in)
- db.recipes.update(session, recipe_slug, recipe.dict())
- return asset_in
diff --git a/mealie/routes/site_settings/__init__.py b/mealie/routes/site_settings/__init__.py
index e69de29bb2d1..ec356e63f0cb 100644
--- a/mealie/routes/site_settings/__init__.py
+++ b/mealie/routes/site_settings/__init__.py
@@ -0,0 +1,9 @@
+from fastapi import APIRouter
+
+from . import all_settings, custom_pages, site_settings
+
+settings_router = APIRouter()
+
+settings_router.include_router(all_settings.router)
+settings_router.include_router(custom_pages.router)
+settings_router.include_router(site_settings.router)
diff --git a/mealie/routes/theme_routes.py b/mealie/routes/theme_routes.py
index 9f25c8eaecff..6bbe5ef0c666 100644
--- a/mealie/routes/theme_routes.py
+++ b/mealie/routes/theme_routes.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, Depends, status, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
@@ -21,27 +21,27 @@ def create_theme(data: SiteTheme, session: Session = Depends(generate_session),
db.themes.create(session, data.dict())
-@router.get("/themes/{theme_name}")
-def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
+@router.get("/themes/{id}")
+def get_single_theme(id: int, session: Session = Depends(generate_session)):
""" Returns a named theme """
- return db.themes.get(session, theme_name)
+ return db.themes.get(session, id)
-@router.put("/themes/{theme_name}", status_code=status.HTTP_200_OK)
+@router.put("/themes/{id}", status_code=status.HTTP_200_OK)
def update_theme(
- theme_name: str,
+ id: int,
data: SiteTheme,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Update a theme database entry """
- db.themes.update(session, theme_name, data.dict())
+ db.themes.update(session, id, data.dict())
-@router.delete("/themes/{theme_name}", status_code=status.HTTP_200_OK)
-def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
+@router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
+def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
""" Deletes theme from the database """
try:
- db.themes.delete(session, theme_name)
+ db.themes.delete(session, id)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
diff --git a/mealie/routes/users/__init__.py b/mealie/routes/users/__init__.py
index e69de29bb2d1..de249ec01039 100644
--- a/mealie/routes/users/__init__.py
+++ b/mealie/routes/users/__init__.py
@@ -0,0 +1,9 @@
+from fastapi import APIRouter
+
+from . import auth, crud, sign_up
+
+user_router = APIRouter()
+
+user_router.include_router(auth.router)
+user_router.include_router(sign_up.router)
+user_router.include_router(crud.router)
diff --git a/mealie/routes/users/users.py b/mealie/routes/users/users.py
deleted file mode 100644
index 9f3088a99118..000000000000
--- a/mealie/routes/users/users.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from fastapi import APIRouter
-from mealie.routes.users import auth, crud, sign_up
-
-router = APIRouter()
-
-router.include_router(sign_up.router)
-router.include_router(auth.router)
-router.include_router(sign_up.router)
-router.include_router(crud.router)
diff --git a/mealie/schema/theme.py b/mealie/schema/theme.py
index 32b25282e665..62f48c2c0c8d 100644
--- a/mealie/schema/theme.py
+++ b/mealie/schema/theme.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
from pydantic import BaseModel
@@ -5,7 +7,7 @@ class Colors(BaseModel):
primary: str = "#E58325"
accent: str = "#00457A"
secondary: str = "#973542"
- success: str = "#4CAF50"
+ success: str = "#43A047"
info: str = "#4990BA"
warning: str = "#FF4081"
error: str = "#EF5350"
@@ -15,6 +17,7 @@ class Colors(BaseModel):
class SiteTheme(BaseModel):
+ id: Optional[int]
name: str = "default"
colors: Colors = Colors()
diff --git a/tests/integration_tests/test_settings_routes.py b/tests/integration_tests/test_settings_routes.py
index 2a6f30510b01..2076d37db99a 100644
--- a/tests/integration_tests/test_settings_routes.py
+++ b/tests/integration_tests/test_settings_routes.py
@@ -14,18 +14,19 @@ def default_settings():
@pytest.fixture(scope="session")
def default_theme():
- return SiteTheme().dict()
+ return SiteTheme(id=1).dict()
@pytest.fixture(scope="session")
def new_theme():
return {
+ "id": 2,
"name": "myTestTheme",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
- "success": "#5AB1BB",
+ "success": "#43A047",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
@@ -54,7 +55,7 @@ def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme):
- response = api_client.get(api_routes.themes_theme_name("default"))
+ response = api_client.get(api_routes.themes_theme_name(1))
assert response.status_code == 200
assert json.loads(response.content) == default_theme
@@ -64,7 +65,7 @@ def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme,
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
assert response.status_code == 201
- response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")), headers=token)
+ response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")), headers=token)
assert response.status_code == 200
assert json.loads(response.content) == new_theme
@@ -77,7 +78,7 @@ def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
for theme in [default_theme, new_theme]:
- response = api_client.get(api_routes.themes_theme_name(theme.get("name")))
+ response = api_client.get(api_routes.themes_theme_name(theme.get("id")))
assert response.status_code == 200
assert json.loads(response.content) == theme
@@ -94,14 +95,14 @@ def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, defa
}
new_theme["colors"] = theme_colors
- response = api_client.put(api_routes.themes_theme_name(new_theme.get("name")), json=new_theme, headers=token)
+ response = api_client.put(api_routes.themes_theme_name(new_theme.get("id")), json=new_theme, headers=token)
assert response.status_code == 200
- response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")))
+ response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")))
assert json.loads(response.content) == new_theme
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
for theme in [default_theme, new_theme]:
- response = api_client.delete(api_routes.themes_theme_name(theme.get("name")), headers=token)
+ response = api_client.delete(api_routes.themes_theme_name(theme.get("id")), headers=token)
assert response.status_code == 200