mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Feature/authentication (#206)
* 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 Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
bbf114799f
commit
4a9955450c
103 changed files with 4255 additions and 2503 deletions
|
@ -7,7 +7,7 @@ RUN npm run build
|
|||
|
||||
FROM python:3.9-alpine
|
||||
|
||||
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2 caddy
|
||||
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2 caddy libffi-dev
|
||||
ENV ENV prod
|
||||
EXPOSE 80
|
||||
WORKDIR /app
|
||||
|
|
|
@ -1,5 +1,36 @@
|
|||
# Release Notes
|
||||
|
||||
## v0.4.0 Whoa, What a Release! [DRAFT]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### Features and Improvements
|
||||
- Authentication! Tons of stuff went into creating a flexible authentication platform for a lot of different use cases. Review the documentation for more information on how to use the authentication, and how everything works together. Some key features include
|
||||
- Sign Up Links
|
||||
- Admin and User Roles
|
||||
- Group Management
|
||||
- Create/Edit/Delete Restrictions
|
||||
- Recipe Database Refactoring. Tons of new information is now stored for recipes in the database. Not all is accessible via the UI, but it's coming.
|
||||
- Nutrition Information
|
||||
- calories
|
||||
- fatContent
|
||||
- fiberContent
|
||||
- proteinContent
|
||||
- sodiumContent
|
||||
- sugarContent
|
||||
- recipeCuisine has been added
|
||||
- "categories" has been migrated to "recipeCategory" to adhear closer to the standard schema
|
||||
- "tool" - a list of tools used for the recipe
|
||||
- Removed CDN dependencies
|
||||
- Completed Redesign of the Admin Panel
|
||||
- Profile Pages
|
||||
- Side Panel Menu
|
||||
- Language selector is now displayed on all pages and does not require an account
|
||||
|
||||
### Development / Misc
|
||||
- Database Model Refactoring
|
||||
- File/Folder Name Refactoring
|
||||
|
||||
## v0.3.0
|
||||
|
||||
### Bug Fixes
|
||||
|
|
35
docs/docs/getting-started/ios.md
Normal file
35
docs/docs/getting-started/ios.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Using iOS Shortcuts with Mealie
|
||||
{: align=right style="height:400px;width:400px"}
|
||||
|
||||
|
||||
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/hay-kot/mealie/issues/103) for interested users. This is a useful utility for iOS users who browse for recipes in their web browser from their devices.
|
||||
|
||||
Don't know what an iOS shortcut is? Neither did I! Experienced iOS users may already be familiar with this utility but for the uninitiated, here is the official Apple explanation:
|
||||
|
||||
|
||||
> A shortcut is a quick way to get one or more tasks done with your apps. The Shortcuts app lets you create your own shortcuts with multiple steps. For example, build a “Surf Time” shortcut that grabs the surf report, gives an ETA to the beach, and launches your surf music playlist.
|
||||
|
||||
|
||||
Basically it is a visual scripting language that lets a user build an automation in a guided fashion. The automation can be [shared with anyone](https://www.icloud.com/shortcuts/6ae356d5fc644cfa8983a3c90f242fbb) but if it is a user creation, you'll have to jump through a few hoops to make an untrusted automation work on your device. In brasilikum's shortcut, you need to make changes for it to work. Recent updates to the project have changed some of the syntax and folder structure since its original creation.
|
||||
|
||||
|
||||
{: align=right style="height:500;width:400px"}
|
||||
|
||||
|
||||
|
||||
!!! tip
|
||||
You may need to change the url depending on which version you're using. Recipe is now plural and there is no trailing "/" at the end of the string.
|
||||
|
||||
```
|
||||
api/recipe/create-url/
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```
|
||||
api/recipes/create-url
|
||||
```
|
||||
|
||||
|
||||
|
||||
Having made those changes, you should now be able to share a website to the shortcut and have mealie grab all the necessary information!
|
BIN
docs/docs/img/ios-shortcut-image.jpg
Normal file
BIN
docs/docs/img/ios-shortcut-image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
docs/docs/img/iphone-image.png
Normal file
BIN
docs/docs/img/iphone-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 350 KiB |
|
@ -29,6 +29,7 @@ nav:
|
|||
- Getting Started:
|
||||
- Installation: "getting-started/install.md"
|
||||
- Working With Recipes: "getting-started/recipes.md"
|
||||
- iOS Shortcuts: "getting-started/ios.md"
|
||||
- User Management: "getting-started/users.md"
|
||||
- Planning Meals: "getting-started/meal-planner.md"
|
||||
- Site Settings: "getting-started/site-settings.md"
|
||||
|
|
3003
frontend/package-lock.json
generated
3003
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -12,33 +12,35 @@
|
|||
"@adapttive/vue-markdown": "^3.0.3",
|
||||
"@smartweb/vue-flash-message": "^0.6.10",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.8.2",
|
||||
"core-js": "^3.9.1",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"qs": "^6.9.6",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"v-jsoneditor": "^1.4.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-i18n": "^8.22.4",
|
||||
"vue-router": "^3.4.9",
|
||||
"vue-i18n": "^8.24.1",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.4.2",
|
||||
"vuex": "^3.6.0",
|
||||
"vuetify": "^2.4.6",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/vue-i18n-loader": "^1.0.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.10",
|
||||
"@vue/cli-plugin-eslint": "^4.5.10",
|
||||
"@vue/cli-service": "^4.5.10",
|
||||
"@intlify/vue-i18n-loader": "^1.1.0",
|
||||
"@mdi/font": "^5.9.55",
|
||||
"@vue/cli-plugin-babel": "^4.5.11",
|
||||
"@vue/cli-plugin-eslint": "^4.5.11",
|
||||
"@vue/cli-service": "^4.5.11",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"sass": "^1.32.4",
|
||||
"sass-loader": "^8.0.0",
|
||||
"sass": "^1.32.8",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-cli-plugin-i18n": "~1.0.1",
|
||||
"vue-cli-plugin-vuetify": "^2.0.8",
|
||||
"vue-cli-plugin-vuetify": "^2.2.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.3.0"
|
||||
"vuetify-loader": "^1.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title> Mealie </title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> -->
|
||||
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"> -->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<v-expand-x-transition>
|
||||
<SearchBar
|
||||
ref="mainSearchBar"
|
||||
class="mt-7"
|
||||
v-if="search"
|
||||
:show-results="true"
|
||||
@selected="navigateFromSearch"
|
||||
|
@ -78,6 +77,8 @@ export default {
|
|||
this.$store.dispatch("initTheme");
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
this.$store.dispatch("requestHomePageSettings");
|
||||
this.$store.dispatch("requestSiteSettings");
|
||||
this.$store.dispatch("refreshToken");
|
||||
this.darkModeSystemCheck();
|
||||
this.darkModeAddEventListener();
|
||||
},
|
||||
|
|
|
@ -47,7 +47,7 @@ const apiReq = {
|
|||
return response;
|
||||
} else return;
|
||||
});
|
||||
// processResponse(response);
|
||||
processResponse(response);
|
||||
return response;
|
||||
},
|
||||
|
||||
|
|
34
frontend/src/api/groups.js
Normal file
34
frontend/src/api/groups.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
const groupPrefix = baseURL + "groups";
|
||||
|
||||
const groupsURLs = {
|
||||
groups: `${groupPrefix}`,
|
||||
create: `${groupPrefix}`,
|
||||
delete: id => `${groupPrefix}/${id}`,
|
||||
current: `${groupPrefix}/self`,
|
||||
update: id => `${groupPrefix}/${id}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
async allGroups() {
|
||||
let response = await apiReq.get(groupsURLs.groups);
|
||||
return response.data;
|
||||
},
|
||||
async create(name) {
|
||||
let response = await apiReq.post(groupsURLs.create, { name: name });
|
||||
return response.data;
|
||||
},
|
||||
async delete(id) {
|
||||
let response = await apiReq.delete(groupsURLs.delete(id));
|
||||
return response.data;
|
||||
},
|
||||
async current() {
|
||||
let response = await apiReq.get(groupsURLs.current);
|
||||
return response.data;
|
||||
},
|
||||
async update(data) {
|
||||
let response = await apiReq.put(groupsURLs.update(data.id), data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -9,9 +9,12 @@ import category from "./category";
|
|||
import meta from "./meta";
|
||||
import users from "./users";
|
||||
import signUps from "./signUps";
|
||||
import groups from "./groups";
|
||||
import siteSettings from "./siteSettings";
|
||||
|
||||
export default {
|
||||
recipes: recipe,
|
||||
siteSettings: siteSettings,
|
||||
backups: backup,
|
||||
mealPlans: mealplan,
|
||||
settings: settings,
|
||||
|
@ -22,4 +25,5 @@ export default {
|
|||
meta: meta,
|
||||
users: users,
|
||||
signUps: signUps,
|
||||
groups: groups,
|
||||
};
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
router.push(`/`);
|
||||
},
|
||||
|
||||
async allByKeys(recipeKeys, num = 999) {
|
||||
async allByKeys(recipeKeys, num = 9999) {
|
||||
const response = await apiReq.get(recipeURLs.allRecipes, {
|
||||
params: {
|
||||
keys: recipeKeys,
|
||||
|
|
22
frontend/src/api/siteSettings.js
Normal file
22
frontend/src/api/siteSettings.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
|
||||
const settingsBase = baseURL + "site-settings";
|
||||
|
||||
const settingsURLs = {
|
||||
siteSettings: `${settingsBase}`,
|
||||
updateSiteSettings: `${settingsBase}`,
|
||||
testWebhooks: `${settingsBase}/webhooks/test`,
|
||||
};
|
||||
|
||||
export default {
|
||||
async get() {
|
||||
let response = await apiReq.get(settingsURLs.siteSettings);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async update(body) {
|
||||
let response = await apiReq.put(settingsURLs.updateSiteSettings, body);
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,10 +1,12 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import axios from "axios";
|
||||
const authPrefix = baseURL + "auth";
|
||||
const userPrefix = baseURL + "users";
|
||||
|
||||
const authURLs = {
|
||||
token: `${authPrefix}/token`,
|
||||
refresh: `${authPrefix}/refresh`,
|
||||
};
|
||||
|
||||
|
||||
|
@ -24,6 +26,12 @@ export default {
|
|||
});
|
||||
return response;
|
||||
},
|
||||
async refresh() {
|
||||
let response = await axios.get(authURLs.refresh).catch(function(event) {
|
||||
console.log("Fetch failed", event);
|
||||
});
|
||||
return response.data ? response.data : false;
|
||||
},
|
||||
async allUsers() {
|
||||
let response = await apiReq.get(usersURLs.users);
|
||||
return response.data;
|
||||
|
|
|
@ -3,9 +3,12 @@
|
|||
<v-card-text>
|
||||
<h2 class="mt-1 mb-1">{{ $t("settings.homepage.home-page") }}</h2>
|
||||
<v-row align="center" justify="center" dense class="mb-n7 pb-n5">
|
||||
<v-col cols="1">
|
||||
<LanguageMenu @select-lang="writeLang" :site-settings="true" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="3" md="2">
|
||||
<v-switch
|
||||
v-model="showRecent"
|
||||
v-model="settings.showRecent"
|
||||
:label="$t('settings.homepage.show-recent')"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
|
@ -13,7 +16,7 @@
|
|||
<v-slider
|
||||
class="pt-sm-4"
|
||||
:label="$t('settings.homepage.card-per-section')"
|
||||
v-model="showLimit"
|
||||
v-model="settings.cardsPerSection"
|
||||
max="30"
|
||||
dense
|
||||
color="primary"
|
||||
|
@ -35,7 +38,7 @@
|
|||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
Home Page Categories
|
||||
Home Page Sections
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
@ -43,14 +46,14 @@
|
|||
<v-list height="300" dense style="overflow:auto">
|
||||
<v-list-item-group>
|
||||
<draggable
|
||||
v-model="homeCategories"
|
||||
v-model="settings.categories"
|
||||
group="categories"
|
||||
:style="{
|
||||
minHeight: `150px`,
|
||||
}"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in homeCategories"
|
||||
v-for="(item, index) in settings.categories"
|
||||
:key="`${item.name}-${index}`"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
|
@ -85,14 +88,14 @@
|
|||
<v-list height="300" dense style="overflow:auto">
|
||||
<v-list-item-group>
|
||||
<draggable
|
||||
v-model="categories"
|
||||
v-model="allCategories"
|
||||
group="categories"
|
||||
:style="{
|
||||
minHeight: `150px`,
|
||||
}"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in categories"
|
||||
v-for="(item, index) in allCategories"
|
||||
:key="`${item.name}-${index}`"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
|
@ -127,47 +130,50 @@
|
|||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
LanguageMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
homeCategories: null,
|
||||
showLimit: null,
|
||||
showRecent: true,
|
||||
settings: {
|
||||
language: "en",
|
||||
showRecent: null,
|
||||
cardsPerSection: null,
|
||||
categories: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getOptions();
|
||||
},
|
||||
computed: {
|
||||
categories() {
|
||||
allCategories() {
|
||||
return this.$store.getters.getCategories;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
writeLang(val) {
|
||||
console.log(val);
|
||||
this.settings.language = val;
|
||||
},
|
||||
deleteCategoryfromDatabase(category) {
|
||||
api.categories.delete(category);
|
||||
this.$store.dispatch("requestHomePageSettings");
|
||||
},
|
||||
getOptions() {
|
||||
this.showLimit = this.$store.getters.getShowLimit;
|
||||
this.showRecent = this.$store.getters.getShowRecent;
|
||||
this.homeCategories = this.$store.getters.getHomeCategories;
|
||||
async getOptions() {
|
||||
this.settings = await api.siteSettings.get();
|
||||
},
|
||||
deleteActiveCategory(index) {
|
||||
this.homeCategories.splice(index, 1);
|
||||
this.settings.categories.splice(index, 1);
|
||||
},
|
||||
saveSettings() {
|
||||
this.homeCategories.forEach((element, index) => {
|
||||
element.position = index + 1;
|
||||
});
|
||||
this.$store.commit("setShowRecent", this.showRecent);
|
||||
this.$store.commit("setShowLimit", this.showLimit);
|
||||
this.$store.commit("setHomeCategories", this.homeCategories);
|
||||
async saveSettings() {
|
||||
await api.siteSettings.update(this.settings);
|
||||
this.getOptions();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
123
frontend/src/components/Admin/ManageUsers/GroupCard.vue
Normal file
123
frontend/src/components/Admin/ManageUsers/GroupCard.vue
Normal file
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<div>
|
||||
<Confirmation
|
||||
ref="deleteGroupConfirm"
|
||||
title="Confirm Group Deletion"
|
||||
:message="`Are you sure you want to delete <b>${group.name}<b/>`"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteGroup"
|
||||
:width="450"
|
||||
@close="closeGroupDelete"
|
||||
/>
|
||||
<v-card class="ma-auto" tile min-height="325px">
|
||||
<v-list dense>
|
||||
<v-card-title class="py-1">{{ group.name }}</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-subheader>Group ID: {{ group.id }}</v-subheader>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="property in groupProps" :key="property.text">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || "mdi-account" }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-4 flex row justify-space-between">
|
||||
<div>{{ property.text }}</div>
|
||||
<div>{{ property.value }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="confirmDelete"
|
||||
:disabled="ableToDelete"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
<!-- Coming Soon! -->
|
||||
<v-btn small color="success" disabled>
|
||||
Edit
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const RENDER_EVENT = "update";
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
props: {
|
||||
group: {
|
||||
default: {
|
||||
name: "DEFAULT_NAME",
|
||||
id: 1,
|
||||
users: [],
|
||||
mealplans: [],
|
||||
categories: [],
|
||||
webhookUrls: [],
|
||||
webhookTime: "00:00",
|
||||
webhookEnable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groupProps: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ableToDelete() {
|
||||
return this.group.users.length >= 1 ? true : false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.buildData();
|
||||
},
|
||||
methods: {
|
||||
confirmDelete() {
|
||||
this.$refs.deleteGroupConfirm.open();
|
||||
},
|
||||
async deleteGroup() {
|
||||
await api.groups.delete(this.group.id);
|
||||
this.$emit(RENDER_EVENT);
|
||||
},
|
||||
closeGroupDelete() {
|
||||
console.log("Close Delete");
|
||||
},
|
||||
buildData() {
|
||||
this.groupProps = [
|
||||
{
|
||||
text: "Total Users",
|
||||
icon: "mdi-account",
|
||||
value: this.group.users.length,
|
||||
},
|
||||
{
|
||||
text: "Total MealPlans",
|
||||
icon: "mdi-food",
|
||||
value: this.group.mealplans.length,
|
||||
},
|
||||
{
|
||||
text: "Webhooks Enabled",
|
||||
icon: "mdi-webhook",
|
||||
value: this.group.webhookEnable ? "True" : "False",
|
||||
},
|
||||
{
|
||||
text: "Webhook Time",
|
||||
icon: "mdi-clock-outline",
|
||||
value: this.group.webhookTime,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
120
frontend/src/components/Admin/ManageUsers/GroupDashboard.vue
Normal file
120
frontend/src/components/Admin/ManageUsers/GroupDashboard.vue
Normal file
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card outlined class="mt-n1">
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<div width="100px">
|
||||
<v-text-field
|
||||
v-model="filter"
|
||||
clearable
|
||||
class="mr-2 pt-0"
|
||||
append-icon="mdi-filter"
|
||||
label="Filter"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-dialog v-model="groupDialog" max-width="400">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
class="mx-2"
|
||||
small
|
||||
color="success"
|
||||
dark
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
Create Group
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account-group
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
Create Group
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="newGroup">
|
||||
<v-text-field
|
||||
v-model="newGroupName"
|
||||
label="Group Name"
|
||||
:rules="[existsRule]"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="groupDialog = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="createGroup">
|
||||
Create
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card-actions>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="3"
|
||||
:xl="3"
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
>
|
||||
<GroupCard
|
||||
:group="group"
|
||||
@update="$store.dispatch('requestAllGroups')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validators } from "@/mixins/validators";
|
||||
import api from "@/api";
|
||||
import GroupCard from "@/components/Admin/ManageUsers/GroupCard";
|
||||
export default {
|
||||
components: { GroupCard },
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
filter: "",
|
||||
groupDialog: false,
|
||||
newGroupName: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
groups() {
|
||||
return this.$store.getters.getGroups;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async createGroup() {
|
||||
this.groupLoading = true;
|
||||
let response = await api.groups.create(this.newGroupName);
|
||||
if (response.created) {
|
||||
this.groupLoading = false;
|
||||
this.groupDialog = false;
|
||||
this.$store.dispatch("requestAllGroups");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,252 +0,0 @@
|
|||
<template>
|
||||
<v-card outlined class="mt-n1">
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm User Deletion"
|
||||
:message="
|
||||
`Are you sure you want to delete the user <b>${activeName} ID: ${activeId}<b/>`
|
||||
"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-icon large color="accent" class="mr-1">
|
||||
mdi-account-group
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headine">
|
||||
User Groups
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
Create Group
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ formTitle }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title class="headline">
|
||||
User ID: {{ editedItem.id }}
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="newUser">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
||||
<v-text-field
|
||||
v-model="editedItem.password"
|
||||
label="User Password"
|
||||
:rules="[existsRule, minRule]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-switch v-model="editedItem.admin" label="Admin"></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="save">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="users" sort-by="calories">
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-btn small color="success" @click="editItem(item)">
|
||||
<v-icon small left class="mr-2">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-btn color="primary" @click="initialize">
|
||||
Reset
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "User ID",
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
users: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formTitle() {
|
||||
return this.editedIndex === -1 ? "New User" : "Edit User";
|
||||
},
|
||||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.activeId = item.id;
|
||||
this.activeName = item.fullName;
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.$refs.deleteUserDialog.open();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.users.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.users.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -68,6 +68,7 @@
|
|||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="links" sort-by="calories">
|
||||
<template v-slot:item.token="{ item }">
|
||||
|
|
|
@ -12,14 +12,18 @@
|
|||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-icon large color="accent" class="mr-1">
|
||||
mdi-account
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headine">
|
||||
Users
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<div width="100px">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
class="mr-2"
|
||||
append-icon="mdi-filter"
|
||||
label="Filter"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
|
@ -62,13 +66,16 @@
|
|||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></v-text-field>
|
||||
<v-select
|
||||
dense
|
||||
v-model="editedItem.group"
|
||||
:items="existingGroups"
|
||||
label="User Group"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
||||
<v-text-field
|
||||
dense
|
||||
v-model="editedItem.password"
|
||||
label="User Password"
|
||||
:rules="[existsRule, minRule]"
|
||||
|
@ -95,7 +102,12 @@
|
|||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="users" sort-by="calories">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="users"
|
||||
sort-by="calories"
|
||||
:search="search"
|
||||
>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
|
@ -131,6 +143,7 @@ export default {
|
|||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
search: "",
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
|
@ -143,7 +156,7 @@ export default {
|
|||
},
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Group", value: "group" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
|
@ -154,7 +167,7 @@ export default {
|
|||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
group: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
|
@ -162,7 +175,7 @@ export default {
|
|||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
group: "",
|
||||
admin: false,
|
||||
},
|
||||
}),
|
||||
|
@ -174,6 +187,9 @@ export default {
|
|||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
existingGroups() {
|
||||
return this.$store.getters.getGroupNames;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<UploadBtn
|
||||
class="mt-1"
|
||||
:url="`/api/migrations/${folder}/upload`"
|
||||
fileName="archive"
|
||||
@uploaded="$emit('refresh')"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
<h3>{{ theme.name }} {{ current ? "(Current)" : "" }}</h3>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-row flex align-center>
|
||||
<v-card
|
||||
v-for="(color, index) in theme.colors"
|
||||
:key="index"
|
||||
class="mx-1"
|
||||
class="ma-1 mx-auto"
|
||||
height="34"
|
||||
width="36"
|
||||
:color="color"
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
have a valid invitation link. If you haven't recieved an invitation you
|
||||
are unable to sign-up. To recieve a link, contact the sites administrator.
|
||||
<v-divider class="mt-3"></v-divider>
|
||||
<v-form>
|
||||
<v-form ref="signUpForm">
|
||||
<v-text-field
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
prepend-icon="mdi-account"
|
||||
validate-on-blur
|
||||
:rules="[existsRule]"
|
||||
label="Display Name"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
|
@ -35,6 +36,7 @@
|
|||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
validate-on-blur
|
||||
:rules="[existsRule, emailRule]"
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
|
@ -43,10 +45,10 @@
|
|||
light="light"
|
||||
class="mb-2s"
|
||||
prepend-icon="mdi-lock"
|
||||
validate-on-blur
|
||||
:label="$t('login.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
:rules="[minRule]"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.passwordConfirm"
|
||||
|
@ -56,6 +58,9 @@
|
|||
:label="$t('login.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:rules="[
|
||||
user.password === user.passwordConfirm || 'Password must match',
|
||||
]"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
|
@ -80,7 +85,9 @@
|
|||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
@ -121,15 +128,23 @@ export default {
|
|||
const userData = {
|
||||
fullName: this.user.name,
|
||||
email: this.user.email,
|
||||
family: "public",
|
||||
group: "default",
|
||||
password: this.user.password,
|
||||
admin: false,
|
||||
};
|
||||
|
||||
await api.signUps.createUser(this.token, userData);
|
||||
let successUser = false;
|
||||
if (this.$refs.signUpForm.validate()) {
|
||||
let response = await api.signUps.createUser(this.token, userData);
|
||||
successUser = response.snackbar.text.includes("Created");
|
||||
}
|
||||
|
||||
this.$emit("user-created");
|
||||
|
||||
this.loading = false;
|
||||
if (successUser) {
|
||||
this.$router.push("/");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -118,6 +118,19 @@ export default {
|
|||
async mounted() {
|
||||
let settings = await api.settings.requestAll();
|
||||
this.items = await api.recipes.getAllByCategory(settings.planCategories);
|
||||
console.log(this.items);
|
||||
|
||||
if (this.items.length === 0) {
|
||||
const keys = [
|
||||
"name",
|
||||
"slug",
|
||||
"image",
|
||||
"description",
|
||||
"dateAdded",
|
||||
"rating",
|
||||
];
|
||||
this.items = await api.recipes.allByKeys(keys);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
chips
|
||||
item-color="secondary"
|
||||
deletable-chips
|
||||
v-model="value.categories"
|
||||
v-model="value.recipeCategory"
|
||||
hide-selected
|
||||
:items="categories"
|
||||
text="name"
|
||||
|
@ -359,7 +359,7 @@ export default {
|
|||
this.value.notes.splice(index, 1);
|
||||
},
|
||||
removeCategory(index) {
|
||||
this.value.categories.splice(index, 1);
|
||||
this.value.recipeCategory.splice(index, 1);
|
||||
},
|
||||
removeTags(index) {
|
||||
this.value.tags.splice(index, 1);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="items[0]">
|
||||
<div v-if="items && items.length > 0">
|
||||
<h2 class="mt-4">{{ title }}</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
|
|
53
frontend/src/components/UI/BasicModal.vue
Normal file
53
frontend/src/components/UI/BasicModal.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="dialog" :width="modalWidth + 'px'">
|
||||
<v-app-bar dark :color="color" class="mt-n1 mb-2">
|
||||
<v-icon large left v-if="!loading">
|
||||
{{ titleIcon }}
|
||||
</v-icon>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="white"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
color: {
|
||||
default: "primary",
|
||||
},
|
||||
title: {
|
||||
default: "Modal Title",
|
||||
},
|
||||
titleIcon: {
|
||||
default: "mdi-account",
|
||||
},
|
||||
modalWidth: {
|
||||
default: "500",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -8,10 +8,10 @@
|
|||
@keydown.esc="cancel"
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar v-if="Boolean(title)" :color="color" dense flat dark>
|
||||
<v-app-bar v-if="Boolean(title)" :color="color" dense flat dark>
|
||||
<v-icon v-if="Boolean(icon)" left> {{ icon }}</v-icon>
|
||||
<v-toolbar-title v-text="title" />
|
||||
</v-toolbar>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text
|
||||
v-show="!!message"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<LoginDialog ref="loginDialog" />
|
||||
<v-menu
|
||||
transition="slide-x-transition"
|
||||
bottom
|
||||
|
@ -35,10 +34,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import LoginDialog from "../Login/LoginDialog";
|
||||
const SELECT_EVENT = "select-lang";
|
||||
export default {
|
||||
components: {
|
||||
LoginDialog,
|
||||
props: {
|
||||
siteSettings: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
|
@ -68,7 +69,11 @@ export default {
|
|||
|
||||
methods: {
|
||||
setLanguage(selectedLanguage) {
|
||||
this.$store.commit("setLang", selectedLanguage);
|
||||
if (this.siteSettings) {
|
||||
this.$emit(SELECT_EVENT, selectedLanguage);
|
||||
} else {
|
||||
this.$store.commit("setLang", selectedLanguage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,31 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-autocomplete
|
||||
:items="autoResults"
|
||||
v-model="searchSlug"
|
||||
item-value="item.slug"
|
||||
item-text="item.name"
|
||||
dense
|
||||
light
|
||||
:label="$t('search.search-mealie')"
|
||||
:search-input.sync="search"
|
||||
hide-no-data
|
||||
cache-items
|
||||
solo
|
||||
autofocus
|
||||
auto-select-first
|
||||
>
|
||||
<template
|
||||
v-if="showResults"
|
||||
v-slot:item="{ item }"
|
||||
style="max-width: 750px"
|
||||
<v-menu v-model="menuModel" offset-y readonly max-width="450">
|
||||
<template #activator="{ attrs }">
|
||||
<v-text-field
|
||||
class="mt-6"
|
||||
v-model="search"
|
||||
v-bind="attrs"
|
||||
dense
|
||||
light
|
||||
:label="$t('search.search-mealie')"
|
||||
solo
|
||||
autofocus
|
||||
style="max-width: 450px;"
|
||||
@focus="onFocus"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.image)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content @click="selected(item.item.slug)">
|
||||
<v-list-item-title>
|
||||
{{ item.item.name }}
|
||||
</v-text-field>
|
||||
</template>
|
||||
<v-card v-if="showResults" max-height="500" min-width="98%" class="">
|
||||
<v-card-text class="py-1">Results</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list scrollable>
|
||||
<v-list-item
|
||||
v-for="(item, index) in autoResults"
|
||||
:key="index"
|
||||
:to="showResults ? `/recipe/${item.item.slug}` : null"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.image)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content
|
||||
@click="showResults ? null : selected(item.item.slug)"
|
||||
>
|
||||
<v-list-item-title v-html="highlight(item.item.name)">
|
||||
</v-list-item-title>
|
||||
<v-rating
|
||||
dense
|
||||
v-if="item.item.rating"
|
||||
|
@ -33,14 +39,13 @@
|
|||
size="12"
|
||||
>
|
||||
</v-rating>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.item.description }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</div>
|
||||
<v-list-item-subtitle v-html="highlight(item.item.description)">
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -56,7 +61,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
searchSlug: "",
|
||||
search: " ",
|
||||
search: "",
|
||||
menuModel: false,
|
||||
data: [],
|
||||
result: [],
|
||||
autoResults: [],
|
||||
|
@ -66,9 +72,10 @@ export default {
|
|||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
findAllMatches: true,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: ["name", "slug", "description"],
|
||||
minMatchCharLength: 2,
|
||||
keys: ["name", "description"],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -80,8 +87,15 @@ export default {
|
|||
fuse() {
|
||||
return new Fuse(this.data, this.options);
|
||||
},
|
||||
isSearching() {
|
||||
return this.search && this.search.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isSearching(val) {
|
||||
val ? (this.menuModel = true) : null;
|
||||
},
|
||||
|
||||
search() {
|
||||
try {
|
||||
this.result = this.fuse.search(this.search.trim());
|
||||
|
@ -101,18 +115,34 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
highlight(string) {
|
||||
if (!this.search) {
|
||||
return string;
|
||||
}
|
||||
return string.replace(
|
||||
new RegExp(this.search, "gi"),
|
||||
match => `<mark>${match}</mark>`
|
||||
);
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
selected(slug) {
|
||||
this.$emit("selected", slug);
|
||||
},
|
||||
async onFocus() {
|
||||
clearTimeout(this.timeout);
|
||||
this.isFocused = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.color-transition {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
</style>
|
|
@ -87,9 +87,6 @@ export default {
|
|||
},
|
||||
mounted() {},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
filteredItems() {
|
||||
if (this.loggedIn) {
|
||||
return this.items.filter(x => x.restricted == true);
|
||||
|
@ -97,6 +94,9 @@ export default {
|
|||
return this.items.filter(x => x.restricted == false);
|
||||
}
|
||||
},
|
||||
loggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -6,6 +6,8 @@ import VueRouter from "vue-router";
|
|||
import { routes } from "./routes";
|
||||
import i18n from "./i18n";
|
||||
import FlashMessage from "@smartweb/vue-flash-message";
|
||||
import "@mdi/font/css/materialdesignicons.css";
|
||||
import "typeface-roboto/index.css";
|
||||
|
||||
Vue.use(FlashMessage);
|
||||
Vue.config.productionTip = false;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-items v-model="tab" >
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item>
|
||||
<TheUserTable />
|
||||
</v-tab-item>
|
||||
|
@ -34,7 +34,7 @@
|
|||
<TheSignUpTable />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<TheGroupTable />
|
||||
<GroupDashboard />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card>
|
||||
|
@ -43,15 +43,18 @@
|
|||
|
||||
<script>
|
||||
import TheUserTable from "@/components/Admin/ManageUsers/TheUserTable";
|
||||
import TheGroupTable from "@/components/Admin/ManageUsers/TheGroupTable";
|
||||
import GroupDashboard from "@/components/Admin/ManageUsers/GroupDashboard";
|
||||
import TheSignUpTable from "@/components/Admin/ManageUsers/TheSignUpTable";
|
||||
export default {
|
||||
components: { TheUserTable, TheGroupTable, TheSignUpTable },
|
||||
components: { TheUserTable, GroupDashboard, TheSignUpTable },
|
||||
data() {
|
||||
return {
|
||||
tab: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("requestAllGroups");
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,13 +13,17 @@
|
|||
outlined
|
||||
:flat="isFlat"
|
||||
elavation="0"
|
||||
v-model="planCategories"
|
||||
v-model="groupSettings.categories"
|
||||
:items="categories"
|
||||
item-text="name"
|
||||
item-value="name"
|
||||
return-object
|
||||
multiple
|
||||
chips
|
||||
:hint="$t('meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans')"
|
||||
:hint="
|
||||
$t(
|
||||
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
|
||||
)
|
||||
"
|
||||
class="mt-2"
|
||||
persistent-hint
|
||||
>
|
||||
|
@ -50,12 +54,15 @@
|
|||
"settings.webhooks.the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at"
|
||||
)
|
||||
}}
|
||||
<strong>{{ time }}</strong>
|
||||
<strong>{{ groupSettings.webhookTime }}</strong>
|
||||
</p>
|
||||
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="2" sm="5">
|
||||
<v-switch v-model="enabled" :label="$t('general.enabled')"></v-switch>
|
||||
<v-switch
|
||||
v-model="groupSettings.webhookEnable"
|
||||
:label="$t('general.enabled')"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3" sm="5">
|
||||
<TimePickerDialog @save-time="saveTime" />
|
||||
|
@ -68,7 +75,12 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-for="(url, index) in webhooks" :key="index" align="center" dense>
|
||||
<v-row
|
||||
v-for="(url, index) in groupSettings.webhookUrls"
|
||||
:key="index"
|
||||
align="center"
|
||||
dense
|
||||
>
|
||||
<v-col cols="1">
|
||||
<v-btn icon color="error" @click="removeWebhook(index)">
|
||||
<v-icon>mdi-minus</v-icon>
|
||||
|
@ -76,7 +88,7 @@
|
|||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="webhooks[index]"
|
||||
v-model="groupSettings.webhookUrls[index]"
|
||||
:label="$t('settings.webhooks.webhook-url')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
@ -87,7 +99,7 @@
|
|||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" @click="saveWebhooks" class="mr-2 mb-1">
|
||||
<v-btn color="success" @click="saveGroupSettings" class="mr-2 mb-1">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
|
@ -104,14 +116,19 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
name: "main",
|
||||
webhooks: [],
|
||||
enabled: false,
|
||||
time: "",
|
||||
planCategories: [],
|
||||
groupSettings: {
|
||||
name: "home",
|
||||
id: 1,
|
||||
mealplans: [],
|
||||
categories: [],
|
||||
webhookUrls: [],
|
||||
webhookTime: "00:00",
|
||||
webhookEnable: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
this.getSiteSettings();
|
||||
},
|
||||
computed: {
|
||||
|
@ -119,44 +136,39 @@ export default {
|
|||
return this.$store.getters.getCategories;
|
||||
},
|
||||
isFlat() {
|
||||
return this.planCategories ? true : false;
|
||||
return this.groupSettings.categories >= 1 ? true : false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveTime(value) {
|
||||
this.time = value;
|
||||
this.groupSettings.webhookTime = value;
|
||||
},
|
||||
async getSiteSettings() {
|
||||
let settings = await api.settings.requestAll();
|
||||
this.webhooks = settings.webhooks.webhookURLs;
|
||||
this.name = settings.name;
|
||||
this.time = settings.webhooks.webhookTime;
|
||||
this.enabled = settings.webhooks.enabled;
|
||||
this.planCategories = settings.planCategories;
|
||||
getSiteSettings() {
|
||||
let settings = this.$store.getters.getCurrentGroup;
|
||||
|
||||
this.groupSettings.name = settings.name;
|
||||
this.groupSettings.id = settings.id;
|
||||
this.groupSettings.categories = settings.categories;
|
||||
this.groupSettings.webhookUrls = settings.webhookUrls;
|
||||
this.groupSettings.webhookTime = settings.webhookTime;
|
||||
this.groupSettings.webhookEnable = settings.webhookEnable;
|
||||
},
|
||||
addWebhook() {
|
||||
this.webhooks.push(" ");
|
||||
this.groupSettings.webhookUrls.push(" ");
|
||||
},
|
||||
removeWebhook(index) {
|
||||
this.webhooks.splice(index, 1);
|
||||
this.groupSettings.webhookUrls.splice(index, 1);
|
||||
},
|
||||
saveWebhooks() {
|
||||
const body = {
|
||||
name: this.name,
|
||||
planCategories: this.planCategories,
|
||||
webhooks: {
|
||||
webhookURLs: this.webhooks,
|
||||
webhookTime: this.time,
|
||||
enabled: this.enabled,
|
||||
},
|
||||
};
|
||||
api.settings.update(body);
|
||||
async saveGroupSettings() {
|
||||
await api.groups.update(this.groupSettings);
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
this.getSiteSettings();
|
||||
},
|
||||
testWebhooks() {
|
||||
api.settings.testWebhooks();
|
||||
},
|
||||
removeCategory(index) {
|
||||
this.planCategories.splice(index, 1);
|
||||
this.groupSettings.categories.splice(index, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -55,11 +55,11 @@
|
|||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Family"
|
||||
label="Group"
|
||||
readonly
|
||||
v-model="user.family"
|
||||
v-model="user.group"
|
||||
persistent-hint
|
||||
hint="Family groups can only be set by administrators"
|
||||
hint="Group groups can only be set by administrators"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
|
@ -167,7 +167,7 @@ export default {
|
|||
user: {
|
||||
fullName: "Change Me",
|
||||
email: "changeme@email.com",
|
||||
family: "public",
|
||||
group: "public",
|
||||
admin: true,
|
||||
id: 1,
|
||||
},
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<AdminSidebar />
|
||||
<v-slide-x-transition hide-on-leave>
|
||||
<router-view></router-view>
|
||||
</v-slide-x-transition>
|
||||
<!-- <v-footer fixed>
|
||||
<v-col class="text-center" cols="12">
|
||||
<div>
|
||||
<v-container height="100%">
|
||||
<AdminSidebar />
|
||||
<v-slide-x-transition hide-on-leave>
|
||||
<router-view></router-view>
|
||||
</v-slide-x-transition>
|
||||
</v-container>
|
||||
<!-- <v-footer absolute>
|
||||
<div class="flex text-center" cols="12">
|
||||
{{ $t("settings.current") }}
|
||||
{{ version }} |
|
||||
{{ $t("settings.latest") }}
|
||||
|
@ -21,9 +23,9 @@
|
|||
>
|
||||
{{ $t("settings.contribute") }}
|
||||
</a>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-footer> -->
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
:description="recipeDetails.description"
|
||||
:instructions="recipeDetails.recipeInstructions"
|
||||
:tags="recipeDetails.tags"
|
||||
:categories="recipeDetails.categories"
|
||||
:categories="recipeDetails.recipeCategory"
|
||||
:notes="recipeDetails.notes"
|
||||
:rating="recipeDetails.rating"
|
||||
:yields="recipeDetails.recipeYield"
|
||||
|
|
|
@ -5,22 +5,25 @@ import createPersistedState from "vuex-persistedstate";
|
|||
import userSettings from "./modules/userSettings";
|
||||
import language from "./modules/language";
|
||||
import homePage from "./modules/homePage";
|
||||
import siteSettings from "./modules/siteSettings";
|
||||
import groups from "./modules/groups";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const store = new Vuex.Store({
|
||||
plugins: [
|
||||
createPersistedState({
|
||||
paths: ["userSettings", "language", "homePage"],
|
||||
paths: ["userSettings", "language", "homePage", "SideSettings"],
|
||||
}),
|
||||
],
|
||||
modules: {
|
||||
userSettings,
|
||||
language,
|
||||
homePage,
|
||||
siteSettings,
|
||||
groups,
|
||||
},
|
||||
state: {
|
||||
|
||||
// All Recipe Data Store
|
||||
recentRecipes: [],
|
||||
allRecipes: [],
|
||||
|
|
39
frontend/src/store/modules/groups.js
Normal file
39
frontend/src/store/modules/groups.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import api from "@/api";
|
||||
|
||||
const state = {
|
||||
groups: [],
|
||||
currentGroup: {},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
setGroups(state, payload) {
|
||||
state.groups = payload;
|
||||
},
|
||||
setCurrentGroup(state, payload) {
|
||||
state.currentGroup = payload;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async requestAllGroups({ commit }) {
|
||||
const groups = await api.groups.allGroups();
|
||||
commit("setGroups", groups);
|
||||
},
|
||||
async requestCurrentGroup({ commit }) {
|
||||
const group = await api.groups.current();
|
||||
commit("setCurrentGroup", group);
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getGroups: state => state.groups,
|
||||
getGroupNames: state => Array.from(state.groups, x => x.name),
|
||||
getCurrentGroup: state => state.currentGroup
|
||||
};
|
||||
|
||||
export default {
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
getters,
|
||||
};
|
34
frontend/src/store/modules/siteSettings.js
Normal file
34
frontend/src/store/modules/siteSettings.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import api from "@/api";
|
||||
|
||||
const state = {
|
||||
siteSettings: {
|
||||
language: "en",
|
||||
showRecent: true,
|
||||
cardsPerSection: 9,
|
||||
categories: [],
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
setSettings(state, payload) {
|
||||
state.settings = payload;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async requestSiteSettings() {
|
||||
let settings = await api.siteSettings.get();
|
||||
this.commit("setSettings", settings);
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getSiteSettings: state => state.siteSettings,
|
||||
};
|
||||
|
||||
export default {
|
||||
state,
|
||||
mutations,
|
||||
actions,
|
||||
getters,
|
||||
};
|
|
@ -63,6 +63,20 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
async refreshToken({ commit, getters }) {
|
||||
if (!getters.getIsLoggedIn) {
|
||||
commit("setIsLoggedIn", false); // This is to be here... for some reasons? ¯\_(ツ)_/¯
|
||||
console.log("Not Logged In");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let authResponse = await api.users.refresh();
|
||||
commit("setToken", authResponse.access_token);
|
||||
} catch {
|
||||
console.log("Failed Token Refresh, Logging Out...");
|
||||
commit("setIsLoggedIn", false);
|
||||
}
|
||||
},
|
||||
|
||||
async initTheme({ dispatch, getters }) {
|
||||
//If theme is empty resetTheme
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { vueApp } from "../main";
|
||||
|
||||
|
||||
// TODO: Migrate to Mixins
|
||||
const notifyHelpers = {
|
||||
baseCSS: "notify-base",
|
||||
error: "notify-error-color",
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.logger import logger
|
||||
|
||||
# import utils.startup as startup
|
||||
from core.config import APP_VERSION, PORT, SECRET, docs_url, redoc_url
|
||||
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 (
|
||||
|
@ -13,6 +14,7 @@ from routes import (
|
|||
setting_routes,
|
||||
theme_routes,
|
||||
)
|
||||
from routes.groups import groups
|
||||
from routes.recipe import (
|
||||
all_recipe_routes,
|
||||
category_routes,
|
||||
|
@ -20,7 +22,6 @@ from routes.recipe import (
|
|||
tag_routes,
|
||||
)
|
||||
from routes.users import users
|
||||
from fastapi.logger import logger
|
||||
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
|
@ -42,11 +43,13 @@ def start_scheduler():
|
|||
def api_routers():
|
||||
# Authentication
|
||||
app.include_router(users.router)
|
||||
app.include_router(groups.router)
|
||||
# Recipes
|
||||
app.include_router(all_recipe_routes.router)
|
||||
app.include_router(category_routes.router)
|
||||
app.include_router(tag_routes.router)
|
||||
app.include_router(recipe_crud_routes.router)
|
||||
|
||||
# Meal Routes
|
||||
app.include_router(meal_routes.router)
|
||||
# Settings Routes
|
||||
|
|
|
@ -83,6 +83,7 @@ else:
|
|||
|
||||
# Mongo Database
|
||||
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
|
||||
DEFAULT_GROUP = os.getenv("default_group", "home")
|
||||
DB_USERNAME = os.getenv("db_username", "root")
|
||||
DB_PASSWORD = os.getenv("db_password", "example")
|
||||
DB_HOST = os.getenv("db_host", "mongo")
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
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 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 import Category, RecipeModel, Tag
|
||||
from db.models.settings import SiteSettingsModel
|
||||
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
|
||||
|
||||
"""
|
||||
# TODO
|
||||
- [ ] Abstract Classes to use save_new, and update from base models
|
||||
"""
|
||||
|
||||
|
||||
class _Recipes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
self.sql_model: RecipeModel = RecipeModel
|
||||
self.orm_mode = True
|
||||
self.schema: Recipe = Recipe
|
||||
|
||||
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
|
||||
entry: RecipeModel = self._query_one(session, match_value=slug)
|
||||
|
@ -31,51 +36,71 @@ class _Categories(BaseDocument):
|
|||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = Category
|
||||
self.orm_mode = True
|
||||
self.schema = RecipeCategoryResponse
|
||||
|
||||
|
||||
class _Tags(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = Tag
|
||||
self.orm_mode = True
|
||||
self.schema = RecipeTagResponse
|
||||
|
||||
|
||||
class _Meals(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "uid"
|
||||
self.sql_model = MealPlanModel
|
||||
self.orm_mode = True
|
||||
self.schema = MealPlanInDB
|
||||
|
||||
|
||||
class _Settings(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.sql_model = SiteSettingsModel
|
||||
self.primary_key = "id"
|
||||
self.sql_model = SiteSettings
|
||||
self.orm_mode = True
|
||||
self.schema = SiteSettingsSchema
|
||||
|
||||
|
||||
class _Themes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.sql_model = SiteThemeModel
|
||||
self.orm_mode = True
|
||||
self.schema = SiteTheme
|
||||
|
||||
|
||||
class _Users(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = User
|
||||
self.orm_mode = True
|
||||
self.schema = UserInDB
|
||||
|
||||
def update_password(self, session, id, password: str):
|
||||
entry = self._query_one(session=session, match_value=id)
|
||||
entry.update_password(password)
|
||||
return_data = entry.dict()
|
||||
session.commit()
|
||||
|
||||
return return_data
|
||||
return self.schema.from_orm(entry)
|
||||
|
||||
|
||||
class _Groups(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = Group
|
||||
self.orm_mode = True
|
||||
self.schema = GroupInDB
|
||||
|
||||
|
||||
class _SignUps(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "token"
|
||||
self.sql_model = SignUp
|
||||
self.orm_mode = True
|
||||
self.schema = SignUpOut
|
||||
|
||||
|
||||
class Database:
|
||||
|
@ -88,6 +113,7 @@ class Database:
|
|||
self.tags = _Tags()
|
||||
self.users = _Users()
|
||||
self.sign_ups = _SignUps()
|
||||
self.groups = _Groups()
|
||||
|
||||
|
||||
db = Database()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import load_only
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
@ -11,11 +12,20 @@ class BaseDocument:
|
|||
self.primary_key: str
|
||||
self.store: str
|
||||
self.sql_model: SqlAlchemyBase
|
||||
self.orm_mode = False
|
||||
self.schema: BaseModel
|
||||
|
||||
# TODO: Improve Get All Query Functionality
|
||||
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()
|
||||
]
|
||||
|
||||
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
|
||||
|
||||
if limit == 1:
|
||||
|
@ -105,15 +115,13 @@ class BaseDocument:
|
|||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
db_entries = [x.dict() for x in result]
|
||||
|
||||
if limit == 1:
|
||||
try:
|
||||
return db_entries[0]
|
||||
return self.schema.from_orm(result[0])
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
return db_entries
|
||||
return [self.schema.from_orm(x) for x in result]
|
||||
|
||||
def create(self, session: Session, document: dict) -> dict:
|
||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||
|
@ -128,6 +136,10 @@ class BaseDocument:
|
|||
new_document = self.sql_model(session=session, **document)
|
||||
session.add(new_document)
|
||||
session.commit()
|
||||
|
||||
if self.orm_mode:
|
||||
return self.schema.from_orm(new_document)
|
||||
|
||||
return_data = new_document.dict()
|
||||
return return_data
|
||||
|
||||
|
@ -145,9 +157,13 @@ class BaseDocument:
|
|||
|
||||
entry = self._query_one(session=session, match_value=match_value)
|
||||
entry.update(session=session, **new_data)
|
||||
|
||||
if self.orm_mode:
|
||||
session.commit()
|
||||
return self.schema.from_orm(entry)
|
||||
|
||||
return_data = entry.dict()
|
||||
session.commit()
|
||||
|
||||
return return_data
|
||||
|
||||
def delete(self, session: Session, primary_key_value) -> dict:
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from core.config import DEFAULT_GROUP
|
||||
from core.security import get_password_hash
|
||||
from fastapi.logger import logger
|
||||
from schema.settings import SiteSettings, Webhooks
|
||||
from schema.settings import SiteSettings
|
||||
from schema.theme import SiteTheme
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
@ -12,6 +14,7 @@ def init_db(db: Session = None) -> None:
|
|||
if not db:
|
||||
db = create_session()
|
||||
|
||||
default_group_init(db)
|
||||
default_settings_init(db)
|
||||
default_theme_init(db)
|
||||
default_user_init(db)
|
||||
|
@ -20,34 +23,25 @@ def init_db(db: Session = None) -> None:
|
|||
|
||||
|
||||
def default_theme_init(session: Session):
|
||||
default_theme = {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
db.themes.create(session, SiteTheme().dict())
|
||||
|
||||
try:
|
||||
db.themes.create(session, default_theme)
|
||||
logger.info("Generating default theme...")
|
||||
except:
|
||||
logger.info("Default Theme Exists.. skipping generation")
|
||||
|
||||
|
||||
def default_settings_init(session: Session):
|
||||
try:
|
||||
webhooks = Webhooks()
|
||||
default_entry = SiteSettings(name="main", webhooks=webhooks)
|
||||
document = db.settings.create(session, default_entry.dict())
|
||||
logger.info(f"Created Site Settings: \n {document}")
|
||||
except:
|
||||
pass
|
||||
data = {"language": "en", "home_page_settings": {"categories": []}}
|
||||
document = db.settings.create(session, SiteSettings().dict())
|
||||
logger.info(f"Created Site Settings: \n {document}")
|
||||
|
||||
|
||||
def default_group_init(session: Session):
|
||||
default_group = {"name": DEFAULT_GROUP}
|
||||
logger.info("Generating Default Group")
|
||||
db.groups.create(session, default_group)
|
||||
pass
|
||||
|
||||
|
||||
def default_user_init(session: Session):
|
||||
|
@ -55,7 +49,7 @@ def default_user_init(session: Session):
|
|||
"full_name": "Change Me",
|
||||
"email": "changeme@email.com",
|
||||
"password": get_password_hash("MyPassword"),
|
||||
"family": "public",
|
||||
"group": DEFAULT_GROUP,
|
||||
"admin": True,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from db.models.mealplan import *
|
||||
from db.models.recipe 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 *
|
||||
|
|
75
mealie/db/models/group.py
Normal file
75
mealie/db/models/group.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
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 fastapi.logger import logger
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
||||
class WebhookURLModel(SqlAlchemyBase):
|
||||
__tablename__ = "webhook_urls"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
url = sa.Column(sa.String)
|
||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id"))
|
||||
|
||||
|
||||
class Group(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "groups"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
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
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
id=None,
|
||||
users=None,
|
||||
mealplans=None,
|
||||
categories=[],
|
||||
session=None,
|
||||
webhook_enable=False,
|
||||
webhook_time="00:00",
|
||||
webhook_urls=[],
|
||||
) -> None:
|
||||
self.name = name
|
||||
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
|
||||
self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls]
|
||||
|
||||
def update(self, session: Session, *args, **kwargs):
|
||||
self._sql_remove_list(session, [WebhookURLModel], self.id)
|
||||
|
||||
self.__init__(session=session, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = DEFAULT_GROUP):
|
||||
try:
|
||||
result = session.query(Group).filter(Group.name == name).one()
|
||||
if result:
|
||||
logger.info("Category exists, associating recipe")
|
||||
return result
|
||||
else:
|
||||
logger.info("Category doesn't exists, creating tag")
|
||||
return Group(name=name)
|
||||
except:
|
||||
logger.info("Category doesn't exists, creating category")
|
||||
return Group(name=name)
|
|
@ -13,32 +13,16 @@ class Meal(SqlAlchemyBase):
|
|||
slug = sa.Column(sa.String)
|
||||
name = sa.Column(sa.String)
|
||||
date = sa.Column(sa.Date)
|
||||
dateText = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
description = sa.Column(sa.String)
|
||||
|
||||
def __init__(
|
||||
self, slug, name, date, dateText, image, description, session=None
|
||||
) -> None:
|
||||
def __init__(self, slug, name, date, image, description, session=None) -> None:
|
||||
self.slug = slug
|
||||
self.name = name
|
||||
self.date = date
|
||||
self.dateText = dateText
|
||||
self.image = image
|
||||
self.description = description
|
||||
|
||||
def dict(self) -> dict:
|
||||
data = {
|
||||
"slug": self.slug,
|
||||
"name": self.name,
|
||||
"date": self.date,
|
||||
"dateText": self.dateText,
|
||||
"image": self.image,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class MealPlanModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "mealplan"
|
||||
|
@ -46,6 +30,8 @@ class MealPlanModel(SqlAlchemyBase, BaseMixins):
|
|||
startDate = sa.Column(sa.Date)
|
||||
endDate = sa.Column(sa.Date)
|
||||
meals: List[Meal] = orm.relation(Meal)
|
||||
group_id = sa.Column(sa.String, sa.ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="mealplans")
|
||||
|
||||
def __init__(self, startDate, endDate, meals, uid=None, session=None) -> None:
|
||||
self.startDate = startDate
|
||||
|
@ -56,13 +42,3 @@ class MealPlanModel(SqlAlchemyBase, BaseMixins):
|
|||
MealPlanModel._sql_remove_list(session, [Meal], uid)
|
||||
|
||||
self.__init__(startDate, endDate, meals)
|
||||
|
||||
def dict(self) -> dict:
|
||||
data = {
|
||||
"uid": self.uid,
|
||||
"startDate": self.startDate,
|
||||
"endDate": self.endDate,
|
||||
"meals": [meal.dict() for meal in self.meals],
|
||||
}
|
||||
|
||||
return data
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
from typing import List
|
||||
|
||||
import sqlalchemy.ext.declarative as dec
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
SqlAlchemyBase = dec.declarative_base()
|
||||
|
||||
|
||||
class BaseMixins:
|
||||
@staticmethod
|
||||
def _sql_remove_list(session, list_of_tables: list, parent_id):
|
||||
|
||||
def _sql_remove_list(session: Session, list_of_tables: list, parent_id):
|
||||
for table in list_of_tables:
|
||||
session.query(table).filter_by(parent_id=parent_id).delete()
|
||||
session.query(table).filter(parent_id == parent_id).delete()
|
||||
|
||||
@staticmethod
|
||||
def _flatten_dict(list_of_dict: List[dict]):
|
||||
|
@ -20,3 +20,29 @@ class BaseMixins:
|
|||
finalMap.update(d.dict())
|
||||
|
||||
return finalMap
|
||||
|
||||
|
||||
# ! Don't use!
|
||||
def update_generics(func):
|
||||
"""An experimental function that does the initial work of updating attributes on a class
|
||||
and passing "complex" data types recuresively to an "self.update()" function if one exists.
|
||||
|
||||
Args:
|
||||
func ([type]): [description]
|
||||
"""
|
||||
|
||||
def wrapper(class_object, session, new_data: dict):
|
||||
complex_attributed = {}
|
||||
for key, value in new_data.items():
|
||||
|
||||
attribute = getattr(class_object, key, None)
|
||||
|
||||
if attribute and isinstance(attribute, SqlAlchemyBase):
|
||||
attribute.update(session, value)
|
||||
|
||||
elif attribute:
|
||||
setattr(class_object, key, value)
|
||||
print("Complex", complex_attributed)
|
||||
func(class_object, complex_attributed)
|
||||
|
||||
return wrapper
|
||||
|
|
|
@ -1,352 +0,0 @@
|
|||
import datetime
|
||||
from datetime import date
|
||||
from typing import List
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from slugify import slugify
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import validates
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
class ApiExtras(SqlAlchemyBase):
|
||||
__tablename__ = "api_extras"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
key_name = sa.Column(sa.String, unique=True)
|
||||
value = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, key, value) -> None:
|
||||
self.key_name = key
|
||||
self.value = value
|
||||
|
||||
def dict(self):
|
||||
return {self.key_name: self.value}
|
||||
|
||||
|
||||
recipes2categories = sa.Table(
|
||||
"recipes2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
)
|
||||
|
||||
recipes2tags = sa.Table(
|
||||
"recipes2tags",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
|
||||
)
|
||||
|
||||
|
||||
class Category(SqlAlchemyBase):
|
||||
__tablename__ = "categories"
|
||||
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="categories"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(name)
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
try:
|
||||
result = session.query(Category).filter(Category.slug == test_slug).one()
|
||||
if result:
|
||||
logger.info("Category exists, associating recipe")
|
||||
return result
|
||||
else:
|
||||
logger.info("Category doesn't exists, creating tag")
|
||||
return Category(name=name)
|
||||
except:
|
||||
logger.info("Category doesn't exists, creating category")
|
||||
return Category(name=name)
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"slug": self.slug,
|
||||
"name": self.name,
|
||||
"recipes": [x.dict() for x in self.recipes],
|
||||
}
|
||||
|
||||
def dict_no_recipes(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"slug": self.slug,
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
|
||||
class Tag(SqlAlchemyBase):
|
||||
__tablename__ = "tags"
|
||||
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"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"slug": self.slug,
|
||||
"name": self.name,
|
||||
"recipes": [x.dict() for x in self.recipes],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
try:
|
||||
result = session.query(Tag).filter(Tag.slug == test_slug).first()
|
||||
|
||||
if result:
|
||||
logger.info("Tag exists, associating recipe")
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.info("Tag doesn't exists, creating tag")
|
||||
return Tag(name=name)
|
||||
except:
|
||||
logger.info("Tag doesn't exists, creating tag")
|
||||
return Tag(name=name)
|
||||
|
||||
|
||||
class Note(SqlAlchemyBase):
|
||||
__tablename__ = "notes"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
title = sa.Column(sa.String)
|
||||
text = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, title, text) -> None:
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
def dict(self):
|
||||
return {"title": self.title, "text": self.text}
|
||||
|
||||
|
||||
class RecipeIngredient(SqlAlchemyBase):
|
||||
__tablename__ = "recipes_ingredients"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
position = sa.Column(sa.Integer)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
ingredient = sa.Column(sa.String)
|
||||
|
||||
def update(self, ingredient):
|
||||
self.ingredient = ingredient
|
||||
|
||||
def to_str(self):
|
||||
return self.ingredient
|
||||
|
||||
|
||||
class RecipeInstruction(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_instructions"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
position = sa.Column(sa.Integer)
|
||||
type = sa.Column(sa.String, default="")
|
||||
text = sa.Column(sa.String)
|
||||
|
||||
def dict(self):
|
||||
data = {"@type": self.type, "text": self.text}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipes"
|
||||
# Database Specific
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
|
||||
# General Recipe Properties
|
||||
name = sa.Column(sa.String, nullable=False)
|
||||
description = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
recipeYield = sa.Column(sa.String)
|
||||
recipeIngredient: List[RecipeIngredient] = orm.relationship(
|
||||
"RecipeIngredient",
|
||||
cascade="all, delete",
|
||||
order_by="RecipeIngredient.position",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
recipeInstructions: List[RecipeInstruction] = orm.relationship(
|
||||
"RecipeInstruction",
|
||||
cascade="all, delete",
|
||||
order_by="RecipeInstruction.position",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
|
||||
# How to Properties
|
||||
totalTime = sa.Column(sa.String)
|
||||
prepTime = sa.Column(sa.String)
|
||||
performTime = sa.Column(sa.String)
|
||||
|
||||
# Mealie Specific
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
categories: List = orm.relationship(
|
||||
"Category", secondary=recipes2categories, 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)
|
||||
orgURL = sa.Column(sa.String)
|
||||
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete")
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
totalTime: str = None,
|
||||
prepTime: str = None,
|
||||
performTime: str = None,
|
||||
slug: str = None,
|
||||
categories: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.image = image
|
||||
self.recipeYield = recipeYield
|
||||
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
|
||||
]
|
||||
self.totalTime = totalTime
|
||||
self.prepTime = prepTime
|
||||
self.performTime = performTime
|
||||
|
||||
# Mealie Specific
|
||||
self.slug = slug
|
||||
self.categories = [
|
||||
Category.create_if_not_exist(session=session, name=cat)
|
||||
for cat in categories
|
||||
]
|
||||
|
||||
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
|
||||
|
||||
self.dateAdded = dateAdded
|
||||
self.notes = [Note(**note) for note in notes]
|
||||
self.rating = rating
|
||||
self.orgURL = orgURL
|
||||
self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()]
|
||||
|
||||
def update(
|
||||
self,
|
||||
session,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
totalTime: str = None,
|
||||
prepTime: str = None,
|
||||
performTime: str = None,
|
||||
slug: str = None,
|
||||
categories: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
list_of_tables = [RecipeIngredient, RecipeInstruction, ApiExtras]
|
||||
RecipeModel._sql_remove_list(session, list_of_tables, self.id)
|
||||
|
||||
self.__init__(
|
||||
session=session,
|
||||
name=name,
|
||||
description=description,
|
||||
image=image,
|
||||
recipeYield=recipeYield,
|
||||
recipeIngredient=recipeIngredient,
|
||||
recipeInstructions=recipeInstructions,
|
||||
totalTime=totalTime,
|
||||
prepTime=prepTime,
|
||||
performTime=performTime,
|
||||
slug=slug,
|
||||
categories=categories,
|
||||
tags=tags,
|
||||
dateAdded=dateAdded,
|
||||
notes=notes,
|
||||
rating=rating,
|
||||
orgURL=orgURL,
|
||||
extras=extras,
|
||||
)
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"image": self.image,
|
||||
"recipeYield": self.recipeYield,
|
||||
"recipeIngredient": [x.to_str() for x in self.recipeIngredient],
|
||||
"recipeInstructions": [x.dict() for x in self.recipeInstructions],
|
||||
"totalTime": self.totalTime,
|
||||
"prepTime": self.prepTime,
|
||||
"performTime": self.performTime,
|
||||
# Mealie
|
||||
"slug": self.slug,
|
||||
"categories": [x.to_str() for x in self.categories],
|
||||
"tags": [x.to_str() for x in self.tags],
|
||||
"dateAdded": self.dateAdded,
|
||||
"notes": [x.dict() for x in self.notes],
|
||||
"rating": self.rating,
|
||||
"orgURL": self.orgURL,
|
||||
"extras": RecipeModel._flatten_dict(self.extras),
|
||||
}
|
||||
|
||||
return data
|
16
mealie/db/models/recipe/api_extras.py
Normal file
16
mealie/db/models/recipe/api_extras.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from datetime import date
|
||||
|
||||
import sqlalchemy as sa
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class ApiExtras(SqlAlchemyBase):
|
||||
__tablename__ = "api_extras"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
key_name = sa.Column(sa.String, unique=True)
|
||||
value = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, key, value) -> None:
|
||||
self.key_name = key
|
||||
self.value = value
|
65
mealie/db/models/recipe/category.py
Normal file
65
mealie/db/models/recipe/category.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
from fastapi.logger import logger
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm import validates
|
||||
|
||||
site_settings2categories = sa.Table(
|
||||
"site_settings2categoories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("sidebar_id", sa.Integer, sa.ForeignKey("site_settings.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
)
|
||||
|
||||
group2categories = sa.Table(
|
||||
"group2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("group_id", sa.Integer, sa.ForeignKey("groups.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
)
|
||||
|
||||
recipes2categories = sa.Table(
|
||||
"recipes2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||
)
|
||||
|
||||
|
||||
class Category(SqlAlchemyBase):
|
||||
__tablename__ = "categories"
|
||||
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"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(name)
|
||||
|
||||
@staticmethod
|
||||
def get_ref(session, slug: str):
|
||||
return session.query(Category).filter(Category.slug == slug).one()
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
try:
|
||||
result = session.query(Category).filter(Category.slug == test_slug).one()
|
||||
if result:
|
||||
logger.info("Category exists, associating recipe")
|
||||
return result
|
||||
else:
|
||||
logger.info("Category doesn't exists, creating tag")
|
||||
return Category(name=name)
|
||||
except:
|
||||
logger.info("Category doesn't exists, creating category")
|
||||
return Category(name=name)
|
13
mealie/db/models/recipe/ingredient.py
Normal file
13
mealie/db/models/recipe/ingredient.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import sqlalchemy as sa
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class RecipeIngredient(SqlAlchemyBase):
|
||||
__tablename__ = "recipes_ingredients"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
position = sa.Column(sa.Integer)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
ingredient = sa.Column(sa.String)
|
||||
|
||||
def update(self, ingredient):
|
||||
self.ingredient = ingredient
|
11
mealie/db/models/recipe/instruction.py
Normal file
11
mealie/db/models/recipe/instruction.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
import sqlalchemy as sa
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class RecipeInstruction(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_instructions"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
position = sa.Column(sa.Integer)
|
||||
type = sa.Column(sa.String, default="")
|
||||
text = sa.Column(sa.String)
|
15
mealie/db/models/recipe/note.py
Normal file
15
mealie/db/models/recipe/note.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import sqlalchemy as sa
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class Note(SqlAlchemyBase):
|
||||
__tablename__ = "notes"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
title = sa.Column(sa.String)
|
||||
text = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, title, text) -> None:
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
31
mealie/db/models/recipe/nutrition.py
Normal file
31
mealie/db/models/recipe/nutrition.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
import sqlalchemy as sa
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class Nutrition(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_nutrition"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
calories = sa.Column(sa.String)
|
||||
fatContent = sa.Column(sa.String)
|
||||
fiberContent = sa.Column(sa.String)
|
||||
proteinContent = sa.Column(sa.String)
|
||||
sodiumContent = sa.Column(sa.String)
|
||||
sugarContent = sa.Column(sa.String)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
calories=None,
|
||||
fatContent=None,
|
||||
fiberContent=None,
|
||||
proteinContent=None,
|
||||
sodiumContent=None,
|
||||
sugarContent=None,
|
||||
) -> None:
|
||||
self.calories = calories
|
||||
self.fatContent = fatContent
|
||||
self.fiberContent = fiberContent
|
||||
self.proteinContent = proteinContent
|
||||
self.sodiumContent = sodiumContent
|
||||
self.sugarContent = sugarContent
|
||||
|
184
mealie/db/models/recipe/recipe.py
Normal file
184
mealie/db/models/recipe/recipe.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
import datetime
|
||||
from datetime import date
|
||||
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 sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import validates
|
||||
|
||||
|
||||
class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipes"
|
||||
# Database Specific
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
|
||||
# General Recipe Properties
|
||||
name = sa.Column(sa.String, nullable=False)
|
||||
description = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
totalTime = sa.Column(sa.String)
|
||||
prepTime = sa.Column(sa.String)
|
||||
performTime = sa.Column(sa.String)
|
||||
cookTime = sa.Column(sa.String)
|
||||
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"
|
||||
)
|
||||
|
||||
recipeIngredient: List[RecipeIngredient] = orm.relationship(
|
||||
"RecipeIngredient",
|
||||
cascade="all, delete",
|
||||
order_by="RecipeIngredient.position",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
recipeInstructions: List[RecipeInstruction] = orm.relationship(
|
||||
"RecipeInstruction",
|
||||
cascade="all, delete",
|
||||
order_by="RecipeInstruction.position",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
|
||||
# Mealie Specific
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
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)
|
||||
orgURL = sa.Column(sa.String)
|
||||
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete")
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
recipeCuisine: str = None,
|
||||
totalTime: str = None,
|
||||
prepTime: str = None,
|
||||
nutrition: dict = None,
|
||||
tool: list[str] = [],
|
||||
performTime: str = None,
|
||||
slug: str = None,
|
||||
recipeCategory: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.image = image
|
||||
self.recipeCuisine = recipeCuisine
|
||||
|
||||
if self.nutrition:
|
||||
self.nutrition = Nutrition(**nutrition)
|
||||
else:
|
||||
self.nutrition = Nutrition()
|
||||
|
||||
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.recipeInstructions = [
|
||||
RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None))
|
||||
for instruc in recipeInstructions
|
||||
]
|
||||
self.totalTime = totalTime
|
||||
self.prepTime = prepTime
|
||||
self.performTime = performTime
|
||||
|
||||
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]
|
||||
self.slug = slug
|
||||
self.dateAdded = dateAdded
|
||||
self.notes = [Note(**note) for note in notes]
|
||||
self.rating = rating
|
||||
self.orgURL = orgURL
|
||||
self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()]
|
||||
|
||||
def update(
|
||||
self,
|
||||
session,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
recipeCuisine: str = None,
|
||||
totalTime: str = None,
|
||||
tool: list[str] = [],
|
||||
prepTime: str = None,
|
||||
performTime: str = None,
|
||||
nutrition: dict = None,
|
||||
slug: str = None,
|
||||
recipeCategory: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
list_of_tables = [RecipeIngredient, RecipeInstruction, ApiExtras, Tool]
|
||||
RecipeModel._sql_remove_list(session, list_of_tables, self.id)
|
||||
|
||||
self.__init__(
|
||||
session=session,
|
||||
name=name,
|
||||
description=description,
|
||||
image=image,
|
||||
recipeYield=recipeYield,
|
||||
recipeIngredient=recipeIngredient,
|
||||
recipeInstructions=recipeInstructions,
|
||||
totalTime=totalTime,
|
||||
recipeCuisine=recipeCuisine,
|
||||
prepTime=prepTime,
|
||||
performTime=performTime,
|
||||
nutrition=nutrition,
|
||||
tool=tool,
|
||||
slug=slug,
|
||||
recipeCategory=recipeCategory,
|
||||
tags=tags,
|
||||
dateAdded=dateAdded,
|
||||
notes=notes,
|
||||
rating=rating,
|
||||
orgURL=orgURL,
|
||||
extras=extras,
|
||||
)
|
50
mealie/db/models/recipe/tag.py
Normal file
50
mealie/db/models/recipe/tag.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
from fastapi.logger import logger
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm import validates
|
||||
|
||||
recipes2tags = sa.Table(
|
||||
"recipes2tags",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
|
||||
)
|
||||
|
||||
|
||||
class Tag(SqlAlchemyBase):
|
||||
__tablename__ = "tags"
|
||||
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"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
try:
|
||||
result = session.query(Tag).filter(Tag.slug == test_slug).first()
|
||||
|
||||
if result:
|
||||
logger.info("Tag exists, associating recipe")
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.info("Tag doesn't exists, creating tag")
|
||||
return Tag(name=name)
|
||||
except:
|
||||
logger.info("Tag doesn't exists, creating tag")
|
||||
return Tag(name=name)
|
15
mealie/db/models/recipe/tool.py
Normal file
15
mealie/db/models/recipe/tool.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import sqlalchemy as sa
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class Tool(SqlAlchemyBase):
|
||||
__tablename__ = "tools"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
tool = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, tool) -> None:
|
||||
self.tool = tool
|
||||
|
||||
def str(self):
|
||||
return self.tool
|
|
@ -1,93 +1,38 @@
|
|||
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 sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class SiteSettingsModel(SqlAlchemyBase, BaseMixins):
|
||||
class SiteSettings(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "site_settings"
|
||||
name = sa.Column(sa.String, primary_key=True)
|
||||
planCategories = orm.relationship(
|
||||
"MealCategory", uselist=True, cascade="all, delete"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
language = sa.Column(sa.String)
|
||||
categories = orm.relationship(
|
||||
"Category",
|
||||
secondary=site_settings2categories,
|
||||
single_parent=True,
|
||||
)
|
||||
webhooks = orm.relationship("WebHookModel", uselist=False, cascade="all, delete")
|
||||
show_recent = sa.Column(sa.Boolean, default=True)
|
||||
cards_per_section = sa.Column(sa.Integer)
|
||||
|
||||
def __init__(
|
||||
self, name: str = None, webhooks: dict = None, planCategories=[], session=None
|
||||
self,
|
||||
session: Session = None,
|
||||
language="en",
|
||||
categories: list = [],
|
||||
show_recent=True,
|
||||
cards_per_section: int = 9,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.planCategories = [MealCategory(cat) for cat in planCategories]
|
||||
self.webhooks = WebHookModel(**webhooks)
|
||||
session.commit()
|
||||
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
|
||||
]
|
||||
|
||||
def update(self, session, name, webhooks: dict, planCategories=[]) -> dict:
|
||||
|
||||
self._sql_remove_list(session, [MealCategory], self.name)
|
||||
self.name = name
|
||||
self.planCategories = [MealCategory(x) for x in planCategories]
|
||||
self.webhooks.update(session=session, **webhooks)
|
||||
return
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"name": self.name,
|
||||
"planCategories": [cat.to_str() for cat in self.planCategories],
|
||||
"webhooks": self.webhooks.dict(),
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class MealCategory(SqlAlchemyBase):
|
||||
__tablename__ = "meal_plan_categories"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String)
|
||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("site_settings.name"))
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class WebHookModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "webhook_settings"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_settings.name"))
|
||||
webhookURLs = orm.relationship(
|
||||
"WebhookURLModel", uselist=True, cascade="all, delete"
|
||||
)
|
||||
webhookTime = sa.Column(sa.String, default="00:00")
|
||||
enabled = sa.Column(sa.Boolean, default=False)
|
||||
|
||||
def __init__(
|
||||
self, webhookURLs: list, webhookTime: str, enabled: bool = False, session=None
|
||||
) -> None:
|
||||
|
||||
self.webhookURLs = [WebhookURLModel(url=x) for x in webhookURLs]
|
||||
self.webhookTime = webhookTime
|
||||
self.enabled = enabled
|
||||
|
||||
def update(
|
||||
self, session, webhookURLs: list, webhookTime: str, enabled: bool
|
||||
) -> None:
|
||||
|
||||
self._sql_remove_list(session, [WebhookURLModel], self.id)
|
||||
|
||||
self.__init__(webhookURLs, webhookTime, enabled)
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"webhookURLs": [url.to_str() for url in self.webhookURLs],
|
||||
"webhookTime": self.webhookTime,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class WebhookURLModel(SqlAlchemyBase):
|
||||
__tablename__ = "webhook_urls"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
url = sa.Column(sa.String)
|
||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("webhook_settings.id"))
|
||||
|
||||
def to_str(self):
|
||||
return self.url
|
||||
def update(self, *args, **kwarg):
|
||||
self.__init__(*args, **kwarg)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
from sqlalchemy import Boolean, Column, Integer, String
|
||||
|
||||
|
||||
class SignUp(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -19,11 +19,3 @@ class SignUp(SqlAlchemyBase, BaseMixins):
|
|||
self.token = token
|
||||
self.name = name
|
||||
self.admin = admin
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"token": self.token,
|
||||
"admin": self.admin
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class SiteThemeModel(SqlAlchemyBase):
|
||||
|
@ -14,11 +14,7 @@ class SiteThemeModel(SqlAlchemyBase):
|
|||
|
||||
def update(self, session=None, name: str = None, colors: dict = None) -> dict:
|
||||
self.colors.update(**colors)
|
||||
return self.dict()
|
||||
|
||||
def dict(self):
|
||||
data = {"name": self.name, "colors": self.colors.dict()}
|
||||
return data
|
||||
return self
|
||||
|
||||
|
||||
class ThemeColorsModel(SqlAlchemyBase):
|
||||
|
@ -50,15 +46,3 @@ class ThemeColorsModel(SqlAlchemyBase):
|
|||
self.info = info
|
||||
self.warning = warning
|
||||
self.error = error
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"primary": self.primary,
|
||||
"accent": self.accent,
|
||||
"secondary": self.secondary,
|
||||
"success": self.success,
|
||||
"info": self.info,
|
||||
"warning": self.warning,
|
||||
"error": self.error,
|
||||
}
|
||||
return data
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
from core.config import DEFAULT_GROUP
|
||||
from db.models.group import Group
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy import Boolean, Column, Integer, String
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||
|
||||
# I'm not sure this is necessasry, browser based settings may be sufficient
|
||||
# class UserSettings(SqlAlchemyBase, BaseMixins):
|
||||
# __tablename__ = "user_settings"
|
||||
# id = Column(Integer, primary_key=True, index=True)
|
||||
# parent_id = Column(String, ForeignKey("users.id"))
|
||||
|
||||
|
||||
class User(SqlAlchemyBase, BaseMixins):
|
||||
|
@ -8,9 +16,9 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||
full_name = Column(String, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
password = Column(String)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
family = Column(String)
|
||||
admin = Column(Boolean(), default=False)
|
||||
group_id = Column(String, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="users")
|
||||
admin = Column(Boolean, default=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -18,29 +26,21 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||
full_name,
|
||||
email,
|
||||
password,
|
||||
family="public",
|
||||
group: str = DEFAULT_GROUP,
|
||||
admin=False,
|
||||
) -> None:
|
||||
|
||||
group = group if group else DEFAULT_GROUP
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.family = family
|
||||
self.group = Group.create_if_not_exist(session, group)
|
||||
self.admin = admin
|
||||
self.password = password
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"full_name": self.full_name,
|
||||
"email": self.email,
|
||||
"admin": self.admin,
|
||||
"family": self.family,
|
||||
"password": self.password,
|
||||
}
|
||||
|
||||
def update(self, full_name, email, family, admin, session=None):
|
||||
def update(self, full_name, email, group, admin, session=None):
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.family = family
|
||||
self.group = Group.create_if_not_exist(session, group)
|
||||
self.admin = admin
|
||||
|
||||
def update_password(self, password):
|
||||
|
|
|
@ -5,11 +5,11 @@ from core.config import BACKUP_DIR, TEMPLATE_DIR
|
|||
from 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 sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/backups", tags=["Backups"])
|
||||
|
||||
|
|
|
@ -20,5 +20,5 @@ def query_user(user_email: str, session: Session = None) -> UserInDB:
|
|||
session = session if session else create_session()
|
||||
user = db.users.get(session, user_email, "email")
|
||||
session.close()
|
||||
return UserInDB(**user)
|
||||
return user
|
||||
|
||||
|
|
80
mealie/routes/groups/crud.py
Normal file
80
mealie/routes/groups/crud.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from db.database import db
|
||||
from 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 sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/groups", tags=["Groups"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[GroupInDB])
|
||||
async def get_all_groups(
|
||||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of all groups in the database """
|
||||
|
||||
return db.groups.get_all(session)
|
||||
|
||||
|
||||
@router.get("/self", response_model=GroupInDB)
|
||||
async def get_current_user_group(
|
||||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns the Group Data for the Current User """
|
||||
current_user: UserInDB
|
||||
|
||||
return db.groups.get(session, current_user.group, "name")
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_group(
|
||||
group_data: GroupBase,
|
||||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Creates a Group in the Database """
|
||||
|
||||
try:
|
||||
db.groups.create(session, group_data.dict())
|
||||
return SnackResponse.success("User Group Created", {"created": True})
|
||||
except:
|
||||
return SnackResponse.error("User Group Creation Failed")
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_group_data(
|
||||
id: int,
|
||||
group_data: UpdateGroup,
|
||||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Updates a User Group """
|
||||
db.groups.update(session, id, group_data.dict())
|
||||
|
||||
return SnackResponse.success("Group Settings Updated")
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
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:
|
||||
return SnackResponse.error("Cannot delete default group")
|
||||
|
||||
group: GroupInDB = db.groups.get(session, id)
|
||||
|
||||
if not group:
|
||||
return SnackResponse.error("Group not found")
|
||||
|
||||
if not group.users == []:
|
||||
return SnackResponse.error("Cannot delete group with users")
|
||||
|
||||
db.groups.delete(session, id)
|
||||
|
||||
return
|
7
mealie/routes/groups/groups.py
Normal file
7
mealie/routes/groups/groups.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from fastapi import APIRouter
|
||||
from routes.groups import crud
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(crud.router)
|
||||
|
|
@ -1,20 +1,23 @@
|
|||
from datetime import date
|
||||
from typing import List
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from schema.meal import MealPlanBase, MealPlanInDB
|
||||
from schema.recipe import Recipe
|
||||
from schema.snackbar import SnackResponse
|
||||
from services.meal_services import MealPlan
|
||||
from services.meal_services import get_todays_meal, process_meals
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[MealPlan])
|
||||
@router.get("/all", response_model=List[MealPlanInDB])
|
||||
def get_all_meals(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of all available Meal Plan """
|
||||
|
||||
return MealPlan.get_all(session)
|
||||
return db.meals.get_all(session)
|
||||
|
||||
|
||||
@router.get("/{id}/shopping-list")
|
||||
|
@ -22,10 +25,11 @@ def get_shopping_list(id: str, session: Session = Depends(generate_session)):
|
|||
|
||||
#! Refactor into Single Database Call
|
||||
mealplan = db.meals.get(session, id)
|
||||
slugs = [x.get("slug") for x in mealplan.get("meals")]
|
||||
recipes = [db.recipes.get(session, x) for x in slugs]
|
||||
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.get("name"), "recipeIngredient": x.get("recipeIngredient")}
|
||||
{"name": x.name, "recipeIngredient": x.recipeIngredient}
|
||||
for x in recipes
|
||||
if x
|
||||
]
|
||||
|
@ -34,28 +38,29 @@ def get_shopping_list(id: str, session: Session = Depends(generate_session)):
|
|||
|
||||
|
||||
@router.post("/create")
|
||||
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
|
||||
def create_meal_plan(data: MealPlanBase, session: Session = Depends(generate_session)):
|
||||
""" Creates a meal plan database entry """
|
||||
data.process_meals(session)
|
||||
data.save_to_db(session)
|
||||
processed_plan = process_meals(session, data)
|
||||
db.meals.create(session, processed_plan.dict())
|
||||
|
||||
return SnackResponse.success("Mealplan Created")
|
||||
|
||||
|
||||
@router.get("/this-week", response_model=MealPlan)
|
||||
@router.get("/this-week", response_model=MealPlanInDB)
|
||||
def get_this_week(session: Session = Depends(generate_session)):
|
||||
""" Returns the meal plan data for this week """
|
||||
|
||||
return MealPlan.this_week(session)
|
||||
return db.meals.get_all(session, limit=1, order_by="startDate")
|
||||
|
||||
|
||||
@router.put("/{plan_id}")
|
||||
def update_meal_plan(
|
||||
plan_id: str, meal_plan: MealPlan, session: Session = Depends(generate_session)
|
||||
plan_id: str, meal_plan: MealPlanBase, session: Session = Depends(generate_session)
|
||||
):
|
||||
""" Updates a meal plan based off ID """
|
||||
meal_plan.process_meals(session)
|
||||
meal_plan.update(session, plan_id)
|
||||
processed_plan = process_meals(session, meal_plan)
|
||||
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
|
||||
db.meals.update(session, plan_id, processed_plan.dict())
|
||||
|
||||
return SnackResponse.info("Mealplan Updated")
|
||||
|
||||
|
@ -64,7 +69,7 @@ def update_meal_plan(
|
|||
def delete_meal_plan(plan_id, session: Session = Depends(generate_session)):
|
||||
""" Removes a meal plan from the database """
|
||||
|
||||
MealPlan.delete(session, plan_id)
|
||||
db.meals.delete(session, plan_id)
|
||||
|
||||
return SnackResponse.error("Mealplan Deleted")
|
||||
|
||||
|
@ -76,4 +81,4 @@ def get_today(session: Session = Depends(generate_session)):
|
|||
If no meal is scheduled nothing is returned
|
||||
"""
|
||||
|
||||
return MealPlan.today(session)
|
||||
return get_todays_meal(session)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from db.database import db
|
||||
from 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 RecipeURLIn
|
||||
from schema.recipe import Recipe, RecipeURLIn
|
||||
from schema.snackbar import SnackResponse
|
||||
from services.image_services import read_image, write_image
|
||||
from services.recipe_services import Recipe
|
||||
from services.scraper.scraper import create_from_url
|
||||
from sqlalchemy.orm.session import Session
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/recipes",
|
||||
|
@ -16,49 +16,47 @@ router = APIRouter(
|
|||
|
||||
|
||||
@router.post("/create", status_code=201, response_model=str)
|
||||
def create_from_json(data: Recipe, db: Session = Depends(generate_session)) -> str:
|
||||
def create_from_json(data: Recipe, session: Session = Depends(generate_session)) -> str:
|
||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||
new_recipe_slug = data.save_to_db(db)
|
||||
recipe: Recipe = db.recipes.create(session, data.dict())
|
||||
|
||||
return new_recipe_slug
|
||||
return recipe.slug
|
||||
|
||||
|
||||
@router.post("/create-url", status_code=201, response_model=str)
|
||||
def parse_recipe_url(url: RecipeURLIn, db: Session = Depends(generate_session)):
|
||||
def parse_recipe_url(url: RecipeURLIn, session: Session = Depends(generate_session)):
|
||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||
|
||||
recipe = create_from_url(url.url)
|
||||
|
||||
recipe.save_to_db(db)
|
||||
recipe: Recipe = db.recipes.create(session, recipe.dict())
|
||||
|
||||
return recipe.slug
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_slug: str, db: Session = Depends(generate_session)):
|
||||
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session)):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
recipe = Recipe.get_by_slug(db, recipe_slug)
|
||||
|
||||
return recipe
|
||||
return db.recipes.get(session, recipe_slug)
|
||||
|
||||
|
||||
@router.put("/{recipe_slug}")
|
||||
def update_recipe(
|
||||
recipe_slug: str, data: Recipe, db: Session = Depends(generate_session)
|
||||
recipe_slug: str, data: Recipe, session: Session = Depends(generate_session)
|
||||
):
|
||||
""" Updates a recipe by existing slug and data. """
|
||||
|
||||
new_slug = data.update(db, recipe_slug)
|
||||
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
|
||||
|
||||
return new_slug
|
||||
return recipe.slug
|
||||
|
||||
|
||||
@router.delete("/{recipe_slug}")
|
||||
def delete_recipe(recipe_slug: str, db: Session = Depends(generate_session)):
|
||||
def delete_recipe(recipe_slug: str, session: Session = Depends(generate_session)):
|
||||
""" Deletes a recipe by slug """
|
||||
|
||||
try:
|
||||
Recipe.delete(db, recipe_slug)
|
||||
db.recipes.delete(session, recipe_slug)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")
|
||||
|
@ -86,6 +84,6 @@ def update_recipe_image(
|
|||
):
|
||||
""" Removes an existing image and replaces it with the incoming file. """
|
||||
response = write_image(recipe_slug, image, extension)
|
||||
Recipe.update_image(session, recipe_slug, extension)
|
||||
db.recipes.update_image(session, recipe_slug, extension)
|
||||
|
||||
return response
|
||||
|
|
|
@ -2,9 +2,9 @@ from db.database import db
|
|||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from schema.settings import SiteSettings
|
||||
from schema.snackbar import SnackResponse
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.post_webhooks import post_webhooks
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
|
||||
|
@ -13,17 +13,15 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
|||
def get_main_settings(session: Session = Depends(generate_session)):
|
||||
""" Returns basic site settings """
|
||||
|
||||
try:
|
||||
data = db.settings.get(session, "main")
|
||||
except:
|
||||
return
|
||||
data = db.settings.get(session, 1)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@router.put("")
|
||||
def update_settings(data: SiteSettings, session: Session = Depends(generate_session)):
|
||||
""" Returns Site Settings """
|
||||
db.settings.update(session, "main", data.dict())
|
||||
db.settings.update(session, 1, data.dict())
|
||||
|
||||
return SnackResponse.success("Settings Updated")
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from schema.snackbar import SnackResponse
|
|||
from schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["Auth"])
|
||||
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
||||
|
||||
|
||||
@router.post("/token")
|
||||
|
@ -60,10 +60,8 @@ def get_long_token(
|
|||
)
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
):
|
||||
@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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
@ -65,9 +66,10 @@ async def update_user(
|
|||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
access_token = None
|
||||
if current_user.id == id or current_user.admin:
|
||||
updated_user = db.users.update(session, id, new_data.dict())
|
||||
email = updated_user.get("email")
|
||||
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)
|
||||
|
@ -82,7 +84,6 @@ async def get_user_image(id: str):
|
|||
""" Returns a users profile picture """
|
||||
user_dir = USER_DIR.joinpath(id)
|
||||
for recipe_image in user_dir.glob("profile_image.*"):
|
||||
print(recipe_image)
|
||||
return FileResponse(recipe_image)
|
||||
else:
|
||||
return False
|
||||
|
@ -128,7 +129,6 @@ async def update_password(
|
|||
match_passwords = verify_password(
|
||||
password_change.current_password, current_user.password
|
||||
)
|
||||
print(match_passwords)
|
||||
match_id = current_user.id == id
|
||||
|
||||
if match_passwords and match_id:
|
||||
|
|
|
@ -56,12 +56,12 @@ async def create_user_with_token(
|
|||
""" Creates a user with a valid sign up token """
|
||||
|
||||
# Validate Token
|
||||
db_entry = db.sign_ups.get(session, token, limit=1)
|
||||
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
|
||||
if not db_entry:
|
||||
return {"details": "invalid token"}
|
||||
return SnackResponse.error("Invalid Token")
|
||||
|
||||
# Create User
|
||||
new_user.admin = db_entry.get("admin")
|
||||
new_user.admin = db_entry.admin
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
data = db.users.create(session, new_user.dict())
|
||||
|
||||
|
|
|
@ -5,4 +5,5 @@ router = APIRouter()
|
|||
|
||||
router.include_router(sign_up.router)
|
||||
router.include_router(auth.router)
|
||||
router.include_router(sign_up.router)
|
||||
router.include_router(crud.router)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
## Migrations
|
||||
# TODO
|
||||
|
||||
# Database Init
|
||||
|
||||
## Web Server
|
||||
caddy start --config ./Caddyfile
|
||||
|
||||
|
|
|
@ -1,14 +1,29 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from pydantic.main import BaseModel
|
||||
from services.recipe_services import Recipe
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
from schema.recipe import Recipe
|
||||
|
||||
|
||||
class RecipeCategoryResponse(BaseModel):
|
||||
class CategoryBase(CamelModel):
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeCategoryResponse(CategoryBase):
|
||||
recipes: Optional[List[Recipe]]
|
||||
|
||||
class Config:
|
||||
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
|
||||
|
||||
|
||||
class TagBase(CategoryBase):
|
||||
pass
|
||||
|
||||
|
||||
class RecipeTagResponse(TagBase):
|
||||
pass
|
||||
|
|
|
@ -1,29 +1,48 @@
|
|||
from datetime import date
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
||||
class Meal(BaseModel):
|
||||
slug: Optional[str]
|
||||
class MealIn(BaseModel):
|
||||
name: Optional[str]
|
||||
date: date
|
||||
dateText: str
|
||||
slug: Optional[str]
|
||||
date: Optional[date]
|
||||
|
||||
|
||||
class MealOut(MealIn):
|
||||
image: Optional[str]
|
||||
description: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class MealData(BaseModel):
|
||||
name: Optional[str]
|
||||
slug: str
|
||||
dateText: str
|
||||
|
||||
class MealPlanBase(BaseModel):
|
||||
startDate: date
|
||||
endDate: date
|
||||
meals: List[MealIn]
|
||||
|
||||
@validator("endDate")
|
||||
def endDate_after_startDate(cls, v, values, **kwargs):
|
||||
if "startDate" in values and v < values["startDate"]:
|
||||
raise ValueError("EndDate should be greater than StartDate")
|
||||
return v
|
||||
|
||||
|
||||
class MealPlanProcessed(MealPlanBase):
|
||||
meals: list[MealOut]
|
||||
|
||||
|
||||
class MealPlanInDB(MealPlanProcessed):
|
||||
uid: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class MealPlan(BaseModel):
|
||||
uid: Optional[str]
|
||||
startDate: date
|
||||
endDate: date
|
||||
meals: List[Meal]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from db.models.recipe.recipe import RecipeModel
|
||||
from pydantic import BaseModel, validator
|
||||
from pydantic.utils import GetterDict
|
||||
from slugify import slugify
|
||||
|
||||
|
||||
|
@ -9,19 +11,38 @@ class RecipeNote(BaseModel):
|
|||
title: str
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeStep(BaseModel):
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class Nutrition(BaseModel):
|
||||
calories: Optional[str]
|
||||
fatContent: Optional[str]
|
||||
fiberContent: Optional[str]
|
||||
proteinContent: Optional[str]
|
||||
sodiumContent: Optional[str]
|
||||
sugarContent: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class Recipe(BaseModel):
|
||||
# Standard Schema
|
||||
name: str
|
||||
description: Optional[str]
|
||||
image: Optional[Any]
|
||||
recipeYield: Optional[str]
|
||||
recipeIngredient: Optional[list]
|
||||
recipeInstructions: Optional[list]
|
||||
recipeCategory: Optional[List[str]] = []
|
||||
recipeIngredient: Optional[list[str]]
|
||||
recipeInstructions: Optional[list[RecipeStep]]
|
||||
nutrition: Optional[Nutrition]
|
||||
|
||||
totalTime: Optional[str] = None
|
||||
prepTime: Optional[str] = None
|
||||
|
@ -29,7 +50,6 @@ class Recipe(BaseModel):
|
|||
|
||||
# Mealie Specific
|
||||
slug: Optional[str] = ""
|
||||
categories: Optional[List[str]] = []
|
||||
tags: Optional[List[str]] = []
|
||||
dateAdded: Optional[datetime.date]
|
||||
notes: Optional[List[RecipeNote]] = []
|
||||
|
@ -38,6 +58,18 @@ class Recipe(BaseModel):
|
|||
extras: Optional[dict] = {}
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, name_orm: RecipeModel):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
"recipeIngredient": [x.ingredient for x in name_orm.recipeIngredient],
|
||||
"recipeCategory": [x.name for x in name_orm.recipeCategory],
|
||||
"tags": [x.name for x in name_orm.tags],
|
||||
"extras": {x.key_name: x.value for x in name_orm.extras},
|
||||
}
|
||||
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
|
@ -56,7 +88,7 @@ class Recipe(BaseModel):
|
|||
],
|
||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"tags": ["favorite", "yummy!"],
|
||||
"categories": ["Dinner", "Pasta"],
|
||||
"recipeCategory": ["Dinner", "Pasta"],
|
||||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
||||
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"rating": 3,
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
from schema.category import CategoryBase
|
||||
|
||||
|
||||
class Webhooks(BaseModel):
|
||||
webhookTime: str = "00:00"
|
||||
webhookURLs: Optional[List[str]] = []
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class SiteSettings(BaseModel):
|
||||
name: str = "main"
|
||||
planCategories: list[str] = []
|
||||
webhooks: Webhooks
|
||||
class SiteSettings(CamelModel):
|
||||
language: str = "en"
|
||||
show_recent: bool = True
|
||||
cards_per_section: int = 9
|
||||
categories: Optional[list[CategoryBase]] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "main",
|
||||
"planCategories": ["dinner", "lunch"],
|
||||
"webhooks": {
|
||||
"webhookTime": "00:00",
|
||||
"webhookURLs": ["https://mywebhookurl.com/webhook"],
|
||||
"enable": False,
|
||||
},
|
||||
"language": "en",
|
||||
"showRecent": True,
|
||||
"categories": [
|
||||
{"id": 1, "name": "thanksgiving", "slug": "thanksgiving"},
|
||||
{"id": 2, "name": "homechef", "slug": "homechef"},
|
||||
{"id": 3, "name": "potatoes", "slug": "potatoes"},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,3 +12,6 @@ class SignUpToken(SignUpIn):
|
|||
|
||||
class SignUpOut(SignUpToken):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Colors(BaseModel):
|
||||
primary: str
|
||||
accent: str
|
||||
secondary: str
|
||||
success: str
|
||||
info: str
|
||||
warning: str
|
||||
error: str
|
||||
primary: str = "#E58325"
|
||||
accent: str = "#00457A"
|
||||
secondary: str = "#973542"
|
||||
success: str = "#4CAF50"
|
||||
info: str = "#4990BA"
|
||||
warning: str = "#FF4081"
|
||||
error: str = "#EF5350"
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class SiteTheme(BaseModel):
|
||||
name: str
|
||||
colors: Colors
|
||||
name: str = "default"
|
||||
colors: Colors = Colors()
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "default",
|
||||
|
@ -28,4 +33,4 @@ class SiteTheme(BaseModel):
|
|||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from core.config import DEFAULT_GROUP
|
||||
from db.models.group import Group
|
||||
from db.models.users import User
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
# from pydantic import EmailStr
|
||||
from schema.category import CategoryBase
|
||||
from schema.meal import MealPlanInDB
|
||||
|
||||
|
||||
class ChangePassword(CamelModel):
|
||||
|
@ -10,17 +15,33 @@ class ChangePassword(CamelModel):
|
|||
new_password: str
|
||||
|
||||
|
||||
class GroupBase(CamelModel):
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UserBase(CamelModel):
|
||||
full_name: Optional[str] = None
|
||||
email: str
|
||||
family: str
|
||||
admin: bool
|
||||
group: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, name_orm: User):
|
||||
return {
|
||||
**GetterDict(name_orm),
|
||||
"group": name_orm.group.name,
|
||||
}
|
||||
|
||||
schema_extra = {
|
||||
"fullName": "Change Me",
|
||||
"email": "changeme@email.com",
|
||||
"family": "public",
|
||||
"group": DEFAULT_GROUP,
|
||||
"admin": "false",
|
||||
}
|
||||
|
||||
|
@ -31,7 +52,47 @@ class UserIn(UserBase):
|
|||
|
||||
class UserOut(UserBase):
|
||||
id: int
|
||||
group: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(cls, ormModel: User):
|
||||
return {
|
||||
**GetterDict(ormModel),
|
||||
"group": ormModel.group.name,
|
||||
}
|
||||
|
||||
|
||||
class UserInDB(UserIn, UserOut):
|
||||
class UserInDB(UserOut):
|
||||
password: str
|
||||
pass
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class UpdateGroup(GroupBase):
|
||||
id: int
|
||||
name: str
|
||||
categories: Optional[list[CategoryBase]] = []
|
||||
|
||||
webhook_urls: list[str] = []
|
||||
webhook_time: str = "00:00"
|
||||
webhook_enable: bool
|
||||
|
||||
|
||||
class GroupInDB(UpdateGroup):
|
||||
users: Optional[list[UserOut]]
|
||||
mealplans: Optional[list[MealPlanInDB]]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(_cls, orm_model: Group):
|
||||
return {
|
||||
**GetterDict(orm_model),
|
||||
"webhook_urls": [x.url for x in orm_model.webhook_urls if x],
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@ from db.database import db
|
|||
from db.db_setup import create_session
|
||||
from fastapi.logger import logger
|
||||
from jinja2 import Template
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from schema.recipe import Recipe
|
||||
|
||||
|
||||
class ExportDatabase:
|
||||
|
@ -57,26 +56,27 @@ class ExportDatabase:
|
|||
dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def export_recipes(self):
|
||||
all_recipes = Recipe.get_all(self.session)
|
||||
all_recipes = db.recipes.get_all(self.session)
|
||||
|
||||
for recipe in all_recipes:
|
||||
recipe: Recipe
|
||||
logger.info(f"Backing Up Recipes: {recipe}")
|
||||
|
||||
filename = recipe.get("slug") + ".json"
|
||||
filename = recipe.slug + ".json"
|
||||
file_path = self.recipe_dir.joinpath(filename)
|
||||
|
||||
ExportDatabase._write_json_file(recipe, file_path)
|
||||
ExportDatabase._write_json_file(recipe.dict(), file_path)
|
||||
|
||||
if self.templates:
|
||||
self._export_template(recipe)
|
||||
|
||||
def _export_template(self, recipe_data: dict):
|
||||
def _export_template(self, recipe_data: Recipe):
|
||||
for template_path in self.templates:
|
||||
|
||||
with open(template_path, "r") as f:
|
||||
template = Template(f.read())
|
||||
|
||||
filename = recipe_data.get("name") + template_path.suffix
|
||||
filename = recipe_data.name + template_path.suffix
|
||||
out_file = self.templates_dir.joinpath(filename)
|
||||
|
||||
content = template.render(recipe=recipe_data)
|
||||
|
@ -101,7 +101,7 @@ class ExportDatabase:
|
|||
|
||||
def export_meals(self):
|
||||
#! Problem Parseing Datetime Objects... May come back to this
|
||||
meal_plans = MealPlan.get_all(self.session)
|
||||
meal_plans = db.meals.get_all(self.session)
|
||||
if meal_plans:
|
||||
meal_plans = [x.dict() for x in meal_plans]
|
||||
|
||||
|
|
|
@ -6,11 +6,12 @@ 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 fastapi.logger import logger
|
||||
from schema.recipe import Recipe
|
||||
from schema.restore import RecipeImport, SettingsImport, ThemeImport
|
||||
from schema.theme import SiteTheme
|
||||
from services.recipe_services import Recipe
|
||||
from sqlalchemy.orm.session import Session
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
class ImportDatabase:
|
||||
|
@ -75,6 +76,7 @@ class ImportDatabase:
|
|||
}
|
||||
|
||||
def import_recipes(self):
|
||||
session = create_session()
|
||||
recipe_dir: Path = self.import_dir.joinpath("recipes")
|
||||
|
||||
imports = []
|
||||
|
@ -85,8 +87,12 @@ class ImportDatabase:
|
|||
recipe_dict = json.loads(f.read())
|
||||
recipe_dict = ImportDatabase._recipe_migration(recipe_dict)
|
||||
try:
|
||||
if recipe_dict.get("categories", False):
|
||||
recipe_dict["recipeCategory"] = recipe_dict.get("categories")
|
||||
del recipe_dict["categories"]
|
||||
|
||||
recipe_obj = Recipe(**recipe_dict)
|
||||
recipe_obj.save_to_db(self.session)
|
||||
db.recipes.create(session, recipe_obj.dict())
|
||||
import_status = RecipeImport(
|
||||
name=recipe_obj.name, slug=recipe_obj.slug, status=True
|
||||
)
|
||||
|
|
|
@ -1,111 +1,43 @@
|
|||
from datetime import date, timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
from db.database import db
|
||||
from pydantic import BaseModel, validator
|
||||
from schema.meal import MealIn, MealOut, MealPlanBase, MealPlanProcessed
|
||||
from schema.recipe import Recipe
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from services.recipe_services import Recipe
|
||||
|
||||
def process_meals(session: Session, meal_plan_base: MealPlanBase) -> MealPlanProcessed:
|
||||
meals = []
|
||||
for x, meal in enumerate(meal_plan_base.meals):
|
||||
meal: MealIn
|
||||
try:
|
||||
recipe: Recipe = db.recipes.get(session, meal.slug)
|
||||
|
||||
meal_data = MealOut(
|
||||
slug=recipe.slug,
|
||||
name=recipe.name,
|
||||
date=meal_plan_base.startDate + timedelta(days=x),
|
||||
image=recipe.image,
|
||||
description=recipe.description,
|
||||
)
|
||||
|
||||
except:
|
||||
|
||||
meal_data = MealOut(
|
||||
date=meal_plan_base.startDate + timedelta(days=x),
|
||||
)
|
||||
|
||||
meals.append(meal_data)
|
||||
|
||||
return MealPlanProcessed(
|
||||
meals=meals, startDate=meal_plan_base.startDate, endDate=meal_plan_base.endDate
|
||||
)
|
||||
|
||||
|
||||
class Meal(BaseModel):
|
||||
slug: Optional[str]
|
||||
name: Optional[str]
|
||||
date: date
|
||||
dateText: str
|
||||
image: Optional[str]
|
||||
description: Optional[str]
|
||||
def get_todays_meal(session):
|
||||
meal_plan = db.meals.get_all(session, limit=1, order_by="startDate")
|
||||
|
||||
|
||||
class MealData(BaseModel):
|
||||
name: Optional[str]
|
||||
slug: str
|
||||
dateText: str
|
||||
|
||||
|
||||
class MealPlan(BaseModel):
|
||||
uid: Optional[str]
|
||||
startDate: date
|
||||
endDate: date
|
||||
meals: List[Meal]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"startDate": date.today(),
|
||||
"endDate": date.today(),
|
||||
"meals": [
|
||||
{"slug": "Packed Mac and Cheese", "date": date.today()},
|
||||
{"slug": "Eggs and Toast", "date": date.today()},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@validator('endDate')
|
||||
def endDate_after_startDate(cls, v, values, **kwargs):
|
||||
if 'startDate' in values and v < values['startDate']:
|
||||
raise ValueError('EndDate should be greater than StartDate')
|
||||
return v
|
||||
|
||||
def process_meals(self, session: Session):
|
||||
meals = []
|
||||
for x, meal in enumerate(self.meals):
|
||||
|
||||
try:
|
||||
recipe = Recipe.get_by_slug(session, meal.slug)
|
||||
|
||||
meal_data = {
|
||||
"slug": recipe.slug,
|
||||
"name": recipe.name,
|
||||
"date": self.startDate + timedelta(days=x),
|
||||
"dateText": meal.dateText,
|
||||
"image": recipe.image,
|
||||
"description": recipe.description,
|
||||
}
|
||||
except:
|
||||
meal_data = {
|
||||
"date": self.startDate + timedelta(days=x),
|
||||
"dateText": meal.dateText,
|
||||
}
|
||||
|
||||
meals.append(Meal(**meal_data))
|
||||
|
||||
self.meals = meals
|
||||
|
||||
def save_to_db(self, session: Session):
|
||||
db.meals.create(session, self.dict())
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session) -> List:
|
||||
|
||||
all_meals = [
|
||||
MealPlan(**x) for x in db.meals.get_all(session, order_by="startDate")
|
||||
]
|
||||
|
||||
return all_meals
|
||||
|
||||
def update(self, session, uid):
|
||||
db.meals.update(session, uid, self.dict())
|
||||
|
||||
@staticmethod
|
||||
def delete(session, uid):
|
||||
db.meals.delete(session, uid)
|
||||
|
||||
@staticmethod
|
||||
def today(session: Session) -> str:
|
||||
""" Returns the meal slug for Today """
|
||||
meal_plan = db.meals.get_all(session, limit=1, order_by="startDate")
|
||||
|
||||
meal_docs = [Meal(**meal) for meal in meal_plan["meals"]]
|
||||
|
||||
for meal in meal_docs:
|
||||
if meal.date == date.today():
|
||||
return meal.slug
|
||||
|
||||
return "No Meal Today"
|
||||
|
||||
@staticmethod
|
||||
def this_week(session: Session):
|
||||
meal_plan = db.meals.get_all(session, limit=1, order_by="startDate")
|
||||
|
||||
return meal_plan
|
||||
for meal in meal_plan:
|
||||
meal: MealOut
|
||||
if meal.date == date.today():
|
||||
return meal.slug
|
||||
|
|
|
@ -3,7 +3,8 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
from core.config import IMG_DIR, TEMP_DIR
|
||||
from services.recipe_services import Recipe
|
||||
from db.database import db
|
||||
from schema.recipe import Recipe
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.unzip import unpack_zip
|
||||
|
||||
|
@ -49,32 +50,42 @@ def read_chowdown_file(recipe_file: Path) -> Recipe:
|
|||
"tags": recipe_data.get("tags").split(","),
|
||||
}
|
||||
|
||||
new_recipe = Recipe(**reformat_data)
|
||||
print(reformat_data)
|
||||
|
||||
reformated_list = []
|
||||
for instruction in new_recipe.recipeInstructions:
|
||||
for instruction in reformat_data["recipeInstructions"]:
|
||||
reformated_list.append({"text": instruction})
|
||||
reformat_data["recipeInstructions"] = reformated_list
|
||||
|
||||
new_recipe.recipeInstructions = reformated_list
|
||||
|
||||
return new_recipe
|
||||
return Recipe(**reformat_data)
|
||||
|
||||
|
||||
def chowdown_migrate(session: Session, zip_file: Path):
|
||||
temp_dir = unpack_zip(zip_file)
|
||||
|
||||
temp_dir = unpack_zip(zip_file)
|
||||
print(temp_dir.name)
|
||||
|
||||
path = Path(temp_dir.name)
|
||||
for p in path.iterdir():
|
||||
print("ItterDir", p)
|
||||
for p in p.iterdir():
|
||||
print("Sub Itter", p)
|
||||
with temp_dir as dir:
|
||||
image_dir = TEMP_DIR.joinpath(dir, zip_file.stem, "images")
|
||||
recipe_dir = TEMP_DIR.joinpath(dir, zip_file.stem, "_recipes")
|
||||
chow_dir = next(Path(dir).iterdir())
|
||||
image_dir = TEMP_DIR.joinpath(chow_dir, "images")
|
||||
recipe_dir = TEMP_DIR.joinpath(chow_dir, "_recipes")
|
||||
|
||||
print(image_dir.exists())
|
||||
print(recipe_dir.exists())
|
||||
|
||||
failed_recipes = []
|
||||
successful_recipes = []
|
||||
for recipe in recipe_dir.glob("*.md"):
|
||||
try:
|
||||
new_recipe = read_chowdown_file(recipe)
|
||||
new_recipe.save_to_db(session)
|
||||
successful_recipes.append(recipe.stem)
|
||||
except:
|
||||
db.recipes.create(session, new_recipe.dict())
|
||||
successful_recipes.append(new_recipe.name)
|
||||
except Exception as inst:
|
||||
failed_recipes.append(recipe.stem)
|
||||
|
||||
failed_images = []
|
||||
|
@ -82,7 +93,8 @@ def chowdown_migrate(session: Session, zip_file: Path):
|
|||
try:
|
||||
if not image.stem in failed_recipes:
|
||||
shutil.copy(image, IMG_DIR.joinpath(image.name))
|
||||
except:
|
||||
except Exception as inst:
|
||||
print(inst)
|
||||
failed_images.append(image.name)
|
||||
|
||||
report = {"successful": successful_recipes, "failed": failed_recipes}
|
||||
|
|
|
@ -5,9 +5,10 @@ import zipfile
|
|||
from pathlib import Path
|
||||
|
||||
from core.config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
|
||||
from services.recipe_services import Recipe
|
||||
from schema.recipe import Recipe
|
||||
from services.scraper.cleaner import Cleaner
|
||||
from core.config import IMG_DIR, TEMP_DIR
|
||||
from db.database import db
|
||||
|
||||
|
||||
def process_selection(selection: Path) -> Path:
|
||||
|
@ -77,7 +78,8 @@ def migrate(session, selection: str):
|
|||
|
||||
try:
|
||||
recipe = import_recipes(dir)
|
||||
recipe.save_to_db(session)
|
||||
db.recipes.create(session, recipe.dict())
|
||||
|
||||
successful_imports.append(recipe.name)
|
||||
except:
|
||||
logging.error(f"Failed Nextcloud Import: {dir.name}")
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from db.database import db
|
||||
from pydantic import BaseModel, validator
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from services.image_services import delete_image
|
||||
|
||||
|
||||
class RecipeNote(BaseModel):
|
||||
title: str
|
||||
text: str
|
||||
|
||||
|
||||
class RecipeStep(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class Recipe(BaseModel):
|
||||
# Standard Schema
|
||||
name: str
|
||||
description: Optional[str]
|
||||
image: Optional[Any]
|
||||
recipeYield: Optional[str]
|
||||
recipeIngredient: Optional[list]
|
||||
recipeInstructions: Optional[list]
|
||||
|
||||
totalTime: Optional[str] = None
|
||||
prepTime: Optional[str] = None
|
||||
performTime: Optional[str] = None
|
||||
|
||||
# Mealie Specific
|
||||
slug: Optional[str] = ""
|
||||
categories: Optional[List[str]] = []
|
||||
tags: Optional[List[str]] = []
|
||||
dateAdded: Optional[datetime.date]
|
||||
notes: Optional[List[RecipeNote]] = []
|
||||
rating: Optional[int] = 0
|
||||
orgURL: Optional[str] = ""
|
||||
extras: Optional[dict] = {}
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
||||
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
|
||||
"recipeYield": "4 Servings",
|
||||
"recipeIngredient": [
|
||||
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
|
||||
"Kosher salt, freshly ground pepper",
|
||||
"3 Tbsp. unsalted butter, divided",
|
||||
],
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"text": "Season chicken with salt and pepper.",
|
||||
},
|
||||
],
|
||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"tags": ["favorite", "yummy!"],
|
||||
"categories": ["Dinner", "Pasta"],
|
||||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
||||
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"rating": 3,
|
||||
"extras": {"message": "Don't forget to defrost the chicken!"},
|
||||
}
|
||||
}
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values):
|
||||
name: str = values["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
|
||||
if slug == calc_slug:
|
||||
return slug
|
||||
else:
|
||||
slug = calc_slug
|
||||
return slug
|
||||
|
||||
@classmethod
|
||||
def get_by_slug(cls, session, slug: str):
|
||||
""" Returns a Recipe Object by Slug """
|
||||
|
||||
document = db.recipes.get(session, slug, "slug")
|
||||
|
||||
return cls(**document)
|
||||
|
||||
def save_to_db(self, session) -> str:
|
||||
recipe_dict = self.dict()
|
||||
|
||||
try:
|
||||
extension = Path(recipe_dict["image"]).suffix
|
||||
recipe_dict["image"] = recipe_dict.get("slug") + extension
|
||||
except:
|
||||
recipe_dict["image"] = "no image"
|
||||
|
||||
recipe_doc = db.recipes.create(session, recipe_dict)
|
||||
recipe = Recipe(**recipe_doc)
|
||||
|
||||
return recipe.slug
|
||||
|
||||
@staticmethod
|
||||
def delete(session: Session, recipe_slug: str) -> str:
|
||||
""" Removes the recipe from the database by slug """
|
||||
delete_image(recipe_slug)
|
||||
db.recipes.delete(session, recipe_slug)
|
||||
return "Document Deleted"
|
||||
|
||||
def update(self, session: Session, recipe_slug: str):
|
||||
""" Updates the recipe from the database by slug"""
|
||||
updated_slug = db.recipes.update(session, recipe_slug, self.dict())
|
||||
return updated_slug.get("slug")
|
||||
|
||||
@staticmethod
|
||||
def update_image(session: Session, slug: str, extension: str = None) -> str:
|
||||
"""A helper function to pass the new image name and extension
|
||||
into the database.
|
||||
|
||||
Args:
|
||||
slug (str): The current recipe slug
|
||||
extension (str): the file extension of the new image
|
||||
"""
|
||||
return db.recipes.update_image(session, slug, extension)
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session):
|
||||
return db.recipes.get_all(session)
|
|
@ -8,7 +8,7 @@ from schema.settings import SiteSettings
|
|||
from db.database import db
|
||||
from utils.post_webhooks import post_webhooks
|
||||
|
||||
|
||||
# TODO Fix Scheduler
|
||||
@scheduler.scheduled_job(trigger="interval", minutes=15)
|
||||
def update_webhook_schedule():
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import html
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from slugify import slugify
|
||||
|
@ -24,10 +25,16 @@ class Cleaner:
|
|||
Returns:
|
||||
dict: cleaned recipe dictionary
|
||||
"""
|
||||
recipe_data["totalTime"] = Cleaner.time(recipe_data.get("totalTime"))
|
||||
recipe_data["description"] = Cleaner.html(recipe_data.get("description", ""))
|
||||
recipe_data["prepTime"] = Cleaner.time(recipe_data.get("prepTime"))
|
||||
recipe_data["performTime"] = Cleaner.time(recipe_data.get("performTime"))
|
||||
|
||||
# Times
|
||||
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["recipeYield"] = Cleaner.yield_amount(
|
||||
recipe_data.get("recipeYield")
|
||||
)
|
||||
|
@ -41,8 +48,16 @@ class Cleaner:
|
|||
recipe_data["slug"] = slugify(recipe_data.get("name"))
|
||||
recipe_data["orgURL"] = url
|
||||
|
||||
|
||||
return recipe_data
|
||||
|
||||
@staticmethod
|
||||
def category(category: str):
|
||||
if type(category) == type(str):
|
||||
return [category]
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def html(raw_html):
|
||||
cleanr = re.compile("<.*?>")
|
||||
|
@ -68,7 +83,7 @@ class Cleaner:
|
|||
return []
|
||||
|
||||
# One long string split by (possibly multiple) new lines
|
||||
if type(instructions) == str:
|
||||
if isinstance(instructions, str):
|
||||
return [
|
||||
{"text": Cleaner._instruction(line)}
|
||||
for line in instructions.splitlines()
|
||||
|
@ -95,7 +110,7 @@ class Cleaner:
|
|||
sectionSteps = []
|
||||
for step in instructions:
|
||||
if step["@type"] == "HowToSection":
|
||||
[sectionSteps.append(item) for item in step["itemListELement"]]
|
||||
[sectionSteps.append(item) for item in step["itemListElement"]]
|
||||
|
||||
if len(sectionSteps) > 0:
|
||||
return [
|
||||
|
@ -144,8 +159,13 @@ class Cleaner:
|
|||
return yld
|
||||
|
||||
@staticmethod
|
||||
def time(time_entry) -> str:
|
||||
if type(time_entry) == type(None):
|
||||
def time(time_entry):
|
||||
print(time_entry, type(time_entry))
|
||||
if time_entry == None:
|
||||
return None
|
||||
elif type(time_entry) == datetime:
|
||||
print(time_entry)
|
||||
elif type(time_entry) != str:
|
||||
return str(time_entry)
|
||||
elif time_entry != None:
|
||||
return time_entry
|
||||
|
|
|
@ -6,7 +6,7 @@ import scrape_schema_recipe
|
|||
from core.config import DEBUG_DIR
|
||||
from fastapi.logger import logger
|
||||
from services.image_services import scrape_image
|
||||
from services.recipe_services import Recipe
|
||||
from schema.recipe import Recipe
|
||||
from services.scraper import open_graph
|
||||
from services.scraper.cleaner import Cleaner
|
||||
|
||||
|
@ -25,6 +25,7 @@ def create_from_url(url: str) -> Recipe:
|
|||
"""
|
||||
r = requests.get(url)
|
||||
new_recipe = extract_recipe_from_html(r.text, url)
|
||||
print(new_recipe)
|
||||
new_recipe = Cleaner.clean(new_recipe, url)
|
||||
new_recipe = download_image_for_recipe(new_recipe)
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from pathlib import Path
|
||||
from core.config import TEMP_DIR
|
||||
|
||||
import pytest
|
||||
from core.config import TEMP_DIR
|
||||
from schema.recipe import Recipe
|
||||
from services.image_services import IMG_DIR
|
||||
from services.migrations.nextcloud import (
|
||||
cleanup,
|
||||
|
@ -9,7 +10,6 @@ from services.migrations.nextcloud import (
|
|||
prep,
|
||||
process_selection,
|
||||
)
|
||||
from services.recipe_services import Recipe
|
||||
from tests.test_config import TEST_NEXTCLOUD_DIR
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
|
|
@ -19,12 +19,10 @@ def get_meal_plan_template(first=None, second=None):
|
|||
{
|
||||
"slug": first,
|
||||
"date": "2021-1-17",
|
||||
"dateText": "Monday, January 18, 2021",
|
||||
},
|
||||
{
|
||||
"slug": second,
|
||||
"date": "2021-1-18",
|
||||
"dateText": "Tueday, January 19, 2021",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ def test_read_update(api_client, recipe_data):
|
|||
recipe["notes"] = test_notes
|
||||
|
||||
test_categories = ["one", "two", "three"]
|
||||
recipe["categories"] = test_categories
|
||||
recipe["recipeCategory"] = test_categories
|
||||
|
||||
response = api_client.put(
|
||||
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
||||
|
@ -77,7 +77,7 @@ def test_read_update(api_client, recipe_data):
|
|||
recipe = json.loads(response.content)
|
||||
|
||||
assert recipe["notes"] == test_notes
|
||||
assert recipe["categories"].sort() == test_categories.sort()
|
||||
assert recipe["recipeCategory"].sort() == test_categories.sort()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import json
|
||||
from schema.settings import SiteSettings
|
||||
from schema.theme import SiteTheme
|
||||
|
||||
import pytest
|
||||
from tests.utils.routes import (
|
||||
|
@ -11,30 +13,12 @@ from tests.utils.routes import (
|
|||
|
||||
@pytest.fixture(scope="function")
|
||||
def default_settings():
|
||||
return {
|
||||
"name": "main",
|
||||
"planCategories": [],
|
||||
"webhooks": {"webhookTime": "00:00", "webhookURLs": [], "enabled": False},
|
||||
}
|
||||
return SiteSettings().dict(by_alias=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def default_theme(api_client):
|
||||
|
||||
default_theme = {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
|
||||
return default_theme
|
||||
def default_theme():
|
||||
return SiteTheme().dict()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
@ -62,11 +46,8 @@ def test_default_settings(api_client, default_settings):
|
|||
|
||||
|
||||
def test_update_settings(api_client, default_settings):
|
||||
default_settings["webhooks"]["webhookURLs"] = [
|
||||
"https://test1.url.com",
|
||||
"https://test2.url.com",
|
||||
"https://test3.url.com",
|
||||
]
|
||||
default_settings["language"] = "fr"
|
||||
default_settings["showRecent"] = False
|
||||
|
||||
response = api_client.put(SETTINGS_UPDATE, json=default_settings)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ def default_user():
|
|||
"id": 1,
|
||||
"fullName": "Change Me",
|
||||
"email": "changeme@email.com",
|
||||
"family": "public",
|
||||
"group": "home",
|
||||
"admin": True
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ def new_user():
|
|||
"id": 2,
|
||||
"fullName": "My New User",
|
||||
"email": "newuser@email.com",
|
||||
"family": "public",
|
||||
"group": "home",
|
||||
"admin": False
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ def test_create_user(api_client: requests, token, new_user):
|
|||
"fullName": "My New User",
|
||||
"email": "newuser@email.com",
|
||||
"password": "MyStrongPassword",
|
||||
"family": "public",
|
||||
"group": "home",
|
||||
"admin": False
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ def test_update_user(api_client: requests, token):
|
|||
"id": 1,
|
||||
"fullName": "Updated Name",
|
||||
"email": "updated@email.com",
|
||||
"family": "public",
|
||||
"group": "home",
|
||||
"admin": True
|
||||
}
|
||||
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue