Sqlite (#75)
* file structure * auto-test * take 2 * refactor ap scheduler and startup process * fixed scraper error * database abstraction * database abstraction * port recipes over to new schema * meal migration * start settings migration * finale mongo port * backup improvements * migration imports to new DB structure * unused import cleanup * docs strings * settings and theme import logic * cleanup * fixed tinydb error * requirements * fuzzy search * remove scratch file * sqlalchemy models * improved search ui * recipe models almost done * sql modal population * del scratch * rewrite database model mixins * mostly grabage * recipe updates * working sqllite * remove old files and reorganize * final cleanup Co-authored-by: Hayden <hay-kot@pm.me>
|
@ -1,3 +1,2 @@
|
|||
*/node_modules
|
||||
*/dist
|
||||
##
|
2
.github/workflows/build-docs.yml
vendored
|
@ -2,7 +2,7 @@ name: Publish docs via GitHub Pages
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
127
.gitignore
vendored
|
@ -151,3 +151,130 @@ ENV/
|
|||
# Node Modules
|
||||
node_modules/
|
||||
mealie/data/debug/last_recipe.json
|
||||
mealie/data/temp/active_import/images/.gitkeep
|
||||
mealie/data/temp/active_import/images/banana-bread.jpg
|
||||
mealie/data/temp/active_import/images/bon-appetit-s-perfect-pizza.jpg
|
||||
mealie/data/temp/active_import/images/braised-beans-and-sardines-with-fennel.jpg
|
||||
mealie/data/temp/active_import/images/broccoli-beer-cheese-soup.jpg
|
||||
mealie/data/temp/active_import/images/buttery-kimchi-chicken.jpg
|
||||
mealie/data/temp/active_import/images/cauliflower-cacciatore.jpg
|
||||
mealie/data/temp/active_import/images/chicken-salad-with-citrus-and-chile-oil.jpg
|
||||
mealie/data/temp/active_import/images/coffee-hazelnut-biscotti.jpg
|
||||
mealie/data/temp/active_import/images/corn-and-crab-beignets-with-yaji-aioli.jpg
|
||||
mealie/data/temp/active_import/images/crispy-carrots.jpg
|
||||
mealie/data/temp/active_import/images/crispy-rice-with-ginger-citrus-celery-salad.jpg
|
||||
mealie/data/temp/active_import/images/crockpot-buffalo-chicken.jpg
|
||||
mealie/data/temp/active_import/images/detroit-style-pepperoni-pizza.jpg
|
||||
mealie/data/temp/active_import/images/downtown-marinade.jpg
|
||||
mealie/data/temp/active_import/images/falafel-hummus-plate.jpg
|
||||
mealie/data/temp/active_import/images/five-spice-popcorn-chicken.jpg
|
||||
mealie/data/temp/active_import/images/ginger-citrus-cookies.jpg
|
||||
mealie/data/temp/active_import/images/green-chile-stew.jpg
|
||||
mealie/data/temp/active_import/images/green-seasoning-baked-cod.jpg
|
||||
mealie/data/temp/active_import/images/green-spaghetti.jpg
|
||||
mealie/data/temp/active_import/images/huevos-rancheros-con-rajas-y-champinones.jpg
|
||||
mealie/data/temp/active_import/images/jalapeno-cornbread.jpg
|
||||
mealie/data/temp/active_import/images/marinated-tofu-with-brussels-sprouts-and-farro.jpg
|
||||
mealie/data/temp/active_import/images/marranitos-enfiestados.jpg
|
||||
mealie/data/temp/active_import/images/mississippi-pot-roast.jpg
|
||||
mealie/data/temp/active_import/images/mushroom-risotto.jpg
|
||||
mealie/data/temp/active_import/images/new-york-strip.jpg
|
||||
mealie/data/temp/active_import/images/nilla-wafer-french-toast.jpg
|
||||
mealie/data/temp/active_import/images/one-minute-muffin.jpg
|
||||
mealie/data/temp/active_import/images/one-pot-chicken-and-rice.jpg
|
||||
mealie/data/temp/active_import/images/pace-pork.jpg
|
||||
mealie/data/temp/active_import/images/pasta-with-mushrooms-and-cashew-cream.jpg
|
||||
mealie/data/temp/active_import/images/pizzettes.jpg
|
||||
mealie/data/temp/active_import/images/pork-steaks.jpg
|
||||
mealie/data/temp/active_import/images/roasted-brussels-sprouts.jpg
|
||||
mealie/data/temp/active_import/images/roasted-okra.jpg
|
||||
mealie/data/temp/active_import/images/salt-vinegar-potatoes.jpg
|
||||
mealie/data/temp/active_import/images/shrimp-and-cabbage-curry.jpg
|
||||
mealie/data/temp/active_import/images/smashed-carrots.jpg
|
||||
mealie/data/temp/active_import/images/tamarind-chicken-thighs-with-collard-greens-salad.jpg
|
||||
mealie/data/temp/active_import/images/tequila-beer-and-citrus-cocktail.jpg
|
||||
mealie/data/temp/active_import/recipes/banana-bread.json
|
||||
mealie/data/temp/active_import/recipes/bon-appetit-s-perfect-pizza.json
|
||||
mealie/data/temp/active_import/recipes/braised-beans-and-sardines-with-fennel.json
|
||||
mealie/data/temp/active_import/recipes/broccoli-beer-cheese-soup.json
|
||||
mealie/data/temp/active_import/recipes/buttery-kimchi-chicken.json
|
||||
mealie/data/temp/active_import/recipes/cauliflower-cacciatore.json
|
||||
mealie/data/temp/active_import/recipes/chicken-salad-with-citrus-and-chile-oil.json
|
||||
mealie/data/temp/active_import/recipes/coffee-hazelnut-biscotti.json
|
||||
mealie/data/temp/active_import/recipes/corn-and-crab-beignets-with-yaji-aioli.json
|
||||
mealie/data/temp/active_import/recipes/crispy-carrots.json
|
||||
mealie/data/temp/active_import/recipes/crispy-rice-with-ginger-citrus-celery-salad.json
|
||||
mealie/data/temp/active_import/recipes/crockpot-buffalo-chicken.json
|
||||
mealie/data/temp/active_import/recipes/detroit-style-pepperoni-pizza.json
|
||||
mealie/data/temp/active_import/recipes/downtown-marinade.json
|
||||
mealie/data/temp/active_import/recipes/falafel-hummus-plate.json
|
||||
mealie/data/temp/active_import/recipes/five-spice-popcorn-chicken.json
|
||||
mealie/data/temp/active_import/recipes/ginger-citrus-cookies.json
|
||||
mealie/data/temp/active_import/recipes/green-chile-stew.json
|
||||
mealie/data/temp/active_import/recipes/green-seasoning-baked-cod.json
|
||||
mealie/data/temp/active_import/recipes/green-spaghetti.json
|
||||
mealie/data/temp/active_import/recipes/huevos-rancheros-con-rajas-y-champinones.json
|
||||
mealie/data/temp/active_import/recipes/jalapeno-cornbread.json
|
||||
mealie/data/temp/active_import/recipes/marinated-tofu-with-brussels-sprouts-and-farro.json
|
||||
mealie/data/temp/active_import/recipes/marranitos-enfiestados.json
|
||||
mealie/data/temp/active_import/recipes/mississippi-pot-roast.json
|
||||
mealie/data/temp/active_import/recipes/mushroom-risotto.json
|
||||
mealie/data/temp/active_import/recipes/new-york-strip.json
|
||||
mealie/data/temp/active_import/recipes/nilla-wafer-french-toast.json
|
||||
mealie/data/temp/active_import/recipes/one-minute-muffin.json
|
||||
mealie/data/temp/active_import/recipes/one-pot-chicken-and-rice.json
|
||||
mealie/data/temp/active_import/recipes/pace-pork.json
|
||||
mealie/data/temp/active_import/recipes/pasta-with-mushrooms-and-cashew-cream.json
|
||||
mealie/data/temp/active_import/recipes/pizzettes.json
|
||||
mealie/data/temp/active_import/recipes/pork-steaks.json
|
||||
mealie/data/temp/active_import/recipes/roasted-brussels-sprouts.json
|
||||
mealie/data/temp/active_import/recipes/roasted-okra.json
|
||||
mealie/data/temp/active_import/recipes/salt-vinegar-potatoes.json
|
||||
mealie/data/temp/active_import/recipes/shrimp-and-cabbage-curry.json
|
||||
mealie/data/temp/active_import/recipes/smashed-carrots.json
|
||||
mealie/data/temp/active_import/recipes/tamarind-chicken-thighs-with-collard-greens-salad.json
|
||||
mealie/data/temp/active_import/recipes/tequila-beer-and-citrus-cocktail.json
|
||||
mealie/data/temp/active_import/settings/settings.json
|
||||
mealie/data/temp/active_import/templates/Banana Bread.md
|
||||
mealie/data/temp/active_import/templates/Bon Appétit's Perfect Pizza.md
|
||||
mealie/data/temp/active_import/templates/Braised Beans and Sardines With Fennel.md
|
||||
mealie/data/temp/active_import/templates/Broccoli Beer Cheese Soup.md
|
||||
mealie/data/temp/active_import/templates/Buttery Kimchi Chicken.md
|
||||
mealie/data/temp/active_import/templates/Cauliflower Cacciatore.md
|
||||
mealie/data/temp/active_import/templates/Chicken Salad With Citrus and Chile Oil.md
|
||||
mealie/data/temp/active_import/templates/Coffee-Hazelnut Biscotti.md
|
||||
mealie/data/temp/active_import/templates/Corn and Crab Beignets With Yaji Aioli.md
|
||||
mealie/data/temp/active_import/templates/Crispy Carrots.md
|
||||
mealie/data/temp/active_import/templates/Crispy Rice With Ginger-Citrus Celery Salad.md
|
||||
mealie/data/temp/active_import/templates/Crockpot Buffalo Chicken.md
|
||||
mealie/data/temp/active_import/templates/Detroit-Style Pepperoni Pizza.md
|
||||
mealie/data/temp/active_import/templates/Downtown Marinade.md
|
||||
mealie/data/temp/active_import/templates/Falafel-Hummus Plate.md
|
||||
mealie/data/temp/active_import/templates/Five Spice Popcorn Chicken.md
|
||||
mealie/data/temp/active_import/templates/Ginger-Citrus Cookies.md
|
||||
mealie/data/temp/active_import/templates/Green Chile Stew.md
|
||||
mealie/data/temp/active_import/templates/Green Seasoning Baked Cod.md
|
||||
mealie/data/temp/active_import/templates/Green Spaghetti.md
|
||||
mealie/data/temp/active_import/templates/Huevos Rancheros con Rajas y Champiñones.md
|
||||
mealie/data/temp/active_import/templates/Jalapeno Cornbread.md
|
||||
mealie/data/temp/active_import/templates/Marinated Tofu With Brussels Sprouts and Farro.md
|
||||
mealie/data/temp/active_import/templates/Marranitos Enfiestados.md
|
||||
mealie/data/temp/active_import/templates/Mississippi Pot Roast.md
|
||||
mealie/data/temp/active_import/templates/Mushroom Risotto.md
|
||||
mealie/data/temp/active_import/templates/New York Strip.md
|
||||
mealie/data/temp/active_import/templates/Nilla Wafer French Toast.md
|
||||
mealie/data/temp/active_import/templates/One Minute Muffin.md
|
||||
mealie/data/temp/active_import/templates/One-Pot Chicken and Rice.md
|
||||
mealie/data/temp/active_import/templates/Pace Pork.md
|
||||
mealie/data/temp/active_import/templates/Pasta With Mushrooms and Cashew Cream.md
|
||||
mealie/data/temp/active_import/templates/Pizzettes.md
|
||||
mealie/data/temp/active_import/templates/Pork Steaks.md
|
||||
mealie/data/temp/active_import/templates/Roasted Brussels Sprouts.md
|
||||
mealie/data/temp/active_import/templates/Roasted Okra.md
|
||||
mealie/data/temp/active_import/templates/Salt & Vinegar Potatoes.md
|
||||
mealie/data/temp/active_import/templates/Shrimp and Cabbage Curry.md
|
||||
mealie/data/temp/active_import/templates/Smashed Carrots.md
|
||||
mealie/data/temp/active_import/templates/Tamarind Chicken Thighs With Collard Greens Salad.md
|
||||
mealie/data/temp/active_import/templates/Tequila, Beer, and Citrus Cocktail.md
|
||||
mealie/data/temp/active_import/themes/themes.json
|
||||
mealie/data/mealie.sqlite
|
||||
|
|
588
.pylintrc
Normal file
|
@ -0,0 +1,588 @@
|
|||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Specify a score threshold to be exceeded before program exits with error.
|
||||
fail-under=10.0
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
|
||||
# which contain the number of messages in each category, as well as 'statement'
|
||||
# which is the total number of statements analyzed. This score is used by the
|
||||
# global evaluation report (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
#notes-rgx=
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=pydantic.*
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
w54
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
12
.vscode/settings.json
vendored
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"python.formatting.provider": "black",
|
||||
"python.pythonPath": "venv/bin/python",
|
||||
"python.pythonPath": ".venv/bin/python3.8",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],
|
||||
|
@ -8,13 +8,7 @@
|
|||
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.discoverTest": true,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"cSpell.enableFiletypes": [
|
||||
"!javascript",
|
||||
"!python"
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"mealie"
|
||||
]
|
||||
"cSpell.enableFiletypes": ["!javascript", "!python"],
|
||||
"python.testing.pytestArgs": ["mealie"]
|
||||
}
|
||||
|
|
|
@ -16,27 +16,38 @@ Don't forget to [join the Discord](https://discord.gg/R6QDyJgbD2)!
|
|||
|
||||
# Todo's
|
||||
|
||||
Documentation
|
||||
- [ ] V0.1.0 Release Notes
|
||||
- [ ] Nextcloud Migration How To
|
||||
- [ ] New Docker Setup with Sqlite
|
||||
- [ ] New Roadmap / Milestones
|
||||
|
||||
Frontend
|
||||
- [x] .Vue file reorganized into something that makes sense
|
||||
- [ ] Prep / Cook / Total Time Indicator + Editor
|
||||
- [ ] No Meal Today Page instead of Null
|
||||
- [ ] Recipe Print Page
|
||||
- [x] Catch 400 / bad response on create from URL
|
||||
- [ ] Recipe Editor Data Validation Client Side
|
||||
- [x] Favicon
|
||||
- [x] Rename Window
|
||||
- [x] Add version indicator and notification for new version available
|
||||
- [ ] Enhanced Search Functionality
|
||||
- [ ] Organize Home Page my Category, ideally user selectable.
|
||||
- [ ] Advanced Search Page, draft started
|
||||
- [ ] Search Bar Re-design
|
||||
- [ ] Replace Backups card with something like Home Assistant
|
||||
- [ ] Replace import card with something like Home Assistant
|
||||
- [ ] Select which imports to do
|
||||
|
||||
Backend
|
||||
- [x] Add Debug folder for writing the last pulled recipe data to.
|
||||
- [x] Recipe Editor Data Validation Server Side
|
||||
- [ ] Normalize Recipe data on scrape
|
||||
- [ ] Database Import
|
||||
- [x] Recipes
|
||||
- [x] Images
|
||||
- [ ] Meal Plans
|
||||
- [ ] Settings
|
||||
- [ ] Themes
|
||||
- [ ] Remove Print / Debug Code
|
||||
- [ ] Support how to Sections and how to steps
|
||||
- [ ] Export Markdown on Auto backups
|
||||
- [ ] Recipe request by category/tags
|
||||
- [ ] Add Additional Migrations, See mealie/services/migrations/chowdown.py for examples of how to do this.
|
||||
- [ ] Open Eats [See Issue #4](https://github.com/hay-kot/mealie/issues/4)
|
||||
- [ ] NextCloud [See Issue #14](https://github.com/hay-kot/mealie/issues/14)
|
||||
|
||||
|
||||
SQL
|
||||
- [ ] Setup Database Migrations
|
||||
|
||||
# Draft Changelog
|
||||
## v0.0.2
|
||||
|
|
|
@ -37,4 +37,3 @@ if __name__ == "__main__":
|
|||
data = json.dumps(theme)
|
||||
response = requests.post(POST_URL, data)
|
||||
response = requests.get(GET_URL)
|
||||
print(response.text)
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
# Release Notes
|
||||
|
||||
## v0.1.0 - Initial Beta
|
||||
### Bug Fixes
|
||||
- Fixed Can't delete recipe after changing name - Closes Issue #67
|
||||
- Fixed No image when added by URL, and can;t add an image - Closes Issue #66
|
||||
- Fixed Images saved with no way to delete when add recipe via URL fails - Closes Issue #43
|
||||
|
||||
### Features
|
||||
- Additional database! sqlite is now supported! Closes #48
|
||||
- All site data is now backed up.
|
||||
|
||||
|
||||
### Code Improvements
|
||||
- Unified Database Access Layers
|
||||
- Poetry / pyproject.toml support
|
||||
- Local development without database is now possible!
|
||||
- Major code refactoring to support new database layer
|
||||
- Global variable refactor
|
||||
|
||||
### Break Changes
|
||||
|
||||
!!! warning
|
||||
As I've adopted the SQL database model I find that using 2 different types of databases will inevitably hinder development. As such after release v0.1.0 support for mongoDB will not longer be available. Prior to upgrading to v0.2.0 you will need to export your site and import your settings after updating. This should be a painless process and require minimal intervention on the users part. Moving forward we will do our best to minimize changes that require user intervention like this.
|
||||
|
||||
|
||||
## v0.0.2 - Pre-release Second Patch
|
||||
A quality update with major props to [zackbcom](https://github.com/zackbcom) for working hard on making the theming just that much better!
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed empty backup failure without markdown template
|
||||
- Fixed opacity issues with marked steps - [mtoohey31](https://github.com/mtoohey31)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
All recipe data can be imported and exported as necessary from the UI. Under the admin page you'll find the section for using Backups and Exports.
|
||||
|
||||
To create an export simple add the tag and the markdown template and click Backup Recipes and your backup will be created on the server. The backup is a standard zipfile containing all the images, json files, and rendered markdown files for each recipe. Markdown files are rendered from jinja2 templates. Adding your own markdown file into the templates folder will automatically show up as an option to select when creating a backup. To view the availible variables, open a recipe in the json editor.
|
||||
To create an export simple add the tag and the markdown template and click Backup Recipes and your backup will be created on the server. The backup is a standard zipfile containing all the images, json files, and rendered markdown files for each recipe. Markdown files are rendered from jinja2 templates. Adding your own markdown file into the templates folder will automatically show up as an option to select when creating a backup. To view the available variables, open a recipe in the json editor.
|
||||
|
||||
To import a backup it must be in your backups folder. If it is in the backup folder it will automatically show up as an source to restore from. Selected the desired backup and import the backup file.
|
||||
|
||||
|
|
138
docs/docs/img/app_diagram.drawio.svg
Normal file
After Width: | Height: | Size: 67 KiB |
165
frontend/package-lock.json
generated
|
@ -1738,16 +1738,6 @@
|
|||
"integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&sync_timestamp=1594428402513&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz",
|
||||
|
@ -1774,34 +1764,6 @@
|
|||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz?cache=0&sync_timestamp=1583735626956&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-cache-dir%2Fdownload%2Ffind-cache-dir-3.3.1.tgz",
|
||||
|
@ -1823,25 +1785,6 @@
|
|||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz?cache=0&sync_timestamp=1597081764621&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flocate-path%2Fdownload%2Flocate-path-5.0.0.tgz",
|
||||
|
@ -1906,16 +1849,6 @@
|
|||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz?cache=0&sync_timestamp=1603882075288&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-2.3.8.tgz",
|
||||
|
@ -1932,18 +1865,6 @@
|
|||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5663,6 +5584,11 @@
|
|||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"fuse.js": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.6.tgz",
|
||||
"integrity": "sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw=="
|
||||
},
|
||||
"gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npm.taobao.org/gensync/download/gensync-1.0.0-beta.2.tgz?cache=0&sync_timestamp=1603829621482&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fgensync%2Fdownload%2Fgensync-1.0.0-beta.2.tgz",
|
||||
|
@ -11202,6 +11128,87 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.8.2",
|
||||
"fuse.js": "^6.4.6",
|
||||
"qs": "^6.9.4",
|
||||
"v-jsoneditor": "^1.4.2",
|
||||
"vue": "^2.6.11",
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<v-app-bar dense app color="primary" dark class="d-print-none">
|
||||
<v-btn @click="$router.push('/')" icon class="d-flex align-center">
|
||||
<v-icon size="40">
|
||||
mdi-silverware-variant
|
||||
</v-icon>
|
||||
<v-btn @click="$router.push('/')" icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
<div btn class="pl-2">
|
||||
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-expand-x-transition>
|
||||
<SearchBar
|
||||
class="mt-7"
|
||||
v-if="search"
|
||||
:show-results="true"
|
||||
@selected="navigateFromSearch"
|
||||
/>
|
||||
</v-expand-x-transition>
|
||||
<v-btn icon @click="toggleSearch">
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
|
@ -22,10 +27,6 @@
|
|||
<v-container>
|
||||
<AddRecipeFab />
|
||||
<SnackBar />
|
||||
<v-expand-transition>
|
||||
<SearchHeader v-show="search" />
|
||||
</v-expand-transition>
|
||||
|
||||
<router-view></router-view>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
@ -34,7 +35,7 @@
|
|||
|
||||
<script>
|
||||
import Menu from "./components/UI/Menu";
|
||||
import SearchHeader from "./components/UI/SearchHeader";
|
||||
import SearchBar from "./components/UI/SearchBar";
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
||||
import SnackBar from "./components/UI/SnackBar";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
|
@ -44,14 +45,14 @@ export default {
|
|||
components: {
|
||||
Menu,
|
||||
AddRecipeFab,
|
||||
SearchHeader,
|
||||
SnackBar
|
||||
SnackBar,
|
||||
SearchBar,
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -62,7 +63,7 @@ export default {
|
|||
},
|
||||
|
||||
data: () => ({
|
||||
search: false
|
||||
search: false,
|
||||
}),
|
||||
methods: {
|
||||
/**
|
||||
|
@ -90,8 +91,11 @@ export default {
|
|||
} else {
|
||||
this.search = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ export default {
|
|||
},
|
||||
|
||||
async create(tag, template) {
|
||||
if (typeof template == String) {
|
||||
template = [template];
|
||||
}
|
||||
console.log(tag, template);
|
||||
let response = apiReq.post(backupURLs.createBackup, {
|
||||
tag: tag,
|
||||
template: template,
|
||||
|
|
|
@ -51,8 +51,9 @@ export default {
|
|||
async update(data) {
|
||||
const recipeSlug = data.slug;
|
||||
|
||||
apiReq.post(recipeURLs.update(recipeSlug), data);
|
||||
let response = await apiReq.post(recipeURLs.update(recipeSlug), data);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async delete(recipeSlug) {
|
||||
|
|
|
@ -121,10 +121,7 @@ export default {
|
|||
async createBackup() {
|
||||
this.backupLoading = true;
|
||||
|
||||
let response = await api.backups.create(
|
||||
this.backupTag,
|
||||
this.selectedTemplate
|
||||
);
|
||||
let response = await api.backups.create(this.backupTag, this.templates);
|
||||
|
||||
if (response.status == 201) {
|
||||
this.selectedBackup = null;
|
||||
|
|
108
frontend/src/components/UI/SearchBar.vue
Normal file
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-autocomplete
|
||||
:items="autoResults"
|
||||
item-value="item.slug"
|
||||
item-text="item.name"
|
||||
dense
|
||||
light
|
||||
label="Search Mealie"
|
||||
:search-input.sync="search"
|
||||
hide-no-data
|
||||
cache-items
|
||||
solo
|
||||
>
|
||||
<template
|
||||
v-if="showResults"
|
||||
v-slot:item="{ item }"
|
||||
style="max-width: 750px"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.image)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content @click="selected(item.item.slug)">
|
||||
<v-list-item-title>
|
||||
{{ item.item.name }}
|
||||
<v-rating
|
||||
dense
|
||||
v-if="item.item.rating"
|
||||
:value="item.item.rating"
|
||||
size="12"
|
||||
>
|
||||
</v-rating>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.item.description }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showResults: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: "",
|
||||
result: [],
|
||||
autoResults: [],
|
||||
isDark: false,
|
||||
options: {
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: ["name", "slug"],
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.isDark = this.$store.getters.getIsDark;
|
||||
},
|
||||
computed: {
|
||||
data() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
fuse() {
|
||||
return new Fuse(this.data, this.options);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search() {
|
||||
if (this.search.trim() === "") this.result = this.list;
|
||||
else this.result = this.fuse.search(this.search.trim());
|
||||
console.log("test");
|
||||
|
||||
this.$emit("results", this.result);
|
||||
if (this.showResults === true) {
|
||||
this.autoResults = this.result;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
selected(slug) {
|
||||
this.$emit("selected", slug);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.color-transition {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
|
@ -21,7 +21,7 @@ new Vue({
|
|||
}).$mount("#app");
|
||||
|
||||
// Truncate
|
||||
let filter = function (text, length, clamp) {
|
||||
let filter = function(text, length, clamp) {
|
||||
clamp = clamp || "...";
|
||||
let node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
|
@ -32,5 +32,3 @@ let filter = function (text, length, clamp) {
|
|||
Vue.filter("truncate", filter);
|
||||
|
||||
export { router };
|
||||
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ export default {
|
|||
api.recipes.delete(this.recipeDetails.slug);
|
||||
},
|
||||
async saveRecipe() {
|
||||
await api.recipes.update(this.recipeDetails);
|
||||
let slug = await api.recipes.update(this.recipeDetails);
|
||||
|
||||
if (this.fileObject) {
|
||||
await api.recipes.updateImage(this.recipeDetails.slug, this.fileObject);
|
||||
|
@ -138,6 +138,7 @@ export default {
|
|||
|
||||
this.form = false;
|
||||
this.imageKey += 1;
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
showForm() {
|
||||
this.form = true;
|
||||
|
|
58
frontend/src/pages/SearchPage.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-row justify="center">
|
||||
<v-col cols="1"> </v-col>
|
||||
<v-col>
|
||||
<SearchBar @results="updateResults" :show-results="false" />
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn icon>
|
||||
<v-icon large> mdi-filter </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="searchResults">
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="item in searchResults.slice(0, 10)"
|
||||
:key="item.item.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:name="item.item.name"
|
||||
:description="item.item.description"
|
||||
:slug="item.item.slug"
|
||||
:rating="item.item.rating"
|
||||
:image="item.item.image"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchBar from "../components/UI/SearchBar";
|
||||
import RecipeCard from "../components/UI/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
RecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateResults(results) {
|
||||
this.searchResults = results;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
import HomePage from "./pages/HomePage";
|
||||
import Page404 from "./pages/404Page";
|
||||
import SearchPage from "./pages/SearchPage";
|
||||
import RecipePage from "./pages/RecipePage";
|
||||
import RecipeNewPage from "./pages/RecipeNewPage";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
|
@ -10,6 +11,7 @@ import api from "./api";
|
|||
export const routes = [
|
||||
{ path: "/", component: HomePage },
|
||||
{ path: "/mealie", component: HomePage },
|
||||
{ path: "/search", component: SearchPage },
|
||||
{ path: "/recipe/:recipe", component: RecipePage },
|
||||
{ path: "/new/", component: RecipeNewPage },
|
||||
{ path: "/settings/site", component: SettingsPage },
|
||||
|
|
|
@ -17,6 +17,7 @@ function inDarkMode(payload) {
|
|||
const state = {
|
||||
activeTheme: {},
|
||||
darkMode: "system",
|
||||
isDark: false,
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
@ -30,6 +31,7 @@ const mutations = {
|
|||
|
||||
if (isDark !== null) {
|
||||
Vuetify.framework.theme.dark = isDark;
|
||||
state.isDark = isDark;
|
||||
state.darkMode = payload;
|
||||
}
|
||||
},
|
||||
|
@ -60,6 +62,7 @@ const actions = {
|
|||
const getters = {
|
||||
getActiveTheme: (state) => state.activeTheme,
|
||||
getDarkMode: (state) => state.darkMode,
|
||||
getIsDark: (state) => state.isDark,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
from pathlib import Path
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import startup
|
||||
from routes import (
|
||||
backup_routes,
|
||||
meal_routes,
|
||||
migration_routes,
|
||||
recipe_routes,
|
||||
setting_routes,
|
||||
static_routes,
|
||||
user_routes,
|
||||
)
|
||||
from routes.setting_routes import scheduler # ! This has to be imported for scheduling
|
||||
from settings import PORT, PRODUCTION, docs_url, redoc_url
|
||||
import utils.startup as startup
|
||||
from routes import (backup_routes, meal_routes, migration_routes,
|
||||
recipe_routes, setting_routes, static_routes, user_routes)
|
||||
from settings import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
||||
from utils.api_docs import generate_api_docs
|
||||
from utils.logger import logger
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
WEB_PATH = CWD.joinpath("dist")
|
||||
startup.pre_start()
|
||||
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
|
@ -33,7 +23,6 @@ app = FastAPI(
|
|||
if PRODUCTION:
|
||||
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
||||
|
||||
|
||||
# API Routes
|
||||
app.include_router(recipe_routes.router)
|
||||
app.include_router(meal_routes.router)
|
||||
|
@ -50,12 +39,10 @@ def invalid_api():
|
|||
|
||||
app.include_router(static_routes.router)
|
||||
|
||||
startup.ensure_dirs()
|
||||
startup.generate_default_theme()
|
||||
|
||||
# Generate API Documentation
|
||||
if not PRODUCTION:
|
||||
startup.generate_api_docs(app)
|
||||
generate_api_docs(app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("-----SYSTEM STARTUP-----")
|
||||
|
|
BIN
mealie/data/db/mealie.sqlite
Normal file
|
@ -1,18 +1,91 @@
|
|||
{
|
||||
"name": "Carottes Rapp\u00e9s with Rice and Sunflower Seeds \u2014 FEED THE SWIMMERS",
|
||||
"description": " Carottes R\u00e2p\u00e9es with Rice and Sunflower Seeds thanks to @think_rice and @thefeedfeed. Carottes R\u00e2p\u00e9es is a classic French Salad found ready to go (think picnic) at every charcuterie and on most cafe menus. This is one of those insanely simple salads that explode with flavor! The carrots ar",
|
||||
"image": "carottes-rappes-with-rice-and-sunflower-seeds-feed-the-swimmers.JPG?format=1500w",
|
||||
"recipeYield": "",
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Recipe",
|
||||
"articleBody": "At her L.A. bakery Friends and Family, Roxana Jullapat bakes these blondies in a round cake pan, which ensures that each slice has a chewy, toasted edge and a soft center. \u201cThis caramel-flavored treat demonstrates the powerful pairing of brown butter and barley,\u201d Roxana writes in her cookbook Mother Grains (out April 2021). Barley flour gives baked goods a silky, chewy texture, dense crumb, and butterscotch-y flavor. Want to experiment? Make these with 3\u20444 cup (80 g) einkorn flour in place of the barley flour for an even chewier blondie.",
|
||||
"alternativeHeadline": "Barley flour gives these blondies a chewy texture and butterscotch-like flavor.",
|
||||
"dateModified": "2021-01-11 14:23:12.455000",
|
||||
"datePublished": "2021-01-12 04:00:00",
|
||||
"keywords": [
|
||||
"recipes",
|
||||
"oil",
|
||||
"macadamia nut",
|
||||
"vanilla",
|
||||
"butter",
|
||||
"flour",
|
||||
"barley",
|
||||
"kosher salt",
|
||||
"brown sugar",
|
||||
"egg",
|
||||
"vanilla extract",
|
||||
"web"
|
||||
],
|
||||
"thumbnailUrl": "https://assets.bonappetit.com/photos/5ff4b06a2f7e5df08337ef60/1:1/w_1005,h_1005,c_limit/Mother-Grains-Macadamia-and-Brown-Butter-Blondies.jpg",
|
||||
"publisher": {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Bon App\u00e9tit",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://www.bonappetit.com/verso/static/bon-appetit/assets/logo-seo.328de564b950e3d5d1fbe3e42f065290ca1d3844.png",
|
||||
"width": "479px",
|
||||
"height": "100px"
|
||||
},
|
||||
"url": "https://www.bonappetit.com"
|
||||
},
|
||||
"isPartOf": {
|
||||
"@type": [
|
||||
"CreativeWork",
|
||||
"Product"
|
||||
],
|
||||
"name": "Bon App\u00e9tit"
|
||||
},
|
||||
"isAccessibleForFree": true,
|
||||
"author": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "Roxana Jullapat",
|
||||
"sameAs": "https://bon-appetit.com/contributor/roxana-jullapat/"
|
||||
}
|
||||
],
|
||||
"description": "Barley flour gives these blondies a chewy texture and butterscotch-like flavor.",
|
||||
"image": "macadamia-and-brown-butter-blondies.jpg",
|
||||
"headline": "Macadamia and Brown Butter Blondies",
|
||||
"name": "Macadamia and Brown Butter Blondies",
|
||||
"recipeIngredient": [
|
||||
"Could not detect ingredients"
|
||||
"Nonstick vegetable oil spray",
|
||||
"\u00bd cup (65 g) whole raw or toasted macadamia nuts",
|
||||
"\u00bd vanilla bean, split lengthwise",
|
||||
"18 Tbsp. unsalted butter",
|
||||
"\u00be cup plus 2 Tbsp. (105 g) all-purpose flour",
|
||||
"\u00be cup (85 g) barley flour",
|
||||
"1\u00be tsp. baking powder",
|
||||
"1\u00bd tsp. Diamond Crystal or \u00be tsp. Morton kosher salt",
|
||||
"1 cup plus 7 Tbsp. (packed; 285 g) dark brown sugar",
|
||||
"2 large eggs",
|
||||
"1 tsp. vanilla extract",
|
||||
"2 pints ice cream of choice (optional)"
|
||||
],
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"text": "Could not detect instructions"
|
||||
"text": "Preheat oven to 350\u00b0. Lightly coat a 9\"-diameter cake pan with nonstick spray and line bottom with a parchment paper round. If using raw macadamia nuts, toast on a rimmed baking sheet, tossing once, until golden, 8\u201310 minutes. Let cool, then coarsely chop."
|
||||
},
|
||||
{
|
||||
"text": "Scrape vanilla seeds into a small saucepan; add pod and butter. Set over medium-low heat and cook, stirring occasionally, until butter foams, then browns, 6\u20138 minutes. Transfer to a medium bowl, scraping in all of the browned bits. Using tongs, remove and discard vanilla pod."
|
||||
},
|
||||
{
|
||||
"text": "Whisk all-purpose and barley flours, baking powder, and salt in another medium bowl. Add brown sugar to brown butter and stir to combine. Add eggs one at a time, stirring well after each addition. Stir in dry ingredients, then vanilla extract and nuts. Scrape batter into prepared pan; smooth top."
|
||||
},
|
||||
{
|
||||
"text": "Bake blondies, rotating halfway through, until top is golden brown and a tester inserted into the center comes out clean, 40\u201345 minutes. Let cool."
|
||||
},
|
||||
{
|
||||
"text": "Turn out blondies, remove parchment, and cut into 12 wedges. Serve each with a scoop of ice cream if desired."
|
||||
}
|
||||
],
|
||||
"slug": "carottes-rappes-with-rice-and-sunflower-seeds-feed-the-swimmers",
|
||||
"orgURL": "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds",
|
||||
"recipeYield": "Makes 12",
|
||||
"url": "https://www.bonappetit.com/recipe/macadamia-and-brown-butter-blondies",
|
||||
"slug": "macadamia-and-brown-butter-blondies",
|
||||
"orgURL": "https://www.bonappetit.com/recipe/macadamia-and-brown-butter-blondies",
|
||||
"categories": [],
|
||||
"tags": [],
|
||||
"dateAdded": null,
|
||||
|
|
Before Width: | Height: | Size: 519 KiB |
Before Width: | Height: | Size: 371 KiB |
Before Width: | Height: | Size: 259 KiB |
Before Width: | Height: | Size: 794 KiB |
Before Width: | Height: | Size: 572 KiB |
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 528 KiB |
Before Width: | Height: | Size: 452 KiB |
Before Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 393 KiB |
Before Width: | Height: | Size: 556 KiB |
Before Width: | Height: | Size: 317 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 309 KiB |
Before Width: | Height: | Size: 650 KiB |
Before Width: | Height: | Size: 889 KiB |
Before Width: | Height: | Size: 294 KiB |
Before Width: | Height: | Size: 602 KiB |
Before Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 788 KiB |
Before Width: | Height: | Size: 664 KiB |
Before Width: | Height: | Size: 985 KiB |
Before Width: | Height: | Size: 154 KiB |
Before Width: | Height: | Size: 413 KiB |
Before Width: | Height: | Size: 184 KiB |
15
mealie/db/database.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from db.db_mealplan import _Meals
|
||||
from db.db_recipes import _Recipes
|
||||
from db.db_settings import _Settings
|
||||
from db.db_themes import _Themes
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
self.recipes = _Recipes()
|
||||
self.meals = _Meals()
|
||||
self.settings = _Settings()
|
||||
self.themes = _Themes()
|
||||
|
||||
|
||||
db = Database()
|
191
mealie/db/db_base.py
Normal file
|
@ -0,0 +1,191 @@
|
|||
import json
|
||||
from typing import Union
|
||||
|
||||
import mongoengine
|
||||
from settings import USE_MONGO, USE_SQL
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class BaseDocument:
|
||||
def __init__(self) -> None:
|
||||
self.primary_key: str
|
||||
self.store: str
|
||||
self.document: mongoengine.Document
|
||||
self.sql_model: SqlAlchemyBase
|
||||
self.create_session = create_session
|
||||
|
||||
@staticmethod # TODO: Probably Put a version in each class to speed up reads?
|
||||
def _unpack_mongo(document) -> dict:
|
||||
document = json.loads(document.to_json())
|
||||
del document["_id"]
|
||||
|
||||
# Recipe Cleanup
|
||||
try:
|
||||
document["dateAdded"] = document["dateAdded"]["$date"]
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
document["uid"] = document["uid"]["$uuid"]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Meal Plan
|
||||
try:
|
||||
document["startDate"] = document["startDate"]["$date"]
|
||||
document["endDate"] = document["endDate"]["$date"]
|
||||
|
||||
meals = []
|
||||
for meal in document["meals"]:
|
||||
meal["date"] = meal["date"]["$date"]
|
||||
meals.append(meal)
|
||||
document["meals"] = meals
|
||||
except:
|
||||
pass
|
||||
|
||||
return document
|
||||
|
||||
def get_all(self, limit: int = None, order_by: str = None):
|
||||
if USE_MONGO:
|
||||
if order_by:
|
||||
documents = self.document.objects.order_by(str(order_by)).limit(limit)
|
||||
elif limit == None:
|
||||
documents = self.document.objects()
|
||||
else:
|
||||
documents = self.document.objects().limit(limit)
|
||||
|
||||
docs = [BaseDocument._unpack_mongo(item) for item in documents]
|
||||
|
||||
if limit == 1:
|
||||
return docs[0]
|
||||
return docs
|
||||
|
||||
elif USE_SQL:
|
||||
session = create_session()
|
||||
list = [x.dict() for x in session.query(self.sql_model).all()]
|
||||
session.close()
|
||||
|
||||
if limit == 1:
|
||||
return list[0]
|
||||
|
||||
return list
|
||||
|
||||
def _query_one(
|
||||
self, match_value: str, match_key: str = None
|
||||
) -> Union[Session, SqlAlchemyBase]:
|
||||
"""Query the sql database for one item an return the sql alchemy model
|
||||
object. If no match key is provided the primary_key attribute will be used.
|
||||
|
||||
Args:
|
||||
match_value (str): The value to use in the query
|
||||
match_key (str, optional): the key/property to match against. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Union[Session, SqlAlchemyBase]: Will return both the session and found model
|
||||
"""
|
||||
session = self.create_session()
|
||||
|
||||
if match_key == None:
|
||||
match_key = self.primary_key
|
||||
|
||||
result = (
|
||||
session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
|
||||
)
|
||||
|
||||
return session, result
|
||||
|
||||
def get(
|
||||
self, match_value: str, match_key: str = None, limit=1
|
||||
) -> dict or list[dict]:
|
||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||
key is provided the class objects primary key will be used to match against.
|
||||
|
||||
|
||||
Args: \n
|
||||
match_value (str): A value used to match against the key/value in the database \n
|
||||
match_key (str, optional): They key to match the value against. Defaults to None. \n
|
||||
limit (int, optional): A limit to returned responses. Defaults to 1. \n
|
||||
|
||||
Returns:
|
||||
dict or list[dict]:
|
||||
"""
|
||||
if match_key == None:
|
||||
match_key = self.primary_key
|
||||
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(**{str(match_key): match_value})
|
||||
db_entry = BaseDocument._unpack_mongo(document)
|
||||
|
||||
elif USE_SQL:
|
||||
session = self.create_session()
|
||||
result = (
|
||||
session.query(self.sql_model)
|
||||
.filter_by(**{match_key: match_value})
|
||||
.one()
|
||||
)
|
||||
db_entry = result.dict()
|
||||
session.close()
|
||||
|
||||
return db_entry
|
||||
|
||||
else:
|
||||
raise Exception("No database type established")
|
||||
|
||||
if limit == 1 and type(db_entry) == list:
|
||||
return db_entry[0]
|
||||
else:
|
||||
return db_entry
|
||||
|
||||
def save_new(self, document: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
new_document = self.document(**document)
|
||||
new_document.save()
|
||||
return BaseDocument._unpack_mongo(new_document)
|
||||
elif USE_SQL:
|
||||
session = self.create_session()
|
||||
new_document = self.sql_model(**document)
|
||||
session.add(new_document)
|
||||
return_data = new_document.dict()
|
||||
session.commit()
|
||||
|
||||
return return_data
|
||||
|
||||
def update(self, match_value, new_data) -> dict:
|
||||
if USE_MONGO:
|
||||
return_data = self.update_mongo(match_value, new_data)
|
||||
elif USE_SQL:
|
||||
session, entry = self._query_one(match_value=match_value)
|
||||
entry.update(session=session, **new_data)
|
||||
return_data = entry.dict()
|
||||
session.commit()
|
||||
|
||||
session.close()
|
||||
else:
|
||||
raise Exception("No Database Configured")
|
||||
|
||||
return return_data
|
||||
|
||||
def delete(self, primary_key_value) -> dict:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(
|
||||
**{str(self.primary_key): primary_key_value}
|
||||
)
|
||||
|
||||
if document:
|
||||
document.delete()
|
||||
elif USE_SQL:
|
||||
session = create_session()
|
||||
|
||||
result = (
|
||||
session.query(self.sql_model)
|
||||
.filter_by(**{self.primary_key: primary_key_value})
|
||||
.one()
|
||||
)
|
||||
|
||||
session.delete(result)
|
||||
|
||||
session.commit()
|
||||
session.close()
|
63
mealie/db/db_mealplan.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from typing import List
|
||||
|
||||
from settings import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.db_setup import USE_MONGO, USE_SQL
|
||||
from db.mongo.meal_models import MealDocument, MealPlanDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.meal_models import MealPlanModel
|
||||
|
||||
|
||||
class _Meals(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "uid"
|
||||
if USE_SQL:
|
||||
self.sql_model = MealPlanModel
|
||||
self.create_session = create_session
|
||||
|
||||
self.document = MealPlanDocument
|
||||
|
||||
@staticmethod
|
||||
def _process_meals(meals: List[dict]) -> List[MealDocument]:
|
||||
"""Turns a list of Meals in dictionary form into a list of
|
||||
MealDocuments that can be attached to a MealPlanDocument
|
||||
|
||||
|
||||
Args: \n
|
||||
meals (List[dict]): From a Pydantic Class in meal_services.py \n
|
||||
|
||||
Returns:
|
||||
a List of MealDocuments
|
||||
"""
|
||||
meal_docs = []
|
||||
for meal in meals:
|
||||
meal_doc = MealDocument(**meal)
|
||||
meal_docs.append(meal_doc)
|
||||
|
||||
return meal_docs
|
||||
|
||||
def save_new_mongo(self, plan_data: dict) -> None:
|
||||
"""Saves a new meal plan into the database
|
||||
|
||||
Args: \n
|
||||
plan_data (dict): From a Pydantic Class in meal_services.py \n
|
||||
"""
|
||||
|
||||
if USE_MONGO:
|
||||
plan_data["meals"] = _Meals._process_meals(plan_data["meals"])
|
||||
document = self.document(**plan_data)
|
||||
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
pass
|
||||
|
||||
def update_mongo(self, uid: str, plan_data: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(uid=uid)
|
||||
if document:
|
||||
new_meals = _Meals._process_meals(plan_data["meals"])
|
||||
document.update(set__meals=new_meals)
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
pass
|
68
mealie/db/db_recipes.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from settings import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.mongo.recipe_models import RecipeDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.recipe_models import RecipeModel
|
||||
|
||||
|
||||
class _Recipes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
if USE_SQL:
|
||||
self.sql_model = RecipeModel
|
||||
self.create_session = create_session
|
||||
else:
|
||||
self.document = RecipeDocument
|
||||
|
||||
def save_new_sql(self, recipe_data: dict):
|
||||
session = self.create_session()
|
||||
new_recipe = self.sql_model(**recipe_data)
|
||||
session.add(new_recipe)
|
||||
session.commit()
|
||||
|
||||
return recipe_data
|
||||
|
||||
def update_mongo(self, slug: str, new_data: dict) -> None:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(slug=slug)
|
||||
|
||||
if document:
|
||||
document.update(set__name=new_data.get("name"))
|
||||
document.update(set__description=new_data.get("description"))
|
||||
document.update(set__image=new_data.get("image"))
|
||||
document.update(set__recipeYield=new_data.get("recipeYield"))
|
||||
document.update(set__recipeIngredient=new_data.get("recipeIngredient"))
|
||||
document.update(
|
||||
set__recipeInstructions=new_data.get("recipeInstructions")
|
||||
)
|
||||
document.update(set__totalTime=new_data.get("totalTime"))
|
||||
|
||||
document.update(set__slug=new_data.get("slug"))
|
||||
document.update(set__categories=new_data.get("categories"))
|
||||
document.update(set__tags=new_data.get("tags"))
|
||||
document.update(set__notes=new_data.get("notes"))
|
||||
document.update(set__orgURL=new_data.get("orgURL"))
|
||||
document.update(set__rating=new_data.get("rating"))
|
||||
document.update(set__extras=new_data.get("extras"))
|
||||
document.save()
|
||||
|
||||
return new_data
|
||||
# elif USE_SQL:
|
||||
# session, recipe = self._query_one(match_value=slug)
|
||||
# recipe.update(session=session, **new_data)
|
||||
# recipe_dict = recipe.dict()
|
||||
# session.commit()
|
||||
|
||||
# session.close()
|
||||
|
||||
# return recipe_dict
|
||||
|
||||
def update_image(self, slug: str, extension: str) -> None:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(slug=slug)
|
||||
|
||||
if document:
|
||||
document.update(set__image=f"{slug}.{extension}")
|
||||
elif USE_SQL:
|
||||
pass
|
44
mealie/db/db_settings.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from settings import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.db_setup import USE_MONGO, USE_SQL
|
||||
from db.mongo.settings_models import SiteSettingsDocument, WebhooksDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.settings_models import SiteSettingsModel
|
||||
|
||||
|
||||
class _Settings(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
|
||||
self.primary_key = "name"
|
||||
|
||||
if USE_SQL:
|
||||
self.sql_model = SiteSettingsModel
|
||||
self.create_session = create_session
|
||||
|
||||
self.document = SiteSettingsDocument
|
||||
|
||||
def save_new(self, main: dict, webhooks: dict) -> str:
|
||||
|
||||
if USE_MONGO:
|
||||
main["webhooks"] = WebhooksDocument(**webhooks)
|
||||
new_doc = self.document(**main)
|
||||
return new_doc.save()
|
||||
|
||||
elif USE_SQL:
|
||||
session = create_session()
|
||||
new_settings = self.sql_model(main.get("name"), webhooks)
|
||||
|
||||
session.add(new_settings)
|
||||
session.commit()
|
||||
|
||||
return new_settings.dict()
|
||||
|
||||
def update_mongo(self, name: str, new_data: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(name=name)
|
||||
if document:
|
||||
document.update(set__webhooks=WebhooksDocument(**new_data["webhooks"]))
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
return
|
12
mealie/db/db_setup.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from settings import DATA_DIR, USE_MONGO, USE_SQL
|
||||
|
||||
from db.sql.db_session import globa_init as sql_global_init
|
||||
|
||||
if USE_SQL:
|
||||
db_file = DATA_DIR.joinpath("db", "mealie.sqlite")
|
||||
sql_global_init(db_file)
|
||||
|
||||
elif USE_MONGO:
|
||||
from db.mongo.mongo_setup import global_init as mongo_global_init
|
||||
|
||||
mongo_global_init()
|
56
mealie/db/db_themes.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from settings import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.db_setup import USE_MONGO, USE_SQL
|
||||
from db.mongo.settings_models import SiteThemeDocument, ThemeColorsDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.theme_models import SiteThemeModel
|
||||
|
||||
|
||||
class _Themes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
if USE_SQL:
|
||||
self.sql_model = SiteThemeModel
|
||||
self.create_session = create_session
|
||||
else:
|
||||
self.document = SiteThemeDocument
|
||||
|
||||
def save_new(self, theme_data: dict) -> None:
|
||||
if USE_MONGO:
|
||||
theme_data["colors"] = ThemeColorsDocument(**theme_data["colors"])
|
||||
|
||||
document = self.document(**theme_data)
|
||||
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
session = self.create_session()
|
||||
new_theme = self.sql_model(**theme_data)
|
||||
|
||||
session.add(new_theme)
|
||||
session.commit()
|
||||
|
||||
return_data = new_theme.dict()
|
||||
|
||||
session.close()
|
||||
return return_data
|
||||
|
||||
def update(self, data: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
colors = ThemeColorsDocument(**data["colors"])
|
||||
theme_document = self.document.objects.get(name=data.get("name"))
|
||||
|
||||
if theme_document:
|
||||
theme_document.update(set__colors=colors)
|
||||
theme_document.save()
|
||||
else:
|
||||
raise Exception("No database entry was found to update")
|
||||
|
||||
elif USE_SQL:
|
||||
session, theme_model = self._query_one(
|
||||
match_value=data["name"], match_key="name"
|
||||
)
|
||||
|
||||
theme_model.update(**data)
|
||||
session.commit()
|
||||
session.close()
|
|
@ -1,5 +1,6 @@
|
|||
import mongoengine
|
||||
from settings import DB_HOST, DB_PASSWORD, DB_PORT, DB_USERNAME, MEALIE_DB_NAME
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
def global_init():
|
||||
|
@ -12,3 +13,5 @@ def global_init():
|
|||
password=DB_PASSWORD,
|
||||
authentication_source="admin",
|
||||
)
|
||||
|
||||
logger.info("Mongo Data Initialized")
|
|
@ -1,5 +1,4 @@
|
|||
import datetime
|
||||
import uuid
|
||||
|
||||
import mongoengine
|
||||
|
||||
|
@ -19,7 +18,7 @@ class RecipeDocument(mongoengine.Document):
|
|||
slug = mongoengine.StringField(required=True, unique=True)
|
||||
categories = mongoengine.ListField(default=[])
|
||||
tags = mongoengine.ListField(default=[])
|
||||
dateAdded = mongoengine.DateTimeField(binary=True, default=datetime.date.today())
|
||||
dateAdded = mongoengine.DateTimeField(binary=True, default=datetime.date.today)
|
||||
notes = mongoengine.ListField(default=[])
|
||||
rating = mongoengine.IntField(required=True, default=0)
|
||||
orgURL = mongoengine.URLField(required=False)
|
6
mealie/db/mongo/user_models.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
# import mongoengine
|
||||
|
||||
# class User(mongoengine.Document):
|
||||
# username: mongoengine.EmailField()
|
||||
# password: mongoengine.ReferenceField
|
4
mealie/db/sql/_all_models.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from db.sql.meal_models import *
|
||||
from db.sql.recipe_models import *
|
||||
from db.sql.settings_models import *
|
||||
from db.sql.theme_models import *
|
30
mealie/db/sql/db_session.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from pathlib import Path
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import SqlAlchemyBase
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
__factory = None
|
||||
|
||||
|
||||
def globa_init(db_file: Path):
|
||||
global __factory
|
||||
|
||||
if __factory:
|
||||
return
|
||||
|
||||
conn_str = "sqlite:///" + str(db_file.absolute())
|
||||
|
||||
engine = sa.create_engine(conn_str, echo=False)
|
||||
|
||||
__factory = orm.sessionmaker(bind=engine)
|
||||
|
||||
import db.sql._all_models
|
||||
|
||||
SqlAlchemyBase.metadata.create_all(engine)
|
||||
|
||||
|
||||
def create_session() -> Session:
|
||||
global __factory
|
||||
return __factory()
|
66
mealie/db/sql/meal_models.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
import uuid
|
||||
from typing import List
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
|
||||
class Meal(SqlAlchemyBase):
|
||||
__tablename__ = "meal"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("mealplan.uid"))
|
||||
slug = sa.Column(sa.String)
|
||||
name = sa.Column(sa.String)
|
||||
date = sa.Column(sa.Date)
|
||||
dateText = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
description = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, slug, name, date, dateText, image, description) -> None:
|
||||
self.slug = slug
|
||||
self.name = name
|
||||
self.date = date
|
||||
self.dateText = dateText
|
||||
self.image = image
|
||||
self.description = description
|
||||
|
||||
def dict(self) -> dict:
|
||||
data = {
|
||||
"slug": self.slug,
|
||||
"name": self.name,
|
||||
"date": self.date,
|
||||
"dateText": self.dateText,
|
||||
"image": self.image,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class MealPlanModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "mealplan"
|
||||
uid = sa.Column(sa.Integer, primary_key=True, unique=True) #! Probably Bad?
|
||||
startDate = sa.Column(sa.Date)
|
||||
endDate = sa.Column(sa.Date)
|
||||
meals: List[Meal] = orm.relation(Meal)
|
||||
|
||||
def __init__(self, startDate, endDate, meals, uid=None) -> None:
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
self.meals = [Meal(**meal) for meal in meals]
|
||||
|
||||
def update(self, session, startDate, endDate, meals, uid) -> None:
|
||||
MealPlanModel._sql_remove_list(session, [Meal], uid)
|
||||
|
||||
self.__init__(startDate, endDate, meals)
|
||||
|
||||
def dict(self) -> dict:
|
||||
data = {
|
||||
"uid": self.uid,
|
||||
"startDate": self.startDate,
|
||||
"endDate": self.endDate,
|
||||
"meals": [meal.dict() for meal in self.meals],
|
||||
}
|
||||
|
||||
return data
|
22
mealie/db/sql/model_base.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from typing import List
|
||||
|
||||
import sqlalchemy.ext.declarative as dec
|
||||
|
||||
SqlAlchemyBase = dec.declarative_base()
|
||||
|
||||
|
||||
class BaseMixins:
|
||||
@staticmethod
|
||||
def _sql_remove_list(session, list_of_tables: list, parent_id):
|
||||
|
||||
for table in list_of_tables:
|
||||
session.query(table).filter_by(parent_id=parent_id).delete()
|
||||
|
||||
@staticmethod
|
||||
def _flatten_dict(list_of_dict: List[dict]):
|
||||
finalMap = {}
|
||||
for d in list_of_dict:
|
||||
|
||||
finalMap.update(d.dict())
|
||||
|
||||
return finalMap
|
244
mealie/db/sql/recipe_models.py
Normal file
|
@ -0,0 +1,244 @@
|
|||
import datetime
|
||||
from datetime import date
|
||||
from typing import List
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
|
||||
|
||||
class ApiExtras(SqlAlchemyBase):
|
||||
__tablename__ = "api_extras"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
key_name = sa.Column(sa.String, unique=True)
|
||||
value = sa.Column(sa.String)
|
||||
|
||||
def __init__(self, key, value) -> None:
|
||||
self.key_name = key
|
||||
self.value = value
|
||||
|
||||
def dict(self):
|
||||
return {self.key_name: self.value}
|
||||
|
||||
|
||||
class Category(SqlAlchemyBase):
|
||||
__tablename__ = "categories"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
name = sa.Column(sa.String, index=True)
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Tag(SqlAlchemyBase):
|
||||
__tablename__ = "tags"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
name = sa.Column(sa.String, index=True)
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Note(SqlAlchemyBase):
|
||||
__tablename__ = "notes"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
title = sa.Column(sa.String)
|
||||
text = sa.Column(sa.String)
|
||||
|
||||
def dict(self):
|
||||
return {"title": self.title, "text": self.text}
|
||||
|
||||
|
||||
class RecipeIngredient(SqlAlchemyBase):
|
||||
__tablename__ = "recipes_ingredients"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
position = sa.Column(sa.Integer)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
ingredient = sa.Column(sa.String)
|
||||
|
||||
def update(self, ingredient):
|
||||
self.ingredient = ingredient
|
||||
|
||||
def to_str(self):
|
||||
return self.ingredient
|
||||
|
||||
|
||||
class RecipeInstruction(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_instructions"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||
position = sa.Column(sa.Integer)
|
||||
type = sa.Column(sa.String, default="")
|
||||
text = sa.Column(sa.String)
|
||||
|
||||
def dict(self):
|
||||
data = {"@type": self.type, "text": self.text}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipes"
|
||||
# Database Specific
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
|
||||
# General Recipe Properties
|
||||
name = sa.Column(sa.String)
|
||||
description = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
recipeYield = sa.Column(sa.String)
|
||||
recipeIngredient: List[RecipeIngredient] = orm.relationship(
|
||||
"RecipeIngredient",
|
||||
cascade="all, delete",
|
||||
order_by="RecipeIngredient.position",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
recipeInstructions: List[RecipeInstruction] = orm.relationship(
|
||||
"RecipeInstruction",
|
||||
cascade="all, delete",
|
||||
order_by="RecipeInstruction.position",
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
|
||||
# How to Properties
|
||||
totalTime = sa.Column(sa.String)
|
||||
prepTime = sa.Column(sa.String)
|
||||
performTime = sa.Column(sa.String)
|
||||
|
||||
# Mealie Specific
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
categories: List[Category] = orm.relationship(
|
||||
"Category",
|
||||
cascade="all, delete",
|
||||
)
|
||||
tags: List[Tag] = orm.relationship(
|
||||
"Tag",
|
||||
cascade="all, delete",
|
||||
)
|
||||
dateAdded = sa.Column(sa.Date, default=date.today)
|
||||
notes: List[Note] = orm.relationship(
|
||||
"Note",
|
||||
cascade="all, delete",
|
||||
)
|
||||
rating = sa.Column(sa.Integer)
|
||||
orgURL = sa.Column(sa.String)
|
||||
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
totalTime: str = None,
|
||||
prepTime: str = None,
|
||||
performTime: str = None,
|
||||
slug: str = None,
|
||||
categories: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.image = image
|
||||
self.recipeYield = recipeYield
|
||||
self.recipeIngredient = [
|
||||
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
|
||||
]
|
||||
self.recipeInstructions = [
|
||||
RecipeInstruction(text=instruc.get("text"), type=instruc.get("text"))
|
||||
for instruc in recipeInstructions
|
||||
]
|
||||
self.totalTime = totalTime
|
||||
self.prepTime = prepTime
|
||||
self.performTime = performTime
|
||||
|
||||
# Mealie Specific
|
||||
self.slug = slug
|
||||
self.categories = [Category(name=cat) for cat in categories]
|
||||
self.tags = [Tag(name=tag) for tag in tags]
|
||||
self.dateAdded = dateAdded
|
||||
self.notes = [Note(note) for note in notes]
|
||||
self.rating = rating
|
||||
self.orgURL = orgURL
|
||||
self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()]
|
||||
|
||||
def update(
|
||||
self,
|
||||
session,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
totalTime: str = None,
|
||||
prepTime: str = None,
|
||||
performTime: str = None,
|
||||
slug: str = None,
|
||||
categories: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
list_of_tables = [RecipeIngredient, RecipeInstruction, Category, Tag, ApiExtras]
|
||||
RecipeModel._sql_remove_list(session, list_of_tables, self.id)
|
||||
|
||||
self.__init__(
|
||||
name=name,
|
||||
description=description,
|
||||
image=image,
|
||||
recipeYield=recipeYield,
|
||||
recipeIngredient=recipeIngredient,
|
||||
recipeInstructions=recipeInstructions,
|
||||
totalTime=totalTime,
|
||||
prepTime=prepTime,
|
||||
performTime=performTime,
|
||||
slug=slug,
|
||||
categories=categories,
|
||||
tags=tags,
|
||||
dateAdded=dateAdded,
|
||||
notes=notes,
|
||||
rating=rating,
|
||||
orgURL=orgURL,
|
||||
extras=extras,
|
||||
)
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"image": self.image,
|
||||
"recipeYield": self.recipeYield,
|
||||
"recipeIngredient": [x.to_str() for x in self.recipeIngredient],
|
||||
"recipeInstructions": [x.dict() for x in self.recipeInstructions],
|
||||
"totalTime": self.totalTime,
|
||||
"prepTime": self.prepTime,
|
||||
"performTime": self.performTime,
|
||||
# Mealie
|
||||
"slug": self.slug,
|
||||
"categories": [x.to_str() for x in self.categories],
|
||||
"tags": [x.to_str() for x in self.tags],
|
||||
"dateAdded": self.dateAdded,
|
||||
"notes": [x.dict() for x in self.notes],
|
||||
"rating": self.rating,
|
||||
"orgURL": self.orgURL,
|
||||
"extras": RecipeModel._flatten_dict(self.extras),
|
||||
}
|
||||
|
||||
return data
|
67
mealie/db/sql/settings_models.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
|
||||
class SiteSettingsModel(SqlAlchemyBase):
|
||||
__tablename__ = "site_settings"
|
||||
name = sa.Column(sa.String, primary_key=True)
|
||||
webhooks = orm.relationship("WebHookModel", uselist=False, cascade="all, delete")
|
||||
|
||||
def __init__(self, name: str = None, webhooks: dict = None) -> None:
|
||||
self.name = name
|
||||
self.webhooks = WebHookModel(**webhooks)
|
||||
|
||||
def update(self, session, name, webhooks: dict) -> dict:
|
||||
self.name = name
|
||||
self.webhooks.update(session=session, **webhooks)
|
||||
return
|
||||
|
||||
def dict(self):
|
||||
data = {"name": self.name, "webhooks": self.webhooks.dict()}
|
||||
return data
|
||||
|
||||
|
||||
class WebHookModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "webhook_settings"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_settings.name"))
|
||||
webhookURLs = orm.relationship(
|
||||
"WebhookURLModel", uselist=True, cascade="all, delete"
|
||||
)
|
||||
webhookTime = sa.Column(sa.String, default="00:00")
|
||||
enabled = sa.Column(sa.Boolean, default=False)
|
||||
|
||||
def __init__(
|
||||
self, webhookURLs: list, webhookTime: str, enabled: bool = False
|
||||
) -> None:
|
||||
|
||||
self.webhookURLs = [WebhookURLModel(url=x) for x in webhookURLs]
|
||||
self.webhookTime = webhookTime
|
||||
self.enabled = enabled
|
||||
|
||||
def update(
|
||||
self, session, webhookURLs: list, webhookTime: str, enabled: bool
|
||||
) -> None:
|
||||
|
||||
self._sql_remove_list(session, [WebhookURLModel], self.id)
|
||||
|
||||
self.__init__(webhookURLs, webhookTime, enabled)
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"webhookURLs": [url.to_str() for url in self.webhookURLs],
|
||||
"webhookTime": self.webhookTime,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class WebhookURLModel(SqlAlchemyBase):
|
||||
__tablename__ = "webhook_urls"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
url = sa.Column(sa.String)
|
||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("webhook_settings.id"))
|
||||
|
||||
def to_str(self):
|
||||
return self.url
|
64
mealie/db/sql/theme_models.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
|
||||
class SiteThemeModel(SqlAlchemyBase):
|
||||
__tablename__ = "site_theme"
|
||||
name = sa.Column(sa.String, primary_key=True)
|
||||
colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete")
|
||||
|
||||
def __init__(self, name: str, colors: dict) -> None:
|
||||
self.name = name
|
||||
self.colors = ThemeColorsModel(**colors)
|
||||
|
||||
def update(self, name, colors: dict) -> dict:
|
||||
self.colors.update(**colors)
|
||||
return self.dict()
|
||||
|
||||
def dict(self):
|
||||
data = {"name": self.name, "colors": self.colors.dict()}
|
||||
return data
|
||||
|
||||
|
||||
class ThemeColorsModel(SqlAlchemyBase):
|
||||
__tablename__ = "theme_colors"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
|
||||
primary = sa.Column(sa.String)
|
||||
accent = sa.Column(sa.String)
|
||||
secondary = sa.Column(sa.String)
|
||||
success = sa.Column(sa.String)
|
||||
info = sa.Column(sa.String)
|
||||
warning = sa.Column(sa.String)
|
||||
error = sa.Column(sa.String)
|
||||
|
||||
def update(
|
||||
self,
|
||||
primary: str = None,
|
||||
accent: str = None,
|
||||
secondary: str = None,
|
||||
success: str = None,
|
||||
info: str = None,
|
||||
warning: str = None,
|
||||
error: str = None,
|
||||
) -> None:
|
||||
self.primary = primary
|
||||
self.accent = accent
|
||||
self.secondary = secondary
|
||||
self.success = success
|
||||
self.info = info
|
||||
self.warning = warning
|
||||
self.error = error
|
||||
|
||||
def dict(self):
|
||||
data = {
|
||||
"primary": self.primary,
|
||||
"accent": self.accent,
|
||||
"secondary": self.secondary,
|
||||
"success": self.success,
|
||||
"info": self.info,
|
||||
"warning": self.warning,
|
||||
"error": self.error,
|
||||
}
|
||||
return data
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
import mongoengine
|
||||
|
||||
class User(mongoengine.Document):
|
||||
username: mongoengine.EmailField()
|
||||
# password: mongoengine.ReferenceField
|
|
@ -5,7 +5,7 @@ from pydantic import BaseModel
|
|||
|
||||
class BackupJob(BaseModel):
|
||||
tag: Optional[str]
|
||||
template: Optional[str]
|
||||
template: Optional[List[str]]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from models.backup_models import BackupJob, Imports
|
||||
from services.backup_services import (
|
||||
BACKUP_DIR,
|
||||
TEMPLATE_DIR,
|
||||
export_db,
|
||||
import_from_archive,
|
||||
)
|
||||
from services.backups.exports import backup_all
|
||||
from services.backups.imports import ImportDatabase
|
||||
from settings import BACKUP_DIR, TEMPLATE_DIR
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/backups/available/", tags=["Import / Export"], response_model=Imports)
|
||||
async def available_imports():
|
||||
def available_imports():
|
||||
"""Returns a list of avaiable .zip files for import into Mealie."""
|
||||
imports = []
|
||||
templates = []
|
||||
|
@ -26,26 +23,32 @@ async def available_imports():
|
|||
|
||||
|
||||
@router.post("/api/backups/export/database/", tags=["Import / Export"], status_code=201)
|
||||
async def export_database(data: BackupJob):
|
||||
def export_database(data: BackupJob):
|
||||
"""Generates a backup of the recipe database in json format."""
|
||||
|
||||
export_path = backup_all(data.tag, data.template)
|
||||
try:
|
||||
export_path = export_db(data.tag, data.template)
|
||||
return SnackResponse.success("Backup Created at " + export_path)
|
||||
except:
|
||||
HTTPException(
|
||||
status_code=400,
|
||||
detail=SnackResponse.error("Error Creating Backup. See Log File"),
|
||||
)
|
||||
|
||||
return SnackResponse.success("Backup Created at " + export_path)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/backups/{file_name}/import/", tags=["Import / Export"], status_code=200
|
||||
)
|
||||
async def import_database(file_name: str):
|
||||
def import_database(file_name: str):
|
||||
""" Import a database backup file generated from Mealie. """
|
||||
imported = import_from_archive(file_name)
|
||||
|
||||
import_db = ImportDatabase(
|
||||
zip_archive=file_name,
|
||||
import_recipes=True,
|
||||
import_settings=False,
|
||||
import_themes=False,
|
||||
)
|
||||
|
||||
imported = import_db.run()
|
||||
return imported
|
||||
|
||||
|
||||
|
@ -54,7 +57,7 @@ async def import_database(file_name: str):
|
|||
tags=["Import / Export"],
|
||||
status_code=200,
|
||||
)
|
||||
async def delete_backup(backup_name: str):
|
||||
def delete_backup(backup_name: str):
|
||||
""" Removes a database backup from the file system """
|
||||
|
||||
try:
|
||||
|
|
|
@ -9,14 +9,14 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/api/meal-plan/all/", tags=["Meal Plan"], response_model=List[MealPlan])
|
||||
async def get_all_meals():
|
||||
def get_all_meals():
|
||||
""" Returns a list of all available Meal Plan """
|
||||
|
||||
return MealPlan.get_all()
|
||||
|
||||
|
||||
@router.post("/api/meal-plan/create/", tags=["Meal Plan"])
|
||||
async def set_meal_plan(data: MealPlan):
|
||||
def set_meal_plan(data: MealPlan):
|
||||
""" Creates a meal plan database entry """
|
||||
data.process_meals()
|
||||
data.save_to_db()
|
||||
|
@ -30,23 +30,24 @@ async def set_meal_plan(data: MealPlan):
|
|||
|
||||
|
||||
@router.post("/api/meal-plan/{plan_id}/update/", tags=["Meal Plan"])
|
||||
async def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
||||
def update_meal_plan(plan_id: str, meal_plan: MealPlan):
|
||||
""" Updates a meal plan based off ID """
|
||||
|
||||
try:
|
||||
meal_plan.process_meals()
|
||||
meal_plan.update(plan_id)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=SnackResponse.error("Unable to Update Mealplan"),
|
||||
)
|
||||
meal_plan.process_meals()
|
||||
meal_plan.update(plan_id)
|
||||
# try:
|
||||
# meal_plan.process_meals()
|
||||
# meal_plan.update(plan_id)
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=404,
|
||||
# detail=SnackResponse.error("Unable to Update Mealplan"),
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Mealplan Updated")
|
||||
|
||||
|
||||
@router.delete("/api/meal-plan/{plan_id}/delete/", tags=["Meal Plan"])
|
||||
async def delete_meal_plan(plan_id):
|
||||
def delete_meal_plan(plan_id):
|
||||
""" Removes a meal plan from the database """
|
||||
|
||||
MealPlan.delete(plan_id)
|
||||
|
@ -58,7 +59,7 @@ async def delete_meal_plan(plan_id):
|
|||
"/api/meal-plan/today/",
|
||||
tags=["Meal Plan"],
|
||||
)
|
||||
async def get_today():
|
||||
def get_today():
|
||||
"""
|
||||
Returns the recipe slug for the meal scheduled for today.
|
||||
If no meal is scheduled nothing is returned
|
||||
|
@ -68,7 +69,7 @@ async def get_today():
|
|||
|
||||
|
||||
@router.get("/api/meal-plan/this-week/", tags=["Meal Plan"], response_model=MealPlan)
|
||||
async def get_this_week():
|
||||
def get_this_week():
|
||||
""" Returns the meal plan data for this week """
|
||||
|
||||
return MealPlan.this_week()
|
||||
|
|
|
@ -12,7 +12,7 @@ router = APIRouter()
|
|||
|
||||
# Chowdown
|
||||
@router.post("/api/migration/chowdown/repo/", tags=["Migration"])
|
||||
async def import_chowdown_recipes(repo: ChowdownURL):
|
||||
def import_chowdown_recipes(repo: ChowdownURL):
|
||||
""" Import Chowsdown Recipes from Repo URL """
|
||||
try:
|
||||
report = chowdow_migrate(repo.url)
|
||||
|
@ -31,7 +31,7 @@ async def import_chowdown_recipes(repo: ChowdownURL):
|
|||
|
||||
# Nextcloud
|
||||
@router.get("/api/migration/nextcloud/available/", tags=["Migration"])
|
||||
async def get_avaiable_nextcloud_imports():
|
||||
def get_avaiable_nextcloud_imports():
|
||||
""" Returns a list of avaiable directories that can be imported into Mealie """
|
||||
available = []
|
||||
for dir in MIGRATION_DIR.iterdir():
|
||||
|
@ -44,14 +44,14 @@ async def get_avaiable_nextcloud_imports():
|
|||
|
||||
|
||||
@router.post("/api/migration/nextcloud/{selection}/import/", tags=["Migration"])
|
||||
async def import_nextcloud_directory(selection: str):
|
||||
def import_nextcloud_directory(selection: str):
|
||||
""" Imports all the recipes in a given directory """
|
||||
|
||||
return nextcloud_migrate(selection)
|
||||
|
||||
|
||||
@router.delete("/api/migration/{file_folder_name}/delete/", tags=["Migration"])
|
||||
async def delete_migration_data(file_folder_name: str):
|
||||
def delete_migration_data(file_folder_name: str):
|
||||
""" Removes migration data from the file system """
|
||||
|
||||
remove_path = MIGRATION_DIR.joinpath(file_folder_name)
|
||||
|
@ -67,7 +67,7 @@ async def delete_migration_data(file_folder_name: str):
|
|||
|
||||
|
||||
@router.post("/api/migration/upload/", tags=["Migration"])
|
||||
async def upload_nextcloud_zipfile(archive: UploadFile = File(...)):
|
||||
def upload_nextcloud_zipfile(archive: UploadFile = File(...)):
|
||||
""" Upload a .zip File to later be imported into Mealie """
|
||||
dest = MIGRATION_DIR.joinpath(archive.filename)
|
||||
|
||||
|
|
|
@ -12,9 +12,7 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/api/all-recipes/", tags=["Recipes"], response_model=List[dict])
|
||||
async def get_all_recipes(
|
||||
keys: Optional[List[str]] = Query(...), num: Optional[int] = 100
|
||||
):
|
||||
def get_all_recipes(keys: Optional[List[str]] = Query(...), num: Optional[int] = 100):
|
||||
"""
|
||||
Returns key data for all recipes based off the query paramters provided.
|
||||
For example, if slug, image, and name are provided you will recieve a list of
|
||||
|
@ -31,7 +29,7 @@ async def get_all_recipes(
|
|||
|
||||
|
||||
@router.post("/api/all-recipes/", tags=["Recipes"], response_model=List[dict])
|
||||
async def get_all_recipes_post(body: AllRecipeRequest):
|
||||
def get_all_recipes_post(body: AllRecipeRequest):
|
||||
"""
|
||||
Returns key data for all recipes based off the body data provided.
|
||||
For example, if slug, image, and name are provided you will recieve a list of
|
||||
|
@ -47,7 +45,7 @@ async def get_all_recipes_post(body: AllRecipeRequest):
|
|||
|
||||
|
||||
@router.get("/api/recipe/{recipe_slug}/", tags=["Recipes"], response_model=Recipe)
|
||||
async def get_recipe(recipe_slug: str):
|
||||
def get_recipe(recipe_slug: str):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
recipe = Recipe.get_by_slug(recipe_slug)
|
||||
|
||||
|
@ -55,7 +53,7 @@ async def get_recipe(recipe_slug: str):
|
|||
|
||||
|
||||
@router.get("/api/recipe/image/{recipe_slug}/", tags=["Recipes"])
|
||||
async def get_recipe_img(recipe_slug: str):
|
||||
def get_recipe_img(recipe_slug: str):
|
||||
""" Takes in a recipe slug, returns the static image """
|
||||
recipe_image = read_image(recipe_slug)
|
||||
|
||||
|
@ -69,7 +67,7 @@ async def get_recipe_img(recipe_slug: str):
|
|||
status_code=201,
|
||||
response_model=str,
|
||||
)
|
||||
async def parse_recipe_url(url: RecipeURLIn):
|
||||
def parse_recipe_url(url: RecipeURLIn):
|
||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||
|
||||
slug = create_from_url(url.url)
|
||||
|
@ -78,7 +76,7 @@ async def parse_recipe_url(url: RecipeURLIn):
|
|||
|
||||
|
||||
@router.post("/api/recipe/create/", tags=["Recipes"])
|
||||
async def create_from_json(data: Recipe) -> str:
|
||||
def create_from_json(data: Recipe) -> str:
|
||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||
created_recipe = data.save_to_db()
|
||||
|
||||
|
@ -91,21 +89,22 @@ def update_recipe_image(
|
|||
):
|
||||
""" Removes an existing image and replaces it with the incoming file. """
|
||||
response = write_image(recipe_slug, image, extension)
|
||||
Recipe.update_image(recipe_slug, extension)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/api/recipe/{recipe_slug}/update/", tags=["Recipes"])
|
||||
async def update_recipe(recipe_slug: str, data: Recipe):
|
||||
""" Updates a recipe by existing slug and data. Data should containt """
|
||||
def update_recipe(recipe_slug: str, data: Recipe):
|
||||
""" Updates a recipe by existing slug and data. """
|
||||
|
||||
data.update(recipe_slug)
|
||||
new_slug = data.update(recipe_slug)
|
||||
|
||||
return {"message": "PLACEHOLDER"}
|
||||
return new_slug
|
||||
|
||||
|
||||
@router.delete("/api/recipe/{recipe_slug}/delete/", tags=["Recipes"])
|
||||
async def delete_recipe(recipe_slug: str):
|
||||
def delete_recipe(recipe_slug: str):
|
||||
""" Deletes a recipe by slug """
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,99 +1,91 @@
|
|||
from typing import List
|
||||
|
||||
from db.mongo_setup import global_init
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from services.scheduler_services import Scheduler, post_webhooks
|
||||
from services.scheduler_services import post_webhooks
|
||||
from services.settings_services import SiteSettings, SiteTheme
|
||||
from utils.global_scheduler import scheduler
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter()
|
||||
global_init()
|
||||
|
||||
scheduler = Scheduler()
|
||||
scheduler.startup_scheduler()
|
||||
|
||||
|
||||
@router.get("/api/site-settings/", tags=["Settings"])
|
||||
async def get_main_settings():
|
||||
def get_main_settings():
|
||||
""" Returns basic site settings """
|
||||
|
||||
return SiteSettings.get_site_settings()
|
||||
|
||||
|
||||
@router.post("/api/site-settings/webhooks/test/", tags=["Settings"])
|
||||
async def test_webhooks():
|
||||
def test_webhooks():
|
||||
""" Run the function to test your webhooks """
|
||||
|
||||
return post_webhooks()
|
||||
|
||||
|
||||
@router.post("/api/site-settings/update/", tags=["Settings"])
|
||||
async def update_settings(data: SiteSettings):
|
||||
def update_settings(data: SiteSettings):
|
||||
""" Returns Site Settings """
|
||||
|
||||
try:
|
||||
data.update()
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=SnackResponse.error("Unable to Save Settings")
|
||||
)
|
||||
data.update()
|
||||
# try:
|
||||
# data.update()
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
|
||||
# )
|
||||
|
||||
scheduler.reschedule_webhooks()
|
||||
return SnackResponse.success("Settings Updated")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/site-settings/themes/", tags=["Themes"]
|
||||
)
|
||||
async def get_all_themes():
|
||||
@router.get("/api/site-settings/themes/", tags=["Themes"])
|
||||
def get_all_themes():
|
||||
""" Returns all site themes """
|
||||
|
||||
return SiteTheme.get_all()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/site-settings/themes/{theme_name}/", tags=["Themes"]
|
||||
)
|
||||
async def get_single_theme(theme_name: str):
|
||||
@router.get("/api/site-settings/themes/{theme_name}/", tags=["Themes"])
|
||||
def get_single_theme(theme_name: str):
|
||||
""" Returns a named theme """
|
||||
return SiteTheme.get_by_name(theme_name)
|
||||
|
||||
|
||||
@router.post("/api/site-settings/themes/create/", tags=["Themes"])
|
||||
async def create_theme(data: SiteTheme):
|
||||
def create_theme(data: SiteTheme):
|
||||
""" Creates a site color theme database entry """
|
||||
|
||||
try:
|
||||
data.save_to_db()
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=SnackResponse.error("Unable to Save Theme")
|
||||
)
|
||||
data.save_to_db()
|
||||
# try:
|
||||
# data.save_to_db()
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Save Theme")
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Theme Saved")
|
||||
|
||||
|
||||
@router.post("/api/site-settings/themes/{theme_name}/update/", tags=["Themes"])
|
||||
async def update_theme(theme_name: str, data: SiteTheme):
|
||||
def update_theme(theme_name: str, data: SiteTheme):
|
||||
""" Update a theme database entry """
|
||||
try:
|
||||
data.update_document()
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=SnackResponse.error("Unable to Update Theme")
|
||||
)
|
||||
data.update_document()
|
||||
|
||||
# try:
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Update Theme")
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Theme Updated")
|
||||
|
||||
|
||||
@router.delete("/api/site-settings/themes/{theme_name}/delete/", tags=["Themes"])
|
||||
async def delete_theme(theme_name: str):
|
||||
def delete_theme(theme_name: str):
|
||||
""" Deletes theme from the database """
|
||||
try:
|
||||
SiteTheme.delete_theme(theme_name)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=SnackResponse.error("Unable to Delete Theme")
|
||||
)
|
||||
SiteTheme.delete_theme(theme_name)
|
||||
# try:
|
||||
# SiteTheme.delete_theme(theme_name)
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Delete Theme")
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Theme Deleted")
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from db.recipe_models import RecipeDocument
|
||||
from jinja2 import Template
|
||||
from utils.logger import logger
|
||||
|
||||
from services.recipe_services import IMG_DIR
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
BACKUP_DIR = CWD.parent.joinpath("data", "backups")
|
||||
TEMPLATE_DIR = CWD.parent.joinpath("data", "templates")
|
||||
TEMP_DIR = CWD.parent.joinpath("data", "temp")
|
||||
|
||||
|
||||
def auto_backup_job():
|
||||
for backup in BACKUP_DIR.glob("Auto*.zip"):
|
||||
backup.unlink()
|
||||
|
||||
templates = []
|
||||
for template in TEMPLATE_DIR.iterdir():
|
||||
templates.append(template)
|
||||
|
||||
export_db(tag="Auto", templates=templates)
|
||||
logger.info("Auto Backup Called")
|
||||
|
||||
|
||||
def import_migration(recipe_dict: dict) -> dict:
|
||||
del recipe_dict["_id"]
|
||||
del recipe_dict["dateAdded"]
|
||||
|
||||
# Migration from list to Object Type Data
|
||||
if type(recipe_dict["extras"]) == list:
|
||||
recipe_dict["extras"] = {}
|
||||
|
||||
return recipe_dict
|
||||
|
||||
|
||||
def import_from_archive(file_name: str) -> list:
|
||||
successful_imports = []
|
||||
failed_imports = []
|
||||
|
||||
file_path = BACKUP_DIR.joinpath(file_name)
|
||||
|
||||
with zipfile.ZipFile(file_path, "r") as zip_ref:
|
||||
zip_ref.extractall(TEMP_DIR)
|
||||
|
||||
recipe_dir = TEMP_DIR.joinpath("recipes")
|
||||
for recipe in recipe_dir.glob("*.json"):
|
||||
with open(recipe, "r") as f:
|
||||
recipe_dict = json.loads(f.read())
|
||||
|
||||
try:
|
||||
recipe_dict = import_migration(recipe_dict)
|
||||
recipeDoc = RecipeDocument(**recipe_dict)
|
||||
recipeDoc.save()
|
||||
successful_imports.append(recipe.stem)
|
||||
except:
|
||||
logger.info(f"Failed Import: {recipe.stem}")
|
||||
failed_imports.append(recipe.stem)
|
||||
|
||||
image_dir = TEMP_DIR.joinpath("images")
|
||||
for image in image_dir.iterdir():
|
||||
if image.stem in successful_imports:
|
||||
shutil.copy(image, IMG_DIR)
|
||||
|
||||
shutil.rmtree(TEMP_DIR)
|
||||
|
||||
return {"successful": successful_imports, "failed": failed_imports}
|
||||
|
||||
|
||||
def export_db(tag=None, templates=None):
|
||||
if tag:
|
||||
export_tag = tag + "_" + datetime.now().strftime("%Y-%b-%d")
|
||||
else:
|
||||
export_tag = datetime.now().strftime("%Y-%b-%d")
|
||||
|
||||
backup_folder = TEMP_DIR.joinpath(export_tag)
|
||||
backup_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
img_folder = backup_folder.joinpath("images")
|
||||
img_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
recipe_folder = backup_folder.joinpath("recipes")
|
||||
recipe_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
export_images(img_folder)
|
||||
|
||||
if type(templates) == list:
|
||||
for template in templates:
|
||||
export_recipes(recipe_folder, template)
|
||||
elif type(templates) == str:
|
||||
export_recipes(recipe_folder, templates)
|
||||
else:
|
||||
export_recipes(recipe_folder)
|
||||
|
||||
zip_path = BACKUP_DIR.joinpath(f"{export_tag}")
|
||||
shutil.make_archive(zip_path, "zip", backup_folder)
|
||||
|
||||
shutil.rmtree(backup_folder)
|
||||
shutil.rmtree(TEMP_DIR)
|
||||
|
||||
return str(zip_path.absolute()) + ".zip"
|
||||
|
||||
|
||||
def export_images(dest_dir) -> Path:
|
||||
for file in IMG_DIR.iterdir():
|
||||
shutil.copy(file, dest_dir.joinpath(file.name))
|
||||
|
||||
|
||||
def export_recipes(dest_dir: Path, template=None) -> Path:
|
||||
all_recipes = RecipeDocument.objects()
|
||||
logger.info(f"Backing Up Recipes: {all_recipes}")
|
||||
for recipe in all_recipes:
|
||||
json_recipe = recipe.to_json(indent=4)
|
||||
|
||||
if template:
|
||||
md_dest = dest_dir.parent.joinpath("templates")
|
||||
md_dest.mkdir(parents=True, exist_ok=True)
|
||||
template = TEMPLATE_DIR.joinpath(template)
|
||||
export_markdown(md_dest, json_recipe, template)
|
||||
|
||||
filename = recipe.slug + ".json"
|
||||
file_path = dest_dir.joinpath(filename)
|
||||
|
||||
with open(file_path, "w") as f:
|
||||
f.write(json_recipe)
|
||||
|
||||
|
||||
def export_markdown(dest_dir: Path, recipe_data: json, template=Path) -> Path:
|
||||
recipe_data: dict = json.loads(recipe_data)
|
||||
recipe_template = TEMPLATE_DIR.joinpath("recipes.md")
|
||||
|
||||
with open(recipe_template, "r") as f:
|
||||
template = Template(f.read())
|
||||
|
||||
out_file = dest_dir.joinpath(recipe_data["slug"] + ".md")
|
||||
|
||||
content = template.render(recipe=recipe_data)
|
||||
|
||||
with open(out_file, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
147
mealie/services/backups/exports.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import Template
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings, SiteTheme
|
||||
from settings import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
class ExportDatabase:
|
||||
def __init__(self, tag=None, templates=None) -> None:
|
||||
"""Export a Mealie database. Export interacts directly with class objects and can be used
|
||||
with any supported backend database platform. By default tags are timestands, and no Jinja2 templates are rendered
|
||||
|
||||
|
||||
Args:
|
||||
tag ([str], optional): A str to be used as a file tag. Defaults to None.
|
||||
templates (list, optional): A list of template file names. Defaults to None.
|
||||
"""
|
||||
if tag:
|
||||
export_tag = tag + "_" + datetime.now().strftime("%Y-%b-%d")
|
||||
else:
|
||||
export_tag = datetime.now().strftime("%Y-%b-%d")
|
||||
|
||||
self.main_dir = TEMP_DIR.joinpath(export_tag)
|
||||
self.img_dir = self.main_dir.joinpath("images")
|
||||
self.recipe_dir = self.main_dir.joinpath("recipes")
|
||||
self.themes_dir = self.main_dir.joinpath("themes")
|
||||
self.settings_dir = self.main_dir.joinpath("settings")
|
||||
self.templates_dir = self.main_dir.joinpath("templates")
|
||||
self.mealplans_dir = self.main_dir.joinpath("mealplans")
|
||||
|
||||
try:
|
||||
self.templates = [TEMPLATE_DIR.joinpath(x) for x in templates]
|
||||
except:
|
||||
self.templates = False
|
||||
logger.info("No Jinja2 Templates Registered for Export")
|
||||
|
||||
required_dirs = [
|
||||
self.main_dir,
|
||||
self.img_dir,
|
||||
self.recipe_dir,
|
||||
self.themes_dir,
|
||||
self.settings_dir,
|
||||
self.templates_dir,
|
||||
self.mealplans_dir,
|
||||
]
|
||||
|
||||
for dir in required_dirs:
|
||||
dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def export_recipes(self):
|
||||
all_recipes = Recipe.get_all()
|
||||
|
||||
for recipe in all_recipes:
|
||||
logger.info(f"Backing Up Recipes: {recipe}")
|
||||
|
||||
filename = recipe.get("slug") + ".json"
|
||||
file_path = self.recipe_dir.joinpath(filename)
|
||||
|
||||
ExportDatabase._write_json_file(recipe, file_path)
|
||||
|
||||
if self.templates:
|
||||
self._export_template(recipe)
|
||||
|
||||
def _export_template(self, recipe_data: dict):
|
||||
for template_path in self.templates:
|
||||
|
||||
with open(template_path, "r") as f:
|
||||
template = Template(f.read())
|
||||
|
||||
filename = recipe_data.get("name") + template_path.suffix
|
||||
out_file = self.templates_dir.joinpath(filename)
|
||||
|
||||
content = template.render(recipe=recipe_data)
|
||||
|
||||
with open(out_file, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
def export_images(self):
|
||||
for file in IMG_DIR.iterdir():
|
||||
shutil.copy(file, self.img_dir.joinpath(file.name))
|
||||
|
||||
def export_settings(self):
|
||||
all_settings = SiteSettings.get_site_settings()
|
||||
out_file = self.settings_dir.joinpath("settings.json")
|
||||
ExportDatabase._write_json_file(all_settings.dict(), out_file)
|
||||
|
||||
def export_themes(self):
|
||||
all_themes = SiteTheme.get_all()
|
||||
if all_themes:
|
||||
all_themes = [x.dict() for x in all_themes]
|
||||
out_file = self.themes_dir.joinpath("themes.json")
|
||||
ExportDatabase._write_json_file(all_themes, out_file)
|
||||
|
||||
def export_meals(
|
||||
self,
|
||||
): #! Problem Parseing Datetime Objects... May come back to this
|
||||
meal_plans = MealPlan.get_all()
|
||||
if meal_plans:
|
||||
meal_plans = [x.dict() for x in meal_plans]
|
||||
|
||||
out_file = self.mealplans_dir.joinpath("mealplans.json")
|
||||
ExportDatabase._write_json_file(meal_plans, out_file)
|
||||
|
||||
@staticmethod
|
||||
def _write_json_file(data, out_file: Path):
|
||||
json_data = json.dumps(data, indent=4, default=str)
|
||||
|
||||
with open(out_file, "w") as f:
|
||||
f.write(json_data)
|
||||
|
||||
def finish_export(self):
|
||||
zip_path = BACKUP_DIR.joinpath(f"{self.main_dir.name}")
|
||||
shutil.make_archive(zip_path, "zip", self.main_dir)
|
||||
|
||||
shutil.rmtree(TEMP_DIR)
|
||||
|
||||
return str(zip_path.absolute()) + ".zip"
|
||||
|
||||
|
||||
def backup_all(tag=None, templates=None):
|
||||
db_export = ExportDatabase(tag=tag, templates=templates)
|
||||
|
||||
db_export.export_recipes()
|
||||
db_export.export_images()
|
||||
db_export.export_settings()
|
||||
db_export.export_themes()
|
||||
db_export.export_meals()
|
||||
#
|
||||
return db_export.finish_export()
|
||||
|
||||
|
||||
def auto_backup_job():
|
||||
for backup in BACKUP_DIR.glob("Auto*.zip"):
|
||||
backup.unlink()
|
||||
|
||||
templates = []
|
||||
for template in TEMPLATE_DIR.iterdir():
|
||||
templates.append(template)
|
||||
|
||||
backup_all(tag="Auto", templates=templates)
|
||||
logger.info("Auto Backup Called")
|
138
mealie/services/backups/imports.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings, SiteTheme
|
||||
from settings import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
class ImportDatabase:
|
||||
def __init__(
|
||||
self,
|
||||
zip_archive: str,
|
||||
import_recipes: bool = True,
|
||||
import_settings: bool = True,
|
||||
import_themes: bool = True,
|
||||
force_import: bool = False,
|
||||
rebase: bool = False,
|
||||
) -> None:
|
||||
"""Import a database.zip file exported from mealie.
|
||||
|
||||
Args:
|
||||
zip_archive (str): The filename contained in the backups directory
|
||||
import_recipes (bool, optional): Import Recipes?. Defaults to True.
|
||||
import_settings (bool, optional): Determines if settings are imported. Defaults to True.
|
||||
import_themes (bool, optional): Determines if themes are imported. Defaults to True.
|
||||
force_import (bool, optional): Force import will update all existing recipes. If False existing recipes are skipped. Defaults to False.
|
||||
rebase (bool, optional): Rebase will first clear the database and then import Recipes. Defaults to False.
|
||||
|
||||
Raises:
|
||||
Exception: If the zip file does not exists an exception raise.
|
||||
"""
|
||||
|
||||
self.archive = BACKUP_DIR.joinpath(zip_archive)
|
||||
self.imp_recipes = import_recipes
|
||||
self.imp_settings = import_settings
|
||||
self.imp_themes = import_themes
|
||||
self.force_imports = force_import
|
||||
self.force_rebase = rebase
|
||||
|
||||
if self.archive.is_file():
|
||||
self.import_dir = TEMP_DIR.joinpath("active_import")
|
||||
self.import_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(self.archive, "r") as zip_ref:
|
||||
zip_ref.extractall(self.import_dir)
|
||||
pass
|
||||
else:
|
||||
raise Exception("Import file does not exist")
|
||||
|
||||
def run(self):
|
||||
if self.imp_recipes:
|
||||
report = self.import_recipes()
|
||||
if self.imp_settings:
|
||||
self.import_settings()
|
||||
if self.imp_themes:
|
||||
self.import_themes()
|
||||
|
||||
self.clean_up()
|
||||
|
||||
return report if report else None
|
||||
|
||||
def import_recipes(self):
|
||||
recipe_dir: Path = self.import_dir.joinpath("recipes")
|
||||
|
||||
successful_imports = []
|
||||
failed_imports = []
|
||||
|
||||
for recipe in recipe_dir.glob("*.json"):
|
||||
with open(recipe, "r") as f:
|
||||
recipe_dict = json.loads(f.read())
|
||||
recipe_dict = ImportDatabase._recipe_migration(recipe_dict)
|
||||
|
||||
|
||||
recipe_obj = Recipe(**recipe_dict)
|
||||
recipe_obj.save_to_db()
|
||||
successful_imports.append(recipe.stem)
|
||||
logger.info(f"Imported: {recipe.stem}")
|
||||
try:
|
||||
recipe_obj = Recipe(**recipe_dict)
|
||||
recipe_obj.save_to_db()
|
||||
successful_imports.append(recipe.stem)
|
||||
logger.info(f"Imported: {recipe.stem}")
|
||||
except:
|
||||
logger.info(f"Failed Import: {recipe.stem}")
|
||||
failed_imports.append(recipe.stem)
|
||||
|
||||
self._import_images(successful_imports)
|
||||
|
||||
return {"successful": successful_imports, "failed": failed_imports}
|
||||
|
||||
@staticmethod
|
||||
def _recipe_migration(recipe_dict: dict) -> dict:
|
||||
try:
|
||||
del recipe_dict["_id"]
|
||||
del recipe_dict["dateAdded"]
|
||||
except:
|
||||
logger.info("Detected new backup Schema, skipping migration...")
|
||||
return recipe_dict
|
||||
# Migration from list to Object Type Data
|
||||
if type(recipe_dict["extras"]) == list:
|
||||
recipe_dict["extras"] = {}
|
||||
|
||||
return recipe_dict
|
||||
|
||||
def _import_images(self, successful_imports: List[str]):
|
||||
image_dir = self.import_dir.joinpath("images")
|
||||
for image in image_dir.iterdir():
|
||||
if image.stem in successful_imports:
|
||||
shutil.copy(image, IMG_DIR)
|
||||
|
||||
def import_themes(self):
|
||||
themes_file = self.import_dir.joinpath("themes", "themes.json")
|
||||
|
||||
with open(themes_file, "r") as f:
|
||||
themes: list = json.loads(f.read())
|
||||
for theme in themes:
|
||||
new_theme = SiteTheme(**theme)
|
||||
try:
|
||||
new_theme.save_to_db()
|
||||
except:
|
||||
logger.info(f"Unable Import Theme {new_theme.name}")
|
||||
|
||||
def import_settings(self):
|
||||
settings_file = self.import_dir.joinpath("settings", "settings.json")
|
||||
|
||||
with open(settings_file, "r") as f:
|
||||
settings: dict = json.loads(f.read())
|
||||
|
||||
settings = SiteSettings(**settings)
|
||||
|
||||
settings.update()
|
||||
|
||||
def clean_up(self):
|
||||
shutil.rmtree(TEMP_DIR)
|
|
@ -2,13 +2,12 @@ import shutil
|
|||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
IMG_DIR = CWD.parent.joinpath("data", "img")
|
||||
|
||||
|
||||
def read_image(recipe_slug: str) -> FileResponse:
|
||||
def read_image(recipe_slug: str) -> Path:
|
||||
if IMG_DIR.joinpath(recipe_slug).is_file():
|
||||
return IMG_DIR.joinpath(recipe_slug)
|
||||
else:
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import json
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from db.meal_models import MealDocument, MealPlanDocument
|
||||
from db.database import db
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.recipe_services import Recipe
|
||||
|
@ -80,73 +79,30 @@ class MealPlan(BaseModel):
|
|||
self.meals = meals
|
||||
|
||||
def save_to_db(self):
|
||||
meal_docs = []
|
||||
for meal in self.meals:
|
||||
meal = meal.dict()
|
||||
meal_doc = MealDocument(**meal)
|
||||
meal_docs.append(meal_doc)
|
||||
|
||||
self.meals = meal_docs
|
||||
|
||||
meal_plan = MealPlanDocument(**self.dict())
|
||||
|
||||
meal_plan.save()
|
||||
db.meals.save_new(self.dict())
|
||||
|
||||
@staticmethod
|
||||
def get_all() -> List:
|
||||
all_meals = []
|
||||
for plan in MealPlanDocument.objects.order_by("startDate"):
|
||||
all_meals.append(MealPlan._unpack_doc(plan))
|
||||
|
||||
print(all_meals)
|
||||
all_meals = [MealPlan(**x) for x in db.meals.get_all(order_by="startDate")]
|
||||
|
||||
return all_meals
|
||||
|
||||
def update(self, uid):
|
||||
document = MealPlanDocument.objects.get(uid=uid)
|
||||
|
||||
meal_docs = []
|
||||
for meal in self.meals:
|
||||
meal = meal.dict()
|
||||
meal_doc = MealDocument(**meal)
|
||||
meal_docs.append(meal_doc)
|
||||
|
||||
self.meals = meal_docs
|
||||
if document:
|
||||
document.update(set__meals=self.meals)
|
||||
document.save()
|
||||
db.meals.update(uid, self.dict())
|
||||
|
||||
@staticmethod
|
||||
def delete(uid):
|
||||
document = MealPlanDocument.objects.get(uid=uid)
|
||||
|
||||
if document:
|
||||
document.delete()
|
||||
|
||||
@staticmethod
|
||||
def _unpack_doc(document: MealPlanDocument):
|
||||
meal_plan = json.loads(document.to_json())
|
||||
del meal_plan["_id"]["$oid"]
|
||||
print(meal_plan)
|
||||
meal_plan["uid"] = meal_plan["uid"]["$uuid"]
|
||||
|
||||
meal_plan["startDate"] = meal_plan["startDate"]["$date"]
|
||||
meal_plan["endDate"] = meal_plan["endDate"]["$date"]
|
||||
|
||||
meals = []
|
||||
for meal in meal_plan["meals"]:
|
||||
meal["date"] = meal["date"]["$date"]
|
||||
meals.append(Meal(**meal))
|
||||
|
||||
meal_plan["meals"] = meals
|
||||
return MealPlan(**meal_plan)
|
||||
db.meals.delete(uid)
|
||||
|
||||
@staticmethod
|
||||
def today() -> str:
|
||||
""" Returns the meal slug for Today """
|
||||
meal_plan = MealPlanDocument.objects.order_by("startDate").limit(1)
|
||||
meal_plan = MealPlan._unpack_doc(meal_plan[0])
|
||||
meal_plan = db.meals.get_all(limit=1, order_by="startDate")
|
||||
|
||||
for meal in meal_plan.meals:
|
||||
meal_docs = [Meal(**meal) for meal in meal_plan["meals"]]
|
||||
|
||||
for meal in meal_docs:
|
||||
if meal.date == date.today():
|
||||
return meal.slug
|
||||
|
||||
|
@ -154,7 +110,6 @@ class MealPlan(BaseModel):
|
|||
|
||||
@staticmethod
|
||||
def this_week():
|
||||
meal_plan = MealPlanDocument.objects.order_by("startDate").limit(1)
|
||||
meal_plan = MealPlan._unpack_doc(meal_plan[0])
|
||||
meal_plan = db.meals.get_all(limit=1, order_by="startDate")
|
||||
|
||||
return meal_plan
|
||||
|
|
|
@ -3,8 +3,8 @@ from pathlib import Path
|
|||
|
||||
import git
|
||||
import yaml
|
||||
from services.image_services import IMG_DIR
|
||||
from services.recipe_services import Recipe
|
||||
from settings import IMG_DIR
|
||||
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
|
|
|
@ -4,11 +4,11 @@ import shutil
|
|||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from services.recipe_services import IMG_DIR, Recipe
|
||||
from services.recipe_services import Recipe
|
||||
from services.scrape_services import normalize_data, process_recipe_data
|
||||
from settings import IMG_DIR, TEMP_DIR
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
TEMP_DIR = CWD.parent.parent.joinpath("data", "temp")
|
||||
MIGRTAION_DIR = CWD.parent.parent.joinpath("data", "migration")
|
||||
|
||||
|
||||
|
|
|
@ -3,16 +3,12 @@ import json
|
|||
from pathlib import Path
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from db.recipe_models import RecipeDocument
|
||||
from db.database import db
|
||||
from pydantic import BaseModel, validator
|
||||
from slugify import slugify
|
||||
|
||||
from services.image_services import delete_image
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
ALL_RECIPES = CWD.parent.joinpath("data", "all_recipes.json")
|
||||
IMG_DIR = CWD.parent.joinpath("data", "img")
|
||||
|
||||
|
||||
class RecipeNote(BaseModel):
|
||||
title: str
|
||||
|
@ -31,7 +27,10 @@ class Recipe(BaseModel):
|
|||
recipeYield: Optional[str]
|
||||
recipeIngredient: Optional[list]
|
||||
recipeInstructions: Optional[list]
|
||||
|
||||
totalTime: Optional[Any]
|
||||
prepTime: Optional[str]
|
||||
performTime: Optional[str]
|
||||
|
||||
# Mealie Specific
|
||||
slug: Optional[str] = ""
|
||||
|
@ -67,9 +66,7 @@ class Recipe(BaseModel):
|
|||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
||||
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"rating": 3,
|
||||
"extras": {
|
||||
"message": "Don't forget to defrost the chicken!"
|
||||
}
|
||||
"extras": {"message": "Don't forget to defrost the chicken!"},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,12 +91,12 @@ class Recipe(BaseModel):
|
|||
return cls(**document)
|
||||
|
||||
@classmethod
|
||||
def get_by_slug(_cls, slug: str):
|
||||
""" Returns a recipe dictionary from the slug """
|
||||
def get_by_slug(cls, slug: str):
|
||||
""" Returns a Recipe Object by Slug """
|
||||
|
||||
document = RecipeDocument.objects.get(slug=slug)
|
||||
document = db.recipes.get(slug, "slug")
|
||||
|
||||
return Recipe._unpack_doc(document)
|
||||
return cls(**document)
|
||||
|
||||
def save_to_db(self) -> str:
|
||||
recipe_dict = self.dict()
|
||||
|
@ -116,41 +113,30 @@ class Recipe(BaseModel):
|
|||
except:
|
||||
pass
|
||||
|
||||
recipeDoc = RecipeDocument(**recipe_dict)
|
||||
recipeDoc.save()
|
||||
recipe_doc = db.recipes.save_new(recipe_dict)
|
||||
recipe = Recipe(**recipe_doc)
|
||||
|
||||
return recipeDoc.slug
|
||||
return recipe.slug
|
||||
|
||||
@staticmethod
|
||||
def delete(recipe_slug: str) -> str:
|
||||
""" Removes the recipe from the database by slug """
|
||||
delete_image(recipe_slug)
|
||||
document = RecipeDocument.objects.get(slug=recipe_slug)
|
||||
|
||||
if document:
|
||||
document.delete()
|
||||
return "Document Deleted"
|
||||
db.recipes.delete(recipe_slug)
|
||||
return "Document Deleted"
|
||||
|
||||
def update(self, recipe_slug: str):
|
||||
""" Updates the recipe from the database by slug"""
|
||||
document = RecipeDocument.objects.get(slug=recipe_slug)
|
||||
updated_slug = db.recipes.update(recipe_slug, self.dict())
|
||||
return updated_slug.get("slug")
|
||||
|
||||
if document:
|
||||
document.update(set__name=self.name)
|
||||
document.update(set__description=self.description)
|
||||
document.update(set__image=self.image)
|
||||
document.update(set__recipeYield=self.recipeYield)
|
||||
document.update(set__recipeIngredient=self.recipeIngredient)
|
||||
document.update(set__recipeInstructions=self.recipeInstructions)
|
||||
document.update(set__totalTime=self.totalTime)
|
||||
@staticmethod
|
||||
def update_image(slug: str, extension: str):
|
||||
db.recipes.update_image(slug, extension)
|
||||
|
||||
document.update(set__categories=self.categories)
|
||||
document.update(set__tags=self.tags)
|
||||
document.update(set__notes=self.notes)
|
||||
document.update(set__orgURL=self.orgURL)
|
||||
document.update(set__rating=self.rating)
|
||||
document.update(set__extras=self.extras)
|
||||
document.save()
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return db.recipes.get_all()
|
||||
|
||||
|
||||
def read_requested_values(keys: list, max_results: int = 0) -> List[dict]:
|
||||
|
@ -166,7 +152,7 @@ def read_requested_values(keys: list, max_results: int = 0) -> List[dict]:
|
|||
|
||||
"""
|
||||
recipe_list = []
|
||||
for recipe in RecipeDocument.objects.order_by("dateAdded").limit(max_results):
|
||||
for recipe in db.recipes.get_all(limit=max_results, order_by="dateAdded"):
|
||||
recipe_details = {}
|
||||
for key in keys:
|
||||
try:
|
||||
|
|
|
@ -5,7 +5,7 @@ import requests
|
|||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from utils.logger import logger
|
||||
|
||||
from services.backup_services import auto_backup_job
|
||||
from services.backups.exports import auto_backup_job
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings
|
||||
|
|
|
@ -85,6 +85,12 @@ def process_recipe_data(new_recipe: dict, url=None) -> dict:
|
|||
|
||||
def extract_recipe_from_html(html: str, url: str) -> dict:
|
||||
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True)
|
||||
|
||||
if not scraped_recipes:
|
||||
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(
|
||||
url, python_objects=True
|
||||
)
|
||||
|
||||
if scraped_recipes:
|
||||
new_recipe: dict = scraped_recipes[0]
|
||||
logger.info(f"Recipe Scraped From Web: {new_recipe}")
|
||||
|
|
|
@ -1,23 +1,14 @@
|
|||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
from db.settings_models import (
|
||||
SiteSettingsDocument,
|
||||
SiteThemeDocument,
|
||||
ThemeColorsDocument,
|
||||
WebhooksDocument,
|
||||
)
|
||||
from db.database import db
|
||||
from pydantic import BaseModel
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
class Webhooks(BaseModel):
|
||||
webhookTime: str
|
||||
webhookURLs: Optional[List[str]]
|
||||
enabled: bool
|
||||
|
||||
@staticmethod
|
||||
def run():
|
||||
pass
|
||||
webhookTime: str = "00:00"
|
||||
webhookURLs: Optional[List[str]] = []
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class SiteSettings(BaseModel):
|
||||
|
@ -37,30 +28,22 @@ class SiteSettings(BaseModel):
|
|||
}
|
||||
|
||||
@staticmethod
|
||||
def _unpack_doc(document: SiteSettingsDocument):
|
||||
document = json.loads(document.to_json())
|
||||
del document["_id"]
|
||||
document["webhhooks"] = Webhooks(**document["webhooks"])
|
||||
return SiteSettings(**document)
|
||||
def get_all():
|
||||
db.settings.get_all()
|
||||
|
||||
@staticmethod
|
||||
def get_site_settings():
|
||||
@classmethod
|
||||
def get_site_settings(cls):
|
||||
try:
|
||||
document = SiteSettingsDocument.objects.get(name="main")
|
||||
document = db.settings.get("main")
|
||||
except:
|
||||
webhooks = WebhooksDocument()
|
||||
document = SiteSettingsDocument(name="main", webhooks=webhooks)
|
||||
document.save()
|
||||
webhooks = Webhooks()
|
||||
default_entry = SiteSettings(name="main", webhooks=webhooks)
|
||||
document = db.settings.save_new(default_entry.dict(), webhooks.dict())
|
||||
|
||||
return SiteSettings._unpack_doc(document)
|
||||
return cls(**document)
|
||||
|
||||
def update(self):
|
||||
document = SiteSettingsDocument.objects.get(name="main")
|
||||
new_webhooks = WebhooksDocument(**self.webhooks.dict())
|
||||
|
||||
document.update(set__webhooks=new_webhooks)
|
||||
|
||||
document.save()
|
||||
db.settings.update("main", new_data=self.dict())
|
||||
|
||||
|
||||
class Colors(BaseModel):
|
||||
|
@ -93,50 +76,56 @@ class SiteTheme(BaseModel):
|
|||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_by_name(theme_name):
|
||||
document = SiteThemeDocument.objects.get(name=theme_name)
|
||||
return SiteTheme._unpack_doc(document)
|
||||
@classmethod
|
||||
def get_by_name(cls, theme_name):
|
||||
db_entry = db.themes.get(theme_name)
|
||||
name = db_entry.get("name")
|
||||
colors = Colors(**db_entry.get("colors"))
|
||||
|
||||
@staticmethod
|
||||
def _unpack_doc(document):
|
||||
document = json.loads(document.to_json())
|
||||
del document["_id"]
|
||||
theme_colors = SiteTheme(**document)
|
||||
return theme_colors
|
||||
return cls(name=name, colors=colors)
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
all_themes = []
|
||||
for theme in SiteThemeDocument.objects():
|
||||
all_themes.append(SiteTheme._unpack_doc(theme))
|
||||
all_themes = db.themes.get_all()
|
||||
for index, theme in enumerate(all_themes):
|
||||
name = theme.get("name")
|
||||
colors = Colors(**theme.get("colors"))
|
||||
|
||||
all_themes[index] = SiteTheme(name=name, colors=colors)
|
||||
|
||||
return all_themes
|
||||
|
||||
def save_to_db(self):
|
||||
theme = self.dict()
|
||||
theme["colors"] = ThemeColorsDocument(**theme["colors"])
|
||||
|
||||
theme_document = SiteThemeDocument(**theme)
|
||||
|
||||
theme_document.save()
|
||||
db.themes.save_new(self.dict())
|
||||
|
||||
def update_document(self):
|
||||
theme = self.dict()
|
||||
theme["colors"] = ThemeColorsDocument(**theme["colors"])
|
||||
|
||||
theme_document = SiteThemeDocument.objects.get(name=self.name)
|
||||
|
||||
if theme_document:
|
||||
theme_document.update(set__colors=theme["colors"])
|
||||
|
||||
theme_document.save()
|
||||
db.themes.update(self.dict())
|
||||
|
||||
@staticmethod
|
||||
def delete_theme(theme_name: str) -> str:
|
||||
""" Removes the theme by name """
|
||||
document = SiteThemeDocument.objects.get(name=theme_name)
|
||||
db.themes.delete(theme_name)
|
||||
|
||||
if document:
|
||||
document.delete()
|
||||
return "Document Deleted"
|
||||
|
||||
def default_theme_init():
|
||||
default_colors = {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
}
|
||||
|
||||
try:
|
||||
SiteTheme.get_by_name("default")
|
||||
return "default theme exists"
|
||||
except:
|
||||
logger.info("Generating Default Theme")
|
||||
colors = Colors(**default_colors)
|
||||
default_theme = SiteTheme(name="default", colors=colors)
|
||||
default_theme.save_to_db()
|
||||
|
||||
|
||||
default_theme_init()
|
||||
|
|
|
@ -3,20 +3,35 @@ from pathlib import Path
|
|||
|
||||
import dotenv
|
||||
|
||||
# Helpful Globas
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
|
||||
# Register ENV
|
||||
ENV = CWD.joinpath(".env")
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
# Helpful Globals
|
||||
BASE_DIR = CWD
|
||||
DATA_DIR = CWD.joinpath("data")
|
||||
WEB_PATH = CWD.joinpath("dist")
|
||||
IMG_DIR = DATA_DIR.joinpath("img")
|
||||
BACKUP_DIR = DATA_DIR.joinpath("backups")
|
||||
DEBUG_DIR = DATA_DIR.joinpath("debug")
|
||||
MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||
TINYDB_DIR = DATA_DIR.joinpath("db")
|
||||
TEMP_DIR = DATA_DIR.joinpath("temp")
|
||||
|
||||
REQUIRED_DIRS = [
|
||||
DATA_DIR,
|
||||
IMG_DIR,
|
||||
BACKUP_DIR,
|
||||
DEBUG_DIR,
|
||||
MIGRATION_DIR,
|
||||
TEMPLATE_DIR,
|
||||
TINYDB_DIR,
|
||||
]
|
||||
|
||||
# Env Variables
|
||||
ENV = CWD.joinpath(".env")
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
# General
|
||||
PRODUCTION = os.environ.get("ENV")
|
||||
|
@ -30,6 +45,22 @@ else:
|
|||
docs_url = None
|
||||
redoc_url = None
|
||||
|
||||
|
||||
# DATABASE ENV
|
||||
DATABASE_TYPE = os.getenv("db_type", "sql") # mongo, tinydb
|
||||
if DATABASE_TYPE == "sql":
|
||||
USE_SQL = True
|
||||
USE_MONGO = False
|
||||
|
||||
elif DATABASE_TYPE == "mongo":
|
||||
USE_MONGO = True
|
||||
USE_SQL = False
|
||||
|
||||
else:
|
||||
raise Exception(
|
||||
"Unable to determine database type. Acceptible options are 'mongo' or 'tinydb' "
|
||||
)
|
||||
|
||||
# Mongo Database
|
||||
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
|
||||
DB_USERNAME = os.getenv("db_username", "root")
|
||||
|
@ -37,6 +68,6 @@ DB_PASSWORD = os.getenv("db_password", "example")
|
|||
DB_HOST = os.getenv("db_host", "mongo")
|
||||
DB_PORT = os.getenv("db_port", 27017)
|
||||
|
||||
# SFTP Email Stuff
|
||||
# SFTP Email Stuff - For use Later down the line!
|
||||
SFTP_USERNAME = os.getenv("sftp_username", None)
|
||||
SFTP_PASSWORD = os.getenv("sftp_password", None)
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from services.settings_services import Colors, SiteTheme
|
||||
from utils.logger import logger
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
DATA_DIR = CWD.joinpath("data")
|
||||
TEMP_DIR = CWD.joinpath("data", "temp")
|
||||
|
||||
|
||||
def ensure_dirs():
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
DATA_DIR.joinpath("img").mkdir(parents=True, exist_ok=True)
|
||||
DATA_DIR.joinpath("backups").mkdir(parents=True, exist_ok=True)
|
||||
DATA_DIR.joinpath("templates").mkdir(parents=True, exist_ok=True)
|
||||
DATA_DIR.joinpath("debug").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def generate_default_theme():
|
||||
default_colors = {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
}
|
||||
|
||||
try:
|
||||
SiteTheme.get_by_name("default")
|
||||
return "default theme exists"
|
||||
except:
|
||||
logger.info("Generating Default Theme")
|
||||
colors = Colors(**default_colors)
|
||||
default_theme = SiteTheme(name="default", colors=colors)
|
||||
default_theme.save_to_db()
|
||||
|
||||
|
||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>My Project - ReDoc</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<style data-styled="" data-styled-version="4.4.1"></style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="redoc-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
|
||||
<script>
|
||||
var spec = %s;
|
||||
Redoc.init(spec, {}, document.getElementById("redoc-container"));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
out_path = CWD.joinpath("temp", "index.html")
|
||||
|
||||
|
||||
def generate_api_docs(app):
|
||||
with open(out_path, "w") as fd:
|
||||
out_path.parent.mkdir(exist_ok=True)
|
||||
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
2
mealie/test/conftest.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from pytest import fixture
|
||||
|
4
mealie/test/pytest.ini
Normal file
|
@ -0,0 +1,4 @@
|
|||
[pytest]
|
||||
python_files = test_*
|
||||
python_classes = *Tests
|
||||
python_functions = test_*
|
0
mealie/test/test_migrations/__init__.py
Normal file
|
@ -11,8 +11,8 @@ from services.migrations.nextcloud import (
|
|||
from services.recipe_services import Recipe
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
NEXTCLOUD_DIR = CWD.joinpath("data", "nextcloud_recipes")
|
||||
TEMP_NEXTCLOUD = CWD.parent.joinpath("data", "temp", "nextcloud")
|
||||
NEXTCLOUD_DIR = CWD.parent.joinpath("data", "nextcloud_recipes")
|
||||
TEMP_NEXTCLOUD = CWD.parent.parent.joinpath("data", "temp", "nextcloud")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -39,5 +39,5 @@ def test_zip_extraction(file_name: str, final_path: Path):
|
|||
)
|
||||
def test_nextcloud_migration(recipe_dir: Path):
|
||||
recipe = import_recipes(recipe_dir)
|
||||
assert type(recipe) == Recipe
|
||||
assert isinstance(recipe, Recipe)
|
||||
IMG_DIR.joinpath(recipe.image).unlink(missing_ok=True)
|
0
mealie/test/test_recipes/__init__.py
Normal file
|
@ -10,8 +10,8 @@ from services.scrape_services import (
|
|||
)
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
RAW_RECIPE_DIR = CWD.joinpath("data", "recipes-raw")
|
||||
RAW_HTML_DIR = CWD.joinpath("data", "html-raw")
|
||||
RAW_RECIPE_DIR = CWD.parent.joinpath("data", "recipes-raw")
|
||||
RAW_HTML_DIR = CWD.parent.joinpath("data", "html-raw")
|
||||
|
||||
# https://github.com/django/django/blob/stable/1.3.x/django/core/validators.py#L45
|
||||
url_validation_regex = re.compile(
|
40
mealie/utils/api_docs.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
import json
|
||||
|
||||
from settings import BASE_DIR
|
||||
|
||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>My Project - ReDoc</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<style data-styled="" data-styled-version="4.4.1"></style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="redoc-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
|
||||
<script>
|
||||
var spec = %s;
|
||||
Redoc.init(spec, {}, document.getElementById("redoc-container"));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
out_path = BASE_DIR.joinpath("temp", "index.html")
|
||||
|
||||
|
||||
def generate_api_docs(app):
|
||||
with open(out_path, "w") as fd:
|
||||
out_path.parent.mkdir(exist_ok=True)
|
||||
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
|
11
mealie/utils/global_scheduler.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from services.scheduler_services import Scheduler
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
global scheduler
|
||||
scheduler = Scheduler()
|
||||
scheduler.startup_scheduler()
|
||||
return scheduler
|
||||
|
||||
|
||||
scheduler = start_scheduler()
|
18
mealie/utils/startup.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from pathlib import Path
|
||||
|
||||
from settings import REQUIRED_DIRS
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
|
||||
def pre_start():
|
||||
ensure_dirs()
|
||||
|
||||
|
||||
def ensure_dirs():
|
||||
for dir in REQUIRED_DIRS:
|
||||
dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|