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:
parent
f4384167f6
commit
95ec13161f
41 changed files with 977 additions and 449 deletions
|
@ -1,12 +1,10 @@
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import slugify
|
import slugify
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from mealie.app import app
|
from mealie.app import app
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
OUT_FILE = CWD.joinpath("output", "app_routes.py")
|
OUT_FILE = CWD.joinpath("output", "app_routes.py")
|
||||||
|
|
|
@ -2,25 +2,32 @@ class AppRoutes:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.prefix = "/api"
|
self.prefix = "/api"
|
||||||
|
|
||||||
self.users_sign_ups = "/api/users/sign-ups"
|
|
||||||
self.auth_token = "/api/auth/token"
|
self.auth_token = "/api/auth/token"
|
||||||
self.auth_token_long = "/api/auth/token/long"
|
self.auth_token_long = "/api/auth/token/long"
|
||||||
self.auth_refresh = "/api/auth/refresh"
|
self.auth_refresh = "/api/auth/refresh"
|
||||||
|
self.users_sign_ups = "/api/users/sign-ups"
|
||||||
self.users = "/api/users"
|
self.users = "/api/users"
|
||||||
self.users_self = "/api/users/self"
|
self.users_self = "/api/users/self"
|
||||||
|
self.users_api_tokens = "/api/users-tokens"
|
||||||
self.groups = "/api/groups"
|
self.groups = "/api/groups"
|
||||||
self.groups_self = "/api/groups/self"
|
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_category = "/api/recipes/category"
|
||||||
self.recipes_tag = "/api/recipes/tag"
|
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 = "/api/recipes/create"
|
||||||
self.recipes_create_url = "/api/recipes/create-url"
|
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_all = "/api/meal-plans/all"
|
||||||
self.meal_plans_create = "/api/meal-plans/create"
|
self.meal_plans_create = "/api/meal-plans/create"
|
||||||
self.meal_plans_this_week = "/api/meal-plans/this-week"
|
self.meal_plans_this_week = "/api/meal-plans/this-week"
|
||||||
self.meal_plans_today = "/api/meal-plans/today"
|
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_custom_pages = "/api/site-settings/custom-pages"
|
||||||
self.site_settings = "/api/site-settings"
|
self.site_settings = "/api/site-settings"
|
||||||
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
|
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_export_database = "/api/backups/export/database"
|
||||||
self.backups_upload = "/api/backups/upload"
|
self.backups_upload = "/api/backups/upload"
|
||||||
self.migrations = "/api/migrations"
|
self.migrations = "/api/migrations"
|
||||||
|
self.debug = "/api/debug"
|
||||||
|
self.debug_statistics = "/api/debug/statistics"
|
||||||
self.debug_version = "/api/debug/version"
|
self.debug_version = "/api/debug/version"
|
||||||
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
|
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):
|
def users_sign_ups_token(self, token):
|
||||||
return f"{self.prefix}/users/sign-ups/{token}"
|
return f"{self.prefix}/users/sign-ups/{token}"
|
||||||
|
@ -48,21 +59,36 @@ class AppRoutes:
|
||||||
def users_id_password(self, id):
|
def users_id_password(self, id):
|
||||||
return f"{self.prefix}/users/{id}/password"
|
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):
|
def groups_id(self, id):
|
||||||
return f"{self.prefix}/groups/{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):
|
def recipes_recipe_slug(self, recipe_slug):
|
||||||
return f"{self.prefix}/recipes/{recipe_slug}"
|
return f"{self.prefix}/recipes/{recipe_slug}"
|
||||||
|
|
||||||
def recipes_recipe_slug_image(self, recipe_slug):
|
def recipes_recipe_slug_image(self, recipe_slug):
|
||||||
return f"{self.prefix}/recipes/{recipe_slug}/image"
|
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):
|
def meal_plans_plan_id(self, plan_id):
|
||||||
return f"{self.prefix}/meal-plans/{plan_id}"
|
return f"{self.prefix}/meal-plans/{plan_id}"
|
||||||
|
|
||||||
|
@ -72,8 +98,8 @@ class AppRoutes:
|
||||||
def site_settings_custom_pages_id(self, id):
|
def site_settings_custom_pages_id(self, id):
|
||||||
return f"{self.prefix}/site-settings/custom-pages/{id}"
|
return f"{self.prefix}/site-settings/custom-pages/{id}"
|
||||||
|
|
||||||
def themes_theme_name(self, theme_name):
|
def themes_id(self, id):
|
||||||
return f"{self.prefix}/themes/{theme_name}"
|
return f"{self.prefix}/themes/{id}"
|
||||||
|
|
||||||
def backups_file_name_download(self, file_name):
|
def backups_file_name_download(self, file_name):
|
||||||
return f"{self.prefix}/backups/{file_name}/download"
|
return f"{self.prefix}/backups/{file_name}/download"
|
||||||
|
@ -84,14 +110,14 @@ class AppRoutes:
|
||||||
def backups_file_name_delete(self, file_name):
|
def backups_file_name_delete(self, file_name):
|
||||||
return f"{self.prefix}/backups/{file_name}/delete"
|
return f"{self.prefix}/backups/{file_name}/delete"
|
||||||
|
|
||||||
def migrations_type_file_name_import(self, type, file_name):
|
def migrations_import_type_file_name_import(self, import_type, file_name):
|
||||||
return f"{self.prefix}/migrations/{type}/{file_name}/import"
|
return f"{self.prefix}/migrations/{import_type}/{file_name}/import"
|
||||||
|
|
||||||
def migrations_type_file_name_delete(self, type, file_name):
|
def migrations_import_type_file_name_delete(self, import_type, file_name):
|
||||||
return f"{self.prefix}/migrations/{type}/{file_name}/delete"
|
return f"{self.prefix}/migrations/{import_type}/{file_name}/delete"
|
||||||
|
|
||||||
def migrations_type_upload(self, type):
|
def migrations_import_type_upload(self, import_type):
|
||||||
return f"{self.prefix}/migrations/{type}/upload"
|
return f"{self.prefix}/migrations/{import_type}/upload"
|
||||||
|
|
||||||
def debug_log_num(self, num):
|
def debug_log_num(self, num):
|
||||||
return f"{self.prefix}/debug/log/{num}"
|
return f"{self.prefix}/debug/log/{num}"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# v0.5.0 COOL TITLE GOES HERE
|
# v0.5.0 Too Many Changes!
|
||||||
|
|
||||||
**App Version: v0.5.0**
|
**App Version: v0.5.0**
|
||||||
|
|
||||||
|
@ -9,11 +9,14 @@
|
||||||
!!! error "Breaking Changes"
|
!!! error "Breaking Changes"
|
||||||
|
|
||||||
#### Database
|
#### 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
|
#### 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.
|
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
|
## Bug Fixes
|
||||||
- Fixed #332 - Language settings are saved for one browser
|
- Fixed #332 - Language settings are saved for one browser
|
||||||
- Fixes #281 - Slow Handling of Large Sets of Recipes
|
- Fixes #281 - Slow Handling of Large Sets of Recipes
|
||||||
|
@ -24,23 +27,31 @@
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
- Beta Support for Postgres! 🎉 See the getting started page for details
|
- 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!
|
- New Toolbox Page!
|
||||||
- Bulk assign categories and tags by keyword search
|
- Bulk assign categories and tags by keyword search
|
||||||
- Title case all Categories or Tags with 1 click
|
- Title case all Categories or Tags with 1 click
|
||||||
- Create/Rename/Delete Operations for Tags/Categories
|
- Create/Rename/Delete Operations for Tags/Categories
|
||||||
- Remove Unused Categories or Tags with 1 click
|
- Remove Unused Categories or Tags with 1 click
|
||||||
- Recipe Cards now have a menu button for quick actions!
|
- Recipe Cards now have a menu button for quick actions!
|
||||||
- Edit
|
- Edit
|
||||||
- Delete
|
- Delete
|
||||||
- Download (As Json)
|
- Download (As Json)
|
||||||
- Copy Link
|
- 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! 🎉
|
- New Admin Dashboard! 🎉
|
||||||
- Now you can get some insight on your application with application statics and events.
|
- Now you can get some insight on your application with application statics and events.
|
||||||
- See uncategorized/untagged recipes and organize them!
|
- See uncategorized/untagged recipes and organize them!
|
||||||
- Backup/Restore right from your dashboard
|
- Backup/Restore right from your dashboard
|
||||||
- See server side events. Now you can know who deleted your favorite recipe!
|
- See server side events. Now you can know who deleted your favorite recipe!
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
- Images are now served up by the Caddy increase performance and offloading some loads from the API server
|
- Images are now served up by the Caddy increase performance and offloading some loads from the API server
|
||||||
|
@ -48,6 +59,16 @@
|
||||||
- All images are now converted to .webp for better compression
|
- All images are now converted to .webp for better compression
|
||||||
|
|
||||||
### General
|
### 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.
|
- Updated recipe editor styles and moved notes to below the steps.
|
||||||
- Redesigned search bar
|
- Redesigned search bar
|
||||||
- 'Dinner this week' shows a warning when no meal is planned yet
|
- 'Dinner this week' shows a warning when no meal is planned yet
|
||||||
|
@ -55,11 +76,11 @@
|
||||||
- More localization
|
- More localization
|
||||||
- Start date for Week is now selectable
|
- Start date for Week is now selectable
|
||||||
- Languages are now managed through Crowdin
|
- 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.
|
- Sidebar can now be toggled everywhere.
|
||||||
- New and improved mobile friendly bottom bar
|
- New and improved mobile friendly bottom bar
|
||||||
- Improved styling for search bar in desktop
|
- Improved styling for search bar in desktop
|
||||||
- Improved search layout on mobile
|
- Improved search layout on mobile
|
||||||
- Profile image now shown on all sidebars
|
- Profile image now shown on all sidebars
|
||||||
|
|
||||||
### Behind the Scenes
|
### Behind the Scenes
|
||||||
|
@ -69,4 +90,4 @@
|
||||||
- Refactor UI components to fit Vue best practices (WIP)
|
- Refactor UI components to fit Vue best practices (WIP)
|
||||||
- The API returns more consistent status codes
|
- The API returns more consistent status codes
|
||||||
- The API returns error code instead of error text when appropriate
|
- The API returns error code instead of error text when appropriate
|
||||||
- ⚠️ May cause side-effects if you were directly consuming the API
|
- ⚠️ May cause side-effects if you were directly consuming the API
|
|
@ -1,5 +1,19 @@
|
||||||
# Organizing Recipes
|
# 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
|
!!! 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! 👍
|
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
|
@ -16,17 +16,10 @@ const usersURLs = {
|
||||||
userID: id => `${userPrefix}/${id}`,
|
userID: id => `${userPrefix}/${id}`,
|
||||||
password: id => `${userPrefix}/${id}/password`,
|
password: id => `${userPrefix}/${id}/password`,
|
||||||
resetPassword: id => `${userPrefix}/${id}/reset-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 = {
|
export const userAPI = {
|
||||||
async login(formData) {
|
async login(formData) {
|
||||||
let response = await apiReq.post(authURLs.token, formData, null, function() {
|
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")
|
() => 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");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -100,7 +100,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
flat() {
|
flat() {
|
||||||
return this.selected.length > 0 && this.solo;
|
if (this.selected) {
|
||||||
|
return this.selected.length > 0 && this.solo;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -7,26 +7,50 @@
|
||||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn text @click="navigateRandom">
|
<v-btn text @click="navigateRandom">
|
||||||
Random
|
<v-icon left>
|
||||||
|
mdi-dice-multiple
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("general.random") }}
|
||||||
</v-btn>
|
</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 }">
|
<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") }}
|
{{ $t("general.sort") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item @click="sortRecipes(EVENTS.az)">
|
<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-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item @click="sortRecipes(EVENTS.rating)">
|
<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-title>{{ $t("general.rating") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</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-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-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item @click="sortRecipes(EVENTS.created)">
|
<v-list-item @click="sortRecipes(EVENTS.shuffle)">
|
||||||
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
|
<v-icon left>
|
||||||
|
mdi-shuffle-variant
|
||||||
|
</v-icon>
|
||||||
|
<v-list-item-title>{{ $t("general.shuffle") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
@ -114,6 +138,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
sortLoading: false,
|
||||||
cardLimit: 30,
|
cardLimit: 30,
|
||||||
loading: false,
|
loading: false,
|
||||||
EVENTS: {
|
EVENTS: {
|
||||||
|
@ -121,6 +146,7 @@ export default {
|
||||||
rating: "rating",
|
rating: "rating",
|
||||||
created: "created",
|
created: "created",
|
||||||
updated: "updated",
|
updated: "updated",
|
||||||
|
shuffle: "shuffle",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -165,6 +191,7 @@ export default {
|
||||||
this.$router.push(`/recipe/${recipe.slug}`);
|
this.$router.push(`/recipe/${recipe.slug}`);
|
||||||
},
|
},
|
||||||
sortRecipes(sortType) {
|
sortRecipes(sortType) {
|
||||||
|
this.sortLoading = true;
|
||||||
let sortTarget = [...this.recipes];
|
let sortTarget = [...this.recipes];
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case this.EVENTS.az:
|
case this.EVENTS.az:
|
||||||
|
@ -179,11 +206,16 @@ export default {
|
||||||
case this.EVENTS.updated:
|
case this.EVENTS.updated:
|
||||||
utils.recipe.sortByUpdated(sortTarget);
|
utils.recipe.sortByUpdated(sortTarget);
|
||||||
break;
|
break;
|
||||||
|
case this.EVENTS.shuffle:
|
||||||
|
utils.recipe.shuffle(sortTarget);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown Event", sortType);
|
console.log("Unknown Event", sortType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit(SORT_EVENT, sortTarget);
|
this.$emit(SORT_EVENT, sortTarget);
|
||||||
|
this.sortLoading = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<v-navigation-drawer v-model="showSidebar" width="180px" clipped app>
|
<v-navigation-drawer v-model="showSidebar" width="180px" clipped app>
|
||||||
<template v-slot:prepend>
|
<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">
|
<v-list-item-avatar color="accent" class="white--text">
|
||||||
<img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" />
|
<img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -133,11 +133,6 @@ export default {
|
||||||
to: "/admin/profile",
|
to: "/admin/profile",
|
||||||
title: this.$t("settings.profile"),
|
title: this.$t("settings.profile"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: "mdi-food",
|
|
||||||
to: "/admin/meal-planner",
|
|
||||||
title: this.$t("meal-plan.meal-planner"),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
adminLinks() {
|
adminLinks() {
|
||||||
|
|
|
@ -74,11 +74,12 @@
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"sort-alphabetically": "A-Z",
|
"sort-alphabetically": "Alphabetical",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"success-count": "Success: {count}",
|
"success-count": "Success: {count}",
|
||||||
"sunday": "Sunday",
|
"sunday": "Sunday",
|
||||||
|
"shuffle": "Shuffle",
|
||||||
"templates": "Templates:",
|
"templates": "Templates:",
|
||||||
"themes": "Themes",
|
"themes": "Themes",
|
||||||
"thursday": "Thursday",
|
"thursday": "Thursday",
|
||||||
|
|
|
@ -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>
|
|
147
frontend/src/pages/Admin/Profile/APITokenCard.vue
Normal file
147
frontend/src/pages/Admin/Profile/APITokenCard.vue
Normal 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>
|
214
frontend/src/pages/Admin/Profile/ProfileGroupCard.vue
Normal file
214
frontend/src/pages/Admin/Profile/ProfileGroupCard.vue
Normal 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>
|
|
@ -35,9 +35,11 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-btn-toggle>
|
</v-btn-toggle>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:bottom>
|
<template v-slot:bottom>
|
||||||
<v-virtual-scroll height="290" item-height="70" :items="availableThemes" class="mt-2">
|
<v-virtual-scroll height="290" item-height="70" :items="availableThemes" class="mt-2">
|
||||||
<template v-slot:default="{ item }">
|
<template v-slot:default="{ item }">
|
||||||
|
<v-divider></v-divider>
|
||||||
<v-list-item @click="selectedTheme = item">
|
<v-list-item @click="selectedTheme = item">
|
||||||
<v-list-item-avatar>
|
<v-list-item-avatar>
|
||||||
<v-icon large dark :color="item.colors.primary">
|
<v-icon large dark :color="item.colors.primary">
|
||||||
|
@ -66,6 +68,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-divider></v-divider>
|
||||||
</template>
|
</template>
|
||||||
</v-virtual-scroll>
|
</v-virtual-scroll>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
@ -116,7 +119,7 @@ export default {
|
||||||
components: { StatCard, BaseDialog, ColorPickerDialog, VJsoneditor },
|
components: { StatCard, BaseDialog, ColorPickerDialog, VJsoneditor },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jsonEditor: true,
|
jsonEditor: false,
|
||||||
jsonEditorOptions: {
|
jsonEditorOptions: {
|
||||||
mode: "code",
|
mode: "code",
|
||||||
search: false,
|
search: false,
|
||||||
|
@ -193,7 +196,6 @@ export default {
|
||||||
this.availableThemes = await api.themes.requestAll();
|
this.availableThemes = await api.themes.requestAll();
|
||||||
},
|
},
|
||||||
editTheme(theme) {
|
editTheme(theme) {
|
||||||
console.log(theme);
|
|
||||||
this.defaultData = theme;
|
this.defaultData = theme;
|
||||||
this.newTheme = false;
|
this.newTheme = false;
|
||||||
this.$refs.themeDialog.open();
|
this.$refs.themeDialog.open();
|
||||||
|
@ -201,11 +203,9 @@ export default {
|
||||||
createTheme() {
|
createTheme() {
|
||||||
this.newTheme = true;
|
this.newTheme = true;
|
||||||
this.$refs.themeDialog.open();
|
this.$refs.themeDialog.open();
|
||||||
console.log("Create Theme");
|
|
||||||
},
|
},
|
||||||
async processSubmit() {
|
async processSubmit() {
|
||||||
if (this.newTheme) {
|
if (this.newTheme) {
|
||||||
console.log("New Theme");
|
|
||||||
await api.themes.create(this.defaultData);
|
await api.themes.create(this.defaultData);
|
||||||
} else {
|
} else {
|
||||||
await api.themes.update(this.defaultData);
|
await api.themes.update(this.defaultData);
|
||||||
|
@ -213,7 +213,6 @@ export default {
|
||||||
this.getAllThemes();
|
this.getAllThemes();
|
||||||
},
|
},
|
||||||
async deleteTheme() {
|
async deleteTheme() {
|
||||||
console.log(this.defaultData);
|
|
||||||
await api.themes.delete(this.defaultData.id);
|
await api.themes.delete(this.defaultData.id);
|
||||||
this.getAllThemes();
|
this.getAllThemes();
|
||||||
},
|
},
|
|
@ -3,25 +3,31 @@
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" sm="12" lg="6">
|
<v-col cols="12" sm="12" lg="6">
|
||||||
<UserCard />
|
<UserCard />
|
||||||
|
<ProfileThemeCard class="mt-10" />
|
||||||
|
<APITokenCard class="mt-10" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="12" lg="6">
|
||||||
|
<ProfileGroupCard />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" lg="6"> </v-col>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row class="mt-7">
|
<v-row class="mt-7">
|
||||||
<v-col cols="12" sm="12" lg="6">
|
<v-col cols="12" sm="12" lg="6"> </v-col>
|
||||||
<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>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ThemeCard from "./ThemeCard";
|
import ProfileThemeCard from "./ProfileThemeCard";
|
||||||
|
import ProfileGroupCard from "./ProfileGroupCard";
|
||||||
|
import APITokenCard from "./APITokenCard";
|
||||||
import UserCard from "./UserCard";
|
import UserCard from "./UserCard";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
UserCard,
|
UserCard,
|
||||||
ThemeCard,
|
ProfileThemeCard,
|
||||||
|
ProfileGroupCard,
|
||||||
|
APITokenCard,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<CardSection
|
<CardSection :sortable="true" :title="title" :recipes="shownRecipes" @sort="assignSorted" />
|
||||||
:sortable="true"
|
|
||||||
:title="title"
|
|
||||||
:recipes="recipes"
|
|
||||||
:card-limit="9999"
|
|
||||||
@sort="sortAZ"
|
|
||||||
@sort-recent="sortRecent"
|
|
||||||
/>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,20 +15,30 @@ export default {
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
recipes: [],
|
recipes: [],
|
||||||
|
sortedResults: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentCategory() {
|
currentCategory() {
|
||||||
return this.$route.params.category;
|
return this.$route.params.category;
|
||||||
},
|
},
|
||||||
|
shownRecipes() {
|
||||||
|
if (this.sortedResults.length > 0) {
|
||||||
|
return this.sortedResults;
|
||||||
|
} else {
|
||||||
|
return this.recipes;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async currentCategory() {
|
async currentCategory() {
|
||||||
|
this.sortedResults = [];
|
||||||
this.getRecipes();
|
this.getRecipes();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getRecipes();
|
this.getRecipes();
|
||||||
|
this.sortedResults = [];
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getRecipes() {
|
async getRecipes() {
|
||||||
|
@ -43,11 +46,8 @@ export default {
|
||||||
this.title = data.name;
|
this.title = data.name;
|
||||||
this.recipes = data.recipes;
|
this.recipes = data.recipes;
|
||||||
},
|
},
|
||||||
sortAZ() {
|
assignSorted(val) {
|
||||||
this.recipes.sort((a, b) => (a.name > b.name ? 1 : -1));
|
this.sortedResults = val.slice();
|
||||||
},
|
|
||||||
sortRecent() {
|
|
||||||
this.recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-card flat height="100%">
|
<v-card flat height="100%">
|
||||||
<v-app-bar flat>
|
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
||||||
<v-spacer></v-spacer>
|
<v-icon large left>
|
||||||
<v-card-title class="text-center justify-center py-3 ">
|
mdi-tag-multiple-outline
|
||||||
{{ title.toUpperCase() }}
|
</v-icon>
|
||||||
</v-card-title>
|
<v-toolbar-title class="headline"> {{ page.name }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<div v-if="render">
|
<div v-if="render">
|
||||||
<v-tabs v-model="tab" background-color="transparent" grow>
|
<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 }}
|
{{ item.name }}
|
||||||
</v-tab>
|
</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<v-tabs-items v-model="tab">
|
<v-tabs-items v-model="tab">
|
||||||
<v-tab-item v-for="(item, index) in categories" :key="item.slug + index">
|
<v-tab-item v-for="(item, index) in page.categories" :key="item.slug + index" :value="item.slug">
|
||||||
<CardSection class="mb-5 mx-1" :recipes="filterRecipe(item.slug)" />
|
<CardSection class="mb-5 mx-1" :recipes="item.recipes" @sort="sortRecipes($event, index)" />
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
</v-tabs-items>
|
</v-tabs-items>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,8 +35,8 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
page: "",
|
||||||
title: "",
|
title: "",
|
||||||
tab: null,
|
|
||||||
render: false,
|
render: false,
|
||||||
recipeStore: [],
|
recipeStore: [],
|
||||||
categories: [],
|
categories: [],
|
||||||
|
@ -47,6 +46,14 @@ export default {
|
||||||
pageSlug() {
|
pageSlug() {
|
||||||
return this.$route.params.customPage;
|
return this.$route.params.customPage;
|
||||||
},
|
},
|
||||||
|
tab: {
|
||||||
|
set(tab) {
|
||||||
|
this.$router.replace({ query: { ...this.$route.query, tab } });
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return this.$route.query.tab;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -61,27 +68,15 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async buildPage() {
|
async buildPage() {
|
||||||
const page = await api.siteSettings.getPage(this.pageSlug);
|
this.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);
|
|
||||||
},
|
},
|
||||||
filterRecipe(slug) {
|
filterRecipe(slug) {
|
||||||
const storeCategory = this.recipeStore.find(element => element.slug === slug);
|
const storeCategory = this.recipeStore.find(element => element.slug === slug);
|
||||||
return storeCategory ? storeCategory.recipes : [];
|
return storeCategory ? storeCategory.recipes : [];
|
||||||
},
|
},
|
||||||
|
sortRecipes(sortedRecipes, destKey) {
|
||||||
|
this.page.categories[destKey].recipes = sortedRecipes;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.header-background {
|
|
||||||
background-color: #121619;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<CardSection
|
<CardSection :sortable="true" :title="title" :recipes="shownRecipes" @sort="assignSorted" />
|
||||||
:sortable="true"
|
|
||||||
:title="title"
|
|
||||||
:recipes="recipes"
|
|
||||||
:card-limit="9999"
|
|
||||||
@sort="sortAZ"
|
|
||||||
@sort-recent="sortRecent"
|
|
||||||
/>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,20 +15,30 @@ export default {
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
recipes: [],
|
recipes: [],
|
||||||
|
sortedResults: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentTag() {
|
currentTag() {
|
||||||
return this.$route.params.tag;
|
return this.$route.params.tag;
|
||||||
},
|
},
|
||||||
|
shownRecipes() {
|
||||||
|
if (this.sortedResults.length > 0) {
|
||||||
|
return this.sortedResults;
|
||||||
|
} else {
|
||||||
|
return this.recipes;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async currentTag() {
|
async currentTag() {
|
||||||
this.getRecipes();
|
this.getRecipes();
|
||||||
|
this.sortedResults = [];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getRecipes();
|
this.getRecipes();
|
||||||
|
this.sortedResults = [];
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getRecipes() {
|
async getRecipes() {
|
||||||
|
@ -43,11 +46,9 @@ export default {
|
||||||
this.title = data.name;
|
this.title = data.name;
|
||||||
this.recipes = data.recipes;
|
this.recipes = data.recipes;
|
||||||
},
|
},
|
||||||
sortAZ() {
|
assignSorted(val) {
|
||||||
this.recipes.sort((a, b) => (a.name > b.name ? 1 : -1));
|
console.log(val);
|
||||||
},
|
this.sortedResults = val.slice();
|
||||||
sortRecent() {
|
|
||||||
this.recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<v-toolbar dense flat>
|
<v-toolbar dense flat>
|
||||||
<v-btn-toggle dense v-model="selected" tile color="primary accent-3" @change="emitMulti" group mandatory>
|
<v-btn-toggle tile group v-model="selected" color="primary accent-3" @change="emitMulti" mandatory>
|
||||||
<v-btn :value="false">
|
<v-btn small :value="false">
|
||||||
{{ $t("search.include") }}
|
{{ $t("search.include") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn :value="true">
|
<v-btn small :value="true">
|
||||||
{{ $t("search.exclude") }}
|
{{ $t("search.exclude") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-btn-toggle>
|
</v-btn-toggle>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn-toggle dense v-model="match" tile color="primary accent-3" @change="emitMulti" group mandatory>
|
<v-btn-toggle tile group v-model="match" color="primary accent-3" @change="emitMulti" mandatory>
|
||||||
<v-btn :value="false">
|
<v-btn small :value="false">
|
||||||
{{ $t("search.and") }}
|
{{ $t("search.and") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn :value="true">
|
<v-btn small :value="true">
|
||||||
{{ $t("search.or") }}
|
{{ $t("search.or") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-btn-toggle>
|
</v-btn-toggle>
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
{{ $t("search.tag-filter") }}
|
{{ $t("search.tag-filter") }}
|
||||||
</h3>
|
</h3>
|
||||||
<FilterSelector class="mb-1" @update="updateTagParams" />
|
<FilterSelector class="mb-1" @update="updateTagParams" />
|
||||||
|
|
||||||
<CategoryTagSelector
|
<CategoryTagSelector
|
||||||
:solo="true"
|
:solo="true"
|
||||||
:dense="false"
|
:dense="false"
|
||||||
|
@ -66,7 +65,6 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchString: "",
|
|
||||||
maxResults: 21,
|
maxResults: 21,
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
catFilter: {
|
catFilter: {
|
||||||
|
@ -96,6 +94,14 @@ export default {
|
||||||
this.$store.dispatch("requestAllRecipes");
|
this.$store.dispatch("requestAllRecipes");
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
searchString: {
|
||||||
|
set(q) {
|
||||||
|
this.$router.replace({ query: { ...this.$route.query, q } });
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return this.$route.query.q || "";
|
||||||
|
},
|
||||||
|
},
|
||||||
allRecipes() {
|
allRecipes() {
|
||||||
return this.$store.getters.getAllRecipes;
|
return this.$store.getters.getAllRecipes;
|
||||||
},
|
},
|
||||||
|
@ -132,11 +138,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
showRecipes(val) {
|
|
||||||
console.log(val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
assignFuzzy(val) {
|
assignFuzzy(val) {
|
||||||
this.sortedResults = val;
|
this.sortedResults = val;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import Admin from "@/pages/Admin";
|
import Admin from "@/pages/Admin";
|
||||||
import MealPlanner from "@/pages/Admin/MealPlanner";
|
|
||||||
import Migration from "@/pages/Admin/Migration";
|
import Migration from "@/pages/Admin/Migration";
|
||||||
import Profile from "@/pages/Admin/Profile";
|
import Profile from "@/pages/Admin/Profile";
|
||||||
import ManageUsers from "@/pages/Admin/ManageUsers";
|
import ManageUsers from "@/pages/Admin/ManageUsers";
|
||||||
|
@ -29,13 +28,6 @@ export const adminRoutes = {
|
||||||
title: "settings.profile",
|
title: "settings.profile",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "meal-planner",
|
|
||||||
component: MealPlanner,
|
|
||||||
meta: {
|
|
||||||
title: "meal-plan.meal-planner",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "migrations",
|
path: "migrations",
|
||||||
component: Migration,
|
component: Migration,
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const mealRoutes = [
|
||||||
async function todaysMealRoute() {
|
async function todaysMealRoute() {
|
||||||
const response = await api.mealPlans.today();
|
const response = await api.mealPlans.today();
|
||||||
if (response.status == 200 && response.data) {
|
if (response.status == 200 && response.data) {
|
||||||
return "/recipe/" + response.data;
|
return "/recipe/" + response.data.slug;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,4 +28,22 @@ export const recipe = {
|
||||||
randomRecipe(list) {
|
randomRecipe(list) {
|
||||||
return list[Math.floor(Math.random() * list.length)];
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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.settings import CustomPage, SiteSettings
|
||||||
from mealie.db.models.sign_up import SignUp
|
from mealie.db.models.sign_up import SignUp
|
||||||
from mealie.db.models.theme import SiteThemeModel
|
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.category import RecipeCategoryResponse, RecipeTagResponse
|
||||||
from mealie.schema.events import Event as EventSchema
|
from mealie.schema.events import Event as EventSchema
|
||||||
from mealie.schema.meal import MealPlanInDB
|
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.settings import SiteSettings as SiteSettingsSchema
|
||||||
from mealie.schema.sign_up import SignUpOut
|
from mealie.schema.sign_up import SignUpOut
|
||||||
from mealie.schema.theme import SiteTheme
|
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
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
logger = getLogger()
|
logger = getLogger()
|
||||||
|
@ -38,12 +38,16 @@ class _Recipes(BaseDocument):
|
||||||
|
|
||||||
def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int:
|
def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int:
|
||||||
return self._countr_attribute(
|
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:
|
def count_untagged(self, session: Session, count=True, override_schema=None) -> int:
|
||||||
return self._countr_attribute(
|
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)
|
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):
|
class _Groups(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "id"
|
self.primary_key = "id"
|
||||||
|
@ -154,6 +165,7 @@ class Database:
|
||||||
self.categories = _Categories()
|
self.categories = _Categories()
|
||||||
self.tags = _Tags()
|
self.tags = _Tags()
|
||||||
self.users = _Users()
|
self.users = _Users()
|
||||||
|
self.api_tokens = _LongLiveToken()
|
||||||
self.sign_ups = _SignUps()
|
self.sign_ups = _SignUps()
|
||||||
self.groups = _Groups()
|
self.groups = _Groups()
|
||||||
self.custom_pages = _CustomPages()
|
self.custom_pages = _CustomPages()
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import sqlalchemy as sa
|
|
||||||
from mealie.db.models.model_base import SqlAlchemyBase
|
from mealie.db.models.model_base import SqlAlchemyBase
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(SqlAlchemyBase):
|
class RecipeIngredient(SqlAlchemyBase):
|
||||||
__tablename__ = "recipes_ingredients"
|
__tablename__ = "recipes_ingredients"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
position = sa.Column(sa.Integer)
|
position = Column(Integer)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
parent_id = Column(Integer, ForeignKey("recipes.id"))
|
||||||
ingredient = sa.Column(sa.String)
|
# title = Column(String)
|
||||||
|
ingredient = Column(String)
|
||||||
|
|
||||||
def update(self, ingredient):
|
def update(self, ingredient):
|
||||||
self.ingredient = ingredient
|
self.ingredient = ingredient
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import sqlalchemy as sa
|
|
||||||
from mealie.db.models.model_base import SqlAlchemyBase
|
from mealie.db.models.model_base import SqlAlchemyBase
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||||
|
|
||||||
|
|
||||||
class RecipeInstruction(SqlAlchemyBase):
|
class RecipeInstruction(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_instructions"
|
__tablename__ = "recipe_instructions"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
parent_id = Column(Integer, ForeignKey("recipes.id"))
|
||||||
position = sa.Column(sa.Integer)
|
position = Column(Integer)
|
||||||
type = sa.Column(sa.String, default="")
|
type = Column(String, default="")
|
||||||
text = sa.Column(sa.String)
|
title = Column(String)
|
||||||
title = sa.Column(sa.String)
|
text = Column(String)
|
||||||
|
|
|
@ -93,7 +93,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
recipe_category: list[str] = None,
|
recipe_category: list[str] = None,
|
||||||
tags: list[str] = None,
|
tags: list[str] = None,
|
||||||
date_added: datetime.date = None,
|
date_added: datetime.date = None,
|
||||||
date_updated: datetime.datetime = None,
|
|
||||||
notes: list[dict] = None,
|
notes: list[dict] = None,
|
||||||
rating: int = None,
|
rating: int = None,
|
||||||
org_url: str = None,
|
org_url: str = None,
|
||||||
|
|
|
@ -3,11 +3,19 @@ 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 sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
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):
|
class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||||
# __tablename__ = "user_settings"
|
__tablename__ = "long_live_tokens"
|
||||||
# id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True)
|
||||||
# parent_id = Column(String, ForeignKey("users.id"))
|
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):
|
class User(SqlAlchemyBase, BaseMixins):
|
||||||
|
@ -19,6 +27,9 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||||
group = orm.relationship("Group", back_populates="users")
|
group = orm.relationship("Group", back_populates="users")
|
||||||
admin = Column(Boolean, default=False)
|
admin = Column(Boolean, default=False)
|
||||||
|
tokens: list[LongLiveToken] = orm.relationship(
|
||||||
|
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -29,6 +40,8 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
group: str = settings.DEFAULT_GROUP,
|
group: str = settings.DEFAULT_GROUP,
|
||||||
admin=False,
|
admin=False,
|
||||||
id=None,
|
id=None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
group = group or settings.DEFAULT_GROUP
|
group = group or settings.DEFAULT_GROUP
|
||||||
|
@ -38,7 +51,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.password = password
|
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.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.group = Group.get_ref(session, group)
|
self.group = Group.get_ref(session, group)
|
||||||
|
@ -49,3 +62,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
|
|
||||||
def update_password(self, password):
|
def update_password(self, password):
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ref(session, id: str):
|
||||||
|
return session.query(User).filter(User.id == id).one()
|
||||||
|
|
|
@ -8,7 +8,8 @@ from mealie.core.config import settings
|
||||||
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.schema.auth import TokenData
|
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")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
|
@ -23,8 +24,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
|
||||||
username: str = payload.get("sub")
|
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:
|
if username is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
token_data = TokenData(username=username)
|
token_data = TokenData(username=username)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
@ -35,6 +42,16 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
|
||||||
return user
|
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:
|
async def validate_file_token(token: Optional[str] = None) -> Path:
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
|
@ -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")
|
group_in_db: GroupInDB = db.groups.get(session, current_user.group, "name")
|
||||||
recipe = get_todays_meal(session, group_in_db)
|
recipe = get_todays_meal(session, group_in_db)
|
||||||
if recipe:
|
if recipe:
|
||||||
return recipe.slug
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
@router.get("/today/image", tags=["Meal Plan"])
|
@router.get("/today/image", tags=["Meal Plan"])
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import auth, crud, sign_up
|
from . import api_tokens, auth, crud, sign_up
|
||||||
|
|
||||||
user_router = APIRouter()
|
user_router = APIRouter()
|
||||||
|
|
||||||
user_router.include_router(auth.router)
|
user_router.include_router(auth.router)
|
||||||
user_router.include_router(sign_up.router)
|
user_router.include_router(sign_up.router)
|
||||||
user_router.include_router(crud.router)
|
user_router.include_router(crud.router)
|
||||||
|
user_router.include_router(api_tokens.router)
|
||||||
|
|
56
mealie/routes/users/api_tokens.py
Normal file
56
mealie/routes/users/api_tokens.py
Normal 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)
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
from mealie.schema.category import CategoryBase
|
from mealie.schema.category import CategoryBase, RecipeCategoryResponse
|
||||||
from pydantic import validator
|
from pydantic import validator
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class CustomPageBase(CamelModel):
|
||||||
name: str
|
name: str
|
||||||
slug: Optional[str]
|
slug: Optional[str]
|
||||||
position: int
|
position: int
|
||||||
categories: list[CategoryBase] = []
|
categories: list[RecipeCategoryResponse] = []
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
|
@ -10,6 +10,25 @@ from pydantic.types import constr
|
||||||
from pydantic.utils import GetterDict
|
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):
|
class ChangePassword(CamelModel):
|
||||||
current_password: str
|
current_password: str
|
||||||
new_password: str
|
new_password: str
|
||||||
|
@ -53,6 +72,7 @@ class UserIn(UserBase):
|
||||||
class UserOut(UserBase):
|
class UserOut(UserBase):
|
||||||
id: int
|
id: int
|
||||||
group: str
|
group: str
|
||||||
|
tokens: Optional[list[LongLiveTokenOut]]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
@ -96,3 +116,11 @@ class GroupInDB(UpdateGroup):
|
||||||
**GetterDict(orm_model),
|
**GetterDict(orm_model),
|
||||||
"webhook_urls": [x.url for x in orm_model.webhook_urls if x],
|
"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
204
poetry.lock
generated
|
@ -70,17 +70,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "20.3.0"
|
version = "21.1.0"
|
||||||
description = "Classes Without Boilerplate"
|
description = "Classes Without Boilerplate"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
|
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"]
|
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
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"]
|
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
|
@ -283,7 +283,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
description = "Lightweight in-process concurrent programming"
|
description = "Lightweight in-process concurrent programming"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -519,7 +519,7 @@ tornado = ">=5.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocs-material"
|
name = "mkdocs-material"
|
||||||
version = "7.1.3"
|
version = "7.1.4"
|
||||||
description = "A Material Design theme for MkDocs"
|
description = "A Material Design theme for MkDocs"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -693,7 +693,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.8.1"
|
version = "2.9.0"
|
||||||
description = "Pygments is a syntax highlighting package written in Python."
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -701,7 +701,7 @@ python-versions = ">=3.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyhumps"
|
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"
|
description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -755,7 +755,7 @@ rdflib = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "6.2.3"
|
version = "6.2.4"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -943,7 +943,7 @@ validators = ">=0.12.4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.15.0"
|
version = "1.16.0"
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -959,7 +959,7 @@ python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "1.4.12"
|
version = "1.4.14"
|
||||||
description = "Database Abstraction Library"
|
description = "Database Abstraction Library"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -1200,8 +1200,8 @@ atomicwrites = [
|
||||||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||||
]
|
]
|
||||||
attrs = [
|
attrs = [
|
||||||
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
|
{file = "attrs-21.1.0-py2.py3-none-any.whl", hash = "sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9"},
|
||||||
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
|
{file = "attrs-21.1.0.tar.gz", hash = "sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8"},
|
||||||
]
|
]
|
||||||
bcrypt = [
|
bcrypt = [
|
||||||
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
|
{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"},
|
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||||
]
|
]
|
||||||
greenlet = [
|
greenlet = [
|
||||||
{file = "greenlet-1.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2"},
|
{file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"},
|
||||||
{file = "greenlet-1.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c"},
|
{file = "greenlet-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3"},
|
||||||
{file = "greenlet-1.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203"},
|
{file = "greenlet-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922"},
|
||||||
{file = "greenlet-1.0.0-cp27-cp27m-win32.whl", hash = "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476"},
|
{file = "greenlet-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821"},
|
||||||
{file = "greenlet-1.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b"},
|
{file = "greenlet-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6"},
|
||||||
{file = "greenlet-1.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379"},
|
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f"},
|
||||||
{file = "greenlet-1.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce"},
|
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c"},
|
{file = "greenlet-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5"},
|
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f"},
|
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6"},
|
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683"},
|
{file = "greenlet-1.1.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-win32.whl", hash = "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70"},
|
{file = "greenlet-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47"},
|
||||||
{file = "greenlet-1.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770"},
|
{file = "greenlet-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee"},
|
{file = "greenlet-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd"},
|
{file = "greenlet-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a"},
|
{file = "greenlet-1.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d"},
|
{file = "greenlet-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2"},
|
{file = "greenlet-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7"},
|
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3"},
|
||||||
{file = "greenlet-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be"},
|
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef"},
|
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128"},
|
{file = "greenlet-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7"},
|
{file = "greenlet-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c"},
|
{file = "greenlet-1.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f"},
|
{file = "greenlet-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c"},
|
{file = "greenlet-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed"},
|
||||||
{file = "greenlet-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85"},
|
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7"},
|
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06"},
|
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36"},
|
{file = "greenlet-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"},
|
{file = "greenlet-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196"},
|
{file = "greenlet-1.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-win32.whl", hash = "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e"},
|
{file = "greenlet-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505"},
|
||||||
{file = "greenlet-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c"},
|
{file = "greenlet-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243"},
|
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df"},
|
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218"},
|
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7"},
|
{file = "greenlet-1.1.0-cp38-cp38-win32.whl", hash = "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664"},
|
{file = "greenlet-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-win32.whl", hash = "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139"},
|
{file = "greenlet-1.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e"},
|
||||||
{file = "greenlet-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0"},
|
{file = "greenlet-1.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c"},
|
||||||
{file = "greenlet-1.0.0.tar.gz", hash = "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8"},
|
{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 = [
|
h11 = [
|
||||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
{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"},
|
{file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"},
|
||||||
]
|
]
|
||||||
mkdocs-material = [
|
mkdocs-material = [
|
||||||
{file = "mkdocs-material-7.1.3.tar.gz", hash = "sha256:e34bba93ad1a0e6f9afc371f4ef55bedabbf13b9a786b013b0ce26ac55ec2932"},
|
{file = "mkdocs-material-7.1.4.tar.gz", hash = "sha256:f2fe6014bd69c1651a2dfe2651de7b0fdea1c33ba652243809c767a7c60daa16"},
|
||||||
{file = "mkdocs_material-7.1.3-py2.py3-none-any.whl", hash = "sha256:437638b0de7a9113d7f1c9ddc93c0a29a3b808c71c3606713d8c1fa437697a3e"},
|
{file = "mkdocs_material-7.1.4-py2.py3-none-any.whl", hash = "sha256:1fb299005b787b62ef70631f7adf418e592c22a359e633fdd2ca5c24be6e451c"},
|
||||||
]
|
]
|
||||||
mkdocs-material-extensions = [
|
mkdocs-material-extensions = [
|
||||||
{file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"},
|
{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"},
|
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
|
||||||
]
|
]
|
||||||
pygments = [
|
pygments = [
|
||||||
{file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
|
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
|
||||||
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
|
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
|
||||||
]
|
]
|
||||||
pyhumps = [
|
pyhumps = [
|
||||||
{file = "pyhumps-1.6.1-py3-none-any.whl", hash = "sha256:58b367b73c57b64e32d211dc769addabd68ff6db07ce64b2e6565f7d5a12291f"},
|
{file = "pyhumps-3.0.2-py3-none-any.whl", hash = "sha256:367b1aadcaa64f8196a3cd14f56559a5602950aeb8486f49318e7394f5e18052"},
|
||||||
{file = "pyhumps-1.6.1.tar.gz", hash = "sha256:01612603c5ad73a407299d806d30708a3935052276fdd93776953bccc0724e0a"},
|
{file = "pyhumps-3.0.2.tar.gz", hash = "sha256:042b4b6eec6c1f862f8310c0eebbae19293e9edab8cafb030ff78c890ef1aa34"},
|
||||||
]
|
]
|
||||||
pylint = [
|
pylint = [
|
||||||
{file = "pylint-2.8.2-py3-none-any.whl", hash = "sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"},
|
{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"},
|
{file = "pyRdfa3-3.5.3.tar.gz", hash = "sha256:157663a92b87df345b6f69bde235dff5f797891608e12fe1e4fa8dad687131ae"},
|
||||||
]
|
]
|
||||||
pytest = [
|
pytest = [
|
||||||
{file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"},
|
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
|
||||||
{file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"},
|
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
|
||||||
]
|
]
|
||||||
pytest-cov = [
|
pytest-cov = [
|
||||||
{file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
|
{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"},
|
{file = "scrape_schema_recipe-0.1.3-py2.py3-none-any.whl", hash = "sha256:7a505d7cd94091ffdfcbac0fad21dd583cceee2d9c7ea12366e8fefac8b4da82"},
|
||||||
]
|
]
|
||||||
six = [
|
six = [
|
||||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
]
|
]
|
||||||
soupsieve = [
|
soupsieve = [
|
||||||
{file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"},
|
{file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"},
|
||||||
{file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"},
|
{file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"},
|
||||||
]
|
]
|
||||||
sqlalchemy = [
|
sqlalchemy = [
|
||||||
{file = "SQLAlchemy-1.4.12-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:8c71a80a5474e6e9c9bbf1957ab1c73cdece9d33cfb26d9ea6e7aed41f535cd6"},
|
{file = "SQLAlchemy-1.4.14-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:98d977df2854be582f83b1d02e32febdd72a93b6396417889448a0c0c20fd856"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b1d513ebb16a204c87296d774c2317950191583b34032540948f20096b63efe4"},
|
{file = "SQLAlchemy-1.4.14-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:32412b74f8920b91b1412a8574167bdfe5dd189fdc9b7676f898ea49892fabca"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp27-cp27m-win32.whl", hash = "sha256:4b749cdedf1afb613c3d31235258110e1f36231c15df9b8b63b3f13c712e4790"},
|
{file = "SQLAlchemy-1.4.14-cp27-cp27m-win32.whl", hash = "sha256:cf3e10181d4b96bafa693a03948b0da2136c977682a902b601b9fa0f35934cd0"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp27-cp27m-win_amd64.whl", hash = "sha256:b58f09f4ea42a92e0a8923f4598001f8935bd2ed0c4c6abb9903c5b4cd0d4015"},
|
{file = "SQLAlchemy-1.4.14-cp27-cp27m-win_amd64.whl", hash = "sha256:b8d6aab7f021ffb25d34671a48dc18007a12fad07c920be81401b6bf35ecf2d9"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b4bf83b05056349265b40de37c836517649ea9edd174301072f5a58c7b374f94"},
|
{file = "SQLAlchemy-1.4.14-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ab659472962815ea945e643166ab67299a1415c05d0fb6d9f9d915f9f4540cd9"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c94fe5ec27dec6a994293d1f194a97fcb904252526bbe72698229ec62c0f7281"},
|
{file = "SQLAlchemy-1.4.14-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b3e88738f82871a35a7c5995075100ddad74543e8697e683f655183860649024"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ac4a48e49e863a4d00d8a5ec94ff5540de1f5bcf96d8d54273a75c3278d8b4af"},
|
{file = "SQLAlchemy-1.4.14-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d4c701fe3d17f2f842b32633573c6e9ec43b75fe0e1140b1c079d2df7d73e6"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e815a729b427bd997d681711dc0b22330e445a0a0c47e16b05d2038e814bd29f"},
|
{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.12-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:aeb389136f3a39399ebb8e8ee17beba18d361cde9638059cfbf7e896354412b7"},
|
{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.12-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0c839000817201310a51af390545d7b316fafd6969ef250dad0a6d28c025214d"},
|
{file = "SQLAlchemy-1.4.14-cp36-cp36m-win32.whl", hash = "sha256:10176150fb4f7bef87e58b496e11ec8c09a06d7aaddec858aa3afe68ecf44be1"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp36-cp36m-win32.whl", hash = "sha256:1e8a884d766fcc918199576bf37f1870327582640fa3302489d7415d815be8a9"},
|
{file = "SQLAlchemy-1.4.14-cp36-cp36m-win_amd64.whl", hash = "sha256:1508b5a58046ca9ec4c4d2a3f1dcd6bc2eb1b04e0e5ef7b85fe73fc1805b504a"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp36-cp36m-win_amd64.whl", hash = "sha256:e11ccaa08975e414df6a16466377bb11af692b2a62255c3a70c0993cb2d7f2d7"},
|
{file = "SQLAlchemy-1.4.14-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:dff5c33036a493c00e252a7067ecaa09ef5bbbeea53337083aba013be903c43a"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:deef50c730ddfb4169417a3a3b6393f1e90b0d5c1e62e1d090c1eb1132529f3f"},
|
{file = "SQLAlchemy-1.4.14-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:238545b8e769c7415395ea2d3716ee116511c2816ae75e69f4148e56bc16c978"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a21f41c4cdb76d7f68a6986b9f5c56bdc8eafbc366893d1031df0c367e832388"},
|
{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.12-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:aec20f0ec5788bee91ecf667e9e30e5ed0add9233b63b0e34e916b21eb5bc850"},
|
{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.12-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d5da8fff36593ac96dd3d60a4eb9495a142fb6d3f0ed23baf5567c0ef7aa9b47"},
|
{file = "SQLAlchemy-1.4.14-cp37-cp37m-win32.whl", hash = "sha256:5129af3c0cd879e37d3f7db417f7c6500c992b6e5d1cae42ba05e6dac97e3e60"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:a4c9c947fc08d2ac48116c64b7dfbac22b9896619cb74923ba59876504ff6256"},
|
{file = "SQLAlchemy-1.4.14-cp37-cp37m-win_amd64.whl", hash = "sha256:e0b75b7712f25d8d51a793cbbffb26688d2f655b97c9dfad02aad67b09d121a6"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp37-cp37m-win32.whl", hash = "sha256:4c8c335b072967da27fef54fb53e74fadadd7d2167c5eb98f0bfb4bfeb3a6948"},
|
{file = "SQLAlchemy-1.4.14-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:bcd85d7a71e2de3567c5c4b26b1a90c57432b3032ff9132dafae1c52a4edeea0"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp37-cp37m-win_amd64.whl", hash = "sha256:01b610951c83452ee5e7d912c4ed9db4538b15d66e96ca6696ec38f0c5ce2908"},
|
{file = "SQLAlchemy-1.4.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c115aaadbb555c787a14218d94185a0929d4a625e6f150fe86a9f7fca0f5d30"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:6b77880e23d3758db7ad65732304ab1c3a42f0cd20505f4a211750862563a161"},
|
{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.12-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f04acd3840bcf33f941b049e24aeef0be5145b2cd5489a89559c11be2d25e262"},
|
{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.12-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:691568d8238c756011d97a655a76820715cbc0295b7d294aa2f1d62fb0be4361"},
|
{file = "SQLAlchemy-1.4.14-cp38-cp38-win32.whl", hash = "sha256:6fd1bd8f4c134aa3a2fd43abe0f83245a3537001ea3c84dc0e28768a40a1a53c"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0646a4caab207279532ffd3f173b4756ae3863f3a94e369b7d1b82831a7ad433"},
|
{file = "SQLAlchemy-1.4.14-cp38-cp38-win_amd64.whl", hash = "sha256:38fce422c8553b14145e4d5ada3cc7643bb720d2761976ab03177b65dc14fb4d"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:2b35206c11c415448caf5b7abddbfac6acbe37f79832ae2d1be013f0dfe252ea"},
|
{file = "SQLAlchemy-1.4.14-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:51b5f73e4a38d07bd883aec6c4ce3292309bdf9e9ce2b21653b92c7ce9bc6f5a"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp38-cp38-win32.whl", hash = "sha256:89e755688476b7a925554a1e8a756e0dd6124dfb8fac80470a90cd8424326bee"},
|
{file = "SQLAlchemy-1.4.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd718612457ac34f6f19ff9b00c80c68ec0f46dae1334671c653a0dc960df9ce"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp38-cp38-win_amd64.whl", hash = "sha256:1bc9ea9e54bbaf65fece8b719f56472748f75777806f4f5fadd8112a165eab19"},
|
{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.12-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:1bdf65dc5263be4651aa34ebe07aa035c61421f145b0d43f4c0b1f3c33bec673"},
|
{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.12-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f90a42db44427bf98128d823502e0af3f4b83f208e09a3d51df5c2cd7f2a76cf"},
|
{file = "SQLAlchemy-1.4.14-cp39-cp39-win32.whl", hash = "sha256:a2c6e766410eebb0e7c4a839acce5534619f5d03bd2aabf44e0caf8b813613b9"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c9047989b8645d8830067dddb2bda544c625419b22b0f546660fd0bfe73341f6"},
|
{file = "SQLAlchemy-1.4.14-cp39-cp39-win_amd64.whl", hash = "sha256:296a3b53e50c3d61ea22969b9d57937714e62f4bd7b5db7bb0afe80e8fbbe44c"},
|
||||||
{file = "SQLAlchemy-1.4.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b7ed6ce2e32a68a3b417a848a409ed5b7e4c8e5fa8911b06c77a6be1cc767658"},
|
{file = "SQLAlchemy-1.4.14.tar.gz", hash = "sha256:fcea0af70a356fdff9efa917d33ec5a8538fe7bde3004ceded7377f1a97d8ef3"},
|
||||||
{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"},
|
|
||||||
]
|
]
|
||||||
starlette = [
|
starlette = [
|
||||||
{file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"},
|
{file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "mealie"
|
name = "mealie"
|
||||||
version = "0.5.0"
|
version = "0.5.0b"
|
||||||
description = "A Recipe Manager"
|
description = "A Recipe Manager"
|
||||||
authors = ["Hayden <hay-kot@pm.me>"]
|
authors = ["Hayden <hay-kot@pm.me>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
@ -2,25 +2,32 @@ class AppRoutes:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.prefix = "/api"
|
self.prefix = "/api"
|
||||||
|
|
||||||
self.users_sign_ups = "/api/users/sign-ups"
|
|
||||||
self.auth_token = "/api/auth/token"
|
self.auth_token = "/api/auth/token"
|
||||||
self.auth_token_long = "/api/auth/token/long"
|
self.auth_token_long = "/api/auth/token/long"
|
||||||
self.auth_refresh = "/api/auth/refresh"
|
self.auth_refresh = "/api/auth/refresh"
|
||||||
|
self.users_sign_ups = "/api/users/sign-ups"
|
||||||
self.users = "/api/users"
|
self.users = "/api/users"
|
||||||
self.users_self = "/api/users/self"
|
self.users_self = "/api/users/self"
|
||||||
|
self.users_api_tokens = "/api/users/api-tokens"
|
||||||
self.groups = "/api/groups"
|
self.groups = "/api/groups"
|
||||||
self.groups_self = "/api/groups/self"
|
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_category = "/api/recipes/category"
|
||||||
self.recipes_tag = "/api/recipes/tag"
|
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 = "/api/recipes/create"
|
||||||
self.recipes_create_url = "/api/recipes/create-url"
|
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_all = "/api/meal-plans/all"
|
||||||
self.meal_plans_create = "/api/meal-plans/create"
|
self.meal_plans_create = "/api/meal-plans/create"
|
||||||
self.meal_plans_this_week = "/api/meal-plans/this-week"
|
self.meal_plans_this_week = "/api/meal-plans/this-week"
|
||||||
self.meal_plans_today = "/api/meal-plans/today"
|
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_custom_pages = "/api/site-settings/custom-pages"
|
||||||
self.site_settings = "/api/site-settings"
|
self.site_settings = "/api/site-settings"
|
||||||
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
|
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_export_database = "/api/backups/export/database"
|
||||||
self.backups_upload = "/api/backups/upload"
|
self.backups_upload = "/api/backups/upload"
|
||||||
self.migrations = "/api/migrations"
|
self.migrations = "/api/migrations"
|
||||||
|
self.debug = "/api/debug"
|
||||||
|
self.debug_statistics = "/api/debug/statistics"
|
||||||
self.debug_version = "/api/debug/version"
|
self.debug_version = "/api/debug/version"
|
||||||
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
|
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):
|
def users_sign_ups_token(self, token):
|
||||||
return f"{self.prefix}/users/sign-ups/{token}"
|
return f"{self.prefix}/users/sign-ups/{token}"
|
||||||
|
@ -48,21 +59,36 @@ class AppRoutes:
|
||||||
def users_id_password(self, id):
|
def users_id_password(self, id):
|
||||||
return f"{self.prefix}/users/{id}/password"
|
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):
|
def groups_id(self, id):
|
||||||
return f"{self.prefix}/groups/{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):
|
def recipes_recipe_slug(self, recipe_slug):
|
||||||
return f"{self.prefix}/recipes/{recipe_slug}"
|
return f"{self.prefix}/recipes/{recipe_slug}"
|
||||||
|
|
||||||
def recipes_recipe_slug_image(self, recipe_slug):
|
def recipes_recipe_slug_image(self, recipe_slug):
|
||||||
return f"{self.prefix}/recipes/{recipe_slug}/image"
|
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):
|
def meal_plans_plan_id(self, plan_id):
|
||||||
return f"{self.prefix}/meal-plans/{plan_id}"
|
return f"{self.prefix}/meal-plans/{plan_id}"
|
||||||
|
|
||||||
|
@ -72,8 +98,8 @@ class AppRoutes:
|
||||||
def site_settings_custom_pages_id(self, id):
|
def site_settings_custom_pages_id(self, id):
|
||||||
return f"{self.prefix}/site-settings/custom-pages/{id}"
|
return f"{self.prefix}/site-settings/custom-pages/{id}"
|
||||||
|
|
||||||
def themes_theme_name(self, theme_name):
|
def themes_id(self, id):
|
||||||
return f"{self.prefix}/themes/{theme_name}"
|
return f"{self.prefix}/themes/{id}"
|
||||||
|
|
||||||
def backups_file_name_download(self, file_name):
|
def backups_file_name_download(self, file_name):
|
||||||
return f"{self.prefix}/backups/{file_name}/download"
|
return f"{self.prefix}/backups/{file_name}/download"
|
||||||
|
@ -84,14 +110,14 @@ class AppRoutes:
|
||||||
def backups_file_name_delete(self, file_name):
|
def backups_file_name_delete(self, file_name):
|
||||||
return f"{self.prefix}/backups/{file_name}/delete"
|
return f"{self.prefix}/backups/{file_name}/delete"
|
||||||
|
|
||||||
def migrations_source_file_name_import(self, source, file_name):
|
def migrations_import_type_file_name_import(self, import_type, file_name):
|
||||||
return f"{self.prefix}/migrations/{source}/{file_name}/import"
|
return f"{self.prefix}/migrations/{import_type}/{file_name}/import"
|
||||||
|
|
||||||
def migrations_source_file_name_delete(self, source, file_name):
|
def migrations_import_type_file_name_delete(self, import_type, file_name):
|
||||||
return f"{self.prefix}/migrations/{source}/{file_name}/delete"
|
return f"{self.prefix}/migrations/{import_type}/{file_name}/delete"
|
||||||
|
|
||||||
def migrations_source_upload(self, source):
|
def migrations_import_type_upload(self, import_type):
|
||||||
return f"{self.prefix}/migrations/{source}/upload"
|
return f"{self.prefix}/migrations/{import_type}/upload"
|
||||||
|
|
||||||
def debug_log_num(self, num):
|
def debug_log_num(self, num):
|
||||||
return f"{self.prefix}/debug/log/{num}"
|
return f"{self.prefix}/debug/log/{num}"
|
||||||
|
|
32
tests/integration_tests/test_long_live_tokens.py
Normal file
32
tests/integration_tests/test_long_live_tokens.py
Normal 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
|
|
@ -23,7 +23,7 @@ def chowdown_zip():
|
||||||
|
|
||||||
|
|
||||||
def test_upload_chowdown_zip(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token):
|
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)
|
response = api_client.post(upload_url, files={"archive": chowdown_zip.open("rb")}, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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
|
api_client.delete(delete_url, headers=token) # TODO: Manage Test Data better
|
||||||
selection = chowdown_zip.name
|
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)
|
response = api_client.post(import_url, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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):
|
def test_delete_chowdown_migration_data(api_client: TestClient, api_routes: AppRoutes, chowdown_zip: Path, token):
|
||||||
selection = chowdown_zip.name
|
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)
|
response = api_client.delete(delete_url, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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):
|
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)
|
response = api_client.post(upload_url, files={"archive": nextcloud_zip.open("rb")}, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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):
|
def test_import_nextcloud_directory(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip, token):
|
||||||
selection = nextcloud_zip.name
|
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)
|
response = api_client.post(import_url, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
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):
|
def test_delete__nextcloud_migration_data(api_client: TestClient, api_routes: AppRoutes, nextcloud_zip: Path, token):
|
||||||
selection = nextcloud_zip.name
|
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)
|
response = api_client.delete(delete_url, headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
|
@ -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):
|
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 response.status_code == 200
|
||||||
assert json.loads(response.content) == default_theme
|
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)
|
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
|
||||||
assert response.status_code == 201
|
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 response.status_code == 200
|
||||||
assert json.loads(response.content) == new_theme
|
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):
|
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
|
||||||
for theme in [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 response.status_code == 200
|
||||||
assert json.loads(response.content) == theme
|
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
|
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
|
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
|
assert json.loads(response.content) == new_theme
|
||||||
|
|
||||||
|
|
||||||
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
|
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
|
||||||
for theme in [default_theme, new_theme]:
|
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
|
assert response.status_code == 200
|
||||||
|
|
|
@ -10,12 +10,12 @@ from tests.app_routes import AppRoutes
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
def default_user():
|
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")
|
@fixture(scope="session")
|
||||||
def new_user():
|
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):
|
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",
|
"password": "MyStrongPassword",
|
||||||
"group": "Home",
|
"group": "Home",
|
||||||
"admin": False,
|
"admin": False,
|
||||||
|
"tokens": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
response = api_client.post(api_routes.users, json=create_data, headers=token)
|
response = api_client.post(api_routes.users, json=create_data, headers=token)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue