feature/favorite-recipes (#443)
* add favorites options * bump dependencies * add badges to all cards * typo * remove console.log * fix site-loader viewport Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
57f7ea3750
commit
6f38fcf81b
38 changed files with 365 additions and 82 deletions
|
@ -74,6 +74,9 @@ class AppRoutes:
|
||||||
def groups_id(self, id):
|
def groups_id(self, id):
|
||||||
return f"{self.prefix}/groups/{id}"
|
return f"{self.prefix}/groups/{id}"
|
||||||
|
|
||||||
|
def meal_plans_id(self, id):
|
||||||
|
return f"{self.prefix}/meal-plans/{id}"
|
||||||
|
|
||||||
def meal_plans_id_shopping_list(self, id):
|
def meal_plans_id_shopping_list(self, id):
|
||||||
return f"{self.prefix}/meal-plans/{id}/shopping-list"
|
return f"{self.prefix}/meal-plans/{id}/shopping-list"
|
||||||
|
|
||||||
|
@ -122,6 +125,12 @@ class AppRoutes:
|
||||||
def users_id(self, id):
|
def users_id(self, id):
|
||||||
return f"{self.prefix}/users/{id}"
|
return f"{self.prefix}/users/{id}"
|
||||||
|
|
||||||
|
def users_id_favorites(self, id):
|
||||||
|
return f"{self.prefix}/users/{id}/favorites/"
|
||||||
|
|
||||||
|
def users_id_favorites_slug(self, id, slug):
|
||||||
|
return f"{self.prefix}/users/{id}/favorites/{slug}"
|
||||||
|
|
||||||
def users_id_image(self, id):
|
def users_id_image(self, id):
|
||||||
return f"{self.prefix}/users/{id}/image"
|
return f"{self.prefix}/users/{id}/image"
|
||||||
|
|
||||||
|
|
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
|
@ -17673,7 +17673,6 @@
|
||||||
"integrity": "sha512-8q67ORQ9O0Ms0nlqsXTVhaBefRBaLrzPxOewAZhdcO7onHwcO5/wRdWtHhZgfpCZlhY7NogkU16z3WnorSSkEA==",
|
"integrity": "sha512-8q67ORQ9O0Ms0nlqsXTVhaBefRBaLrzPxOewAZhdcO7onHwcO5/wRdWtHhZgfpCZlhY7NogkU16z3WnorSSkEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/core": "^7.11.0",
|
|
||||||
"@babel/helper-compilation-targets": "^7.9.6",
|
"@babel/helper-compilation-targets": "^7.9.6",
|
||||||
"@babel/helper-module-imports": "^7.8.3",
|
"@babel/helper-module-imports": "^7.8.3",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
|
@ -17686,7 +17685,6 @@
|
||||||
"@vue/babel-plugin-jsx": "^1.0.3",
|
"@vue/babel-plugin-jsx": "^1.0.3",
|
||||||
"@vue/babel-preset-jsx": "^1.2.4",
|
"@vue/babel-preset-jsx": "^1.2.4",
|
||||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||||
"core-js": "^3.6.5",
|
|
||||||
"core-js-compat": "^3.6.5",
|
"core-js-compat": "^3.6.5",
|
||||||
"semver": "^6.1.0"
|
"semver": "^6.1.0"
|
||||||
}
|
}
|
||||||
|
@ -17849,7 +17847,8 @@
|
||||||
"version": "4.5.12",
|
"version": "4.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.12.tgz",
|
||||||
"integrity": "sha512-STgbvNv/3iHAKArc18b/qjN7RX1FTrfxPeHH26GOr/A8lJes7+CSluZZ8E5R7Zr/vL0zOqOkUVDAjFXVf4zWQA==",
|
"integrity": "sha512-STgbvNv/3iHAKArc18b/qjN7RX1FTrfxPeHH26GOr/A8lJes7+CSluZZ8E5R7Zr/vL0zOqOkUVDAjFXVf4zWQA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@vue/cli-service": {
|
"@vue/cli-service": {
|
||||||
"version": "4.5.12",
|
"version": "4.5.12",
|
||||||
|
@ -18141,7 +18140,8 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
||||||
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@vue/web-component-wrapper": {
|
"@vue/web-component-wrapper": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
@ -18361,7 +18361,8 @@
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
|
||||||
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"acorn-walk": {
|
"acorn-walk": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
|
@ -18400,13 +18401,15 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
||||||
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"alphanum-sort": {
|
"alphanum-sort": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -28209,7 +28212,8 @@
|
||||||
"vuetify": {
|
"vuetify": {
|
||||||
"version": "2.4.11",
|
"version": "2.4.11",
|
||||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.11.tgz",
|
||||||
"integrity": "sha512-xFNwr95tFRfbyGNg5DBuUkWaKazMBr+ptzoSSL4PGrI0qItY5Vuusxh+ETPtjUXxwz76v5zVtGvF5rWvGQjy7A=="
|
"integrity": "sha512-xFNwr95tFRfbyGNg5DBuUkWaKazMBr+ptzoSSL4PGrI0qItY5Vuusxh+ETPtjUXxwz76v5zVtGvF5rWvGQjy7A==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vuetify-loader": {
|
"vuetify-loader": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
|
@ -28259,7 +28263,8 @@
|
||||||
"vuex": {
|
"vuex": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
||||||
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw=="
|
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vuex-persistedstate": {
|
"vuex-persistedstate": {
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.3",
|
||||||
|
|
|
@ -53,6 +53,7 @@ export default {
|
||||||
this.$store.dispatch("requestRecentRecipes");
|
this.$store.dispatch("requestRecentRecipes");
|
||||||
this.$store.dispatch("refreshToken");
|
this.$store.dispatch("refreshToken");
|
||||||
this.$store.dispatch("requestCurrentGroup");
|
this.$store.dispatch("requestCurrentGroup");
|
||||||
|
this.$store.dispatch("requestUserData");
|
||||||
this.$store.dispatch("requestCategories");
|
this.$store.dispatch("requestCategories");
|
||||||
this.$store.dispatch("requestTags");
|
this.$store.dispatch("requestTags");
|
||||||
this.darkModeSystemCheck();
|
this.darkModeSystemCheck();
|
||||||
|
|
|
@ -49,7 +49,7 @@ const apiReq = {
|
||||||
return handleResponse(response, getSuccessText);
|
return handleResponse(response, getSuccessText);
|
||||||
},
|
},
|
||||||
|
|
||||||
get: function(url, data, getErrorText = defaultErrorText) {
|
get: async function(url, data, getErrorText = defaultErrorText) {
|
||||||
return axios.get(url, data).catch(function(error) {
|
return axios.get(url, data).catch(function(error) {
|
||||||
handleError(error, getErrorText);
|
handleError(error, getErrorText);
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,6 +54,7 @@ export const API_ROUTES = {
|
||||||
categoriesCategory: (category) => `${prefix}/categories/${category}`,
|
categoriesCategory: (category) => `${prefix}/categories/${category}`,
|
||||||
debugLogNum: (num) => `${prefix}/debug/log/${num}`,
|
debugLogNum: (num) => `${prefix}/debug/log/${num}`,
|
||||||
groupsId: (id) => `${prefix}/groups/${id}`,
|
groupsId: (id) => `${prefix}/groups/${id}`,
|
||||||
|
mealPlansId: (id) => `${prefix}/meal-plans/${id}`,
|
||||||
mealPlansIdShoppingList: (id) => `${prefix}/meal-plans/${id}/shopping-list`,
|
mealPlansIdShoppingList: (id) => `${prefix}/meal-plans/${id}/shopping-list`,
|
||||||
mealPlansPlanId: (plan_id) => `${prefix}/meal-plans/${plan_id}`,
|
mealPlansPlanId: (plan_id) => `${prefix}/meal-plans/${plan_id}`,
|
||||||
mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) => `${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`,
|
mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) => `${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`,
|
||||||
|
@ -70,6 +71,8 @@ export const API_ROUTES = {
|
||||||
themesId: (id) => `${prefix}/themes/${id}`,
|
themesId: (id) => `${prefix}/themes/${id}`,
|
||||||
usersApiTokensTokenId: (token_id) => `${prefix}/users-tokens/${token_id}`,
|
usersApiTokensTokenId: (token_id) => `${prefix}/users-tokens/${token_id}`,
|
||||||
usersId: (id) => `${prefix}/users/${id}`,
|
usersId: (id) => `${prefix}/users/${id}`,
|
||||||
|
usersIdFavorites: (id) => `${prefix}/users/${id}/favorites`,
|
||||||
|
usersIdFavoritesSlug: (id, slug) => `${prefix}/users/${id}/favorites/${slug}`,
|
||||||
usersIdImage: (id) => `${prefix}/users/${id}/image`,
|
usersIdImage: (id) => `${prefix}/users/${id}/image`,
|
||||||
usersIdPassword: (id) => `${prefix}/users/${id}/password`,
|
usersIdPassword: (id) => `${prefix}/users/${id}/password`,
|
||||||
usersIdResetPassword: (id) => `${prefix}/users/${id}/reset-password`,
|
usersIdResetPassword: (id) => `${prefix}/users/${id}/reset-password`,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { baseURL } from "./api-utils";
|
import { baseURL } from "./api-utils";
|
||||||
|
import { API_ROUTES } from "./apiRoutes";
|
||||||
import { apiReq } from "./api-utils";
|
import { apiReq } from "./api-utils";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18n from "@/i18n.js";
|
import i18n from "@/i18n.js";
|
||||||
|
@ -91,6 +92,27 @@ export const userAPI = {
|
||||||
const response = await apiReq.delete(usersURLs.userAPIDelete(id));
|
const response = await apiReq.delete(usersURLs.userAPIDelete(id));
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
/** Adds a Recipe to the users favorites
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
async getFavorites(id) {
|
||||||
|
const response = await apiReq.get(API_ROUTES.usersIdFavorites(id));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
/** Adds a Recipe to the users favorites
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
async addFavorite(id, slug) {
|
||||||
|
const response = await apiReq.post(API_ROUTES.usersIdFavoritesSlug(id, slug));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
/** Adds a Recipe to the users favorites
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
async removeFavorite(id, slug) {
|
||||||
|
const response = await apiReq.delete(API_ROUTES.usersIdFavoritesSlug(id, slug));
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteErrorText = response => {
|
const deleteErrorText = response => {
|
||||||
|
|
|
@ -87,8 +87,7 @@ export default {
|
||||||
this.clear();
|
this.clear();
|
||||||
this.$store.commit("setToken", response.data.access_token);
|
this.$store.commit("setToken", response.data.access_token);
|
||||||
this.$emit("logged-in");
|
this.$emit("logged-in");
|
||||||
let user = await api.users.self();
|
this.$store.dispatch("requestUserData");
|
||||||
this.$store.commit("setUserData", user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
|
@ -106,14 +106,7 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
value(val) {
|
|
||||||
console.log(val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
console.log(this.value);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getImage(slug) {
|
getImage(slug) {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
|
@ -161,7 +154,6 @@ export default {
|
||||||
this.setSide(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
|
this.setSide(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
console.log("Hello World");
|
|
||||||
this.customMeal = { name: "", slug: null, description: "" };
|
this.customMeal = { name: "", slug: null, description: "" };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,9 +28,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
mealPlan: Object,
|
mealPlan: Object,
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
console.log(this.mealPlan);
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
formatDate(timestamp) {
|
formatDate(timestamp) {
|
||||||
let dateObject = new Date(timestamp);
|
let dateObject = new Date(timestamp);
|
||||||
|
|
54
frontend/src/components/Recipe/FavoriteBadge.vue
Normal file
54
frontend/src/components/Recipe/FavoriteBadge.vue
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
@click.prevent="toggleFavorite"
|
||||||
|
v-if="isFavorite || showAlways"
|
||||||
|
:color="isFavorite && buttonStyle ? 'secondary' : 'primary'"
|
||||||
|
:icon="!buttonStyle"
|
||||||
|
:fab="buttonStyle"
|
||||||
|
>
|
||||||
|
<v-icon :small="!buttonStyle" color="secondary">
|
||||||
|
{{ isFavorite ? "mdi-heart" : "mdi-heart-outline" }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from "@/api";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
slug: {
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
showAlways: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
buttonStyle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user() {
|
||||||
|
return this.$store.getters.getUserData;
|
||||||
|
},
|
||||||
|
isFavorite() {
|
||||||
|
return this.user.favoriteRecipes.indexOf(this.slug) !== -1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async toggleFavorite() {
|
||||||
|
if (!this.isFavorite) {
|
||||||
|
await api.users.addFavorite(this.user.id, this.slug);
|
||||||
|
} else {
|
||||||
|
await api.users.removeFavorite(this.user.id, this.slug);
|
||||||
|
}
|
||||||
|
this.$store.dispatch("requestUserData");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
|
@ -23,6 +23,7 @@
|
||||||
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
|
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
|
||||||
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
|
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
|
||||||
<div class="d-flex justify-center align-center">
|
<div class="d-flex justify-center align-center">
|
||||||
|
<FavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||||
<v-rating
|
<v-rating
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="ml-auto"
|
class="ml-auto"
|
||||||
|
@ -42,10 +43,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
|
||||||
import ContextMenu from "@/components/Recipe/ContextMenu";
|
import ContextMenu from "@/components/Recipe/ContextMenu";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
FavoriteBadge,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -71,6 +74,11 @@ export default {
|
||||||
return api.recipes.recipeSmallImage(slug, this.image);
|
return api.recipes.recipeSmallImage(slug, this.image);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
loggedIn() {
|
||||||
|
return this.$store.getters.getIsLoggedIn;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
|
<FavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||||
<Rating :value="rating" :name="name" :slug="slug" :small="true" />
|
<Rating :value="rating" :name="name" :slug="slug" :small="true" />
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :isCategory="false" />
|
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :isCategory="false" />
|
||||||
|
@ -33,18 +34,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
|
||||||
import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips";
|
import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips";
|
||||||
import ContextMenu from "@/components/Recipe/ContextMenu";
|
import ContextMenu from "@/components/Recipe/ContextMenu";
|
||||||
import CardImage from "@/components/Recipe/CardImage";
|
import CardImage from "@/components/Recipe/CardImage";
|
||||||
import Rating from "@/components/Recipe/Parts/Rating";
|
import Rating from "@/components/Recipe/Parts/Rating";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: { FavoriteBadge, RecipeChips, ContextMenu, Rating, CardImage },
|
||||||
RecipeChips,
|
|
||||||
ContextMenu,
|
|
||||||
Rating,
|
|
||||||
CardImage,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
name: String,
|
name: String,
|
||||||
slug: String,
|
slug: String,
|
||||||
|
@ -64,6 +61,11 @@ export default {
|
||||||
fallBackImage: false,
|
fallBackImage: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
loggedIn() {
|
||||||
|
return this.$store.getters.getIsLoggedIn;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getImage(slug) {
|
getImage(slug) {
|
||||||
return api.recipes.recipeSmallImage(slug, this.image);
|
return api.recipes.recipeSmallImage(slug, this.image);
|
||||||
|
|
|
@ -95,7 +95,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-intersect="bumpList" class="d-flex">
|
<div v-intersect="bumpList" class="d-flex">
|
||||||
<v-expand-x-transition>
|
<v-expand-x-transition>
|
||||||
<SiteLoader v-if="loading" :loading="loading" :size="150" />
|
<SiteLoader v-if="loading" :loading="loading" />
|
||||||
</v-expand-x-transition>
|
</v-expand-x-transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<slot v-bind="{ open, close }"> </slot>
|
<slot v-bind="{ open, close }"> </slot>
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="dialog"
|
v-model="dialog"
|
||||||
:width="isMobile ? undefined : '700'"
|
:width="isMobile ? undefined : '65%'"
|
||||||
:height="isMobile ? undefined : '0'"
|
:height="isMobile ? undefined : '0'"
|
||||||
:fullscreen="isMobile"
|
:fullscreen="isMobile"
|
||||||
content-class="top-dialog"
|
content-class="top-dialog"
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<v-progress-circular class="mx-auto" :width="size / 20" :size="size" color="primary lighten-2" indeterminate>
|
<div class="mx-auto">
|
||||||
<div class="text-center">
|
<v-progress-circular :width="size.width" :size="size.size" color="primary lighten-2" indeterminate>
|
||||||
<v-icon :size="size / 2" color="primary lighten-2">
|
<div class="text-center">
|
||||||
{{ $globals.icons.primary }}
|
<v-icon :size="size.icon" color="primary lighten-2">
|
||||||
</v-icon>
|
{{ $globals.icons.primary }}
|
||||||
<div>
|
</v-icon>
|
||||||
Loading Recipes
|
<div v-if="large" class="text-small">
|
||||||
|
<slot>
|
||||||
|
{{ small ? "" : "Loading Recipes" }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</v-progress-circular>
|
||||||
|
<div v-if="!large" class="text-small">
|
||||||
|
<slot>
|
||||||
|
{{ small ? "" : "Loading Recipes" }}
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</v-progress-circular>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -17,8 +26,39 @@ export default {
|
||||||
loading: {
|
loading: {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
size: {
|
small: {
|
||||||
default: 200,
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
size() {
|
||||||
|
if (this.small) {
|
||||||
|
return {
|
||||||
|
width: 2,
|
||||||
|
icon: 30,
|
||||||
|
size: 50,
|
||||||
|
};
|
||||||
|
} else if (this.large) {
|
||||||
|
return {
|
||||||
|
width: 4,
|
||||||
|
icon: 120,
|
||||||
|
size: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: 3,
|
||||||
|
icon: 75,
|
||||||
|
size: 125,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,17 @@
|
||||||
<v-list-item-subtitle> {{ user.admin ? $t("user.admin") : $t("user.user") }}</v-list-item-subtitle>
|
<v-list-item-subtitle> {{ user.admin ? $t("user.admin") : $t("user.user") }}</v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item dense v-if="isLoggedIn" :to="`/user/${user.id}/favorites`">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>
|
||||||
|
mdi-heart
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title> Favorites </v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
@ -30,7 +41,7 @@
|
||||||
<!-- Version List Item -->
|
<!-- Version List Item -->
|
||||||
<v-list nav dense class="fixedBottom" v-if="!isMain">
|
<v-list nav dense class="fixedBottom" v-if="!isMain">
|
||||||
<v-list-item href="https://github.com/sponsors/hay-kot" target="_target">
|
<v-list-item href="https://github.com/sponsors/hay-kot" target="_target">
|
||||||
<v-list-item-icon >
|
<v-list-item-icon>
|
||||||
<v-icon color="pink">
|
<v-icon color="pink">
|
||||||
mdi-heart
|
mdi-heart
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
export const validators = {
|
export const validators = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
emailRule: v => !v || /^[^@\s]+@[^@\s.]+.[^@.\s]+$/.test(v) || this.$t("user.e-mail-must-be-valid"),
|
emailRule: v => !v || EMAIL_REGEX.test(v) || this.$t("user.e-mail-must-be-valid"),
|
||||||
|
|
||||||
existsRule: value => !!value || this.$t("general.field-required"),
|
existsRule: value => !!value || this.$t("general.field-required"),
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,6 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const response = await api.users.update(this.user);
|
const response = await api.users.update(this.user);
|
||||||
if (response) {
|
if (response) {
|
||||||
this.$store.commit("setToken", response.data.access_token);
|
this.$store.commit("setToken", response.data.access_token);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<CardSection
|
<CardSection
|
||||||
title-icon=""
|
title-icon="mdi-test"
|
||||||
v-if="siteSettings.showRecent"
|
v-if="siteSettings.showRecent"
|
||||||
:title="$t('page.recent')"
|
:title="$t('page.recent')"
|
||||||
:recipes="recentRecipes"
|
:recipes="recentRecipes"
|
||||||
|
|
|
@ -104,7 +104,6 @@ export default {
|
||||||
async requestMeals() {
|
async requestMeals() {
|
||||||
const response = await api.mealPlans.all();
|
const response = await api.mealPlans.all();
|
||||||
this.plannedMeals = response.data;
|
this.plannedMeals = response.data;
|
||||||
console.log(this.plannedMeals);
|
|
||||||
},
|
},
|
||||||
mealPlanURL(uid) {
|
mealPlanURL(uid) {
|
||||||
return window.location.origin + "/meal-plan?id=" + uid;
|
return window.location.origin + "/meal-plan?id=" + uid;
|
||||||
|
|
|
@ -51,7 +51,6 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.mealPlan = await api.mealPlans.thisWeek();
|
this.mealPlan = await api.mealPlans.thisWeek();
|
||||||
}
|
}
|
||||||
console.log(this.mealPlans);
|
|
||||||
if (!this.mealPlan) {
|
if (!this.mealPlan) {
|
||||||
utils.notify.warning(this.$t("meal-plan.no-meal-plan-defined-yet"));
|
utils.notify.warning(this.$t("meal-plan.no-meal-plan-defined-yet"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
class="d-print-none"
|
class="d-print-none"
|
||||||
:key="imageKey"
|
:key="imageKey"
|
||||||
>
|
>
|
||||||
|
<FavoriteBadge class="ma-1" button-style v-if="loggedIn" :slug="recipeDetails.slug" show-always />
|
||||||
<RecipeTimeCard
|
<RecipeTimeCard
|
||||||
:class="isMobile ? undefined : 'force-bottom'"
|
:class="isMobile ? undefined : 'force-bottom'"
|
||||||
:prepTime="recipeDetails.prepTime"
|
:prepTime="recipeDetails.prepTime"
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
|
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
|
||||||
import VJsoneditor from "v-jsoneditor";
|
import VJsoneditor from "v-jsoneditor";
|
||||||
import RecipeViewer from "@/components/Recipe/RecipeViewer";
|
import RecipeViewer from "@/components/Recipe/RecipeViewer";
|
||||||
import PrintView from "@/components/Recipe/PrintView";
|
import PrintView from "@/components/Recipe/PrintView";
|
||||||
|
@ -68,6 +70,7 @@ export default {
|
||||||
RecipeTimeCard,
|
RecipeTimeCard,
|
||||||
PrintView,
|
PrintView,
|
||||||
NoRecipe,
|
NoRecipe,
|
||||||
|
FavoriteBadge,
|
||||||
},
|
},
|
||||||
mixins: [user],
|
mixins: [user],
|
||||||
inject: {
|
inject: {
|
||||||
|
@ -127,6 +130,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
loggedIn() {
|
||||||
|
return this.$store.getters.getIsLoggedIn;
|
||||||
|
},
|
||||||
isMobile() {
|
isMobile() {
|
||||||
return this.$vuetify.breakpoint.name === "xs";
|
return this.$vuetify.breakpoint.name === "xs";
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@sort="assignSorted"
|
@sort="assignSorted"
|
||||||
/>
|
/>
|
||||||
<v-row class="d-flex">
|
<v-row class="d-flex">
|
||||||
<SiteLoader class="mx-auto" v-if="loading" :loading="loading" :size="200" />
|
<SiteLoader class="mx-auto" v-if="loading" :loading="loading" />
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
49
frontend/src/pages/Recipes/Favorites.vue
Normal file
49
frontend/src/pages/Recipes/Favorites.vue
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<CardSection
|
||||||
|
:sortable="true"
|
||||||
|
:title-icon="$globals.icons.user"
|
||||||
|
:title="userData.username"
|
||||||
|
:recipes="shownRecipes"
|
||||||
|
@sort="assignSorted"
|
||||||
|
/>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from "@/api";
|
||||||
|
import CardSection from "@/components/UI/CardSection";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CardSection,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
userData: {},
|
||||||
|
sortedResults: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
shownRecipes() {
|
||||||
|
if (this.sortedResults.length > 0) {
|
||||||
|
return this.sortedResults;
|
||||||
|
} else {
|
||||||
|
return this.userData.favoriteRecipes;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.userData = await api.users.getFavorites(this.$route.params.id);
|
||||||
|
this.sortedResults = [];
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
assignSorted(val) {
|
||||||
|
this.sortedResults = val.slice();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -3,11 +3,13 @@ const NewRecipe = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipe
|
||||||
const CustomPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CustomPage");
|
const CustomPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CustomPage");
|
||||||
const AllRecipes = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/AllRecipes");
|
const AllRecipes = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/AllRecipes");
|
||||||
const CategoryTagPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CategoryTagPage");
|
const CategoryTagPage = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/CategoryTagPage");
|
||||||
|
const Favorites = () => import(/* webpackChunkName: "recipes" */ "@/pages/Recipes/Favorites");
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
|
|
||||||
export const recipeRoutes = [
|
export const recipeRoutes = [
|
||||||
// Recipes
|
// Recipes
|
||||||
{ path: "/recipes/all", component: AllRecipes },
|
{ path: "/recipes/all", component: AllRecipes },
|
||||||
|
{ path: "/user/:id/favorites", component: Favorites },
|
||||||
{ path: "/recipes/tag/:tag", component: CategoryTagPage },
|
{ path: "/recipes/tag/:tag", component: CategoryTagPage },
|
||||||
{ path: "/recipes/tag", component: CategoryTagPage },
|
{ path: "/recipes/tag", component: CategoryTagPage },
|
||||||
{ path: "/recipes/category", component: CategoryTagPage },
|
{ path: "/recipes/category", component: CategoryTagPage },
|
||||||
|
|
|
@ -41,7 +41,6 @@ const actions = {
|
||||||
this.commit("setRecentRecipes", hash);
|
this.commit("setRecentRecipes", hash);
|
||||||
},
|
},
|
||||||
async requestAllRecipes({ getters }) {
|
async requestAllRecipes({ getters }) {
|
||||||
console.log("All Recipes");
|
|
||||||
const all = getters.getAllRecipes;
|
const all = getters.getAllRecipes;
|
||||||
const payload = await api.recipes.allSummary(all.length, 9999);
|
const payload = await api.recipes.allSummary(all.length, 9999);
|
||||||
const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e]));
|
const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e]));
|
||||||
|
|
|
@ -54,9 +54,11 @@ const mutations = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
async requestUserData({ commit }) {
|
async requestUserData({ getters, commit }) {
|
||||||
const userData = await api.users.self();
|
if (getters.getIsLoggedIn) {
|
||||||
commit("setUserData", userData);
|
const userData = await api.users.self();
|
||||||
|
commit("setUserData", userData);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async resetTheme({ commit }) {
|
async resetTheme({ commit }) {
|
||||||
|
|
|
@ -78,7 +78,7 @@ class BaseDocument:
|
||||||
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
|
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False
|
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
|
||||||
) -> Union[BaseModel, list[BaseModel]]:
|
) -> Union[BaseModel, list[BaseModel]]:
|
||||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||||
key is provided the class objects primary key will be used to match against.
|
key is provided the class objects primary key will be used to match against.
|
||||||
|
@ -91,6 +91,7 @@ class BaseDocument:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict or list[dict]:
|
dict or list[dict]:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if match_key is None:
|
if match_key is None:
|
||||||
match_key = self.primary_key
|
match_key = self.primary_key
|
||||||
|
@ -103,12 +104,14 @@ class BaseDocument:
|
||||||
else:
|
else:
|
||||||
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
|
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
|
||||||
|
|
||||||
|
eff_schema = override_schema or self.schema
|
||||||
|
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
try:
|
try:
|
||||||
return self.schema.from_orm(result[0])
|
return eff_schema.from_orm(result[0])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
return [self.schema.from_orm(x) for x in result]
|
return [eff_schema.from_orm(x) for x in result]
|
||||||
|
|
||||||
def create(self, session: Session, document: dict) -> BaseModel:
|
def create(self, session: Session, document: dict) -> BaseModel:
|
||||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||||
|
|
|
@ -19,7 +19,7 @@ class EventNotification(SqlAlchemyBase, BaseMixins):
|
||||||
user = Column(Boolean, default=False)
|
user = Column(Boolean, default=False)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, *args, **kwargs
|
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, **_
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.notification_url = notification_url
|
self.notification_url = notification_url
|
||||||
|
@ -41,7 +41,7 @@ class Event(SqlAlchemyBase, BaseMixins):
|
||||||
time_stamp = Column(DateTime)
|
time_stamp = Column(DateTime)
|
||||||
category = Column(String)
|
category = Column(String)
|
||||||
|
|
||||||
def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None:
|
def __init__(self, title, text, time_stamp, category, **_) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
self.text = text
|
self.text = text
|
||||||
self.time_stamp = time_stamp
|
self.time_stamp = time_stamp
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import sqlalchemy.ext.declarative as dec
|
import sqlalchemy.ext.declarative as dec
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
SqlAlchemyBase = dec.declarative_base()
|
SqlAlchemyBase = dec.declarative_base()
|
||||||
|
|
||||||
|
@ -6,3 +7,7 @@ SqlAlchemyBase = dec.declarative_base()
|
||||||
class BaseMixins:
|
class BaseMixins:
|
||||||
def update(self, *args, **kwarg):
|
def update(self, *args, **kwarg):
|
||||||
self.__init__(*args, **kwarg)
|
self.__init__(*args, **kwarg)
|
||||||
|
|
||||||
|
def get_ref(cls_type, session: Session, match_value: str, match_attr: str = "id"):
|
||||||
|
eff_ref = getattr(cls_type, match_attr)
|
||||||
|
return session.query(cls_type).filter(eff_ref == match_value).one_or_none()
|
||||||
|
|
|
@ -68,6 +68,10 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
date_added = sa.Column(sa.Date, default=date.today)
|
date_added = sa.Column(sa.Date, default=date.today)
|
||||||
date_updated = sa.Column(sa.DateTime)
|
date_updated = sa.Column(sa.DateTime)
|
||||||
|
|
||||||
|
# Favorited By
|
||||||
|
favorited_by_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
|
||||||
|
favorited_by = orm.relationship("User", back_populates="favorite_recipes")
|
||||||
|
|
||||||
@validates("name")
|
@validates("name")
|
||||||
def validate_name(self, key, name):
|
def validate_name(self, key, name):
|
||||||
assert name != ""
|
assert name != ""
|
||||||
|
@ -99,8 +103,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
extras: dict = None,
|
extras: dict = None,
|
||||||
assets: list = None,
|
assets: list = None,
|
||||||
settings: dict = None,
|
settings: dict = None,
|
||||||
*args,
|
**_
|
||||||
**kwargs
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
|
@ -138,6 +141,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
self.date_added = date_added
|
self.date_added = date_added
|
||||||
self.date_updated = datetime.datetime.now()
|
self.date_updated = datetime.datetime.now()
|
||||||
|
|
||||||
def update(self, *args, **kwargs):
|
def update(self, **_):
|
||||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||||
self.__init__(*args, **kwargs)
|
self.__init__(**_)
|
||||||
|
|
|
@ -42,7 +42,7 @@ class CustomPage(SqlAlchemyBase, BaseMixins):
|
||||||
slug = sa.Column(sa.String, nullable=False)
|
slug = sa.Column(sa.String, nullable=False)
|
||||||
categories = orm.relationship("Category", secondary=custom_pages2categories, single_parent=True)
|
categories = orm.relationship("Category", secondary=custom_pages2categories, single_parent=True)
|
||||||
|
|
||||||
def __init__(self, session=None, name=None, slug=None, position=0, categories=[], *args, **kwargs) -> None:
|
def __init__(self, session=None, name=None, slug=None, position=0, categories=[], **_) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
self.position = position
|
self.position = position
|
||||||
|
|
|
@ -9,7 +9,7 @@ class SiteThemeModel(SqlAlchemyBase, BaseMixins):
|
||||||
name = Column(String, nullable=False, unique=True)
|
name = Column(String, nullable=False, unique=True)
|
||||||
colors = orm.relationship("ThemeColorsModel", uselist=False, single_parent=True, cascade="all, delete-orphan")
|
colors = orm.relationship("ThemeColorsModel", uselist=False, single_parent=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
|
def __init__(self, name: str, colors: dict, **_) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.colors = ThemeColorsModel(**colors)
|
self.colors = ThemeColorsModel(**colors)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import settings
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
from mealie.db.models.recipe.recipe import RecipeModel
|
||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,11 +23,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
full_name = Column(String, index=True)
|
full_name = Column(String, index=True)
|
||||||
username = Column(
|
username = Column(String, index=True, unique=True)
|
||||||
String,
|
|
||||||
index=True,
|
|
||||||
unique=True,
|
|
||||||
)
|
|
||||||
email = Column(String, unique=True, index=True)
|
email = Column(String, unique=True, index=True)
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||||
|
@ -36,21 +33,38 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
favorite_recipes: list[RecipeModel] = orm.relationship(RecipeModel, back_populates="favorited_by")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, session, full_name, email, password, group: str = settings.DEFAULT_GROUP, admin=False, **_
|
self,
|
||||||
|
session,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
favorite_recipes: list[str] = None,
|
||||||
|
group: str = settings.DEFAULT_GROUP,
|
||||||
|
admin=False,
|
||||||
|
**_
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
group = group or settings.DEFAULT_GROUP
|
group = group or settings.DEFAULT_GROUP
|
||||||
|
favorite_recipes = favorite_recipes or []
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.group = Group.get_ref(session, group)
|
self.group = Group.get_ref(session, group)
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
self.favorite_recipes = [
|
||||||
|
RecipeModel.get_ref(RecipeModel, session=session, match_value=x, match_attr="slug")
|
||||||
|
for x in favorite_recipes
|
||||||
|
]
|
||||||
|
|
||||||
if self.username is None:
|
if self.username is None:
|
||||||
self.username = full_name
|
self.username = full_name
|
||||||
|
|
||||||
def update(self, full_name, email, group, admin, username, session=None, id=None, password=None, *args, **kwargs):
|
def update(self, full_name, email, group, admin, username, session=None, favorite_recipes=None, password=None, **_):
|
||||||
|
favorite_recipes = favorite_recipes or []
|
||||||
self.username = username
|
self.username = username
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
|
@ -63,6 +77,11 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
if password:
|
if password:
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
self.favorite_recipes = [
|
||||||
|
RecipeModel.get_ref(RecipeModel, session=session, match_value=x, match_attr="slug")
|
||||||
|
for x in favorite_recipes
|
||||||
|
]
|
||||||
|
|
||||||
def update_password(self, password):
|
def update_password(self, password):
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from mealie.core.security import get_password_hash, verify_password
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
from mealie.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut
|
||||||
from mealie.services.events import create_user_event
|
from mealie.services.events import create_user_event
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||||
async def create_user(
|
async def create_user(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
new_user: UserIn,
|
new_user: UserIn,
|
||||||
current_user=Depends(get_current_user),
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -45,24 +45,21 @@ async def get_all_users(
|
||||||
@router.get("/self", response_model=UserOut)
|
@router.get("/self", response_model=UserOut)
|
||||||
async def get_logged_in_user(
|
async def get_logged_in_user(
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
session: Session = Depends(generate_session),
|
|
||||||
):
|
):
|
||||||
return current_user.dict()
|
return current_user.dict()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}", response_model=UserOut)
|
@router.get("/{id}", response_model=UserOut, dependencies=[Depends(get_current_user)])
|
||||||
async def get_user_by_id(
|
async def get_user_by_id(
|
||||||
id: int,
|
id: int,
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
return db.users.get(session, id)
|
return db.users.get(session, id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{id}/reset-password")
|
@router.put("/{id}/reset-password", dependencies=[Depends(get_current_user)])
|
||||||
async def reset_user_password(
|
async def reset_user_password(
|
||||||
id: int,
|
id: int,
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -97,11 +94,10 @@ async def get_user_image(id: str):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{id}/image")
|
@router.post("/{id}/image", dependencies=[Depends(get_current_user)])
|
||||||
async def update_user_image(
|
async def update_user_image(
|
||||||
id: str,
|
id: str,
|
||||||
profile_image: UploadFile = File(...),
|
profile_image: UploadFile = File(...),
|
||||||
current_user: UserInDB = Depends(get_current_user),
|
|
||||||
):
|
):
|
||||||
""" Updates a User Image """
|
""" Updates a User Image """
|
||||||
|
|
||||||
|
@ -139,6 +135,41 @@ async def update_password(
|
||||||
db.users.update_password(session, id, new_password)
|
db.users.update_password(session, id, new_password)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}/favorites", response_model=UserFavorites)
|
||||||
|
async def get_favorites(id: str, session: Session = Depends(generate_session)):
|
||||||
|
""" Adds a Recipe to the users favorites """
|
||||||
|
|
||||||
|
return db.users.get(session, id, override_schema=UserFavorites)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/favorites/{slug}")
|
||||||
|
async def add_favorite(
|
||||||
|
slug: str,
|
||||||
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
):
|
||||||
|
""" Adds a Recipe to the users favorites """
|
||||||
|
|
||||||
|
current_user.favorite_recipes.append(slug)
|
||||||
|
|
||||||
|
db.users.update(session, current_user.id, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}/favorites/{slug}")
|
||||||
|
async def remove_favorite(
|
||||||
|
slug: str,
|
||||||
|
current_user: UserInDB = Depends(get_current_user),
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
):
|
||||||
|
""" Adds a Recipe to the users favorites """
|
||||||
|
|
||||||
|
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
|
||||||
|
|
||||||
|
db.users.update(session, current_user.id, current_user)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}")
|
@router.delete("/{id}")
|
||||||
async def delete_user(
|
async def delete_user(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
|
|
5
mealie/schema/helpers.py
Normal file
5
mealie/schema/helpers.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeSlug(CamelModel):
|
||||||
|
slug: str
|
|
@ -6,6 +6,7 @@ from mealie.db.models.group import Group
|
||||||
from mealie.db.models.users import User
|
from mealie.db.models.users import User
|
||||||
from mealie.schema.category import CategoryBase
|
from mealie.schema.category import CategoryBase
|
||||||
from mealie.schema.meal import MealPlanOut
|
from mealie.schema.meal import MealPlanOut
|
||||||
|
from mealie.schema.recipe import RecipeSummary
|
||||||
from mealie.schema.shopping_list import ShoppingListOut
|
from mealie.schema.shopping_list import ShoppingListOut
|
||||||
from pydantic.types import constr
|
from pydantic.types import constr
|
||||||
from pydantic.utils import GetterDict
|
from pydantic.utils import GetterDict
|
||||||
|
@ -48,6 +49,7 @@ class UserBase(CamelModel):
|
||||||
email: constr(to_lower=True, strip_whitespace=True)
|
email: constr(to_lower=True, strip_whitespace=True)
|
||||||
admin: bool
|
admin: bool
|
||||||
group: Optional[str]
|
group: Optional[str]
|
||||||
|
favorite_recipes: Optional[list[str]] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
@ -76,6 +78,22 @@ class UserOut(UserBase):
|
||||||
id: int
|
id: int
|
||||||
group: str
|
group: str
|
||||||
tokens: Optional[list[LongLiveTokenOut]]
|
tokens: Optional[list[LongLiveTokenOut]]
|
||||||
|
favorite_recipes: Optional[list[str]] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getter_dict(cls, ormModel: User):
|
||||||
|
return {
|
||||||
|
**GetterDict(ormModel),
|
||||||
|
"group": ormModel.group.name,
|
||||||
|
"favorite_recipes": [x.slug for x in ormModel.favorite_recipes],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserFavorites(UserBase):
|
||||||
|
favorite_recipes: list[RecipeSummary] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
@ -90,7 +108,6 @@ class UserOut(UserBase):
|
||||||
|
|
||||||
class UserInDB(UserOut):
|
class UserInDB(UserOut):
|
||||||
password: str
|
password: str
|
||||||
pass
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue