1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-25 08:09:41 +02:00

feature/profile-cards (#391)

* unify format

* pass variables

* remove namespace

* rename

* group-card init

* shuffle + icons

* remove console.logs

* token CRUD

* update changelog

* add profile link

* consolidate mealplan to profile dashboard

* update docs

* add query parameter to search page

* update test routes

* update python depts

* basic token tests

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-05-06 21:08:27 -08:00 committed by GitHub
parent f4384167f6
commit 95ec13161f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 977 additions and 449 deletions

View file

@ -1,12 +1,10 @@
import json
import re
from pathlib import Path
from typing import Optional
import slugify
from jinja2 import Template
from mealie.app import app
from pydantic import BaseModel
CWD = Path(__file__).parent
OUT_FILE = CWD.joinpath("output", "app_routes.py")

View file

@ -2,25 +2,32 @@ class AppRoutes:
def __init__(self) -> None:
self.prefix = "/api"
self.users_sign_ups = "/api/users/sign-ups"
self.auth_token = "/api/auth/token"
self.auth_token_long = "/api/auth/token/long"
self.auth_refresh = "/api/auth/refresh"
self.users_sign_ups = "/api/users/sign-ups"
self.users = "/api/users"
self.users_self = "/api/users/self"
self.users_api_tokens = "/api/users-tokens"
self.groups = "/api/groups"
self.groups_self = "/api/groups/self"
self.recipes = "/api/recipes"
self.recipes_summary = "/api/recipes/summary"
self.recipes_summary_untagged = "/api/recipes/summary/untagged"
self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized"
self.recipes_category = "/api/recipes/category"
self.recipes_tag = "/api/recipes/tag"
self.categories = "/api/categories"
self.recipes_tags = "/api/recipes/tags/"
self.recipes_create = "/api/recipes/create"
self.recipes_create_url = "/api/recipes/create-url"
self.categories = "/api/categories"
self.categories_empty = "/api/categories/empty"
self.tags = "/api/tags"
self.tags_empty = "/api/tags/empty"
self.about_events = "/api/about/events"
self.meal_plans_all = "/api/meal-plans/all"
self.meal_plans_create = "/api/meal-plans/create"
self.meal_plans_this_week = "/api/meal-plans/this-week"
self.meal_plans_today = "/api/meal-plans/today"
self.meal_plans_today_image = "/api/meal-plans/today/image"
self.site_settings_custom_pages = "/api/site-settings/custom-pages"
self.site_settings = "/api/site-settings"
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
@ -30,8 +37,12 @@ class AppRoutes:
self.backups_export_database = "/api/backups/export/database"
self.backups_upload = "/api/backups/upload"
self.migrations = "/api/migrations"
self.debug = "/api/debug"
self.debug_statistics = "/api/debug/statistics"
self.debug_version = "/api/debug/version"
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
self.debug_log = "/api/debug/log"
self.utils_download = "/api/utils/download"
def users_sign_ups_token(self, token):
return f"{self.prefix}/users/sign-ups/{token}"
@ -48,21 +59,36 @@ class AppRoutes:
def users_id_password(self, id):
return f"{self.prefix}/users/{id}/password"
def users_api_tokens_token_id(self, token_id):
return f"{self.prefix}/users-tokens/{token_id}"
def groups_id(self, id):
return f"{self.prefix}/groups/{id}"
def categories_category(self, category):
return f"{self.prefix}/categories/{category}"
def recipes_tags_tag(self, tag):
return f"{self.prefix}/recipes/tags/{tag}"
def recipes_recipe_slug(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}"
def recipes_recipe_slug_image(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}/image"
def recipes_recipe_slug_assets(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}/assets"
def categories_category(self, category):
return f"{self.prefix}/categories/{category}"
def tags_tag(self, tag):
return f"{self.prefix}/tags/{tag}"
def media_recipes_recipe_slug_images_file_name(self, recipe_slug, file_name):
return f"{self.prefix}/media/recipes/{recipe_slug}/images/{file_name}"
def media_recipes_recipe_slug_assets_file_name(self, recipe_slug, file_name):
return f"{self.prefix}/media/recipes/{recipe_slug}/assets/{file_name}"
def about_events_id(self, id):
return f"{self.prefix}/about/events/{id}"
def meal_plans_plan_id(self, plan_id):
return f"{self.prefix}/meal-plans/{plan_id}"
@ -72,8 +98,8 @@ class AppRoutes:
def site_settings_custom_pages_id(self, id):
return f"{self.prefix}/site-settings/custom-pages/{id}"
def themes_theme_name(self, theme_name):
return f"{self.prefix}/themes/{theme_name}"
def themes_id(self, id):
return f"{self.prefix}/themes/{id}"
def backups_file_name_download(self, file_name):
return f"{self.prefix}/backups/{file_name}/download"
@ -84,14 +110,14 @@ class AppRoutes:
def backups_file_name_delete(self, file_name):
return f"{self.prefix}/backups/{file_name}/delete"
def migrations_type_file_name_import(self, type, file_name):
return f"{self.prefix}/migrations/{type}/{file_name}/import"
def migrations_import_type_file_name_import(self, import_type, file_name):
return f"{self.prefix}/migrations/{import_type}/{file_name}/import"
def migrations_type_file_name_delete(self, type, file_name):
return f"{self.prefix}/migrations/{type}/{file_name}/delete"
def migrations_import_type_file_name_delete(self, import_type, file_name):
return f"{self.prefix}/migrations/{import_type}/{file_name}/delete"
def migrations_type_upload(self, type):
return f"{self.prefix}/migrations/{type}/upload"
def migrations_import_type_upload(self, import_type):
return f"{self.prefix}/migrations/{import_type}/upload"
def debug_log_num(self, num):
return f"{self.prefix}/debug/log/{num}"

View file

@ -1,4 +1,4 @@
# v0.5.0 COOL TITLE GOES HERE
# v0.5.0 Too Many Changes!
**App Version: v0.5.0**
@ -9,11 +9,14 @@
!!! error "Breaking Changes"
#### Database
Database version has been bumped from v0.4.x -> v0.5.0. You will need to export and import your data.
Database version has been bumped from v0.4.x -> v0.5.0. You will need to export and import your data. Moving forward, we will be using database migrations (BETA) to do this automatically. Note that you still must backup your data. If you don't, it's entirely possible something may go wrong and you could lose your data on upgrade.
#### Image Directory
the /data/img directory has been depreciated. All images are now stored in the /recipes/{slug}/image directory. Images should be migrated automatically, but you may experience issues related to this change.
#### API Usage
If you have been using the API directly, many of the routes and status codes have changed. You may experience issues with directly consuming the API.
## Bug Fixes
- Fixed #332 - Language settings are saved for one browser
- Fixes #281 - Slow Handling of Large Sets of Recipes
@ -24,7 +27,10 @@
### Highlights
- Beta Support for Postgres! 🎉 See the getting started page for details
- Recipe Steps now support sections, assets, and additional settings.
- Recipe Features
- Step Sections
- Recipe Assets
- Additional View Settings.
- New Toolbox Page!
- Bulk assign categories and tags by keyword search
- Title case all Categories or Tags with 1 click
@ -35,7 +41,12 @@
- Delete
- Download (As Json)
- Copy Link
- Rating can be updated without entering the editor - Closes #25
- New Profile Dashboard!
- Edit Your Profile
- Create/Edit Themes
- View other users in your Group
- See what's for dinner
- Manage Long Live API Tokens (New)
- New Admin Dashboard! 🎉
- Now you can get some insight on your application with application statics and events.
- See uncategorized/untagged recipes and organize them!
@ -48,6 +59,16 @@
- All images are now converted to .webp for better compression
### General
- New 'Dark' Theme Packages with Mealie
- Updated Recipe Card Sections Toolbar
- New Sort Options (They work this time!)
- Alphabetical
- Rating
- Created Date
- Updated Date
- Shuffle (Random Sort)
- New 'Random' Recipe button on recipe sections. Random recipes are selected from the filtered results below. For example, on the "Cakes" category page, you will only get recipes in the "Cakes" category.
- Rating can be updated without entering the editor - Closes #25
- Updated recipe editor styles and moved notes to below the steps.
- Redesigned search bar
- 'Dinner this week' shows a warning when no meal is planned yet
@ -55,7 +76,7 @@
- More localization
- Start date for Week is now selectable
- Languages are now managed through Crowdin
- The main App bar went through a major overhaul
- Application Bar was Rewritten
- Sidebar can now be toggled everywhere.
- New and improved mobile friendly bottom bar
- Improved styling for search bar in desktop

View file

@ -1,5 +1,19 @@
# Organizing Recipes
Below are some general guidelines that were considered when creating the organization structure for recipes.
## From The Community
> My categories are mostly based on the 'course' they belong to. Appetizers, Starters, Main course, but also sauces or beverages. When I'm looking for an idea for an every day dinner, I just browse "main course".
>
> My tags are for picking the exact type of meal I'm looking for, based on my mood or my guests' diet, like gluten-free, vegetarian, sweet-sour or casserole. They can also act as sub-categories, like "alcohol" for beverages or "hot meal" for a main course.
>
> User: [sephrat](https://github.com/sephrat)
## Structure
!!! tip
Below is a suggestion of guidelines my wife and I use for organizing our recipes within Mealie. Mealie is fairly flexible, so feel free to utilize how you'd like! 👍

File diff suppressed because one or more lines are too long

View file

@ -16,17 +16,10 @@ const usersURLs = {
userID: id => `${userPrefix}/${id}`,
password: id => `${userPrefix}/${id}/password`,
resetPassword: id => `${userPrefix}/${id}/reset-password`,
userAPICreate: `${userPrefix}/api-tokens`,
userAPIDelete: id => `${userPrefix}/api-tokens/${id}`,
};
function deleteErrorText(response) {
switch (response.data.detail) {
case "SUPER_USER":
return i18n.t("user.error-cannot-delete-super-user");
default:
return i18n.t("user.you-are-not-allowed-to-delete-this-user");
}
}
export const userAPI = {
async login(formData) {
let response = await apiReq.post(authURLs.token, formData, null, function() {
@ -90,4 +83,21 @@ export const userAPI = {
() => i18n.t("user.password-has-been-reset-to-the-default-password")
);
},
async createAPIToken(name) {
const response = await apiReq.post(usersURLs.userAPICreate, { name });
return response.data;
},
async deleteAPIToken(id) {
const response = await apiReq.delete(usersURLs.userAPIDelete(id));
return response.data;
},
};
const deleteErrorText = response => {
switch (response.data.detail) {
case "SUPER_USER":
return i18n.t("user.error-cannot-delete-super-user");
default:
return i18n.t("user.you-are-not-allowed-to-delete-this-user");
}
};

View file

@ -100,7 +100,10 @@ export default {
}
},
flat() {
if (this.selected) {
return this.selected.length > 0 && this.solo;
}
return false;
},
},
methods: {

View file

@ -7,26 +7,50 @@
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
<v-spacer></v-spacer>
<v-btn text @click="navigateRandom">
Random
<v-icon left>
mdi-dice-multiple
</v-icon>
{{ $t("general.random") }}
</v-btn>
<v-menu offset-y v-if="$listeners.sort">
<v-menu offset-y left v-if="$listeners.sort">
<template v-slot:activator="{ on, attrs }">
<v-btn text v-bind="attrs" v-on="on">
<v-btn text v-bind="attrs" v-on="on" :loading="sortLoading">
<v-icon left>
mdi-sort
</v-icon>
{{ $t("general.sort") }}
</v-btn>
</template>
<v-list>
<v-list-item @click="sortRecipes(EVENTS.az)">
<v-icon left>
mdi-order-alphabetical-ascending
</v-icon>
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipes(EVENTS.rating)">
<v-icon left>
mdi-star
</v-icon>
<v-list-item-title>{{ $t("general.rating") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipes(EVENTS.created)">
<v-icon left>
mdi-new-box
</v-icon>
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipes(EVENTS.updated)">
<v-icon left>
mdi-update
</v-icon>
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="sortRecipes(EVENTS.created)">
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
<v-list-item @click="sortRecipes(EVENTS.shuffle)">
<v-icon left>
mdi-shuffle-variant
</v-icon>
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@ -114,6 +138,7 @@ export default {
},
data() {
return {
sortLoading: false,
cardLimit: 30,
loading: false,
EVENTS: {
@ -121,6 +146,7 @@ export default {
rating: "rating",
created: "created",
updated: "updated",
shuffle: "shuffle",
},
};
},
@ -165,6 +191,7 @@ export default {
this.$router.push(`/recipe/${recipe.slug}`);
},
sortRecipes(sortType) {
this.sortLoading = true;
let sortTarget = [...this.recipes];
switch (sortType) {
case this.EVENTS.az:
@ -179,11 +206,16 @@ export default {
case this.EVENTS.updated:
utils.recipe.sortByUpdated(sortTarget);
break;
case this.EVENTS.shuffle:
utils.recipe.shuffle(sortTarget);
break;
default:
console.log("Unknown Event", sortType);
return;
}
this.$emit(SORT_EVENT, sortTarget);
this.sortLoading = false;
},
},
};

View file

@ -2,7 +2,7 @@
<div>
<v-navigation-drawer v-model="showSidebar" width="180px" clipped app>
<template v-slot:prepend>
<v-list-item two-line v-if="isLoggedIn">
<v-list-item two-line v-if="isLoggedIn" to="/admin/profile">
<v-list-item-avatar color="accent" class="white--text">
<img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" />
<div v-else>
@ -133,11 +133,6 @@ export default {
to: "/admin/profile",
title: this.$t("settings.profile"),
},
{
icon: "mdi-food",
to: "/admin/meal-planner",
title: this.$t("meal-plan.meal-planner"),
},
];
},
adminLinks() {

View file

@ -74,11 +74,12 @@
"save": "Save",
"settings": "Settings",
"sort": "Sort",
"sort-alphabetically": "A-Z",
"sort-alphabetically": "Alphabetical",
"status": "Status",
"submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday",
"shuffle": "Shuffle",
"templates": "Templates:",
"themes": "Themes",
"thursday": "Thursday",

View file

@ -1,137 +0,0 @@
<template>
<v-card>
<v-card-title class="headline">
{{ $t("meal-plan.meal-planner") }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<h2 class="mt-1">{{ $t("recipe.categories") }}</h2>
<CategoryTagSelector
class="mt-4"
:solo="true"
:dense="false"
v-model="groupSettings.categories"
:return-object="true"
:show-add="true"
:hint="$t('meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans')"
/>
</v-card-text>
<v-divider> </v-divider>
<v-card-text>
<h2 class="mt-1 mb-4">
{{ $t("settings.webhooks.meal-planner-webhooks") }}
</h2>
<p>
{{
$t(
"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>{{ groupSettings.webhookTime }}</strong>
</p>
<v-row dense class="flex align-center">
<v-switch class="mx-2" v-model="groupSettings.webhookEnable" :label="$t('general.enabled')"></v-switch>
<TimePickerDialog @save-time="saveTime" class="ma-2" />
<v-btn class="ma-2" color="info" @click="testWebhooks">
<v-icon left> mdi-webhook </v-icon>
{{ $t("settings.webhooks.test-webhooks") }}
</v-btn>
</v-row>
<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>
</v-btn>
</v-col>
<v-col>
<v-text-field
v-model="groupSettings.webhookUrls[index]"
:label="$t('settings.webhooks.webhook-url')"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn icon color="success" @click="addWebhook">
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn color="success" @click="saveGroupSettings" class="mr-2 mb-1">
<v-icon left> mdi-content-save </v-icon>
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import { api } from "@/api";
import TimePickerDialog from "@/components/FormHelpers/TimePickerDialog";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
export default {
components: {
TimePickerDialog,
CategoryTagSelector,
},
data() {
return {
groupSettings: {
name: "home",
id: 1,
mealplans: [],
categories: [],
webhookUrls: [],
webhookTime: "00:00",
webhookEnable: false,
},
};
},
async mounted() {
await this.$store.dispatch("requestCurrentGroup");
this.getSiteSettings();
},
computed: {
categories() {
return this.$store.getters.getAllCategories;
},
isFlat() {
return this.groupSettings.categories >= 1 ? true : false;
},
},
methods: {
saveTime(value) {
this.groupSettings.webhookTime = value;
},
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.groupSettings.webhookUrls.push(" ");
},
removeWebhook(index) {
this.groupSettings.webhookUrls.splice(index, 1);
},
async saveGroupSettings() {
if (await api.groups.update(this.groupSettings)) {
await this.$store.dispatch("requestCurrentGroup");
this.getSiteSettings();
}
},
testWebhooks() {
api.settings.testWebhooks();
},
},
};
</script>
<style></style>

View file

@ -0,0 +1,147 @@
<template>
<StatCard icon="mdi-api" color="accent">
<template v-slot:after-heading>
<div class="ml-auto text-right">
<div class="body-3 grey--text font-weight-light" v-text="'API Tokens'" />
<h3 class="display-2 font-weight-light text--primary">
<small> {{ user.tokens.length }} </small>
</h3>
</div>
</template>
<template v-slot:bottom>
<v-subheader class="mb-n2">ACTIVE TOKENS</v-subheader>
<v-virtual-scroll height="210" item-height="70" :items="user.tokens" class="mt-2">
<template v-slot:default="{ item }">
<v-divider></v-divider>
<v-list-item @click.prevent>
<v-list-item-avatar>
<v-icon large dark color="accent">
mdi-api
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-action class="ml-auto">
<v-btn large icon @click.stop="deleteToken(item.id)">
<v-icon color="accent">mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider></v-divider>
</template>
</v-virtual-scroll>
<v-divider></v-divider>
<v-card-actions class="pb-1 pt-3">
<v-spacer></v-spacer>
<BaseDialog
:title="'Create an API Token'"
title-icon="mdi-api"
@submit="createToken"
:submit-text="buttonText"
:loading="loading"
>
<v-card-text>
<v-form ref="newTokenForm">
<v-text-field v-model="name" label="Token Name" required> </v-text-field>
</v-form>
<div v-if="createdToken != ''">
<v-textarea
class="mb-0 pb-0"
label="API Token"
readonly
v-model="createdToken"
append-outer-icon="mdi-content-copy"
@click:append-outer="copyToken"
>
</v-textarea>
<v-subheader class="text-center">
Copy this token for use with an external application. This token will not be viewable again.
</v-subheader>
</div>
</v-card-text>
<template v-slot:open="{ open }">
<v-btn color="success" @click="open">
<v-icon left> mdi-plus </v-icon>
{{ $t("general.create") }}
</v-btn>
</template>
</BaseDialog>
</v-card-actions>
</template>
</StatCard>
</template>
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import StatCard from "@/components/UI/StatCard";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
export default {
components: {
BaseDialog,
StatCard,
},
mixins: [validators, initials],
data() {
return {
name: "",
loading: false,
createdToken: "",
};
},
mounted() {
this.$store.dispatch("requestUserData");
},
computed: {
user() {
return this.$store.getters.getUserData;
},
buttonText() {
if (this.createdToken === "") {
return "Create";
} else {
return "Close";
}
},
},
methods: {
async createToken() {
if (this.loading === true) {
this.loading = false;
this.$store.dispatch("requestUserData");
this.createdToken = "";
this.name = "";
return;
}
this.loading = true;
if (this.$refs.newTokenForm.validate()) {
const response = await api.users.createAPIToken(this.name);
this.createdToken = response.token;
}
},
async deleteToken(id) {
await api.users.deleteAPIToken(id);
this.$store.dispatch("requestUserData");
},
copyToken() {
const copyText = this.createdToken;
navigator.clipboard.writeText(copyText).then(
() => console.log("Copied", copyText),
() => console.log("Copied Failed", copyText)
);
},
},
};
</script>
<style></style>

View file

@ -0,0 +1,214 @@
<template>
<StatCard icon="mdi-account-group">
<template v-slot:after-heading>
<div class="ml-auto text-right">
<div class="body-3 grey--text font-weight-light" v-text="$t('group.group')" />
<h3 class="display-2 font-weight-light text--primary">
<small> {{ currentGroup.name }} </small>
</h3>
</div>
</template>
<template v-slot:bottom>
<div v-if="todaysMeal">
<v-subheader>DINNER TONIGHT</v-subheader>
<MobileRecipeCard
:name="todaysMeal.name"
:slug="todaysMeal.slug"
:description="todaysMeal.description"
:rating="todaysMeal.rating"
:tags="true"
/>
</div>
<v-subheader>USERS</v-subheader>
<v-divider></v-divider>
<v-virtual-scroll v-if="currentGroup.users" :items="currentGroup.users" height="257" item-height="64">
<template v-slot:default="{ item }">
<v-list-item :key="item.id" @click.prevent>
<v-list-item-action>
<v-btn fab small depressed color="primary">
{{ generateInitials(item.fullName) }}
</v-btn>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ item.fullName }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider></v-divider>
</template>
</v-virtual-scroll>
<div class="mt-3">
<h3 class="display-2 font-weight-light text--primary">
<v-icon x-large>
mdi-food-variant
</v-icon>
<small> Mealplan Settings </small>
</h3>
</div>
<v-divider></v-divider>
<v-subheader>MEALPLAN CATEGORIES</v-subheader>
<v-card-text class="mt-0 pt-0">
{{ $t("meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans") }}
</v-card-text>
<CategoryTagSelector
:solo="true"
:dense="false"
v-model="groupSettings.categories"
:return-object="true"
:show-add="true"
/>
<v-divider></v-divider>
<v-subheader>WEBHOOKS</v-subheader>
<v-card-text class="mt-0 pt-0">
{{
$t(
"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>{{ groupSettings.webhookTime }}</strong>
</v-card-text>
<v-row dense class="flex align-center">
<v-switch class="ml-5 mr-auto" v-model="groupSettings.webhookEnable" :label="$t('general.enabled')"></v-switch>
<TimePickerDialog @save-time="saveTime" class="" />
</v-row>
<v-card-text>
<v-text-field
prepend-icon="mdi-delete"
v-for="(url, index) in groupSettings.webhookUrls"
@click:prepend="removeWebhook(index)"
:key="index"
v-model="groupSettings.webhookUrls[index]"
:label="$t('settings.webhooks.webhook-url')"
></v-text-field>
<v-card-actions class="pa-0">
<v-spacer></v-spacer>
<v-btn small color="success" @click="addWebhook">
<v-icon left> mdi-webhook </v-icon>
New
</v-btn>
</v-card-actions>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pb-0">
<v-btn class="ma-2" color="info" @click="testWebhooks">
<v-icon left> mdi-webhook </v-icon>
{{ $t("settings.webhooks.test-webhooks") }}
</v-btn>
<v-spacer></v-spacer>
<v-btn color="success" @click="saveGroupSettings">
<v-icon left> mdi-content-save </v-icon>
{{ $t("general.update") }}
</v-btn>
</v-card-actions>
</template>
</StatCard>
</template>
<script>
import TimePickerDialog from "@/components/FormHelpers/TimePickerDialog";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import StatCard from "@/components/UI/StatCard";
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
import { api } from "@/api";
export default {
components: {
StatCard,
MobileRecipeCard,
CategoryTagSelector,
TimePickerDialog,
},
mixins: [validators, initials],
data() {
return {
todaysMeal: false,
hideImage: false,
passwordLoading: false,
password: {
current: "",
newOne: "",
newTwo: "",
},
groupSettings: {},
showPassword: false,
loading: false,
user: {
fullName: "",
email: "",
group: "",
admin: false,
id: 0,
},
};
},
computed: {
userProfileImage() {
this.resetImage();
return `api/users/${this.user.id}/image`;
},
currentGroup() {
return this.$store.getters.getCurrentGroup;
},
},
async mounted() {
this.getTodaysMeal();
await this.$store.dispatch("requestCurrentGroup");
this.getSiteSettings();
},
methods: {
async getTodaysMeal() {
const response = await api.mealPlans.today();
this.todaysMeal = response.data;
},
generateInitials(text) {
const allNames = text.trim().split(" ");
return allNames.reduce(
(acc, curr, index) => {
if (index === 0 || index === allNames.length - 1) {
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
}
return acc;
},
[""]
);
},
getSiteSettings() {
this.groupSettings = this.$store.getters.getCurrentGroup;
},
saveTime(value) {
this.groupSettings.webhookTime = value;
},
addWebhook() {
this.groupSettings.webhookUrls.push(" ");
},
removeWebhook(index) {
this.groupSettings.webhookUrls.splice(index, 1);
},
async saveGroupSettings() {
if (await api.groups.update(this.groupSettings)) {
await this.$store.dispatch("requestCurrentGroup");
this.getSiteSettings();
}
},
testWebhooks() {
api.settings.testWebhooks();
},
},
};
</script>
<style></style>

View file

@ -35,9 +35,11 @@
</v-btn>
</v-btn-toggle>
</template>
<template v-slot:bottom>
<v-virtual-scroll height="290" item-height="70" :items="availableThemes" class="mt-2">
<template v-slot:default="{ item }">
<v-divider></v-divider>
<v-list-item @click="selectedTheme = item">
<v-list-item-avatar>
<v-icon large dark :color="item.colors.primary">
@ -66,6 +68,7 @@
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider></v-divider>
</template>
</v-virtual-scroll>
<v-divider></v-divider>
@ -116,7 +119,7 @@ export default {
components: { StatCard, BaseDialog, ColorPickerDialog, VJsoneditor },
data() {
return {
jsonEditor: true,
jsonEditor: false,
jsonEditorOptions: {
mode: "code",
search: false,
@ -193,7 +196,6 @@ export default {
this.availableThemes = await api.themes.requestAll();
},
editTheme(theme) {
console.log(theme);
this.defaultData = theme;
this.newTheme = false;
this.$refs.themeDialog.open();
@ -201,11 +203,9 @@ export default {
createTheme() {
this.newTheme = true;
this.$refs.themeDialog.open();
console.log("Create Theme");
},
async processSubmit() {
if (this.newTheme) {
console.log("New Theme");
await api.themes.create(this.defaultData);
} else {
await api.themes.update(this.defaultData);
@ -213,7 +213,6 @@ export default {
this.getAllThemes();
},
async deleteTheme() {
console.log(this.defaultData);
await api.themes.delete(this.defaultData.id);
this.getAllThemes();
},

View file

@ -3,25 +3,31 @@
<v-row>
<v-col cols="12" sm="12" lg="6">
<UserCard />
<ProfileThemeCard class="mt-10" />
<APITokenCard class="mt-10" />
</v-col>
<v-col cols="12" sm="12" lg="6">
<ProfileGroupCard />
</v-col>
<v-col cols="12" sm="12" lg="6"> </v-col>
</v-row>
<v-row class="mt-7">
<v-col cols="12" sm="12" lg="6">
<ThemeCard />
</v-col>
<v-col cols="12" sm="12" lg="6"> </v-col>
<v-col cols="12" sm="12" lg="6"> </v-col>
</v-row>
</div>
</template>
<script>
import ThemeCard from "./ThemeCard";
import ProfileThemeCard from "./ProfileThemeCard";
import ProfileGroupCard from "./ProfileGroupCard";
import APITokenCard from "./APITokenCard";
import UserCard from "./UserCard";
export default {
components: {
UserCard,
ThemeCard,
ProfileThemeCard,
ProfileGroupCard,
APITokenCard,
},
};
</script>

View file

@ -1,13 +1,6 @@
<template>
<v-container>
<CardSection
:sortable="true"
:title="title"
:recipes="recipes"
:card-limit="9999"
@sort="sortAZ"
@sort-recent="sortRecent"
/>
<CardSection :sortable="true" :title="title" :recipes="shownRecipes" @sort="assignSorted" />
</v-container>
</template>
@ -22,20 +15,30 @@ export default {
return {
title: "",
recipes: [],
sortedResults: [],
};
},
computed: {
currentCategory() {
return this.$route.params.category;
},
shownRecipes() {
if (this.sortedResults.length > 0) {
return this.sortedResults;
} else {
return this.recipes;
}
},
},
watch: {
async currentCategory() {
this.sortedResults = [];
this.getRecipes();
},
},
mounted() {
this.getRecipes();
this.sortedResults = [];
},
methods: {
async getRecipes() {
@ -43,11 +46,8 @@ export default {
this.title = data.name;
this.recipes = data.recipes;
},
sortAZ() {
this.recipes.sort((a, b) => (a.name > b.name ? 1 : -1));
},
sortRecent() {
this.recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
assignSorted(val) {
this.sortedResults = val.slice();
},
},
};

View file

@ -1,24 +1,23 @@
<template>
<v-container>
<v-card flat height="100%">
<v-app-bar flat>
<v-spacer></v-spacer>
<v-card-title class="text-center justify-center py-3 ">
{{ title.toUpperCase() }}
</v-card-title>
<v-spacer></v-spacer>
<v-app-bar color="transparent" flat class="mt-n1 rounded">
<v-icon large left>
mdi-tag-multiple-outline
</v-icon>
<v-toolbar-title class="headline"> {{ page.name }} </v-toolbar-title>
</v-app-bar>
<div v-if="render">
<v-tabs v-model="tab" background-color="transparent" grow>
<v-tab v-for="item in categories" :key="item.slug">
<v-tab v-for="item in page.categories" :key="item.slug" :href="`#${item.slug}`">
{{ item.name }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="(item, index) in categories" :key="item.slug + index">
<CardSection class="mb-5 mx-1" :recipes="filterRecipe(item.slug)" />
<v-tab-item v-for="(item, index) in page.categories" :key="item.slug + index" :value="item.slug">
<CardSection class="mb-5 mx-1" :recipes="item.recipes" @sort="sortRecipes($event, index)" />
</v-tab-item>
</v-tabs-items>
</div>
@ -36,8 +35,8 @@ export default {
},
data() {
return {
page: "",
title: "",
tab: null,
render: false,
recipeStore: [],
categories: [],
@ -47,6 +46,14 @@ export default {
pageSlug() {
return this.$route.params.customPage;
},
tab: {
set(tab) {
this.$router.replace({ query: { ...this.$route.query, tab } });
},
get() {
return this.$route.query.tab;
},
},
},
watch: {
@ -61,27 +68,15 @@ export default {
},
methods: {
async buildPage() {
const page = await api.siteSettings.getPage(this.pageSlug);
this.title = page.name;
this.categories = page.categories;
page.categories.forEach(async element => {
let categoryRecipes = await this.getRecipeByCategory(element.slug);
this.recipeStore.push(categoryRecipes);
});
},
async getRecipeByCategory(category) {
return await api.categories.getRecipesInCategory(category);
this.page = await api.siteSettings.getPage(this.pageSlug);
},
filterRecipe(slug) {
const storeCategory = this.recipeStore.find(element => element.slug === slug);
return storeCategory ? storeCategory.recipes : [];
},
sortRecipes(sortedRecipes, destKey) {
this.page.categories[destKey].recipes = sortedRecipes;
},
},
};
</script>
<style>
.header-background {
background-color: #121619;
}
</style>

View file

@ -1,13 +1,6 @@
<template>
<v-container>
<CardSection
:sortable="true"
:title="title"
:recipes="recipes"
:card-limit="9999"
@sort="sortAZ"
@sort-recent="sortRecent"
/>
<CardSection :sortable="true" :title="title" :recipes="shownRecipes" @sort="assignSorted" />
</v-container>
</template>
@ -22,20 +15,30 @@ export default {
return {
title: "",
recipes: [],
sortedResults: [],
};
},
computed: {
currentTag() {
return this.$route.params.tag;
},
shownRecipes() {
if (this.sortedResults.length > 0) {
return this.sortedResults;
} else {
return this.recipes;
}
},
},
watch: {
async currentTag() {
this.getRecipes();
this.sortedResults = [];
},
},
mounted() {
this.getRecipes();
this.sortedResults = [];
},
methods: {
async getRecipes() {
@ -43,11 +46,9 @@ export default {
this.title = data.name;
this.recipes = data.recipes;
},
sortAZ() {
this.recipes.sort((a, b) => (a.name > b.name ? 1 : -1));
},
sortRecent() {
this.recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
assignSorted(val) {
console.log(val);
this.sortedResults = val.slice();
},
},
};

View file

@ -1,20 +1,20 @@
<template>
<v-toolbar dense flat>
<v-btn-toggle dense v-model="selected" tile color="primary accent-3" @change="emitMulti" group mandatory>
<v-btn :value="false">
<v-btn-toggle tile group v-model="selected" color="primary accent-3" @change="emitMulti" mandatory>
<v-btn small :value="false">
{{ $t("search.include") }}
</v-btn>
<v-btn :value="true">
<v-btn small :value="true">
{{ $t("search.exclude") }}
</v-btn>
</v-btn-toggle>
<v-spacer></v-spacer>
<v-btn-toggle dense v-model="match" tile color="primary accent-3" @change="emitMulti" group mandatory>
<v-btn :value="false">
<v-btn-toggle tile group v-model="match" color="primary accent-3" @change="emitMulti" mandatory>
<v-btn small :value="false">
{{ $t("search.and") }}
</v-btn>
<v-btn :value="true">
<v-btn small :value="true">
{{ $t("search.or") }}
</v-btn>
</v-btn-toggle>

View file

@ -36,7 +36,6 @@
{{ $t("search.tag-filter") }}
</h3>
<FilterSelector class="mb-1" @update="updateTagParams" />
<CategoryTagSelector
:solo="true"
:dense="false"
@ -66,7 +65,6 @@ export default {
},
data() {
return {
searchString: "",
maxResults: 21,
searchResults: [],
catFilter: {
@ -96,6 +94,14 @@ export default {
this.$store.dispatch("requestAllRecipes");
},
computed: {
searchString: {
set(q) {
this.$router.replace({ query: { ...this.$route.query, q } });
},
get() {
return this.$route.query.q || "";
},
},
allRecipes() {
return this.$store.getters.getAllRecipes;
},
@ -132,11 +138,6 @@ export default {
}
},
},
watch: {
showRecipes(val) {
console.log(val);
},
},
methods: {
assignFuzzy(val) {
this.sortedResults = val;

View file

@ -1,5 +1,4 @@
import Admin from "@/pages/Admin";
import MealPlanner from "@/pages/Admin/MealPlanner";
import Migration from "@/pages/Admin/Migration";
import Profile from "@/pages/Admin/Profile";
import ManageUsers from "@/pages/Admin/ManageUsers";
@ -29,13 +28,6 @@ export const adminRoutes = {
title: "settings.profile",
},
},
{
path: "meal-planner",
component: MealPlanner,
meta: {
title: "meal-plan.meal-planner",
},
},
{
path: "migrations",
component: Migration,

View file

@ -38,7 +38,7 @@ export const mealRoutes = [
async function todaysMealRoute() {
const response = await api.mealPlans.today();
if (response.status == 200 && response.data) {
return "/recipe/" + response.data;
return "/recipe/" + response.data.slug;
} else {
return null;
}

View file

@ -28,4 +28,22 @@ export const recipe = {
randomRecipe(list) {
return list[Math.floor(Math.random() * list.length)];
},
shuffle(list) {
let last = list.length;
let n;
while (last > 0) {
n = rand(last);
swap(list, n, --last);
}
},
};
const rand = n =>
Math.floor(Math.random() * n)
function swap(t, i, j) {
let q = t[i];
t[i] = t[j];
t[j] = q;
return t;
}

View file

@ -8,7 +8,7 @@ from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.settings import CustomPage, SiteSettings
from mealie.db.models.sign_up import SignUp
from mealie.db.models.theme import SiteThemeModel
from mealie.db.models.users import User
from mealie.db.models.users import LongLiveToken, User
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.events import Event as EventSchema
from mealie.schema.meal import MealPlanInDB
@ -17,7 +17,7 @@ from mealie.schema.settings import CustomPageOut
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.sign_up import SignUpOut
from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
logger = getLogger()
@ -38,12 +38,16 @@ class _Recipes(BaseDocument):
def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int:
return self._countr_attribute(
session, attribute_name=RecipeModel.recipe_category, attr_match=None, count=True, override_schema=None
session,
attribute_name=RecipeModel.recipe_category,
attr_match=None,
count=count,
override_schema=override_schema,
)
def count_untagged(self, session: Session, count=True, override_schema=None) -> int:
return self._countr_attribute(
session, attribute_name=RecipeModel.tags, attr_match=None, count=True, override_schema=None
session, attribute_name=RecipeModel.tags, attr_match=None, count=count, override_schema=override_schema
)
@ -102,6 +106,13 @@ class _Users(BaseDocument):
return self.schema.from_orm(entry)
class _LongLiveToken(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
self.sql_model = LongLiveToken
self.schema = LongLiveTokenInDB
class _Groups(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
@ -154,6 +165,7 @@ class Database:
self.categories = _Categories()
self.tags = _Tags()
self.users = _Users()
self.api_tokens = _LongLiveToken()
self.sign_ups = _SignUps()
self.groups = _Groups()
self.custom_pages = _CustomPages()

View file

@ -1,13 +1,14 @@
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase
from sqlalchemy import Column, ForeignKey, Integer, String
class RecipeIngredient(SqlAlchemyBase):
__tablename__ = "recipes_ingredients"
id = sa.Column(sa.Integer, primary_key=True)
position = sa.Column(sa.Integer)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
ingredient = sa.Column(sa.String)
id = Column(Integer, primary_key=True)
position = Column(Integer)
parent_id = Column(Integer, ForeignKey("recipes.id"))
# title = Column(String)
ingredient = Column(String)
def update(self, ingredient):
self.ingredient = ingredient

View file

@ -1,12 +1,12 @@
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase
from sqlalchemy import Column, ForeignKey, Integer, String
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
position = sa.Column(sa.Integer)
type = sa.Column(sa.String, default="")
text = sa.Column(sa.String)
title = sa.Column(sa.String)
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("recipes.id"))
position = Column(Integer)
type = Column(String, default="")
title = Column(String)
text = Column(String)

View file

@ -93,7 +93,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipe_category: list[str] = None,
tags: list[str] = None,
date_added: datetime.date = None,
date_updated: datetime.datetime = None,
notes: list[dict] = None,
rating: int = None,
org_url: str = None,

View file

@ -3,11 +3,19 @@ from mealie.db.models.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
# I'm not sure this is necessasry, browser based settings may be sufficient
# class UserSettings(SqlAlchemyBase, BaseMixins):
# __tablename__ = "user_settings"
# id = Column(Integer, primary_key=True, index=True)
# parent_id = Column(String, ForeignKey("users.id"))
class LongLiveToken(SqlAlchemyBase, BaseMixins):
__tablename__ = "long_live_tokens"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("users.id"))
name = Column(String, nullable=False)
token = Column(String, nullable=False)
user = orm.relationship("User")
def __init__(self, session, name, token, parent_id) -> None:
self.name = name
self.token = token
self.user = User.get_ref(session, parent_id)
class User(SqlAlchemyBase, BaseMixins):
@ -19,6 +27,9 @@ class User(SqlAlchemyBase, BaseMixins):
group_id = Column(Integer, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="users")
admin = Column(Boolean, default=False)
tokens: list[LongLiveToken] = orm.relationship(
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
)
def __init__(
self,
@ -29,6 +40,8 @@ class User(SqlAlchemyBase, BaseMixins):
group: str = settings.DEFAULT_GROUP,
admin=False,
id=None,
*args,
**kwargs
) -> None:
group = group or settings.DEFAULT_GROUP
@ -38,7 +51,7 @@ class User(SqlAlchemyBase, BaseMixins):
self.admin = admin
self.password = password
def update(self, full_name, email, group, admin, session=None, id=None, password=None):
def update(self, full_name, email, group, admin, session=None, id=None, password=None, *args, **kwargs):
self.full_name = full_name
self.email = email
self.group = Group.get_ref(session, group)
@ -49,3 +62,7 @@ class User(SqlAlchemyBase, BaseMixins):
def update_password(self, password):
self.password = password
@staticmethod
def get_ref(session, id: str):
return session.query(User).filter(User.id == id).one()

View file

@ -8,7 +8,8 @@ from mealie.core.config import settings
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.schema.auth import TokenData
from mealie.schema.user import UserInDB
from mealie.schema.user import LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
ALGORITHM = "HS256"
@ -23,8 +24,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
try:
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
username: str = payload.get("sub")
long_token: str = payload.get("long_token")
if long_token is not None:
return validate_long_live_token(session, token, payload.get("id"))
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
@ -35,6 +42,16 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
return user
def validate_long_live_token(session: Session, client_token: str, id: int) -> UserInDB:
tokens: list[LongLiveTokenInDB] = db.api_tokens.get(session, id, "parent_id", limit=9999)
for token in tokens:
token: LongLiveTokenInDB
if token.token == client_token:
return token.user
async def validate_file_token(token: Optional[str] = None) -> Path:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View file

@ -75,7 +75,7 @@ def get_today(session: Session = Depends(generate_session), current_user: UserIn
group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name")
recipe = get_todays_meal(session, group_in_db)
if recipe:
return recipe.slug
return recipe
@router.get("/today/image", tags=["Meal Plan"])

View file

@ -1,9 +1,10 @@
from fastapi import APIRouter
from . import auth, crud, sign_up
from . import api_tokens, auth, crud, sign_up
user_router = APIRouter()
user_router.include_router(auth.router)
user_router.include_router(sign_up.router)
user_router.include_router(crud.router)
user_router.include_router(api_tokens.router)

View file

@ -0,0 +1,56 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from fastapi.param_functions import Depends
from mealie.core.security import create_access_token
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["User API Tokens"])
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
async def create_api_token(
token_name: LoingLiveTokenIn,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Create api_token in the Database """
token_data = {"long_token": True, "id": current_user.id}
five_years = timedelta(1825)
token = create_access_token(token_data, five_years)
token_model = CreateToken(
name=token_name.name,
token=token,
parent_id=current_user.id,
)
new_token_in_db = db.api_tokens.create(session, token_model)
if new_token_in_db:
return {"token": token}
@router.delete("/api-tokens/{token_id}")
async def delete_api_token(
token_id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Delete api_token from the Database """
token: LongLiveTokenInDB = db.api_tokens.get(session, token_id)
if not token:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
if token.user.email == current_user.email:
deleted_token = db.api_tokens.delete(session, token_id)
return {"token_delete": deleted_token.name}
else:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)

View file

@ -1,7 +1,7 @@
from typing import Optional
from fastapi_camelcase import CamelModel
from mealie.schema.category import CategoryBase
from mealie.schema.category import CategoryBase, RecipeCategoryResponse
from pydantic import validator
from slugify import slugify
@ -34,7 +34,7 @@ class CustomPageBase(CamelModel):
name: str
slug: Optional[str]
position: int
categories: list[CategoryBase] = []
categories: list[RecipeCategoryResponse] = []
class Config:
orm_mode = True

View file

@ -10,6 +10,25 @@ from pydantic.types import constr
from pydantic.utils import GetterDict
class LoingLiveTokenIn(CamelModel):
name: str
class LongLiveTokenOut(LoingLiveTokenIn):
id: int
class Config:
orm_mode = True
class CreateToken(LoingLiveTokenIn):
parent_id: int
token: str
class Config:
orm_mode = True
class ChangePassword(CamelModel):
current_password: str
new_password: str
@ -53,6 +72,7 @@ class UserIn(UserBase):
class UserOut(UserBase):
id: int
group: str
tokens: Optional[list[LongLiveTokenOut]]
class Config:
orm_mode = True
@ -96,3 +116,11 @@ class GroupInDB(UpdateGroup):
**GetterDict(orm_model),
"webhook_urls": [x.url for x in orm_model.webhook_urls if x],
}
class LongLiveTokenInDB(CreateToken):
id: int
user: UserInDB
class Config:
orm_mode = True

204
poetry.lock generated
View file

@ -70,17 +70,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "20.3.0"
version = "21.1.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
name = "bcrypt"
@ -283,7 +283,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "greenlet"
version = "1.0.0"
version = "1.1.0"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
@ -519,7 +519,7 @@ tornado = ">=5.0"
[[package]]
name = "mkdocs-material"
version = "7.1.3"
version = "7.1.4"
description = "A Material Design theme for MkDocs"
category = "dev"
optional = false
@ -693,7 +693,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.8.1"
version = "2.9.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
@ -701,7 +701,7 @@ python-versions = ">=3.5"
[[package]]
name = "pyhumps"
version = "1.6.1"
version = "3.0.2"
description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node"
category = "main"
optional = false
@ -755,7 +755,7 @@ rdflib = "*"
[[package]]
name = "pytest"
version = "6.2.3"
version = "6.2.4"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@ -943,7 +943,7 @@ validators = ">=0.12.4"
[[package]]
name = "six"
version = "1.15.0"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
@ -959,7 +959,7 @@ python-versions = ">=3.6"
[[package]]
name = "sqlalchemy"
version = "1.4.12"
version = "1.4.14"
description = "Database Abstraction Library"
category = "main"
optional = false
@ -1200,8 +1200,8 @@ atomicwrites = [
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
{file = "attrs-21.1.0-py2.py3-none-any.whl", hash = "sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9"},
{file = "attrs-21.1.0.tar.gz", hash = "sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8"},
]
bcrypt = [
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
@ -1356,49 +1356,55 @@ future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
greenlet = [
{file = "greenlet-1.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2"},
{file = "greenlet-1.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c"},
{file = "greenlet-1.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203"},
{file = "greenlet-1.0.0-cp27-cp27m-win32.whl", hash = "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476"},
{file = "greenlet-1.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b"},
{file = "greenlet-1.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379"},
{file = "greenlet-1.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce"},
{file = "greenlet-1.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6"},
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683"},
{file = "greenlet-1.0.0-cp35-cp35m-win32.whl", hash = "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70"},
{file = "greenlet-1.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770"},
{file = "greenlet-1.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d"},
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2"},
{file = "greenlet-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7"},
{file = "greenlet-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be"},
{file = "greenlet-1.0.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c"},
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f"},
{file = "greenlet-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c"},
{file = "greenlet-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85"},
{file = "greenlet-1.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"},
{file = "greenlet-1.0.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196"},
{file = "greenlet-1.0.0-cp38-cp38-win32.whl", hash = "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e"},
{file = "greenlet-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c"},
{file = "greenlet-1.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7"},
{file = "greenlet-1.0.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664"},
{file = "greenlet-1.0.0-cp39-cp39-win32.whl", hash = "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139"},
{file = "greenlet-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0"},
{file = "greenlet-1.0.0.tar.gz", hash = "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8"},
{file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"},
{file = "greenlet-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3"},
{file = "greenlet-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922"},
{file = "greenlet-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821"},
{file = "greenlet-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6"},
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f"},
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56"},
{file = "greenlet-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22"},
{file = "greenlet-1.1.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5"},
{file = "greenlet-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47"},
{file = "greenlet-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08"},
{file = "greenlet-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131"},
{file = "greenlet-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5"},
{file = "greenlet-1.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e"},
{file = "greenlet-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8"},
{file = "greenlet-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb"},
{file = "greenlet-1.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05"},
{file = "greenlet-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f"},
{file = "greenlet-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a"},
{file = "greenlet-1.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da"},
{file = "greenlet-1.1.0-cp38-cp38-win32.whl", hash = "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad"},
{file = "greenlet-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8"},
{file = "greenlet-1.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832"},
{file = "greenlet-1.1.0-cp39-cp39-win32.whl", hash = "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11"},
{file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535"},
{file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee"},
]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
@ -1596,8 +1602,8 @@ mkdocs = [
{file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"},
]
mkdocs-material = [
{file = "mkdocs-material-7.1.3.tar.gz", hash = "sha256:e34bba93ad1a0e6f9afc371f4ef55bedabbf13b9a786b013b0ce26ac55ec2932"},
{file = "mkdocs_material-7.1.3-py2.py3-none-any.whl", hash = "sha256:437638b0de7a9113d7f1c9ddc93c0a29a3b808c71c3606713d8c1fa437697a3e"},
{file = "mkdocs-material-7.1.4.tar.gz", hash = "sha256:f2fe6014bd69c1651a2dfe2651de7b0fdea1c33ba652243809c767a7c60daa16"},
{file = "mkdocs_material-7.1.4-py2.py3-none-any.whl", hash = "sha256:1fb299005b787b62ef70631f7adf418e592c22a359e633fdd2ca5c24be6e451c"},
]
mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"},
@ -1722,12 +1728,12 @@ pyflakes = [
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pygments = [
{file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
]
pyhumps = [
{file = "pyhumps-1.6.1-py3-none-any.whl", hash = "sha256:58b367b73c57b64e32d211dc769addabd68ff6db07ce64b2e6565f7d5a12291f"},
{file = "pyhumps-1.6.1.tar.gz", hash = "sha256:01612603c5ad73a407299d806d30708a3935052276fdd93776953bccc0724e0a"},
{file = "pyhumps-3.0.2-py3-none-any.whl", hash = "sha256:367b1aadcaa64f8196a3cd14f56559a5602950aeb8486f49318e7394f5e18052"},
{file = "pyhumps-3.0.2.tar.gz", hash = "sha256:042b4b6eec6c1f862f8310c0eebbae19293e9edab8cafb030ff78c890ef1aa34"},
]
pylint = [
{file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"},
@ -1746,8 +1752,8 @@ pyrdfa3 = [
{file = "pyRdfa3-3.5.3.tar.gz", hash = "sha256:157663a92b87df345b6f69bde235dff5f797891608e12fe1e4fa8dad687131ae"},
]
pytest = [
{file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"},
{file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"},
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
]
pytest-cov = [
{file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
@ -1857,48 +1863,44 @@ scrape-schema-recipe = [
{file = "scrape_schema_recipe-0.1.3-py2.py3-none-any.whl", hash = "sha256:7a505d7cd94091ffdfcbac0fad21dd583cceee2d9c7ea12366e8fefac8b4da82"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
soupsieve = [
{file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"},
{file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"},
]
sqlalchemy = [
{file = "SQLAlchemy-1.4.12-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c71a80a5474e6e9c9bbf1957ab1c73cdece9d33cfb26d9ea6e7aed41f535cd6"},
{file = "SQLAlchemy-1.4.12-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b1d513ebb16a204c87296d774c2317950191583b34032540948f20096b63efe4"},
{file = "SQLAlchemy-1.4.12-cp27-cp27m-win32.whl", hash = "sha256:4b749cdedf1afb613c3d31235258110e1f36231c15df9b8b63b3f13c712e4790"},
{file = "SQLAlchemy-1.4.12-cp27-cp27m-win_amd64.whl", hash = "sha256:b58f09f4ea42a92e0a8923f4598001f8935bd2ed0c4c6abb9903c5b4cd0d4015"},
{file = "SQLAlchemy-1.4.12-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b4bf83b05056349265b40de37c836517649ea9edd174301072f5a58c7b374f94"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c94fe5ec27dec6a994293d1f194a97fcb904252526bbe72698229ec62c0f7281"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ac4a48e49e863a4d00d8a5ec94ff5540de1f5bcf96d8d54273a75c3278d8b4af"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e815a729b427bd997d681711dc0b22330e445a0a0c47e16b05d2038e814bd29f"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:aeb389136f3a39399ebb8e8ee17beba18d361cde9638059cfbf7e896354412b7"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0c839000817201310a51af390545d7b316fafd6969ef250dad0a6d28c025214d"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-win32.whl", hash = "sha256:1e8a884d766fcc918199576bf37f1870327582640fa3302489d7415d815be8a9"},
{file = "SQLAlchemy-1.4.12-cp36-cp36m-win_amd64.whl", hash = "sha256:e11ccaa08975e414df6a16466377bb11af692b2a62255c3a70c0993cb2d7f2d7"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:deef50c730ddfb4169417a3a3b6393f1e90b0d5c1e62e1d090c1eb1132529f3f"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a21f41c4cdb76d7f68a6986b9f5c56bdc8eafbc366893d1031df0c367e832388"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:aec20f0ec5788bee91ecf667e9e30e5ed0add9233b63b0e34e916b21eb5bc850"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d5da8fff36593ac96dd3d60a4eb9495a142fb6d3f0ed23baf5567c0ef7aa9b47"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:a4c9c947fc08d2ac48116c64b7dfbac22b9896619cb74923ba59876504ff6256"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-win32.whl", hash = "sha256:4c8c335b072967da27fef54fb53e74fadadd7d2167c5eb98f0bfb4bfeb3a6948"},
{file = "SQLAlchemy-1.4.12-cp37-cp37m-win_amd64.whl", hash = "sha256:01b610951c83452ee5e7d912c4ed9db4538b15d66e96ca6696ec38f0c5ce2908"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:6b77880e23d3758db7ad65732304ab1c3a42f0cd20505f4a211750862563a161"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f04acd3840bcf33f941b049e24aeef0be5145b2cd5489a89559c11be2d25e262"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:691568d8238c756011d97a655a76820715cbc0295b7d294aa2f1d62fb0be4361"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0646a4caab207279532ffd3f173b4756ae3863f3a94e369b7d1b82831a7ad433"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:2b35206c11c415448caf5b7abddbfac6acbe37f79832ae2d1be013f0dfe252ea"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-win32.whl", hash = "sha256:89e755688476b7a925554a1e8a756e0dd6124dfb8fac80470a90cd8424326bee"},
{file = "SQLAlchemy-1.4.12-cp38-cp38-win_amd64.whl", hash = "sha256:1bc9ea9e54bbaf65fece8b719f56472748f75777806f4f5fadd8112a165eab19"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1bdf65dc5263be4651aa34ebe07aa035c61421f145b0d43f4c0b1f3c33bec673"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f90a42db44427bf98128d823502e0af3f4b83f208e09a3d51df5c2cd7f2a76cf"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c9047989b8645d8830067dddb2bda544c625419b22b0f546660fd0bfe73341f6"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b7ed6ce2e32a68a3b417a848a409ed5b7e4c8e5fa8911b06c77a6be1cc767658"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:5ffbd23ac4324e64a100310cd2cab6534f972ecf26bf3652e6847187c2e9e72d"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-win32.whl", hash = "sha256:ac7db7276c0807db73b58984d630404ab294c4ca59cf16157fdc15894dec4507"},
{file = "SQLAlchemy-1.4.12-cp39-cp39-win_amd64.whl", hash = "sha256:ce5fc1099d194fbecc8d7c038c927d9daf75cbb83b3b314df3e43e308d67c33e"},
{file = "SQLAlchemy-1.4.12.tar.gz", hash = "sha256:968e8cf7f269eaeed1b753cb5df4112be998c933df39421229fc7726c413672c"},
{file = "SQLAlchemy-1.4.14-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:98d977df2854be582f83b1d02e32febdd72a93b6396417889448a0c0c20fd856"},
{file = "SQLAlchemy-1.4.14-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:32412b74f8920b91b1412a8574167bdfe5dd189fdc9b7676f898ea49892fabca"},
{file = "SQLAlchemy-1.4.14-cp27-cp27m-win32.whl", hash = "sha256:cf3e10181d4b96bafa693a03948b0da2136c977682a902b601b9fa0f35934cd0"},
{file = "SQLAlchemy-1.4.14-cp27-cp27m-win_amd64.whl", hash = "sha256:b8d6aab7f021ffb25d34671a48dc18007a12fad07c920be81401b6bf35ecf2d9"},
{file = "SQLAlchemy-1.4.14-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ab659472962815ea945e643166ab67299a1415c05d0fb6d9f9d915f9f4540cd9"},
{file = "SQLAlchemy-1.4.14-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b3e88738f82871a35a7c5995075100ddad74543e8697e683f655183860649024"},
{file = "SQLAlchemy-1.4.14-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d4c701fe3d17f2f842b32633573c6e9ec43b75fe0e1140b1c079d2df7d73e6"},
{file = "SQLAlchemy-1.4.14-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:907fc5a1db1bf9a3a26a205eba6529250ba5aced16f2bddb4a59763826294609"},
{file = "SQLAlchemy-1.4.14-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9934f1065bfce44dbd6c8dc68ce8f35492357d7cac5d83ddb585dbec5f3ff6ad"},
{file = "SQLAlchemy-1.4.14-cp36-cp36m-win32.whl", hash = "sha256:10176150fb4f7bef87e58b496e11ec8c09a06d7aaddec858aa3afe68ecf44be1"},
{file = "SQLAlchemy-1.4.14-cp36-cp36m-win_amd64.whl", hash = "sha256:1508b5a58046ca9ec4c4d2a3f1dcd6bc2eb1b04e0e5ef7b85fe73fc1805b504a"},
{file = "SQLAlchemy-1.4.14-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dff5c33036a493c00e252a7067ecaa09ef5bbbeea53337083aba013be903c43a"},
{file = "SQLAlchemy-1.4.14-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:238545b8e769c7415395ea2d3716ee116511c2816ae75e69f4148e56bc16c978"},
{file = "SQLAlchemy-1.4.14-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa861a9211d284430a6669c3debf9cfac7cd738e1c50844575751900ebe16bf"},
{file = "SQLAlchemy-1.4.14-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6c40fce57d12d5b4d99d091b6c33c8a8087cb753402218fd7a2b3be72de7e20"},
{file = "SQLAlchemy-1.4.14-cp37-cp37m-win32.whl", hash = "sha256:5129af3c0cd879e37d3f7db417f7c6500c992b6e5d1cae42ba05e6dac97e3e60"},
{file = "SQLAlchemy-1.4.14-cp37-cp37m-win_amd64.whl", hash = "sha256:e0b75b7712f25d8d51a793cbbffb26688d2f655b97c9dfad02aad67b09d121a6"},
{file = "SQLAlchemy-1.4.14-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:bcd85d7a71e2de3567c5c4b26b1a90c57432b3032ff9132dafae1c52a4edeea0"},
{file = "SQLAlchemy-1.4.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c115aaadbb555c787a14218d94185a0929d4a625e6f150fe86a9f7fca0f5d30"},
{file = "SQLAlchemy-1.4.14-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2048c3f7934c589b2a8521da9a3c8f77b413a8e21b015be96355c6968028351e"},
{file = "SQLAlchemy-1.4.14-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e1570f54585247526e05cb642c46ee9be2eabe7f04945665c0a36b86bf1ee6"},
{file = "SQLAlchemy-1.4.14-cp38-cp38-win32.whl", hash = "sha256:6fd1bd8f4c134aa3a2fd43abe0f83245a3537001ea3c84dc0e28768a40a1a53c"},
{file = "SQLAlchemy-1.4.14-cp38-cp38-win_amd64.whl", hash = "sha256:38fce422c8553b14145e4d5ada3cc7643bb720d2761976ab03177b65dc14fb4d"},
{file = "SQLAlchemy-1.4.14-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:51b5f73e4a38d07bd883aec6c4ce3292309bdf9e9ce2b21653b92c7ce9bc6f5a"},
{file = "SQLAlchemy-1.4.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd718612457ac34f6f19ff9b00c80c68ec0f46dae1334671c653a0dc960df9ce"},
{file = "SQLAlchemy-1.4.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a365e616462e9d4409e87226a9da55d2cf2aa1b17c53208519ba3f8582edd529"},
{file = "SQLAlchemy-1.4.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79957deee6bb1e546dee806c66d3effeac3b2e33233663011550dbfe60cbfecd"},
{file = "SQLAlchemy-1.4.14-cp39-cp39-win32.whl", hash = "sha256:a2c6e766410eebb0e7c4a839acce5534619f5d03bd2aabf44e0caf8b813613b9"},
{file = "SQLAlchemy-1.4.14-cp39-cp39-win_amd64.whl", hash = "sha256:296a3b53e50c3d61ea22969b9d57937714e62f4bd7b5db7bb0afe80e8fbbe44c"},
{file = "SQLAlchemy-1.4.14.tar.gz", hash = "sha256:fcea0af70a356fdff9efa917d33ec5a8538fe7bde3004ceded7377f1a97d8ef3"},
]
starlette = [
{file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"},

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "mealie"
version = "0.5.0"
version = "0.5.0b"
description = "A Recipe Manager"
authors = ["Hayden <hay-kot@pm.me>"]
license = "MIT"

View file

@ -2,25 +2,32 @@ class AppRoutes:
def __init__(self) -> None:
self.prefix = "/api"
self.users_sign_ups = "/api/users/sign-ups"
self.auth_token = "/api/auth/token"
self.auth_token_long = "/api/auth/token/long"
self.auth_refresh = "/api/auth/refresh"
self.users_sign_ups = "/api/users/sign-ups"
self.users = "/api/users"
self.users_self = "/api/users/self"
self.users_api_tokens = "/api/users/api-tokens"
self.groups = "/api/groups"
self.groups_self = "/api/groups/self"
self.recipes = "/api/recipes"
self.recipes_summary = "/api/recipes/summary"
self.recipes_summary_untagged = "/api/recipes/summary/untagged"
self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized"
self.recipes_category = "/api/recipes/category"
self.recipes_tag = "/api/recipes/tag"
self.categories = "/api/categories"
self.recipes_tags = "/api/recipes/tags/"
self.recipes_create = "/api/recipes/create"
self.recipes_create_url = "/api/recipes/create-url"
self.categories = "/api/categories"
self.categories_empty = "/api/categories/empty"
self.tags = "/api/tags"
self.tags_empty = "/api/tags/empty"
self.about_events = "/api/about/events"
self.meal_plans_all = "/api/meal-plans/all"
self.meal_plans_create = "/api/meal-plans/create"
self.meal_plans_this_week = "/api/meal-plans/this-week"
self.meal_plans_today = "/api/meal-plans/today"
self.meal_plans_today_image = "/api/meal-plans/today/image"
self.site_settings_custom_pages = "/api/site-settings/custom-pages"
self.site_settings = "/api/site-settings"
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
@ -30,8 +37,12 @@ class AppRoutes:
self.backups_export_database = "/api/backups/export/database"
self.backups_upload = "/api/backups/upload"
self.migrations = "/api/migrations"
self.debug = "/api/debug"
self.debug_statistics = "/api/debug/statistics"
self.debug_version = "/api/debug/version"
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
self.debug_log = "/api/debug/log"
self.utils_download = "/api/utils/download"
def users_sign_ups_token(self, token):
return f"{self.prefix}/users/sign-ups/{token}"
@ -48,21 +59,36 @@ class AppRoutes:
def users_id_password(self, id):
return f"{self.prefix}/users/{id}/password"
def users_api_tokens_token_id(self, token_id):
return f"{self.prefix}/users/api-tokens/{token_id}"
def groups_id(self, id):
return f"{self.prefix}/groups/{id}"
def categories_category(self, category):
return f"{self.prefix}/categories/{category}"
def recipes_tags_tag(self, tag):
return f"{self.prefix}/recipes/tags/{tag}"
def recipes_recipe_slug(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}"
def recipes_recipe_slug_image(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}/image"
def recipes_recipe_slug_assets(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}/assets"
def categories_category(self, category):
return f"{self.prefix}/categories/{category}"
def tags_tag(self, tag):
return f"{self.prefix}/tags/{tag}"
def media_recipes_recipe_slug_images_file_name(self, recipe_slug, file_name):
return f"{self.prefix}/media/recipes/{recipe_slug}/images/{file_name}"
def media_recipes_recipe_slug_assets_file_name(self, recipe_slug, file_name):
return f"{self.prefix}/media/recipes/{recipe_slug}/assets/{file_name}"
def about_events_id(self, id):
return f"{self.prefix}/about/events/{id}"
def meal_plans_plan_id(self, plan_id):
return f"{self.prefix}/meal-plans/{plan_id}"
@ -72,8 +98,8 @@ class AppRoutes:
def site_settings_custom_pages_id(self, id):
return f"{self.prefix}/site-settings/custom-pages/{id}"
def themes_theme_name(self, theme_name):
return f"{self.prefix}/themes/{theme_name}"
def themes_id(self, id):
return f"{self.prefix}/themes/{id}"
def backups_file_name_download(self, file_name):
return f"{self.prefix}/backups/{file_name}/download"
@ -84,14 +110,14 @@ class AppRoutes:
def backups_file_name_delete(self, file_name):
return f"{self.prefix}/backups/{file_name}/delete"
def migrations_source_file_name_import(self, source, file_name):
return f"{self.prefix}/migrations/{source}/{file_name}/import"
def migrations_import_type_file_name_import(self, import_type, file_name):
return f"{self.prefix}/migrations/{import_type}/{file_name}/import"
def migrations_source_file_name_delete(self, source, file_name):
return f"{self.prefix}/migrations/{source}/{file_name}/delete"
def migrations_import_type_file_name_delete(self, import_type, file_name):
return f"{self.prefix}/migrations/{import_type}/{file_name}/delete"
def migrations_source_upload(self, source):
return f"{self.prefix}/migrations/{source}/upload"
def migrations_import_type_upload(self, import_type):
return f"{self.prefix}/migrations/{import_type}/upload"
def debug_log_num(self, num):
return f"{self.prefix}/debug/log/{num}"

View file

@ -0,0 +1,32 @@
import json
from fastapi.testclient import TestClient
from pytest import fixture
from tests.app_routes import AppRoutes
@fixture
def long_live_token(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test Fixture Token"}, headers=token)
assert response.status_code == 201
return {"Authorization": f"Bearer {json.loads(response.text).get('token')}"}
def test_api_token_creation(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.post(api_routes.users_api_tokens, json={"name": "Test API Token"}, headers=token)
assert response.status_code == 201
def test_use_token(api_client: TestClient, api_routes: AppRoutes, long_live_token):
response = api_client.get(api_routes.users, headers=long_live_token)
assert response.status_code == 200
def test_delete_token(api_client: TestClient, api_routes: AppRoutes, token):
response = api_client.delete(api_routes.users_api_tokens_token_id(1), headers=token)
assert response.status_code == 200
response = api_client.delete(api_routes.users_api_tokens_token_id(2), headers=token)
assert response.status_code == 200

View file

@ -23,7 +23,7 @@ def chowdown_zip():
def test_upload_chowdown_zip(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token):
upload_url = api_routes.migrations_source_upload("chowdown")
upload_url = api_routes.migrations_import_type_upload("chowdown")
response = api_client.post(upload_url, files={"archive": chowdown_zip.open("rb")}, headers=token)
assert response.status_code == 200
@ -36,7 +36,7 @@ def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes
api_client.delete(delete_url, headers=token) # TODO: Manage Test Data better
selection = chowdown_zip.name
import_url = api_routes.migrations_source_file_name_import("chowdown", selection)
import_url = api_routes.migrations_import_type_file_name_import("chowdown", selection)
response = api_client.post(import_url, headers=token)
assert response.status_code == 200
@ -49,7 +49,7 @@ def test_import_chowdown_directory(api_client: TestClient, api_routes: AppRoutes
def test_delete_chowdown_migration_data(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token):
selection = chowdown_zip.name
delete_url = api_routes.migrations_source_file_name_delete("chowdown", selection)
delete_url = api_routes.migrations_import_type_file_name_delete("chowdown", selection)
response = api_client.delete(delete_url, headers=token)
assert response.status_code == 200
@ -71,7 +71,7 @@ def nextcloud_zip():
def test_upload_nextcloud_zip(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token):
upload_url = api_routes.migrations_source_upload("nextcloud")
upload_url = api_routes.migrations_import_type_upload("nextcloud")
response = api_client.post(upload_url, files={"archive": nextcloud_zip.open("rb")}, headers=token)
assert response.status_code == 200
@ -81,7 +81,7 @@ def test_upload_nextcloud_zip(api_client: TestClient, api_routes: AppRoutes, nex
def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token):
selection = nextcloud_zip.name
import_url = api_routes.migrations_source_file_name_import("nextcloud", selection)
import_url = api_routes.migrations_import_type_file_name_import("nextcloud", selection)
response = api_client.post(import_url, headers=token)
assert response.status_code == 200
@ -93,7 +93,7 @@ def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoute
def test_delete__nextcloud_migration_data(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip: Path, token):
selection = nextcloud_zip.name
delete_url = api_routes.migrations_source_file_name_delete("nextcloud", selection)
delete_url = api_routes.migrations_import_type_file_name_delete("nextcloud", selection)
response = api_client.delete(delete_url, headers=token)
assert response.status_code == 200

View file

@ -55,7 +55,7 @@ def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme):
response = api_client.get(api_routes.themes_theme_name(1))
response = api_client.get(api_routes.themes_id(1))
assert response.status_code == 200
assert json.loads(response.content) == default_theme
@ -65,7 +65,7 @@ def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme,
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
assert response.status_code == 201
response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")), headers=token)
response = api_client.get(api_routes.themes_id(new_theme.get("id")), headers=token)
assert response.status_code == 200
assert json.loads(response.content) == new_theme
@ -80,7 +80,7 @@ def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
for theme in [default_theme, new_theme]:
response = api_client.get(api_routes.themes_theme_name(theme.get("id")))
response = api_client.get(api_routes.themes_id(theme.get("id")))
assert response.status_code == 200
assert json.loads(response.content) == theme
@ -97,14 +97,14 @@ def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, defa
}
new_theme["colors"] = theme_colors
response = api_client.put(api_routes.themes_theme_name(new_theme.get("id")), json=new_theme, headers=token)
response = api_client.put(api_routes.themes_id(new_theme.get("id")), json=new_theme, headers=token)
assert response.status_code == 200
response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")))
response = api_client.get(api_routes.themes_id(new_theme.get("id")))
assert json.loads(response.content) == new_theme
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
for theme in [default_theme, new_theme]:
response = api_client.delete(api_routes.themes_theme_name(theme.get("id")), headers=token)
response = api_client.delete(api_routes.themes_id(theme.get("id")), headers=token)
assert response.status_code == 200

View file

@ -10,12 +10,12 @@ from tests.app_routes import AppRoutes
@fixture(scope="session")
def default_user():
return UserOut(id=1, fullName="Change Me", email="changeme@email.com", group="Home", admin=True)
return UserOut(id=1, fullName="Change Me", email="changeme@email.com", group="Home", admin=True, tokens=[])
@fixture(scope="session")
def new_user():
return UserOut(id=3, fullName="My New User", email="newuser@email.com", group="Home", admin=False)
return UserOut(id=3, fullName="My New User", email="newuser@email.com", group="Home", admin=False, tokens=[])
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, token):
@ -45,6 +45,7 @@ def test_create_user(api_client: TestClient, api_routes: AppRoutes, token, new_u
"password": "MyStrongPassword",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=token)